@adia-ai/web-components 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/README.md +5 -2
  2. package/components/chat-thread/chat-input.css +107 -19
  3. package/components/index.js +2 -1
  4. package/components/table/cell-types.js +1 -1
  5. package/core/element.js +63 -2
  6. package/package.json +1 -3
  7. package/styles/colors/semantics.css +4 -4
  8. package/styles/components.css +1 -1
  9. package/traits/_catalog.json +509 -0
  10. package/traits/_motion.js +57 -0
  11. package/traits/_smoke.test.js +111 -0
  12. package/traits/_test-helpers.js +82 -0
  13. package/traits/active-state.js +2 -0
  14. package/traits/active-state.test.js +28 -0
  15. package/traits/anchor-positioning.js +2 -0
  16. package/traits/anchor-positioning.test.js +49 -0
  17. package/traits/attention-pulse.js +11 -0
  18. package/traits/attention-pulse.test.js +26 -0
  19. package/traits/confetti-burst.js +27 -0
  20. package/traits/confetti-burst.test.js +38 -0
  21. package/traits/confetti.js +18 -0
  22. package/traits/confetti.test.js +27 -0
  23. package/traits/count-up.js +17 -0
  24. package/traits/count-up.test.js +54 -0
  25. package/traits/declarative.test.js +138 -0
  26. package/traits/define.js +43 -3
  27. package/traits/dirty-state.js +2 -0
  28. package/traits/dirty-state.test.js +45 -0
  29. package/traits/drag-ghost.js +2 -0
  30. package/traits/drag-ghost.test.js +19 -0
  31. package/traits/draggable.js +2 -0
  32. package/traits/draggable.test.js +60 -0
  33. package/traits/fade-presence.js +2 -0
  34. package/traits/fade-presence.test.js +20 -0
  35. package/traits/focus-trap.js +2 -0
  36. package/traits/focus-trap.test.js +42 -0
  37. package/traits/focusable.js +2 -0
  38. package/traits/focusable.test.js +53 -0
  39. package/traits/glow-focus.js +6 -1
  40. package/traits/glow-focus.test.js +31 -0
  41. package/traits/gradient-shift.js +9 -0
  42. package/traits/gradient-shift.test.js +22 -0
  43. package/traits/haptic-feedback.js +2 -0
  44. package/traits/haptic-feedback.test.js +52 -0
  45. package/traits/hotkey.js +2 -0
  46. package/traits/hotkey.test.js +61 -0
  47. package/traits/hoverable.js +2 -0
  48. package/traits/hoverable.test.js +24 -0
  49. package/traits/index.js +50 -37
  50. package/traits/inertia-drag.js +2 -0
  51. package/traits/inertia-drag.test.js +33 -0
  52. package/traits/intersection-observer.js +2 -0
  53. package/traits/intersection-observer.test.js +38 -0
  54. package/traits/keyboard-nav.js +2 -0
  55. package/traits/keyboard-nav.test.js +41 -0
  56. package/traits/magnetic-hover.js +8 -0
  57. package/traits/magnetic-hover.test.js +30 -0
  58. package/traits/noise-texture.js +2 -0
  59. package/traits/noise-texture.test.js +20 -0
  60. package/traits/parallax.js +9 -0
  61. package/traits/parallax.test.js +26 -0
  62. package/traits/portal.js +2 -0
  63. package/traits/portal.test.js +30 -0
  64. package/traits/pressable.js +2 -0
  65. package/traits/pressable.test.js +73 -0
  66. package/traits/resettable.js +40 -0
  67. package/traits/resettable.test.js +67 -0
  68. package/traits/resizable.js +2 -0
  69. package/traits/resizable.test.js +20 -0
  70. package/traits/resize-observer.js +2 -0
  71. package/traits/resize-observer.test.js +38 -0
  72. package/traits/ripple.js +9 -0
  73. package/traits/ripple.test.js +32 -0
  74. package/traits/roving-tabindex.js +2 -0
  75. package/traits/roving-tabindex.test.js +28 -0
  76. package/traits/scale-press.js +2 -0
  77. package/traits/scale-press.test.js +39 -0
  78. package/traits/scroll-lock.js +2 -0
  79. package/traits/scroll-lock.test.js +45 -0
  80. package/traits/shimmer-loading.js +20 -0
  81. package/traits/shimmer-loading.test.js +43 -0
  82. package/traits/snap-to-grid.js +2 -0
  83. package/traits/snap-to-grid.test.js +40 -0
  84. package/traits/sound-feedback.js +2 -0
  85. package/traits/sound-feedback.test.js +26 -0
  86. package/traits/spring-animate.js +2 -0
  87. package/traits/spring-animate.test.js +28 -0
  88. package/traits/tilt-hover.js +8 -0
  89. package/traits/tilt-hover.test.js +32 -0
  90. package/traits/tossable.js +2 -0
  91. package/traits/tossable.test.js +31 -0
  92. package/traits/traits-host.js +53 -0
  93. package/traits/traits-host.test.js +73 -0
  94. package/traits/typeahead.js +2 -0
  95. package/traits/typeahead.test.js +38 -0
  96. package/traits/typewriter.js +17 -0
  97. package/traits/typewriter.test.js +47 -0
  98. package/traits/validation.js +2 -0
  99. package/traits/validation.test.js +93 -0
  100. package/a2ui/index.js +0 -25
  101. /package/components/stat/{stat.css → stat-ui.css} +0 -0
  102. /package/components/stat/{stat.js → stat-ui.js} +0 -0
