@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.
@@ -0,0 +1,146 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import '../../core/element.js';
3
+ import './field.js';
4
+
5
+ const tick = () => new Promise((r) => queueMicrotask(r));
6
+
7
+ function mount(html) {
8
+ const wrap = document.createElement('div');
9
+ wrap.innerHTML = html;
10
+ document.body.appendChild(wrap);
11
+ return wrap.firstElementChild;
12
+ }
13
+
14
+ describe('field-ui', () => {
15
+ beforeEach(() => { document.body.innerHTML = ''; });
16
+
17
+ it('renders a <label> element carrying the `label` attr text', () => {
18
+ const f = mount('<field-ui label="Email"><input id="e" /></field-ui>');
19
+ const label = f.querySelector('label[data-field-label]');
20
+ expect(label).not.toBeNull();
21
+ // The label's first child is the text node; a hidden required-marker
22
+ // span is its last child (mounted but hidden). Inspecting textContent
23
+ // of the text node isolates the label copy from the marker.
24
+ expect(label.firstChild.nodeValue).toBe('Email');
25
+ });
26
+
27
+ it('auto-mints an id on the slotted control when missing and binds label[for]', () => {
28
+ const f = mount('<field-ui label="Email"><input /></field-ui>');
29
+ const input = f.querySelector('input');
30
+ const label = f.querySelector('label[data-field-label]');
31
+ expect(input.id).toMatch(/^field-ctl-/);
32
+ expect(label.getAttribute('for')).toBe(input.id);
33
+ });
34
+
35
+ it('respects an existing id on the slotted control', () => {
36
+ const f = mount('<field-ui label="Email"><input id="existing" /></field-ui>');
37
+ const input = f.querySelector('input');
38
+ const label = f.querySelector('label[data-field-label]');
39
+ expect(input.id).toBe('existing');
40
+ expect(label.getAttribute('for')).toBe('existing');
41
+ });
42
+
43
+ it('ignores [slot="trailing"] and [slot="action"] when picking the control', () => {
44
+ const f = mount(`
45
+ <field-ui label="Search">
46
+ <span slot="trailing">⌘K</span>
47
+ <input />
48
+ <button slot="action">x</button>
49
+ </field-ui>
50
+ `);
51
+ const input = f.querySelector('input');
52
+ const label = f.querySelector('label[data-field-label]');
53
+ expect(label.getAttribute('for')).toBe(input.id);
54
+ });
55
+
56
+ it('updates label text when the `label` attr changes', async () => {
57
+ const f = mount('<field-ui label="Email"><input /></field-ui>');
58
+ const label = f.querySelector('label[data-field-label]');
59
+ expect(label.firstChild.nodeValue).toBe('Email');
60
+ f.setAttribute('label', 'Username');
61
+ await tick();
62
+ expect(label.firstChild.nodeValue).toBe('Username');
63
+ });
64
+
65
+ it('rebinds label[for] when children change', async () => {
66
+ const f = mount('<field-ui label="X"><input id="first" /></field-ui>');
67
+ const label = f.querySelector('label[data-field-label]');
68
+ expect(label.getAttribute('for')).toBe('first');
69
+ // Replace the control.
70
+ f.querySelector('input').remove();
71
+ const next = document.createElement('input');
72
+ next.id = 'second';
73
+ f.appendChild(next);
74
+ // MutationObserver fires async on microtask.
75
+ await new Promise((r) => setTimeout(r, 10));
76
+ expect(label.getAttribute('for')).toBe('second');
77
+ });
78
+
79
+ it('reflects `inline` as a boolean attr (default false)', () => {
80
+ const f = mount('<field-ui label="X"><input /></field-ui>');
81
+ expect(f.hasAttribute('inline')).toBe(false);
82
+ f.inline = true;
83
+ expect(f.hasAttribute('inline')).toBe(true);
84
+ f.inline = false;
85
+ expect(f.hasAttribute('inline')).toBe(false);
86
+ });
87
+
88
+ it('renders without a label (label attr absent) — hides the <label> element', () => {
89
+ const f = mount('<field-ui><input /></field-ui>');
90
+ const label = f.querySelector('label[data-field-label]');
91
+ expect(label).not.toBeNull();
92
+ const input = f.querySelector('input');
93
+ expect(label.getAttribute('for')).toBe(input.id);
94
+ });
95
+
96
+ it('renders a hint element + wires aria-describedby when `hint` is set', async () => {
97
+ const f = mount('<field-ui label="E" hint="We keep it private"><input /></field-ui>');
98
+ await tick();
99
+ const hint = f.querySelector('[data-field-hint]');
100
+ const input = f.querySelector('input');
101
+ expect(hint?.textContent).toBe('We keep it private');
102
+ expect(hint?.hidden).toBe(false);
103
+ expect(input.getAttribute('aria-describedby')).toBe(hint.id);
104
+ });
105
+
106
+ it('hides hint when `hint` is empty', async () => {
107
+ const f = mount('<field-ui label="E"><input /></field-ui>');
108
+ await tick();
109
+ const hint = f.querySelector('[data-field-hint]');
110
+ expect(hint?.hidden).toBe(true);
111
+ const input = f.querySelector('input');
112
+ expect(input.hasAttribute('aria-describedby')).toBe(false);
113
+ });
114
+
115
+ it('renders an error element + suppresses hint when both set', async () => {
116
+ const f = mount('<field-ui label="E" hint="hi" error="Required"><input /></field-ui>');
117
+ await tick();
118
+ const hint = f.querySelector('[data-field-hint]');
119
+ const err = f.querySelector('[data-field-error]');
120
+ expect(err?.textContent).toBe('Required');
121
+ expect(err?.hidden).toBe(false);
122
+ expect(err?.getAttribute('role')).toBe('alert');
123
+ expect(hint?.hidden).toBe(true); // error wins
124
+ const input = f.querySelector('input');
125
+ expect(input.getAttribute('aria-describedby')).toBe(err.id);
126
+ });
127
+
128
+ it('renders the `*` required marker on the label when `required` is set', async () => {
129
+ const f = mount('<field-ui label="Email" required><input /></field-ui>');
130
+ await tick();
131
+ const label = f.querySelector('label[data-field-label]');
132
+ const mark = label.querySelector('[data-field-required]');
133
+ expect(mark?.textContent).toBe('*');
134
+ expect(mark?.getAttribute('aria-hidden')).toBe('true');
135
+ });
136
+
137
+ it('updates hint/error text reactively when attrs change', async () => {
138
+ const f = mount('<field-ui label="E" hint="a"><input /></field-ui>');
139
+ await tick();
140
+ const hint = f.querySelector('[data-field-hint]');
141
+ expect(hint.textContent).toBe('a');
142
+ f.hint = 'b';
143
+ await tick();
144
+ expect(hint.textContent).toBe('b');
145
+ });
146
+ });
@@ -0,0 +1,155 @@
1
+ # Edit this file; run `npm run build:components` to regenerate a2ui.json.
2
+ $schema: ../../../../scripts/schemas/component.yaml.schema.json
3
+ name: AdiaField
4
+ tag: field-ui
5
+ component: Field
6
+ category: form
7
+ version: 1
8
+ description: >-
9
+ Labeled field wrapper. Composes a <label for="…"> with a form
10
+ control (input-ui, select-ui, textarea-ui, etc.) placed in the
11
+ default slot, plus optional [slot="trailing"] and [slot="action"]
12
+ regions. Auto-mints an id on the slotted control when missing so
13
+ clicking the label focuses the control — an accessibility upgrade
14
+ over setting label="…" on the control directly, which has no
15
+ [for] binding. Two layouts — stacked (default) and inline (the
16
+ `inline` mode attribute collapses everything to a single row).
17
+ props:
18
+ label:
19
+ description: Label text rendered in the label row.
20
+ type: string
21
+ default: ""
22
+ reflect: true
23
+ hint:
24
+ description: >-
25
+ Help text rendered below the control in caption style. Wired
26
+ into the slotted control's aria-describedby so screen readers
27
+ announce it. Suppressed when `error` is set.
28
+ type: string
29
+ default: ""
30
+ reflect: true
31
+ error:
32
+ description: >-
33
+ Validation error message rendered below the control in danger
34
+ style. Takes precedence over `hint` in the same row, and
35
+ carries role="alert" so screen readers announce changes.
36
+ type: string
37
+ default: ""
38
+ reflect: true
39
+ required:
40
+ description: >-
41
+ Renders a "*" marker on the label. Does not itself enforce
42
+ validation — the slotted control's own `required` attr does
43
+ that; this is a visual signal only.
44
+ type: boolean
45
+ default: false
46
+ reflect: true
47
+ inline:
48
+ description: >-
49
+ Lay out label, trailing, control, and action on a single row
50
+ instead of the stacked default (mode attribute — changes grid
51
+ geometry, not tokens). Hint/error still render on their own
52
+ row below.
53
+ type: boolean
54
+ default: false
55
+ reflect: true
56
+ slots:
57
+ default:
58
+ description: >-
59
+ The form control — input-ui, select-ui, textarea-ui,
60
+ check-ui, switch-ui, radio-ui, slider-ui, etc. Auto-id'd for
61
+ the label's [for] binding.
62
+ trailing:
63
+ description: >-
64
+ Secondary text or badge aligned with the label in the stacked
65
+ layout (right-aligned) or between label and control in the
66
+ inline layout.
67
+ action:
68
+ description: >-
69
+ Button adjacent to the control for inline actions (clear,
70
+ reset, help popover).
71
+ states:
72
+ - name: idle
73
+ description: Default, ready for interaction.
74
+ traits: []
75
+ tokens:
76
+ --field-gap:
77
+ description: Gap between rows/cells of the field grid.
78
+ --field-label-color:
79
+ description: Label foreground color.
80
+ --field-label-size:
81
+ description: Label font size.
82
+ --field-label-weight:
83
+ description: Label font weight.
84
+ --field-required-color:
85
+ description: Color of the `*` required marker on the label.
86
+ --field-trailing-color:
87
+ description: Trailing text color.
88
+ --field-trailing-size:
89
+ description: Trailing text size.
90
+ --field-hint-color:
91
+ description: Hint text color.
92
+ --field-hint-size:
93
+ description: Hint text size.
94
+ --field-error-color:
95
+ description: Error text color (also drives the required-marker color).
96
+ --field-error-size:
97
+ description: Error text size.
98
+ a2ui:
99
+ rules: []
100
+ anti_patterns: []
101
+ examples:
102
+ - name: stacked-email-field
103
+ description: >-
104
+ A simple stacked email field with a trailing "Required" hint
105
+ and a clear-button action adjacent to the input.
106
+ a2ui: >-
107
+ [
108
+ {
109
+ "id": "root",
110
+ "component": "Field",
111
+ "label": "Email",
112
+ "children": ["email", "hint", "clear"]
113
+ },
114
+ { "id": "email", "component": "Input", "type": "email", "value": "you@example.com" },
115
+ { "id": "hint", "component": "Text", "slot": "trailing", "text": "Required" },
116
+ { "id": "clear", "component": "Button", "slot": "action", "icon": "x", "variant": "ghost" }
117
+ ]
118
+ - name: inline-search-field
119
+ description: >-
120
+ An inline search field — label beside the input, with a
121
+ trailing keyboard-shortcut hint.
122
+ a2ui: >-
123
+ [
124
+ {
125
+ "id": "root",
126
+ "component": "Field",
127
+ "label": "Search",
128
+ "inline": true,
129
+ "children": ["q", "kbd"]
130
+ },
131
+ { "id": "q", "component": "Input", "type": "search", "placeholder": "Type…" },
132
+ { "id": "kbd", "component": "Kbd", "slot": "trailing", "text": "⌘K" }
133
+ ]
134
+ keywords:
135
+ - field
136
+ - form
137
+ - label
138
+ - input
139
+ - wrapper
140
+ synonyms:
141
+ form:
142
+ - field
143
+ - label
144
+ - input
145
+ label:
146
+ - field
147
+ - form
148
+ related:
149
+ - input
150
+ - select
151
+ - textarea
152
+ - check
153
+ - radio
154
+ - switch
155
+ - slider
@@ -43,6 +43,7 @@ export { AdiaAlert } from './alert/alert.js';
43
43
  export { AdiaKbd } from './kbd/kbd.js';
