@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,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
|
package/components/index.js
CHANGED
|
@@ -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-
|
|
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
|
-
|
|
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-
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
73
|
-
outline
|
|
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);
|
package/core/_cm-core.js
ADDED
|
@@ -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,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
|
+
}
|