package/README.md CHANGED
@@ -57,8 +57,11 @@ web-components/
57
57
  │ gen-ui, a2ui-root) ship in the sibling `@adia-ai/web-modules`
58
58
  │ package as of 0.0.29 — see ADR-0012 for the three-tier rationale.
59
59
 
60
- ├── traits/ — 42 composable behaviors via defineTrait()
61
- (pressable, focusTrap, confetti, resizable, …)
60
+ ├── traits/ — 41 composable behaviors via defineTrait() + the
61
+ <traits-host> wrapper for raw-HTML declarative
62
+ │ composition. Generated catalog at _catalog.json
63
+ │ drives the MCP get_traits tool + per-trait demo
64
+ │ pages. Full contract in docs/specs/traits.md.
62
65
 
63
66
  ├── a2ui/ — deprecation shim for one release
64
67
  │ └── index.js Re-exports @adia-ai/a2ui-utils with a
@@ -1,3 +1,22 @@
1
+ /* Safari 17.x bug: `:scope:hover` and `:scope:not(...) [descendant]:hover`
2
+ inside `@scope` don't match the scope root. Plain selectors outside
3
+ work. See docs/BROWSER-COMPAT.md §3a. Same pattern used by input.css
4
+ and textarea.css.
5
+
6
+ Two rules — one on the host (paint the hover affordance), one on the
7
+ inner textarea-ui's [slot="text"] (suppress the textarea-ui hover
8
+ bg/border/color so it doesn't compound with the host's). */
9
+ chat-input-ui:not([disabled]):hover {
10
+ background: var(--chat-input-bg-hover);
11
+ border-color: var(--chat-input-border-hover);
12
+ color: var(--chat-input-fg-hover);
13
+ }
14
+ chat-input-ui textarea-ui:not([disabled]) [slot="text"]:hover {
15
+ background: transparent;
16
+ border-color: transparent;
17
+ color: inherit;
18
+ }
19
+
1
20
  @scope (chat-input-ui) {
2
21
  :where(:scope) {
3
22
  /* ── Layout ── */
@@ -8,11 +27,35 @@
8
27
  --chat-input-toolbar-py: var(--a-space-1);
9
28
  --chat-input-toolbar-gap: var(--a-space-1);
10
29
 
11
- /* ── Colors ── */
30
+ /* ── Colors ──
31
+ chat-input-ui owns every color the composite paints — no inner
32
+ state styling from textarea-ui leaks through. The block below
33
+ (`Nested-control color isolation`) suppresses textarea-ui's
34
+ hover / focus / disabled / placeholder treatments inside this
35
+ @scope and re-paints them with these tokens. */
12
36
  --chat-input-bg: var(--a-canvas-0);
37
+ --chat-input-fg: var(--a-fg);
13
38
  --chat-input-border: var(--a-border-subtle);
14
39
  --chat-input-caret-color: var(--a-fg-subtle);
40
+ --chat-input-placeholder-fg: var(--a-fg-muted);
41
+
42
+ /* Hover — matches input-ui's affordance (subtle alpha lift on the
43
+ canvas surface + slightly stronger border + brightened fg). The
44
+ host paints this on `chat-input-ui:hover` (Safari 17.x bug
45
+ requires the rule outside @scope — see top of file); the inner
46
+ textarea-ui's own hover is suppressed there too so the alpha
47
+ doesn't compound. */
48
+ --chat-input-bg-hover: var(--a-ui-bg-hover);
49
+ --chat-input-border-hover: var(--a-ui-border-hover);
50
+ --chat-input-fg-hover: var(--a-fg);
51
+
52
+ /* Disabled — host-level chrome (background, border, pointer-events)
53
+ lives on `:scope[disabled]` below. Inner-text disabled color is
54
+ owned here so the inner textarea-ui's `--a-ui-text-disabled` doesn't
55
+ leak through. */
56
+ --chat-input-bg-disabled: transparent;
15
57
  --chat-input-border-disabled: var(--a-border-subtle);
58
+ --chat-input-fg-disabled: var(--a-fg-disabled);
16
59
 
17
60
  /* Canonical focus ring — chat-input is a *nested-control host*.
18
61
  See semantics.css FOCUS block + the nested-control pattern
@@ -38,7 +81,7 @@
38
81
  --chat-input-remove-font: var(--a-ui-tiny);
39
82
 
40
83
  /* ── Transition ── */
41
- --chat-input-duration: var(--a-duration);
84
+ --chat-input-duration: var(--a-duration-fast);
42
85
  --chat-input-easing: var(--a-easing);
43
86
  }
44
87
 
@@ -50,6 +93,15 @@
50
93
  border: 1px solid var(--chat-input-border);
51
94
  border-radius: var(--chat-input-radius);
52
95
  background: var(--chat-input-bg);
96
+ color: var(--chat-input-fg);
97
+ /* Mirrors input-ui's transition surface so hover/focus/invalid
98
+ interpolate together — bg + border + color + ring all snap if
99
+ any one is left out. */
100
+ transition:
101
+ background var(--chat-input-duration) var(--chat-input-easing),
102
+ border-color var(--chat-input-duration) var(--chat-input-easing),
103
+ color var(--chat-input-duration) var(--chat-input-easing),
104
+ box-shadow var(--chat-input-duration) var(--chat-input-easing);
53
105
  }