44
44
  export { AdiaTag } from './tag/tag.js';
45
45
  export { AdiaCol } from './col/col.js';
46
+ export { AdiaField } from './field/field.js';
46
47
  export { AdiaRow } from './row/row.js';
47
48
  export { AdiaGrid } from './grid/grid.js';
48
49
  export { AdiaStack } from './stack/stack.js';
@@ -5,7 +5,8 @@
5
5
  --input-fg: var(--a-ui-text);
6
6
  --input-border: var(--a-ui-border);
7
7
  --input-border-hover: var(--a-ui-border-hover);
8
- --input-border-focus: var(--a-ui-border-active);
8
+ --input-focus-ring: var(--a-focus-ring);
9
+ --input-focus-ring-invalid: var(--a-focus-ring-invalid);
9
10
  --input-radius: var(--a-radius);
10
11
  --input-height: var(--a-size);
11
12
  --input-px: var(--a-ui-px);
@@ -79,9 +80,16 @@
79
80
  color: var(--input-affix-fg-hover);
80
81
  }
81
82
  :scope:not([disabled]):focus-within [slot="field"] {
82
- border-color: var(--input-border-focus);
83
+ /* Canonical ring — consumes the L3 --input-focus-ring token
84
+ which aliases --a-focus-ring. Border stays stable; the ring
85
+ is the focus affordance (WCAG 2.2 SC 2.4.11/2.4.13). */
86
+ box-shadow: var(--input-focus-ring);
83
87
  color: var(--input-fg-focus);
84
88
  }
