@adia-ai/web-components 0.0.10 → 0.0.12
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.
- package/components/calendar-picker/calendar-picker.css +8 -2
- package/components/chat/chat-input.css +41 -5
- package/components/code/code.css +146 -0
- package/components/code/code.js +221 -24
- package/components/field/field.a2ui.json +149 -0
- package/components/field/field.css +111 -0
- package/components/field/field.js +306 -0
- package/components/field/field.test.js +146 -0
- package/components/field/field.yaml +155 -0
- package/components/index.js +1 -0
- package/components/input/input.css +10 -3
- package/components/range/range.css +10 -3
- package/components/select/select.css +8 -3
- package/components/textarea/textarea.css +12 -2
- package/components/upload/upload.css +5 -2
- package/core/_cm-core.js +38 -0
- package/core/_cm-theme.js +58 -0
- package/core/_lang-css.js +2 -0
- package/core/_lang-html.js +2 -0
- package/core/_lang-javascript.js +2 -0
- package/core/_lang-json.js +2 -0
- package/core/_lang-markdown.js +2 -0
- package/core/_lang-yaml.js +2 -0
- package/core/code-editor-bundle.js +63 -0
- package/core/element.test.js +234 -0
- package/core/form.js +26 -0
- package/core/markdown.js +8 -2
- package/core/template.js +2 -11
- package/package.json +1 -1
- package/styles/colors/semantics.css +39 -12
- package/styles/styles.css +1 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { AdiaElement, signal, effect } from './element.js';
|
|
3
|
+
|
|
4
|
+
// ── Shared helpers ──
|
|
5
|
+
|
|
6
|
+
let registrationCounter = 0;
|
|
7
|
+
function registerTestElement(BaseImpl) {
|
|
8
|
+
const tag = `test-el-${++registrationCounter}`;
|
|
9
|
+
customElements.define(tag, BaseImpl);
|
|
10
|
+
return tag;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function mount(tag, attrs = {}) {
|
|
14
|
+
const el = document.createElement(tag);
|
|
15
|
+
for (const [k, v] of Object.entries(attrs)) el.setAttribute(k, v);
|
|
16
|
+
document.body.appendChild(el);
|
|
17
|
+
return el;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Signals flush via queueMicrotask — await one tick to let reactive
|
|
21
|
+
// effects settle after a mutation.
|
|
22
|
+
const tick = () => new Promise((r) => queueMicrotask(r));
|
|
23
|
+
|
|
24
|
+
describe('AdiaElement — construction + lifecycle', () => {
|
|
25
|
+
beforeEach(() => { document.body.innerHTML = ''; });
|
|
26
|
+
|
|
27
|
+
it('instantiates and calls connected() on mount, disconnected() on remove', () => {
|
|
28
|
+
const events = [];
|
|
29
|
+
class El extends AdiaElement {
|
|
30
|
+
connected() { events.push('connected'); }
|
|
31
|
+
disconnected() { events.push('disconnected'); }
|
|
32
|
+
}
|
|
33
|
+
const tag = registerTestElement(El);
|
|
34
|
+
const el = mount(tag);
|
|
35
|
+
expect(events).toEqual(['connected']);
|
|
36
|
+
el.remove();
|
|
37
|
+
expect(events).toEqual(['connected', 'disconnected']);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('remount after disconnect re-runs connected()', () => {
|
|
41
|
+
const events = [];
|
|
42
|
+
class El extends AdiaElement {
|
|
43
|
+
connected() { events.push('connected'); }
|
|
44
|
+
disconnected() { events.push('disconnected'); }
|
|
45
|
+
}
|
|
46
|
+
const tag = registerTestElement(El);
|
|
47
|
+
const el = mount(tag);
|
|
48
|
+
el.remove();
|
|
49
|
+
document.body.appendChild(el);
|
|
50
|
+
expect(events).toEqual(['connected', 'disconnected', 'connected']);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe('AdiaElement — properties', () => {
|
|
55
|
+
beforeEach(() => { document.body.innerHTML = ''; });
|
|
56
|
+
|
|
57
|
+
it('reads default value from static properties', () => {
|
|
58
|
+
class El extends AdiaElement {
|
|
59
|
+
static properties = { label: { type: String, default: 'hi' } };
|
|
60
|
+
}
|
|
61
|
+
const tag = registerTestElement(El);
|
|
62
|
+
const el = mount(tag);
|
|
63
|
+
expect(el.label).toBe('hi');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('reflects Boolean properties to HTML attributes when reflect: true', () => {
|
|
67
|
+
class El extends AdiaElement {
|
|
68
|
+
static properties = { disabled: { type: Boolean, default: false, reflect: true } };
|
|
69
|
+
}
|
|
70
|
+
const tag = registerTestElement(El);
|
|
71
|
+
const el = mount(tag);
|
|
72
|
+
expect(el.hasAttribute('disabled')).toBe(false);
|
|
73
|
+
el.disabled = true;
|
|
74
|
+
expect(el.hasAttribute('disabled')).toBe(true);
|
|
75
|
+
el.disabled = false;
|
|
76
|
+
expect(el.hasAttribute('disabled')).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('reflects String properties as attribute values', () => {
|
|
80
|
+
class El extends AdiaElement {
|
|
81
|
+
static properties = { variant: { type: String, default: 'neutral', reflect: true } };
|
|
82
|
+
}
|
|
83
|
+
const tag = registerTestElement(El);
|
|
84
|
+
const el = mount(tag);
|
|
85
|
+
el.variant = 'accent';
|
|
86
|
+
expect(el.getAttribute('variant')).toBe('accent');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('parses incoming attributes back into typed properties', () => {
|
|
90
|
+
class El extends AdiaElement {
|
|
91
|
+
static properties = {
|
|
92
|
+
count: { type: Number, default: 0 },
|
|
93
|
+
active: { type: Boolean, default: false },
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
const tag = registerTestElement(El);
|
|
97
|
+
const el = mount(tag, { count: '42', active: '' });
|
|
98
|
+
expect(el.count).toBe(42);
|
|
99
|
+
expect(el.active).toBe(true);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('does NOT reflect when reflect: false / omitted', () => {
|
|
103
|
+
class El extends AdiaElement {
|
|
104
|
+
static properties = { internal: { type: String, default: 'x' } };
|
|
105
|
+
}
|
|
106
|
+
const tag = registerTestElement(El);
|
|
107
|
+
const el = mount(tag);
|
|
108
|
+
el.internal = 'y';
|
|
109
|
+
expect(el.hasAttribute('internal')).toBe(false);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe('AdiaElement — reactive effect', () => {
|
|
114
|
+
beforeEach(() => { document.body.innerHTML = ''; });
|
|
115
|
+
|
|
116
|
+
it('calls render() on mount and when a reactive property changes', async () => {
|
|
117
|
+
let renders = 0;
|
|
118
|
+
class El extends AdiaElement {
|
|
119
|
+
static properties = { label: { type: String, default: 'a' } };
|
|
120
|
+
render() { renders++; void this.label; }
|
|
121
|
+
}
|
|
122
|
+
const tag = registerTestElement(El);
|
|
123
|
+
const el = mount(tag);
|
|
124
|
+
expect(renders).toBeGreaterThan(0);
|
|
125
|
+
const before = renders;
|
|
126
|
+
el.label = 'b';
|
|
127
|
+
await tick();
|
|
128
|
+
expect(renders).toBeGreaterThan(before);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('stops re-rendering after disconnect', async () => {
|
|
132
|
+
let renders = 0;
|
|
133
|
+
class El extends AdiaElement {
|
|
134
|
+
static properties = { label: { type: String, default: 'a' } };
|
|
135
|
+
render() { renders++; void this.label; }
|
|
136
|
+
}
|
|
137
|
+
const tag = registerTestElement(El);
|
|
138
|
+
const el = mount(tag);
|
|
139
|
+
el.remove();
|
|
140
|
+
const frozen = renders;
|
|
141
|
+
el.label = 'c';
|
|
142
|
+
await tick();
|
|
143
|
+
expect(renders).toBe(frozen);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe('AdiaElement — subscription-leak guard (0.0.10 regression)', () => {
|
|
148
|
+
beforeEach(() => { document.body.innerHTML = ''; });
|
|
149
|
+
|
|
150
|
+
it('child connected() reads do NOT subscribe an outer effect', async () => {
|
|
151
|
+
// The bug: when a parent effect called appendChild(child) while running,
|
|
152
|
+
// child.connectedCallback would synchronously call connected(), and any
|
|
153
|
+
// template-literal reads of child's reactive properties would subscribe
|
|
154
|
+
// the OUTER effect to the CHILD's signals. A later change to a child
|
|
155
|
+
// prop would spuriously re-run the parent effect.
|
|
156
|
+
//
|
|
157
|
+
// Fix: connected() is now wrapped in untracked(…).
|
|
158
|
+
class Child extends AdiaElement {
|
|
159
|
+
static properties = { value: { type: String, default: 'initial', reflect: true } };
|
|
160
|
+
connected() {
|
|
161
|
+
// Read our own reactive prop exactly the way template-literal
|
|
162
|
+
// interpolation would — this read must not subscribe an ancestor
|
|
163
|
+
// effect.
|
|
164
|
+
void this.value;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
const tag = registerTestElement(Child);
|
|
168
|
+
|
|
169
|
+
const trigger = signal(0);
|
|
170
|
+
let outerRuns = 0;
|
|
171
|
+
let childRef = null;
|
|
172
|
+
|
|
173
|
+
const dispose = effect(() => {
|
|
174
|
+
outerRuns++;
|
|
175
|
+
void trigger.value;
|
|
176
|
+
if (!childRef) {
|
|
177
|
+
childRef = document.createElement(tag);
|
|
178
|
+
document.body.appendChild(childRef);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const outerRunsAfterMount = outerRuns;
|
|
183
|
+
expect(childRef).not.toBeNull();
|
|
184
|
+
|
|
185
|
+
// Now mutate the CHILD's signal — the outer effect MUST NOT re-run.
|
|
186
|
+
childRef.value = 'changed';
|
|
187
|
+
await tick();
|
|
188
|
+
expect(outerRuns).toBe(outerRunsAfterMount);
|
|
189
|
+
|
|
190
|
+
// Sanity check: mutating the trigger signal the outer effect DID
|
|
191
|
+
// subscribe to re-runs the effect, proving the effect is alive.
|
|
192
|
+
trigger.value = 1;
|
|
193
|
+
await tick();
|
|
194
|
+
expect(outerRuns).toBe(outerRunsAfterMount + 1);
|
|
195
|
+
|
|
196
|
+
dispose();
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
describe('AdiaElement — addTrait', () => {
|
|
201
|
+
beforeEach(() => { document.body.innerHTML = ''; });
|
|
202
|
+
|
|
203
|
+
it('applies a trait once, idempotent on repeat', () => {
|
|
204
|
+
let connects = 0;
|
|
205
|
+
const myTrait = () => ({
|
|
206
|
+
connect() { connects++; },
|
|
207
|
+
disconnect() {},
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
class El extends AdiaElement {}
|
|
211
|
+
const tag = registerTestElement(El);
|
|
212
|
+
const el = mount(tag);
|
|
213
|
+
el.addTrait(myTrait);
|
|
214
|
+
el.addTrait(myTrait);
|
|
215
|
+
expect(connects).toBe(1);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('disconnects all applied traits on remove', () => {
|
|
219
|
+
let connects = 0, disconnects = 0;
|
|
220
|
+
const myTrait = () => ({
|
|
221
|
+
connect() { connects++; },
|
|
222
|
+
disconnect() { disconnects++; },
|
|
223
|
+
});
|
|
224
|
+
class El extends AdiaElement {
|
|
225
|
+
static traits = [myTrait];
|
|
226
|
+
}
|
|
227
|
+
const tag = registerTestElement(El);
|
|
228
|
+
const el = mount(tag);
|
|
229
|
+
expect(connects).toBe(1);
|
|
230
|
+
expect(disconnects).toBe(0);
|
|
231
|
+
el.remove();
|
|
232
|
+
expect(disconnects).toBe(1);
|
|
233
|
+
});
|
|
234
|
+
});
|
package/core/form.js
CHANGED
|
@@ -178,8 +178,34 @@ export class AdiaFormElement extends AdiaElement {
|
|
|
178
178
|
this.addEventListener('invalid', this.#onInvalid);
|
|
179
179
|
this.addEventListener('input', this.#onInput);
|
|
180
180
|
this.addEventListener('blur', this.#onBlur, true);
|
|
181
|
+
this.#warnDeprecatedLabel();
|
|
181
182
|
}
|
|
182
183
|
|
|
184
|
+
/**
|
|
185
|
+
* Soft-deprecation warning for the per-control `label` attribute.
|
|
186
|
+
* Wrapping the control in `<field-ui label="…">` (Phase 1 shipped
|
|
187
|
+
* 2026-04-23) gives proper `<label for>` association and composition
|
|
188
|
+
* slots; the per-control `label` attr renders via an inert shadow
|
|
189
|
+
* slot without `[for]`, so click-to-focus doesn't work.
|
|
190
|
+
*
|
|
191
|
+
* One-shot per class so the console isn't spammed on docs pages.
|
|
192
|
+
*/
|
|
193
|
+
#warnDeprecatedLabel() {
|
|
194
|
+
const ctor = this.constructor;
|
|
195
|
+
if (!ctor.properties?.label) return;
|
|
196
|
+
if (!this.hasAttribute('label')) return;
|
|
197
|
+
if (AdiaFormElement.#labelWarned.has(ctor)) return;
|
|
198
|
+
AdiaFormElement.#labelWarned.add(ctor);
|
|
199
|
+
// eslint-disable-next-line no-console
|
|
200
|
+
console.warn(
|
|
201
|
+
`[AdiaUI] <${this.localName} label="…"> is deprecated — wrap in ` +
|
|
202
|
+
`<field-ui label="…"> for proper label association. ` +
|
|
203
|
+
`Docs: https://ui-kit.exe.xyz/site/components/field`
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
static #labelWarned = new Set();
|
|
208
|
+
|
|
183
209
|
disconnected() {
|
|
184
210
|
super.disconnected();
|
|
185
211
|
this.removeEventListener('invalid', this.#onInvalid);
|
package/core/markdown.js
CHANGED
|
@@ -19,7 +19,10 @@ export function renderMarkdown(src) {
|
|
|
19
19
|
while (i < lines.length) {
|
|
20
20
|
const line = lines[i];
|
|
21
21
|
|
|
22
|
-
// Fenced code block
|
|
22
|
+
// Fenced code block — when a language is named, emit <code-ui> so
|
|
23
|
+
// the site picks up the syntax-highlight upgrade shipped in
|
|
24
|
+
// SPEC-CODE-EDITOR-001. Unlabeled fences keep the bare <pre><code>
|
|
25
|
+
// form (no highlight needed; zero CM6 cost).
|
|
23
26
|
const fence = line.match(/^```(\w*)/);
|
|
24
27
|
if (fence) {
|
|
25
28
|
const lang = fence[1];
|
|
@@ -30,7 +33,10 @@ export function renderMarkdown(src) {
|
|
|
30
33
|
i++;
|
|
31
34
|
}
|
|
32
35
|
i++; // skip closing ```
|
|
33
|
-
|
|
36
|
+
const body = code.join('\n');
|
|
37
|
+
out.push(lang
|
|
38
|
+
? `<code-ui language="${lang}">${body}</code-ui>`
|
|
39
|
+
: `<pre><code>${body}</code></pre>`);
|
|
34
40
|
continue;
|
|
35
41
|
}
|
|
36
42
|
|
package/core/template.js
CHANGED
|
@@ -68,21 +68,12 @@ export function stamp(result, container) {
|
|
|
68
68
|
update(inst.p, result.values);
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
// Safari 14.0 lacks ChildNode.replaceChildren. Fallback manually removes
|
|
72
|
-
// existing children, then appends the new content. Runs once per call;
|
|
73
|
-
// cheap enough to be unconditional.
|
|
74
|
-
function replaceChildren(container, ...nodes) {
|
|
75
|
-
if (container.replaceChildren) { container.replaceChildren(...nodes); return; }
|
|
76
|
-
while (container.firstChild) container.removeChild(container.firstChild);
|
|
77
|
-
for (const n of nodes) container.appendChild(n);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
71
|
function mount(result, container) {
|
|
81
72
|
const { strings } = result;
|
|
82
73
|
const tpl = getTemplate(strings);
|
|
83
74
|
const f = tpl.content.cloneNode(true);
|
|
84
75
|
const parts = scan(f, result.values.length);
|
|
85
|
-
replaceChildren(
|
|
76
|
+
container.replaceChildren(f);
|
|
86
77
|
return { s: strings, p: parts };
|
|
87
78
|
}
|
|
88
79
|
|
|
@@ -147,7 +138,7 @@ function applyValue(p, v) {
|
|
|
147
138
|
stamp(v, wrap(p));
|
|
148
139
|
} else if (Array.isArray(v)) {
|
|
149
140
|
const c = wrap(p);
|
|
150
|
-
replaceChildren(
|
|
141
|
+
c.replaceChildren();
|
|
151
142
|
for (const item of v) {
|
|
152
143
|
if (isResult(item)) {
|
|
153
144
|
const el = document.createElement('span');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adia-ai/web-components",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.12",
|
|
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": {
|
|
@@ -419,19 +419,46 @@
|
|
|
419
419
|
--a-danger-border-invalid: var(--a-danger-border);
|
|
420
420
|
|
|
421
421
|
/* ══════════════════════════════════════════════════════════════
|
|
422
|
-
FOCUS
|
|
423
|
-
══════════════════════════════════════════════════════════════
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
422
|
+
FOCUS — canonical ring recipe shared by all form controls
|
|
423
|
+
══════════════════════════════════════════════════════════════
|
|
424
|
+
|
|
425
|
+
The ring is a dual-layer box-shadow: an inner "gap" the canvas
|
|
426
|
+
color wide, then an outer ring the accent color wide. Tuning any
|
|
427
|
+
of the scalar tokens below cascades to every component that
|
|
428
|
+
consumes `--a-focus-ring`:
|
|
429
|
+
|
|
430
|
+
--a-focus-width — outer ring thickness
|
|
431
|
+
--a-focus-offset — inner gap thickness (distance from element
|
|
432
|
+
edge to ring)
|
|
433
|
+
--a-focus-color — ring color (default: accent-strong)
|
|
434
|
+
--a-focus-bg — gap color (default: the surface under the
|
|
435
|
+
control; normally matches --a-canvas-0)
|
|
436
|
+
|
|
437
|
+
Invalid / error state uses the same geometry but a danger color
|
|
438
|
+
via `--a-focus-ring-invalid`.
|
|
439
|
+
|
|
440
|
+
Components must NOT hardcode 2px / 4px / accent hex in their CSS;
|
|
441
|
+
consume either `--a-focus-ring` directly or an L3 alias like
|
|
442
|
+
`--{component}-focus-ring: var(--a-focus-ring)` (the more common
|
|
443
|
+
pattern — gives per-component override headroom without breaking
|
|
444
|
+
the canonical default). */
|
|
445
|
+
|
|
446
|
+
--a-focus-color: var(--a-accent-strong);
|
|
447
|
+
--a-focus-color-invalid: var(--a-danger);
|
|
448
|
+
--a-focus-width: 2px;
|
|
449
|
+
--a-focus-offset: 2px;
|
|
450
|
+
--a-focus-bg: var(--a-canvas-0);
|
|
451
|
+
|
|
452
|
+
--a-focus-ring-shadow: 0 0 0 var(--a-focus-offset) var(--a-focus-bg),
|
|
429
453
|
0 0 0 calc(var(--a-focus-offset) + var(--a-focus-width)) var(--a-focus-color);
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
--a-
|
|
454
|
+
--a-focus-ring-shadow-invalid: 0 0 0 var(--a-focus-offset) var(--a-focus-bg),
|
|
455
|
+
0 0 0 calc(var(--a-focus-offset) + var(--a-focus-width)) var(--a-focus-color-invalid);
|
|
456
|
+
|
|
457
|
+
/* L3 — what components consume. */
|
|
458
|
+
--a-ring: var(--a-focus-color);
|
|
459
|
+
--a-ring-invalid: var(--a-focus-color-invalid);
|
|
460
|
+
--a-focus-ring: var(--a-focus-ring-shadow);
|
|
461
|
+
--a-focus-ring-invalid: var(--a-focus-ring-shadow-invalid);
|
|
435
462
|
|
|
436
463
|
/* ══════════════════════════════════════════════════════════════
|
|
437
464
|
UI (FIELD) — Form input surfaces
|
package/styles/styles.css
CHANGED
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
@import "../components/kbd/kbd.css";
|
|
42
42
|
@import "../components/tag/tag.css";
|
|
43
43
|
@import "../components/col/col.css";
|
|
44
|
+
@import "../components/field/field.css";
|
|
44
45
|
@import "../components/row/row.css";
|
|
45
46
|
@import "../components/grid/grid.css";
|
|
46
47
|
@import "../components/stack/stack.css";
|