54
106
 
55
107
  /* ── Nested-control focus pattern ────────────────────────────────────
@@ -76,35 +128,67 @@
76
128
  box-shadow: var(--chat-input-focus-ring-invalid);
77
129
  }
78
130
 
79
- /* Textarea: no border/bg of its own — container handles it. The
80
- second selector keeps the transparent bg even when the host
131
+ /* ── Nested-control color isolation ─────────────────────────────────
132
+ The inner `textarea-ui` ships its own hover / focus / invalid /
133
+ disabled / placeholder color rules wired to the generic `--a-ui-*`
134
+ family. Inside chat-input-ui those would leak through and fight
135
+ the host's chrome — bg flashes on hover, fg brightens on focus,
136
+ a second focus ring paints alongside the composite ring, etc.
137
+ Every textarea state below is explicitly suppressed and re-painted
138
+ with the `--chat-input-*` tokens above so the composite is the
139
+ sole owner of color across all states.
140
+
141
+ Specificity bump: textarea.css uses
142
+ `textarea-ui:not([disabled]) [slot="text"]:STATE` (0,3,0). Prefixing
143
+ with `:scope` adds (0,1,0) → (0,4,1), which wins. The :hover rule
144
+ in textarea.css lives outside its @scope (Safari 17.x workaround)
145
+ but uses the same shape; same specificity boost applies. */
146
+
147
+ /* Rest — chrome neutralization (no border, no bg) + chat layout +
148
+ `color: inherit` so the inner inherits the host's color (which IS
149
+ transitioned on the `:scope` rule above). All state colors are then
150
+ owned at the host level — disabled / hover / focus all flow into
151
+ the inner via inheritance and animate together with the chrome.
152
+ The second selector keeps the transparent bg even when the inner
81
153
  textarea-ui carries [disabled] (streaming / submit lock) —
82
- otherwise textarea.css's `:scope[disabled] [slot="text"]` rule
83
- (specificity 0,3,0) paints --a-ui-bg-disabled over the container
84
- bg. The `:scope` prefix boosts specificity to 0,3,1 so our rule
85
- wins. */
154
+ textarea.css's `:scope[disabled] [slot="text"]` (0,3,0) would
155
+ otherwise paint --a-ui-bg-disabled. */
86
156
  textarea-ui [slot="text"],