89
+ :scope[aria-invalid="true"]:not([disabled]):focus-within [slot="field"],
90
+ :scope[error]:not([disabled]):focus-within [slot="field"] {
91
+ box-shadow: var(--input-focus-ring-invalid);
92
+ }
85
93
  :scope:not([disabled]):focus-within [slot="label"] {
86
94
  color: var(--input-label-fg-focus);
87
95
  }
@@ -154,7 +162,6 @@
154
162
  --input-bg: transparent;
155
163
  --input-border: transparent;
156
164
  --input-border-hover: transparent;
157
- --input-border-focus: transparent;
158
165
  }
159
166
  :scope[variant="ghost"]:hover {
160
167
  --input-bg: var(--a-bg-muted);
@@ -11,7 +11,9 @@
11
11
  --range-fill-label-fg: var(--a-ui-text-subtle);
12
12
  --range-border: var(--a-ui-border);
13
13
  --range-border-hover: var(--a-ui-border-hover);
14
- --range-border-focus: var(--a-ui-border-active);
14
+ --range-border-dragging: var(--a-ui-border-active);
15
+ --range-focus-ring: var(--a-focus-ring);
16
+ --range-focus-ring-invalid: var(--a-focus-ring-invalid);
15
17
  --range-radius: var(--a-radius);
16
18
  --range-height: var(--a-size);
17
19
  --range-px: var(--a-ui-px);
@@ -64,7 +66,12 @@
64
66
  }
