Compare commits

..

4 Commits

Author SHA1 Message Date
bastian 6aeb4685fc Merge pull request 'SEV-337: add shared animations.css' (#7) from sev-337/shared-animations into main 2026-06-11 14:04:56 -04:00
Bastian de Byl 63b6e185d4 SEV-337: add shared animations.css (keyframes + helper classes)
Centralizes the animation primitives duplicated across public/vendor/admin
style.css — shimmer/.skeleton/.ai-shimmer, fadeIn/.fade-in, fadeInUp/.fade-in-up,
stagger delays, shake/.shake — as a plain-CSS export each app can import
(@switchev/web-shared/animations.css). Bumps to 0.6.0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 14:04:41 -04:00
bastian c0a113dda7 Merge pull request 'SEV-408: combobox displayValue option (v0.5.0)' (#6) from sev-408/combobox-displayvalue into main 2026-06-10 20:21:14 -04:00
Bastian de Byl 6881d31650 SEV-408: combobox displayValue option (show code, search by label) v0.5.0
When displayValue:true, the input shows the selected option's value (e.g. a
state code "TX") while the dropdown stays label-searchable ("Texas") — for a
field searched by name but displayed compactly. Backward-compatible
(default false). 20/20 tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 20:20:59 -04:00
4 changed files with 110 additions and 6 deletions
+6 -4
View File
@@ -1,20 +1,22 @@
{ {
"name": "@switchev/web-shared", "name": "@switchev/web-shared",
"version": "0.4.0", "version": "0.6.0",
"description": "Shared browser helpers (formatters, markdown, image polling, combobox) for the SwitchEV frontends", "description": "Shared browser helpers (formatters, markdown, image polling, combobox, animations) for the SwitchEV frontends",
"type": "module", "type": "module",
"exports": { "exports": {
".": "./src/index.js", ".": "./src/index.js",
"./format": "./src/format.js", "./format": "./src/format.js",
"./markdown": "./src/markdown.js", "./markdown": "./src/markdown.js",
"./wait-image": "./src/wait-image.js", "./wait-image": "./src/wait-image.js",
"./combobox": "./src/combobox.js" "./combobox": "./src/combobox.js",
"./animations.css": "./src/animations.css"
}, },
"files": [ "files": [
"src" "src"
], ],
"sideEffects": [ "sideEffects": [
"./src/markdown.js" "./src/markdown.js",
"./src/animations.css"
], ],
"dependencies": { "dependencies": {
"fuzzysort": "^3.1.0" "fuzzysort": "^3.1.0"
+70
View File
@@ -0,0 +1,70 @@
/* @switchev/web-shared — shared animation primitives (SEV-337).
*
* Plain CSS (no Tailwind utilities), so it can be imported directly by each
* SwitchEV frontend instead of copy-pasting the same keyframes + helper classes
* into every app's style.css. Import once per app (e.g. in main.js):
* import '@switchev/web-shared/animations.css';
* App-specific, non-animation styles (brand theme, star ratings, gradient text)
* stay in each app's own stylesheet.
*/
/* Skeleton loading shimmer */
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
.skeleton {
background: linear-gradient(90deg, #e5e7eb 25%, #f3f4f6 50%, #e5e7eb 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
border-radius: 0.375rem;
}
/* SEV-238: AI-working shimmer — applied to a field while an AI draft is in
flight so the disabled state reads as "AI is writing here", not broken.
Reuses the skeleton shimmer keyframes with a brand-green tint. */
.ai-shimmer {
background-image: linear-gradient(90deg,
rgba(22, 163, 74, 0.06) 25%,
rgba(22, 163, 74, 0.18) 50%,
rgba(22, 163, 74, 0.06) 75%);
background-size: 200% 100%;
animation: shimmer 1.2s ease-in-out infinite;
}
/* Fade-in */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.fade-in {
animation: fadeIn 0.5s ease-out both;
}
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.fade-in-up {
animation: fadeInUp 0.6s ease-out both;
}
/* Staggered animation delays */
.delay-100 { animation-delay: 0.1s; }
.delay-200 { animation-delay: 0.2s; }
.delay-300 { animation-delay: 0.3s; }
.delay-400 { animation-delay: 0.4s; }
/* Shake — invalid input (e.g. a wrong verification code) */
@keyframes shake {
0%, 100% { transform: translateX(0); }
10%, 30%, 50%, 70%, 90% { transform: translateX(-4px); }
20%, 40%, 60%, 80% { transform: translateX(4px); }
}
.shake {
animation: shake 0.5s ease-in-out;
}
+7 -2
View File
@@ -46,6 +46,10 @@ export function comboboxData(config = {}) {
const allowFree = config.allowFree !== false; const allowFree = config.allowFree !== false;
const fuzzy = config.fuzzy !== false; const fuzzy = config.fuzzy !== false;
const limit = config.limit || 50; const limit = config.limit || 50;
// displayValue: after selecting, show the option's VALUE (e.g. a state code
// "TX") in the input instead of its LABEL ("Texas"), while the dropdown stays
// label-searchable. Lets a field be searched by name but display the code.
const displayValue = config.displayValue === true;
return { return {
value: config.value ?? '', value: config.value ?? '',
@@ -89,7 +93,8 @@ export function comboboxData(config = {}) {
return; return;
} }
const match = this._options.find((o) => o.value === String(v)); const match = this._options.find((o) => o.value === String(v));
this.query = match ? match.label : allowFree ? String(v) : ''; if (match) this.query = displayValue ? match.value : match.label;
else this.query = allowFree ? String(v) : '';
}, },
get filtered() { get filtered() {
@@ -121,7 +126,7 @@ export function comboboxData(config = {}) {
choose(opt) { choose(opt) {
this.value = opt.value; this.value = opt.value;
this.query = opt.label; this.query = displayValue ? opt.value : opt.label;
this.close(); this.close();
}, },
+27
View File
@@ -64,3 +64,30 @@ test('setOptions feeds reactive option lists (SEV-392)', () => {
c.query = 'tes'; c.query = 'tes';
assert.deepEqual(c.filtered.map((o) => o.value), ['Tesla']); assert.deepEqual(c.filtered.map((o) => o.value), ['Tesla']);
}); });
test('displayValue shows the code in the input, searches by label (SEV-408)', () => {
const c = comboboxData({
options: [{ value: 'CA', label: 'California' }, { value: 'TX', label: 'Texas' }],
allowFree: false,
displayValue: true,
});
c.init();
// search by name
c.query = 'tex';
assert.deepEqual(c.filtered.map((o) => o.value), ['TX']);
// choosing shows the CODE, not the name
c.choose({ value: 'TX', label: 'Texas' });
assert.equal(c.value, 'TX');
assert.equal(c.query, 'TX');
});
test('displayValue syncs the code from an external value (SEV-408)', () => {
const c = comboboxData({
options: [{ value: 'OR', label: 'Oregon' }],
allowFree: false,
displayValue: true,
value: 'OR',
});
c.init();
assert.equal(c.query, 'OR'); // shows code, not "Oregon"
});