87
157
  :scope textarea-ui[disabled] [slot="text"] {
88
158
  border: none;
89
- background: transparent;
159
+ background: var(--chat-input-bg-disabled); /* transparent at default */
90
160
  border-radius: 0;
91
161
  box-shadow: none;
162
+ color: inherit;
92
163
  max-height: 8rem;
93
164
  padding: var(--chat-input-textarea-pt) var(--chat-input-textarea-px) 0;
94
165
  }
95
166
 
96
- /* Suppress the nested textarea-ui's own focus ringthe host (this
97
- scope) owns the affordance. Required because textarea.css's default
98
- rule paints a ring via box-shadow; without this override, the
99
- composite would show both the host's outer ring AND the inner
100
- control's ring simultaneously.
167
+ /* Hover lives outside this @scope (top of file) Safari 17.x bug
168
+ prevents `:scope [descendant]:hover` from matching here. */
101
169
 
102
- textarea.css's rule is `:scope:not([disabled]) [slot="text"]:focus`
103
- with specificity (0,4,0); we need to beat that using the same
104
- `:not([disabled])` guard on the host lifts ours to (0,4,1). */
170
+ /* Focus suppress the inner ring (host paints via :focus-within)
171
+ and the inner color shift to fg-hover so the host's color cascades. */
105
172
  :scope textarea-ui:not([disabled]) [slot="text"]:focus {
106
173
  border: none;
107
174
  box-shadow: none;
175
+ color: inherit;
176
+ }
177
+
178
+ /* Invalid focus — textarea.css would paint --a-focus-ring-invalid on
179
+ the inner [slot="text"] when the inner control carries
180
+ [aria-invalid="true"] or [error]. Host owns the invalid ring via
181
+ :scope[aria-invalid]:focus-within above; suppress the inner one. */
182
+ :scope textarea-ui:not([disabled])[aria-invalid="true"] [slot="text"]:focus,
183
+ :scope textarea-ui:not([disabled])[error] [slot="text"]:focus {
184
+ box-shadow: none;
185
+ }
186
+
187
+ /* Placeholder — textarea.css paints --a-ui-text-placeholder on the
188
+ `::before` pseudo. Override with the chat-input token so the
189
+ composite controls the placeholder treatment. */
190
+ :scope textarea-ui [slot="text"][data-empty]::before {
191
+ color: var(--chat-input-placeholder-fg);
108
192
  }
109
193
 
110
194
  /* Toolbar */
@@ -174,10 +258,14 @@
174
258
  padding: 0;
175
259
  }
176
260
 
177
- /* Disabled / streaming */
261
+ /* Disabled / streaming — host-level chrome. `color` cascades to the
262
+ inner [slot="text"] via inheritance (the inner uses `color: inherit`
263
+ in the rules above), so the disabled-fg shift transitions on the
264
+ host's `color` transition rather than snapping. */
178
265
  :scope[disabled] {
179
- background: transparent;
266
+ background: var(--chat-input-bg-disabled);
180
267
  border-color: var(--chat-input-border-disabled);
268
+ color: var(--chat-input-fg-disabled);
181
269
  pointer-events: none;
182
270
  }
183
271
  }
@@ -75,7 +75,8 @@ export { UINavItem } from './nav-item/nav-item.js';
75
75
  export { UIOtpInput } from './otp-input/otp-input.js';
76
76
  export { UIImage } from './image/image.js';
77
77
  export { UISearch } from './search/search.js';