65
67
  :scope:focus-visible { outline: none; }
66
68
  :scope:focus-visible [slot="field"] {
67
- border-color: var(--range-border-focus);
69
+ /* Canonical ring via L3 token (see semantics.css FOCUS block). */
70
+ box-shadow: var(--range-focus-ring);
71
+ }
72
+ :scope[aria-invalid="true"]:focus-visible [slot="field"],
73
+ :scope[error]:focus-visible [slot="field"] {
74
+ box-shadow: var(--range-focus-ring-invalid);
68
75
  }
69
76
 
70
77
  /* ── Dual-layer fill: identical layouts, overlay clipped to fill % ── */
@@ -127,7 +134,7 @@
127
134
 
128
135
  /* Dragging: deepest fill, sharper border, instant (no transition lag on the clip) */
129
136
  :scope[data-dragging] [slot="field"] {
130
- border-color: var(--range-border-focus);
137
+ border-color: var(--range-border-dragging);
131
138
  }
132
139
  :scope[data-dragging] [data-layer="fill"] {
133
140
  background: var(--range-fill-bg-active);
@@ -18,7 +18,8 @@
18
18
  --select-bg-hover: var(--a-ui-bg-hover);
19
19
  --select-border: var(--a-ui-border);
20
20
  --select-border-hover: var(--a-ui-border-hover);
21
- --select-border-focus: var(--a-ui-border-active);
21
+ --select-focus-ring: var(--a-focus-ring);
22
+ --select-focus-ring-invalid: var(--a-focus-ring-invalid);
22
23
  --select-label-fg: var(--a-ui-text-muted);
23
24
  --select-placeholder-fg: var(--a-ui-text-placeholder);
24
25
  --select-caret-fg: var(--a-ui-text-muted);
@@ -97,9 +98,14 @@
97
98
  }