78
- export { UIStat } from './stat/stat.js';
78
+ // Suffixed `-ui` to dodge ad-blocker rules that match `/stat.js$script`.
79
+ export { UIStat } from './stat/stat-ui.js';
79
80
  export { UIProgressRow } from './progress-row/progress-row.js';
80
81
  export { UIActionList, UIActionItem } from './action-list/action-list.js';
81
82
  export { UIEmptyState } from './empty-state/empty-state.js';
@@ -186,7 +186,7 @@ registerCellType('avatar', {
186
186
  }
187
187
  const av = wrapper.querySelector('avatar-ui');
188
188
  const span = wrapper.querySelector('span');
189
- av.setAttribute('name', String(value ?? ''));
189
+ av.setAttribute('text', String(value ?? ''));
190
190
  span.textContent = String(value ?? '');
191
191
  },
192
192
  format(value) {
package/core/element.js CHANGED
@@ -19,6 +19,7 @@ export { html, css, repeat } from './template.js';
19
19
  // Internal imports for UIElement
20
20
  import { signal, computed, effect, untracked } from './signals.js';
21
21
  import { stamp, disposeParts, KEY_MAP } from './template.js';
22
+ import { getTrait } from '../traits/define.js';
22
23
 
23
24
  // ═══════════════════════════════════════════════════════════════
24
25
  // ADIA ELEMENT — Light DOM web component base class
@@ -83,7 +84,10 @@ export class UIElement extends HTMLElement {
83
84
  static get properties() { return {}; }
84
85
  static get traits() { return []; }
85
86
  static get observedAttributes() {
86
- return Object.entries(this.properties).map(([k, c]) => c.attribute ?? k.toLowerCase());
87
+ const propAttrs = Object.entries(this.properties).map(([k, c]) => c.attribute ?? k.toLowerCase());
88
+ // Always observe `traits` so HTML authors can declare composable behaviors:
89
+ // <comp-ui traits="pressable scale-press ripple">
90
+ return propAttrs.includes('traits') ? propAttrs : [...propAttrs, 'traits'];
87
91
  }
88
92
 
89
93
  #fx = [];
@@ -133,6 +137,7 @@ export class UIElement extends HTMLElement {
133
137
  }));
134
138
  if (!this.#tf) this.#tf = new Set();
135
139
  for (const t of ctor.traits) this.#applyTrait(t);
140
+ this.#applyDeclaredTraits();
136
141
  if (this.#ctrl) this.#connectCtrl();
137
142
  }
138
143
 
@@ -166,10 +171,11 @@ export class UIElement extends HTMLElement {
166
171
  if (c && this.isConnected) this.#connectCtrl();
167
172
  }
168
173
 
169
- #applyTrait(trait) {
174
+ #applyTrait(trait, { declarative = false } = {}) {
170
175
  this.#tf.add(trait);
171
176
  const instance = trait();
172
177
  instance.connect(this, { host: this, signal, computed, effect });
178
+ if (declarative) instance._declarative = true;
173
179
  this.#tc.push(instance);
174
180
  }
175
181
 
@@ -180,7 +186,62 @@ export class UIElement extends HTMLElement {
180
186
  return this;
181
187
  }
182
188
 
189
+ /**
190
+ * Parse the `traits` attribute and apply each named trait. Idempotent —
191
+ * traits already attached via `static traits = [...]` or `addTrait()` are
192
+ * skipped so an HTML author layering on top of the class declaration can't
193
+ * double-apply.
194
+ *
195
+ * Unknown trait names log a one-line warning rather than throwing — this
196
+ * happens in dev when an app forgot to import the trait barrel.
197
+ */
198
+ #applyDeclaredTraits() {
199
+ const list = this.getAttribute('traits');
200
+ if (!list) return;
201
+ for (const name of list.split(/\s+/).filter(Boolean)) {
202
+ const factory = getTrait(name);
203
+ if (!factory) {
204
+ console.warn(`<${this.localName}> traits="${name}" — trait not found. Did you forget to import it?`);
205
+ continue;
206
+ }
207
+ if (this.#tf.has(factory)) continue;
208
+ this.#applyTrait(factory, { declarative: true });
209
+ }
210
+ }
211
+
212
+ /**
213
+ * On `traits` attribute change, disconnect declarative trait instances
214
+ * and re-apply from the new attribute value. Static + addTrait() instances
215
+ * are preserved.
216
+ */
217
+ #refreshDeclaredTraits() {
218
+ if (!this.#tf) return;
219
+ // Remove instances flagged as declarative.
220
+ const keep = [];
221
+ for (const inst of this.#tc) {
222
+ if (inst._declarative) {
223
+ inst.disconnect(this);
224
+ // Allow this factory to be re-applied via the new attribute value.
225
+ for (const factory of this.#tf) {
226
+ if (factory.schema?.name && inst.schema?.name === factory.schema.name) {
227
+ this.#tf.delete(factory);
228
+ break;
229
+ }
230
+ }
231
+ } else {
232
+ keep.push(inst);
233
+ }
234
+ }
235
+ this.#tc.length = 0;
236
+ this.#tc.push(...keep);
237
+ this.#applyDeclaredTraits();
238
+ }
239
+
183
240
  attributeChangedCallback(name, _, val) {
241
+ if (name === 'traits') {
242
+ if (this.isConnected) this.#refreshDeclaredTraits();
243
+ return;
244
+ }
184
245
  for (const [key, cfg] of Object.entries(this.constructor.properties)) {
185
246
  if ((cfg.attribute ?? key.toLowerCase()) === name) {
186
247
  this[key] = parseAttr(val, cfg.type ?? String);
package/package.json CHANGED
@@ -1,12 +1,11 @@
1
1
  {
2
2
  "name": "@adia-ai/web-components",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "AdiaUI web components — vanilla custom elements. A2UI runtime (renderer, registry, streams, wiring) lives in @adia-ai/a2ui-utils.",
5
5
  "type": "module",
6
6
  "exports": {
7
7
  ".": "./index.js",
8
8
  "./css": "./index.css",
9
- "./a2ui": "./a2ui/index.js",
10
9
  "./core": "./core/index.js",
11
10
  "./core/*": "./core/*.js",
12
11
  "./components": "./components/index.js",
@@ -21,7 +20,6 @@
21
20
  "components/",
22
21
  "styles/",
23
22
  "traits/",
24
- "a2ui/",
25
23
  "index.js",
26
24
  "index.css"
27
25
  ],
@@ -95,9 +95,9 @@
95
95
  --a-canvas-pressed: var(--a-canvas-2); /* pressed/active button states */
96
96
 
97
97
  /* Borders on neutral surfaces — adaptive scrims flip polarity per mode. */
98
- --a-canvas-border-subtle: var(--a-neutral-1-scrim);
99
- --a-canvas-border: var(--a-neutral-3-scrim);
100
- --a-canvas-border-strong: var(--a-neutral-6-scrim);
98
+ --a-canvas-border-subtle: var(--a-neutral-0-scrim);
99
+ --a-canvas-border: var(--a-neutral-2-scrim);
100
+ --a-canvas-border-strong: var(--a-neutral-4-scrim);
101
101
 
102
102
  /* L3 — short-alias matrix (the consumable API for neutral surfaces) */
103
103
  --a-bg: var(--a-canvas-0);
@@ -541,7 +541,7 @@
541
541
  ══════════════════════════════════════════════════════════════ */
542
542
 
543
543
  --a-ui-bg: var(--a-canvas-0-scrim);
544
- --a-ui-bg-hover: var(--a-canvas-2-scrim);
544
+ --a-ui-bg-hover: var(--a-canvas-0-scrim);
545
545
  --a-ui-bg-active: var(--a-canvas-0);
546
546
  --a-ui-bg-selected: var(--a-canvas-0-scrim);
547
547
  --a-ui-bg-disabled: var(--a-canvas-1);
@@ -74,7 +74,7 @@
74
74
  @import "../components/otp-input/otp-input.css";
75
75
  @import "../components/image/image.css";
76
76
  @import "../components/search/search.css";
77
- @import "../components/stat/stat.css";
77
+ @import "../components/stat/stat-ui.css";
78
78
  @import "../components/progress-row/progress-row.css";
79
79
  @import "../components/action-list/action-list.css";
80
80
  @import "../components/empty-state/empty-state.css";