98
99
  :scope:focus-visible { outline: none; }
99
100
  :scope:focus-visible [slot="trigger"] {
100
- border-color: var(--select-border-focus);
101
+ /* Canonical ring via L3 token (see semantics.css FOCUS block). */
102
+ box-shadow: var(--select-focus-ring);
101
103
  color: var(--select-fg);
102
104
  }
105
+ :scope[aria-invalid="true"]:focus-visible [slot="trigger"],
106
+ :scope[error]:focus-visible [slot="trigger"] {
107
+ box-shadow: var(--select-focus-ring-invalid);
108
+ }
103
109
  :scope:focus-visible::before {
104
110
  color: var(--select-fg-subtle);
105
111
  }
@@ -156,7 +162,6 @@
156
162
  --select-bg: transparent;
157
163
  --select-border: transparent;
158
164
  --select-border-hover: transparent;
159
- --select-border-focus: transparent;
160
165
  }
161
166
  :scope[variant="ghost"] [slot="trigger"]:hover {
162
167
  background: var(--select-ghost-bg-hover);
@@ -5,7 +5,8 @@
5
5
  --textarea-fg: var(--a-ui-text);
6
6
  --textarea-border: var(--a-ui-border);
7
7
  --textarea-border-hover: var(--a-ui-border-hover);
8
- --textarea-border-focus: var(--a-ui-border-active);
8
+ --textarea-focus-ring: var(--a-focus-ring);
9
+ --textarea-focus-ring-invalid: var(--a-focus-ring-invalid);
9
10
  --textarea-radius: var(--a-radius);
10
11
  --textarea-min-height: calc(var(--a-size) * 2);
11
12
  --textarea-px: var(--a-ui-px);
@@ -69,9 +70,18 @@
69
70
  color: var(--textarea-fg-hover);
70
71
  }
71
72
  :scope:not([disabled]) [slot="text"]:focus {
72
- border-color: var(--textarea-border-focus);
73
+ /* Canonical ring via L3 token (see semantics.css FOCUS block).
74
+ `:focus` (not :focus-visible) is deliberate — the caret lives
75
+ in the contenteditable [slot="text"] span; :focus-visible on
76
+ the host wouldn't match the actual input surface. */
77
+ outline: none;
78
+ box-shadow: var(--textarea-focus-ring);
73
79
  color: var(--textarea-fg-hover);
74
80
  }
81
+ :scope[aria-invalid="true"]:not([disabled]) [slot="text"]:focus,
82
+ :scope[error]:not([disabled]) [slot="text"]:focus {
83
+ box-shadow: var(--textarea-focus-ring-invalid);
84
+ }
75
85
  :scope:not([disabled]) [slot="text"]:focus + [slot="label"],
76
86
  :scope:not([disabled]):focus-within [slot="label"] {
77
87
  color: var(--textarea-label-fg-focus);
@@ -18,6 +18,8 @@
18
18
  --upload-bg-disabled: var(--a-ui-bg-disabled);
19
19
  --upload-fg-disabled: var(--a-ui-text-disabled);
20
20
  --upload-border-disabled: var(--a-ui-border-disabled);
21
+ --upload-focus-ring: var(--a-focus-ring);
22
+ --upload-focus-ring-invalid: var(--a-focus-ring-invalid);
21
23
 
22
24
  /* ── Spacing ── */
23
25
  --upload-dropzone-pad: var(--a-space-6);
@@ -69,8 +71,9 @@
69
71
  color: var(--upload-fg-hover);
70
72
  }
71
73
  [data-dropzone]:focus-visible {
72
- outline: 2px solid var(--upload-border-hover);
73
- outline-offset: 2px;
74
+ /* Canonical ring via L3 token (see semantics.css FOCUS block). */
75
+ outline: none;
76
+ box-shadow: var(--upload-focus-ring);
74
77
  }
75
78
  [data-dropzone][data-dragover] {
76
79
  border-color: var(--upload-border-dragover);
@@ -0,0 +1,38 @@
1
+ /**
2
+ * CodeMirror 6 core re-exports.
3
+ *
4
+ * Thin shim so the rest of the AdiaUI codebase imports CM symbols from
5
+ * `@core/_cm-core.js` and the production build can split them into a
6
+ * single minified chunk (see scripts/build-site.mjs:bundleCodeEditor).
7
+ */
8
+
9
+ export {
10
+ EditorState,
11
+ EditorSelection,
12
+ Compartment,
13
+ StateEffect,
14
+ } from '@codemirror/state';
15
+
16
+ export {
17
+ EditorView,
18
+ lineNumbers,
19
+ highlightActiveLine,
20
+ highlightActiveLineGutter,
21
+ placeholder,
22
+ drawSelection,
23
+ keymap,
24
+ } from '@codemirror/view';
25
+
26
+ export {
27
+ defaultKeymap,
28
+ history,
29
+ historyKeymap,
30
+ indentWithTab,
31
+ } from '@codemirror/commands';
32
+
33
+ export {
34
+ HighlightStyle,
35
+ syntaxHighlighting,
36
+ LanguageSupport,
37
+ defaultHighlightStyle,
38
+ } from '@codemirror/language';
@@ -0,0 +1,58 @@
1
+ /**
2
+ * AdiaUI base theme + highlight style for CodeMirror 6.
3
+ *
4
+ * The base theme is deliberately near-empty — it only sets the handful
5
+ * of structural rules that MUST live inside `EditorView.theme()`. All
6
+ * colors, spacing, font size, radius, and token-span styling live in
7
+ * `components/code/code.css` inside `@scope (code-ui)` and consume
8
+ * AdiaUI semantic tokens.
9
+ *
10
+ * CodeMirror injects its CSS-in-JS stylesheets at module load; AdiaUI's
11
+ * authored stylesheet loads after that, so `@scope (code-ui) .cm-*`
12
+ * overrides win on natural specificity — no `!important` needed.
13
+ *
14
+ * The highlight style uses `class:` rather than `color:` so CodeMirror
15
+ * emits `<span class="tok-keyword">` instead of inline styles — exactly
16
+ * what our CSS needs. See spec SPEC-CODE-EDITOR-001 §6.3 for the full
17
+ * Lezer tag → class → token mapping.
18
+ */
19
+
20
+ import { EditorView } from '@codemirror/view';
21
+ import { HighlightStyle } from '@codemirror/language';
22
+ import { tags as t } from '@lezer/highlight';
23
+
24
+ export const adiaBaseTheme = EditorView.theme({
25
+ '&': {
26
+ fontFamily: 'inherit',
27
+ fontSize: 'inherit',
28
+ color: 'inherit',
29
+ backgroundColor: 'transparent',
30
+ },
31
+ '.cm-content': {
32
+ padding: '0',
33
+ caretColor: 'inherit',
34
+ },
35
+ '.cm-focused': {
36
+ outline: 'none',
37
+ },
38
+ });
39
+
40
+ export const adiaHighlightStyle = HighlightStyle.define([
41
+ { tag: [t.comment, t.lineComment, t.blockComment, t.docComment], class: 'tok-comment' },
42
+ { tag: [t.keyword, t.controlKeyword, t.modifier, t.operatorKeyword], class: 'tok-keyword' },
43
+ { tag: [t.string, t.character, t.regexp, t.escape, t.special(t.string)], class: 'tok-string' },
44
+ { tag: [t.number, t.integer, t.float], class: 'tok-number' },
45
+ { tag: [t.bool, t.null, t.atom], class: 'tok-boolean' },
46
+ { tag: [t.operator, t.logicOperator, t.arithmeticOperator, t.compareOperator,
47
+ t.updateOperator, t.definitionOperator], class: 'tok-operator' },
48
+ { tag: [t.punctuation, t.bracket, t.paren, t.brace, t.squareBracket,
49
+ t.angleBracket, t.separator], class: 'tok-punctuation' },
50
+ { tag: [t.function(t.variableName), t.function(t.propertyName), t.macroName], class: 'tok-function' },
51
+ { tag: [t.variableName, t.local(t.variableName), t.self], class: 'tok-variable' },
52
+ { tag: [t.typeName, t.className, t.namespace], class: 'tok-type' },
53
+ { tag: [t.propertyName, t.labelName, t.definition(t.variableName)], class: 'tok-property' },
54
+ { tag: [t.tagName, t.heading, t.contentSeparator], class: 'tok-tag' },
55
+ { tag: [t.attributeName, t.attributeValue], class: 'tok-attribute' },
56
+ { tag: [t.url, t.link], class: 'tok-url' },
57
+ { tag: [t.invalid, t.deleted], class: 'tok-invalid' },
58
+ ]);
@@ -0,0 +1,2 @@
1
+ import { css } from '@codemirror/lang-css';
2
+ export const extension = css();
@@ -0,0 +1,2 @@
1
+ import { html } from '@codemirror/lang-html';
2
+ export const extension = html();
@@ -0,0 +1,2 @@
1
+ import { javascript } from '@codemirror/lang-javascript';
2
+ export const extension = javascript({ jsx: false, typescript: false });
@@ -0,0 +1,2 @@
1
+ import { json } from '@codemirror/lang-json';
2
+ export const extension = json();
@@ -0,0 +1,2 @@
1
+ import { markdown } from '@codemirror/lang-markdown';
2
+ export const extension = markdown();
@@ -0,0 +1,2 @@
1
+ import { yaml } from '@codemirror/lang-yaml';
2
+ export const extension = yaml();
@@ -0,0 +1,63 @@
1
+ /**
2
+ * code-editor-bundle — the single entry point `<code-ui>` imports when
3
+ * it needs CodeMirror 6. Re-exports core runtime + AdiaUI theme, and
4
+ * exposes a lazy-load map of language packs (one dynamic import per
5
+ * language, split into its own chunk by esbuild in production).
6
+ *
7
+ * Vite dev: imports resolve against node_modules naturally.
8
+ * Production: scripts/build-site.mjs runs esbuild with
9
+ * splitting: true, producing `code-editor-core.js` + one
10
+ * `code-editor-lang-<lang>.js` per language.
11
+ *
12
+ * Spec: docs/specs/code-editor.md §4 (architecture), §7 (bundling).
13
+ */
14
+
15
+ export {
16
+ EditorState,
17
+ EditorSelection,
18
+ Compartment,
19
+ StateEffect,
20
+ EditorView,
21
+ lineNumbers,
22
+ highlightActiveLine,
23
+ highlightActiveLineGutter,
24
+ placeholder,
25
+ drawSelection,
26
+ keymap,
27
+ defaultKeymap,
28
+ history,
29
+ historyKeymap,
30
+ indentWithTab,
31
+ HighlightStyle,
32
+ syntaxHighlighting,
33
+ LanguageSupport,
34
+ defaultHighlightStyle,
35
+ } from './_cm-core.js';
36
+
37
+ export { adiaBaseTheme, adiaHighlightStyle } from './_cm-theme.js';
38
+
39
+ /** Dynamic-import map — one lazy-loaded chunk per supported language. */
40
+ export const languages = {
41
+ json: () => import('./_lang-json.js'),
42
+ html: () => import('./_lang-html.js'),
43
+ javascript: () => import('./_lang-javascript.js'),
44
+ css: () => import('./_lang-css.js'),
45
+ markdown: () => import('./_lang-markdown.js'),
46
+ yaml: () => import('./_lang-yaml.js'),
47
+ };
48
+
49
+ /** 10-second timeout for any single dynamic import. Matches spec §7.4. */
50
+ export const LOAD_TIMEOUT_MS = 10_000;
51
+
52
+ /**
53
+ * Wrap a dynamic-import promise with a timeout. Rejects on timeout;
54
+ * propagates the original error on fetch failure.
55
+ */
56
+ export function importWithTimeout(loader, label) {
57
+ return Promise.race([
58
+ loader(),
59
+ new Promise((_, reject) =>
60
+ setTimeout(() => reject(new Error(`code-editor: ${label} load timed out after ${LOAD_TIMEOUT_MS}ms`)), LOAD_TIMEOUT_MS),
61
+ ),
62
+ ]);
63
+ }