@adia-ai/web-components 0.0.12 → 0.0.14
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/README.md +10 -10
- package/components/card/card.css +29 -0
- package/components/chart/chart.a2ui.json +43 -6
- package/components/chart/chart.css +224 -0
- package/components/chart/chart.js +1049 -27
- package/components/chart/chart.yaml +62 -6
- package/components/chart-legend/chart-legend.a2ui.json +139 -0
- package/components/chart-legend/chart-legend.css +124 -0
- package/components/chart-legend/chart-legend.js +185 -0
- package/components/chart-legend/chart-legend.yaml +133 -0
- package/components/code/code-editor.js +161 -0
- package/components/code/code.a2ui.json +59 -0
- package/components/code/code.css +78 -2
- package/components/code/code.js +147 -9
- package/components/code/code.yaml +42 -0
- package/components/heatmap/heatmap.js +62 -13
- package/components/index.js +1 -0
- package/components/select/select.css +1 -1
- package/components/slider/slider.js +8 -3
- package/components/stat/stat.a2ui.json +3 -0
- package/components/stat/stat.css +32 -0
- package/components/stat/stat.yaml +6 -0
- package/components/tooltip/tooltip.a2ui.json +29 -4
- package/components/tooltip/tooltip.css +111 -0
- package/components/tooltip/tooltip.js +200 -12
- package/components/tooltip/tooltip.yaml +38 -4
- package/core/icons.js +35 -1
- package/core/index.js +25 -0
- package/core/provider.js +1 -1
- package/index.css +26 -0
- package/index.js +18 -0
- package/package.json +14 -6
- package/patterns/adia-chat/adia-chat.js +1 -1
- package/styles/colors/semantics.css +6 -5
- package/styles/{styles.css → components.css} +9 -111
- package/styles/resets.css +116 -0
- package/styles/tokens.css +8 -2
- package/core/_cm-core.js +0 -38
- package/core/_cm-theme.js +0 -58
- package/core/_lang-css.js +0 -2
- package/core/_lang-html.js +0 -2
- package/core/_lang-javascript.js +0 -2
- package/core/_lang-json.js +0 -2
- package/core/_lang-markdown.js +0 -2
- package/core/_lang-yaml.js +0 -2
- package/core/code-editor-bundle.js +0 -63
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* code-editor — CodeMirror 6 runtime bundled for <code-ui>.
|
|
3
|
+
*
|
|
4
|
+
* Single file for the whole integration: CM core re-exports, the AdiaUI
|
|
5
|
+
* base theme + highlight style, the per-language lazy-load map, and the
|
|
6
|
+
* load-timeout helper. `code.js` does `import('./code-editor.js')` once
|
|
7
|
+
* per element mount.
|
|
8
|
+
*
|
|
9
|
+
* Styling contract: `EditorView.theme()` can't emit CSS custom properties
|
|
10
|
+
* (per CodeMirror docs), so the base theme below is structural-only.
|
|
11
|
+
* Every color, padding, border, and syntax-token style lives in
|
|
12
|
+
* `code.css` under `@scope (code-ui)`. See SPEC-CODE-EDITOR-001 §6.
|
|
13
|
+
*
|
|
14
|
+
* In Vite dev, the bare-specifier imports resolve through node_modules.
|
|
15
|
+
* In production, `scripts/build-site.mjs` runs esbuild with
|
|
16
|
+
* `splitting: true` on this file; each `languages[…]()` dynamic import
|
|
17
|
+
* becomes its own chunk, lazy-loaded on first use.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
EditorState, EditorSelection, Compartment, StateEffect,
|
|
22
|
+
} from '@codemirror/state';
|
|
23
|
+
import {
|
|
24
|
+
EditorView,
|
|
25
|
+
lineNumbers, highlightActiveLine, highlightActiveLineGutter,
|
|
26
|
+
placeholder, drawSelection, keymap,
|
|
27
|
+
} from '@codemirror/view';
|
|
28
|
+
import {
|
|
29
|
+
defaultKeymap, history, historyKeymap, indentWithTab,
|
|
30
|
+
} from '@codemirror/commands';
|
|
31
|
+
import {
|
|
32
|
+
HighlightStyle, syntaxHighlighting, LanguageSupport, defaultHighlightStyle,
|
|
33
|
+
} from '@codemirror/language';
|
|
34
|
+
import { linter, lintGutter } from '@codemirror/lint';
|
|
35
|
+
import { tags as t } from '@lezer/highlight';
|
|
36
|
+
|
|
37
|
+
// ── Base theme (structural only — colors live in code.css) ───────────
|
|
38
|
+
|
|
39
|
+
export const adiaBaseTheme = EditorView.theme({
|
|
40
|
+
'&': { fontFamily: 'inherit', fontSize: 'inherit', color: 'inherit', backgroundColor: 'transparent' },
|
|
41
|
+
'.cm-content': { padding: '0', caretColor: 'inherit' },
|
|
42
|
+
'.cm-focused': { outline: 'none' },
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// ── Syntax highlight (class-based so CSS owns the colors) ────────────
|
|
46
|
+
|
|
47
|
+
export const adiaHighlightStyle = HighlightStyle.define([
|
|
48
|
+
{ tag: [t.comment, t.lineComment, t.blockComment, t.docComment], class: 'tok-comment' },
|
|
49
|
+
{ tag: [t.keyword, t.controlKeyword, t.modifier, t.operatorKeyword], class: 'tok-keyword' },
|
|
50
|
+
{ tag: [t.string, t.character, t.regexp, t.escape, t.special(t.string)], class: 'tok-string' },
|
|
51
|
+
{ tag: [t.number, t.integer, t.float], class: 'tok-number' },
|
|
52
|
+
{ tag: [t.bool, t.null, t.atom], class: 'tok-boolean' },
|
|
53
|
+
{ tag: [t.operator, t.logicOperator, t.arithmeticOperator, t.compareOperator,
|
|
54
|
+
t.updateOperator, t.definitionOperator], class: 'tok-operator' },
|
|
55
|
+
{ tag: [t.punctuation, t.bracket, t.paren, t.brace, t.squareBracket,
|
|
56
|
+
t.angleBracket, t.separator], class: 'tok-punctuation' },
|
|
57
|
+
{ tag: [t.function(t.variableName), t.function(t.propertyName), t.macroName], class: 'tok-function' },
|
|
58
|
+
{ tag: [t.variableName, t.local(t.variableName), t.self], class: 'tok-variable' },
|
|
59
|
+
{ tag: [t.typeName, t.className, t.namespace], class: 'tok-type' },
|
|
60
|
+
{ tag: [t.propertyName, t.labelName, t.definition(t.variableName)], class: 'tok-property' },
|
|
61
|
+
{ tag: [t.tagName, t.heading, t.contentSeparator], class: 'tok-tag' },
|
|
62
|
+
{ tag: [t.attributeName, t.attributeValue], class: 'tok-attribute' },
|
|
63
|
+
{ tag: [t.url, t.link], class: 'tok-url' },
|
|
64
|
+
{ tag: [t.invalid, t.deleted], class: 'tok-invalid' },
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
// ── Per-language lazy loaders ────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
export const languages = {
|
|
70
|
+
json: () => import('@codemirror/lang-json').then((m) => ({ extension: m.json() })),
|
|
71
|
+
html: () => import('@codemirror/lang-html').then((m) => ({ extension: m.html() })),
|
|
72
|
+
javascript: () => import('@codemirror/lang-javascript').then((m) => ({ extension: m.javascript({ jsx: false, typescript: false }) })),
|
|
73
|
+
css: () => import('@codemirror/lang-css').then((m) => ({ extension: m.css() })),
|
|
74
|
+
markdown: () => import('@codemirror/lang-markdown').then((m) => ({ extension: m.markdown() })),
|
|
75
|
+
yaml: () => import('@codemirror/lang-yaml').then((m) => ({ extension: m.yaml() })),
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// ── Load helper ──────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
/** 10s ceiling on any single dynamic import. Per SPEC-CODE-EDITOR-001 §7.4. */
|
|
81
|
+
export const LOAD_TIMEOUT_MS = 10_000;
|
|
82
|
+
|
|
83
|
+
export function importWithTimeout(loader, label) {
|
|
84
|
+
return Promise.race([
|
|
85
|
+
loader(),
|
|
86
|
+
new Promise((_, reject) =>
|
|
87
|
+
setTimeout(
|
|
88
|
+
() => reject(new Error(`code-editor: ${label} load timed out after ${LOAD_TIMEOUT_MS}ms`)),
|
|
89
|
+
LOAD_TIMEOUT_MS,
|
|
90
|
+
),
|
|
91
|
+
),
|
|
92
|
+
]);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ── Language-specific linters ────────────────────────────────────────
|
|
96
|
+
//
|
|
97
|
+
// Kept here (not in code.js) so code.js doesn't depend on the lint
|
|
98
|
+
// primitives directly. `code.js` picks the right linter by language
|
|
99
|
+
// name when editable mode is on. Phase 3 of SPEC-CODE-EDITOR-001 —
|
|
100
|
+
// JSON only for now; extend per-language as need arises.
|
|
101
|
+
|
|
102
|
+
/** JSON linter. Parses the full document; emits one error Diagnostic
|
|
103
|
+
* at the reported position on `SyntaxError`, else clean. The error
|
|
104
|
+
* position is parsed from the engine's message (V8 / Node ≥20 /
|
|
105
|
+
* Firefox / JavaScriptCore all shape these slightly differently) and
|
|
106
|
+
* falls back to the last character so a marker is always visible. */
|
|
107
|
+
export const jsonLinter = linter((view) => {
|
|
108
|
+
const text = view.state.doc.toString();
|
|
109
|
+
if (text.trim() === '') return [];
|
|
110
|
+
try {
|
|
111
|
+
JSON.parse(text);
|
|
112
|
+
return [];
|
|
113
|
+
} catch (err) {
|
|
114
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
115
|
+
const { from, to } = extractJsonErrorPos(msg, text);
|
|
116
|
+
return [{
|
|
117
|
+
from, to,
|
|
118
|
+
severity: 'error',
|
|
119
|
+
message: msg.replace(/^SyntaxError:\s*/, ''),
|
|
120
|
+
source: 'json',
|
|
121
|
+
}];
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
function extractJsonErrorPos(msg, text) {
|
|
126
|
+
const docLen = text.length;
|
|
127
|
+
// "at position N" — V8 / Chromium / pre-Node-20
|
|
128
|
+
const posMatch = msg.match(/at position (\d+)/);
|
|
129
|
+
if (posMatch) {
|
|
130
|
+
const from = Math.min(docLen, Number(posMatch[1]));
|
|
131
|
+
return { from, to: Math.min(docLen, from + 1) };
|
|
132
|
+
}
|
|
133
|
+
// "line N column M" — Firefox / Node ≥20
|
|
134
|
+
const lineColMatch = msg.match(/line (\d+) column (\d+)/);
|
|
135
|
+
if (lineColMatch) {
|
|
136
|
+
const line = Number(lineColMatch[1]);
|
|
137
|
+
const col = Number(lineColMatch[2]);
|
|
138
|
+
let lineStart = 0;
|
|
139
|
+
let currentLine = 1;
|
|
140
|
+
for (let i = 0; i < text.length && currentLine < line; i++) {
|
|
141
|
+
if (text[i] === '\n') { currentLine++; lineStart = i + 1; }
|
|
142
|
+
}
|
|
143
|
+
const from = Math.min(docLen, lineStart + Math.max(0, col - 1));
|
|
144
|
+
return { from, to: Math.min(docLen, from + 1) };
|
|
145
|
+
}
|
|
146
|
+
// Fallback: point at the last character so the marker is visible.
|
|
147
|
+
const end = Math.max(0, docLen);
|
|
148
|
+
return { from: Math.max(0, end - 1), to: end };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ── Re-exports (so code.js gets everything from one import) ─────────
|
|
152
|
+
|
|
153
|
+
export {
|
|
154
|
+
EditorState, EditorSelection, Compartment, StateEffect,
|
|
155
|
+
EditorView,
|
|
156
|
+
lineNumbers, highlightActiveLine, highlightActiveLineGutter,
|
|
157
|
+
placeholder, drawSelection, keymap,
|
|
158
|
+
defaultKeymap, history, historyKeymap, indentWithTab,
|
|
159
|
+
HighlightStyle, syntaxHighlighting, LanguageSupport, defaultHighlightStyle,
|
|
160
|
+
linter, lintGutter,
|
|
161
|
+
};
|
|
@@ -13,9 +13,29 @@
|
|
|
13
13
|
}
|
|
14
14
|
],
|
|
15
15
|
"properties": {
|
|
16
|
+
"required": {
|
|
17
|
+
"description": "Form constraint — submission blocks if the editor is empty",
|
|
18
|
+
"type": "boolean",
|
|
19
|
+
"default": false
|
|
20
|
+
},
|
|
21
|
+
"bare": {
|
|
22
|
+
"description": "Omit the header chrome (label + copy button); for embedded editor surfaces",
|
|
23
|
+
"type": "boolean",
|
|
24
|
+
"default": false
|
|
25
|
+
},
|
|
16
26
|
"component": {
|
|
17
27
|
"const": "Code"
|
|
18
28
|
},
|
|
29
|
+
"disabled": {
|
|
30
|
+
"description": "Disables input and excludes from form submission",
|
|
31
|
+
"type": "boolean",
|
|
32
|
+
"default": false
|
|
33
|
+
},
|
|
34
|
+
"editable": {
|
|
35
|
+
"description": "Editable CodeMirror instance (vs read-only display)",
|
|
36
|
+
"type": "boolean",
|
|
37
|
+
"default": false
|
|
38
|
+
},
|
|
19
39
|
"inline": {
|
|
20
40
|
"description": "Inline code (vs block)",
|
|
21
41
|
"type": "boolean",
|
|
@@ -31,6 +51,21 @@
|
|
|
31
51
|
"type": "boolean",
|
|
32
52
|
"default": false
|
|
33
53
|
},
|
|
54
|
+
"name": {
|
|
55
|
+
"description": "Form-control name — submitted with the enclosing form when `editable` is set",
|
|
56
|
+
"type": "string",
|
|
57
|
+
"default": ""
|
|
58
|
+
},
|
|
59
|
+
"placeholder": {
|
|
60
|
+
"description": "Placeholder text shown when the editor is empty (editable mode only)",
|
|
61
|
+
"type": "string",
|
|
62
|
+
"default": ""
|
|
63
|
+
},
|
|
64
|
+
"readonly": {
|
|
65
|
+
"description": "Blocks input but still submits with the form",
|
|
66
|
+
"type": "boolean",
|
|
67
|
+
"default": false
|
|
68
|
+
},
|
|
34
69
|
"text": {
|
|
35
70
|
"description": "Code text content",
|
|
36
71
|
"type": "string",
|
|
@@ -45,8 +80,20 @@
|
|
|
45
80
|
"anti_patterns": [],
|
|
46
81
|
"category": "display",
|
|
47
82
|
"events": {
|
|
83
|
+
"change": {
|
|
84
|
+
"description": "Fired on blur if the buffer changed since focus (editable mode); detail.value is the final buffer"
|
|
85
|
+
},
|
|
48
86
|
"copy": {
|
|
49
87
|
"description": "Fired when text is copied to clipboard"
|
|
88
|
+
},
|
|
89
|
+
"input": {
|
|
90
|
+
"description": "Fired on every doc change (editable mode); detail.value is the current buffer"
|
|
91
|
+
},
|
|
92
|
+
"language-load-error": {
|
|
93
|
+
"description": "Fired when the language pack or CodeMirror bundle fails to load; detail.phase is 'core' or 'language'"
|
|
94
|
+
},
|
|
95
|
+
"save": {
|
|
96
|
+
"description": "Fired on Mod+S keybind (editable mode); detail.value is the current buffer. Cancelable."
|
|
50
97
|
}
|
|
51
98
|
},
|
|
52
99
|
"examples": [
|
|
@@ -82,6 +129,18 @@
|
|
|
82
129
|
{
|
|
83
130
|
"description": "Default, ready for interaction.",
|
|
84
131
|
"name": "idle"
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
"description": "Editor is disabled — blocks input and excludes from form submission.",
|
|
135
|
+
"name": "disabled"
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
"description": "Editor blocks input but still submits with the form.",
|
|
139
|
+
"name": "readonly"
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
"description": "Editor has a validation error (e.g. `required` and empty). Reflects as `aria-invalid=\"true\"`.",
|
|
143
|
+
"name": "invalid"
|
|
85
144
|
}
|
|
86
145
|
],
|
|
87
146
|
"synonyms": {
|
package/components/code/code.css
CHANGED
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
--code-copy-px: var(--a-space-1);
|
|
9
9
|
|
|
10
10
|
/* ── Colors ── */
|
|
11
|
-
--code-bg: var(--a-bg
|
|
11
|
+
--code-bg: var(--a-bg);
|
|
12
12
|
--code-fg: var(--a-fg);
|
|
13
13
|
--code-border: var(--a-border-subtle);
|
|
14
|
-
--code-header-bg: var(--a-bg-
|
|
14
|
+
--code-header-bg: var(--a-bg-scrim);
|
|
15
15
|
--code-header-fg: var(--a-fg-muted);
|
|
16
16
|
--code-copy-hover-bg: var(--a-bg-muted);
|
|
17
17
|
--code-copy-hover-fg: var(--a-fg);
|
|
@@ -57,6 +57,18 @@
|
|
|
57
57
|
--code-tok-attribute: var(--a-warning-strong);
|
|
58
58
|
--code-tok-url: var(--a-info-strong);
|
|
59
59
|
--code-tok-invalid: var(--a-danger-strong);
|
|
60
|
+
|
|
61
|
+
/* ── Lint markers (Phase 3 — editable JSON) ──
|
|
62
|
+
`@codemirror/lint` paints gutter markers + diagnostic tooltips
|
|
63
|
+
+ inline underlines. Only fires in editable mode with a linter
|
|
64
|
+
attached; read-only instances never see these selectors. */
|
|
65
|
+
--code-lint-gutter-bg: var(--a-bg-subtle);
|
|
66
|
+
--code-lint-error: var(--a-danger-strong);
|
|
67
|
+
--code-lint-warning: var(--a-warning-strong);
|
|
68
|
+
--code-lint-info: var(--a-info-strong);
|
|
69
|
+
--code-lint-tooltip-bg: var(--a-surface-floating);
|
|
70
|
+
--code-lint-tooltip-fg: var(--a-fg);
|
|
71
|
+
--code-lint-tooltip-border: var(--a-border-subtle);
|
|
60
72
|
}
|
|
61
73
|
|
|
62
74
|
/* ── Block (default) ── */
|
|
@@ -196,6 +208,30 @@
|
|
|
196
208
|
background: transparent;
|
|
197
209
|
}
|
|
198
210
|
|
|
211
|
+
/* ── Form states (editable-only surfaces) ──
|
|
212
|
+
`[disabled]` — visually muted, pointer-events disabled so the user
|
|
213
|
+
can't click into the editor. CM's readonly compartment handles the
|
|
214
|
+
actual edit-blocking; these rules are the visual cue.
|
|
215
|
+
`[readonly]` — muted but clickable (user can still select + copy).
|
|
216
|
+
`[aria-invalid="true"]` — error ring for form validation failures. */
|
|
217
|
+
:scope[disabled] {
|
|
218
|
+
opacity: 0.6;
|
|
219
|
+
cursor: not-allowed;
|
|
220
|
+
}
|
|
221
|
+
:scope[disabled] .cm-editor {
|
|
222
|
+
pointer-events: none;
|
|
223
|
+
}
|
|
224
|
+
:scope[readonly] .cm-editor {
|
|
225
|
+
background: var(--a-bg-subtle);
|
|
226
|
+
}
|
|
227
|
+
:scope[aria-invalid="true"] {
|
|
228
|
+
border-color: var(--a-danger-strong);
|
|
229
|
+
}
|
|
230
|
+
:scope[aria-invalid="true"] .cm-focused {
|
|
231
|
+
outline: 2px solid var(--a-danger-strong);
|
|
232
|
+
outline-offset: -1px;
|
|
233
|
+
}
|
|
234
|
+
|
|
199
235
|
/* ── CodeMirror mount ──
|
|
200
236
|
`<code-ui>` inserts `<div data-cm-mount>` in place of `<pre><code>`
|
|
201
237
|
when `[language]` triggers the CM lazy-load. Styles below apply to
|
|
@@ -278,4 +314,44 @@
|
|
|
278
314
|
:scope .tok-attribute { color: var(--code-tok-attribute); }
|
|
279
315
|
:scope .tok-url { color: var(--code-tok-url); text-decoration: underline; }
|
|
280
316
|
:scope .tok-invalid { color: var(--code-tok-invalid); text-decoration: wavy underline; }
|
|
317
|
+
|
|
318
|
+
/* ── Lint gutter + diagnostics ──
|
|
319
|
+
Styles the `@codemirror/lint` markers in the gutter, the inline
|
|
320
|
+
wavy underline on the diagnostic range, and the tooltip panel. */
|
|
321
|
+
:scope .cm-gutter-lint {
|
|
322
|
+
background: var(--code-lint-gutter-bg);
|
|
323
|
+
width: 1em;
|
|
324
|
+
}
|
|
325
|
+
:scope .cm-lint-marker {
|
|
326
|
+
width: 0.7em;
|
|
327
|
+
height: 0.7em;
|
|
328
|
+
margin: 0.15em 0.15em;
|
|
329
|
+
}
|
|
330
|
+
:scope .cm-lint-marker-error { color: var(--code-lint-error); }
|
|
331
|
+
:scope .cm-lint-marker-warning { color: var(--code-lint-warning); }
|
|
332
|
+
:scope .cm-lint-marker-info { color: var(--code-lint-info); }
|
|
333
|
+
|
|
334
|
+
:scope .cm-lintRange-error { background: none; text-decoration: wavy underline var(--code-lint-error); }
|
|
335
|
+
:scope .cm-lintRange-warning { background: none; text-decoration: wavy underline var(--code-lint-warning); }
|
|
336
|
+
:scope .cm-lintRange-info { background: none; text-decoration: wavy underline var(--code-lint-info); }
|
|
337
|
+
|
|
338
|
+
:scope .cm-tooltip.cm-tooltip-lint {
|
|
339
|
+
background: var(--code-lint-tooltip-bg);
|
|
340
|
+
color: var(--code-lint-tooltip-fg);
|
|
341
|
+
border: 1px solid var(--code-lint-tooltip-border);
|
|
342
|
+
border-radius: var(--code-radius-sm);
|
|
343
|
+
padding: var(--a-space-1) var(--a-space-2);
|
|
344
|
+
font-size: var(--a-ui-sm);
|
|
345
|
+
font-family: var(--a-font-family);
|
|
346
|
+
max-inline-size: 40ch;
|
|
347
|
+
}
|
|
348
|
+
:scope .cm-diagnostic {
|
|
349
|
+
padding: 0;
|
|
350
|
+
border-inline-start: 3px solid transparent;
|
|
351
|
+
padding-inline-start: var(--a-space-2);
|
|
352
|
+
}
|
|
353
|
+
:scope .cm-diagnostic-error { border-inline-start-color: var(--code-lint-error); }
|
|
354
|
+
:scope .cm-diagnostic-warning { border-inline-start-color: var(--code-lint-warning); }
|
|
355
|
+
:scope .cm-diagnostic-info { border-inline-start-color: var(--code-lint-info); }
|
|
356
|
+
:scope .cm-diagnosticText { white-space: pre-wrap; }
|
|
281
357
|
}
|
package/components/code/code.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* <code-ui language="js">const x = 42;</code-ui>
|
|
3
3
|
* <code-ui language="css" inline>color: red</code-ui>
|
|
4
4
|
* <code-ui language="json" text='{ "hello": "world" }'></code-ui>
|
|
5
|
+
* <code-ui editable language="json" name="schema" required></code-ui>
|
|
5
6
|
*
|
|
6
7
|
* Inline or block code display with optional language label and copy
|
|
7
8
|
* button. The body comes from (a) declarative children, or (b) the
|
|
@@ -10,8 +11,16 @@
|
|
|
10
11
|
*
|
|
11
12
|
* When `[language]` names a supported language (see SUPPORTED_LANGUAGES),
|
|
12
13
|
* CodeMirror 6 is lazy-loaded and mounted in place of `<pre><code>` for
|
|
13
|
-
* syntax-highlighted
|
|
14
|
-
* `[editable]
|
|
14
|
+
* syntax-highlighted rendering. Phase 1 shipped read-only; Phase 2 added
|
|
15
|
+
* `[editable]`; Phase 3 added lint + form-association (see
|
|
16
|
+
* `docs/specs/code-editor.md §12`).
|
|
17
|
+
*
|
|
18
|
+
* Form participation — editable instances are Form-Associated Custom
|
|
19
|
+
* Elements (FACE). Set `name`, `required`, `disabled`, `readonly` to
|
|
20
|
+
* opt into standard form behavior; the editor's live document
|
|
21
|
+
* becomes the form value. Non-editable instances declare
|
|
22
|
+
* `formAssociated = true` at the class level (required for FACE) but
|
|
23
|
+
* contribute nothing to form submission when `[editable]` is absent.
|
|
15
24
|
*/
|
|
16
25
|
|
|
17
26
|
import { AdiaElement } from '../../core/element.js';
|
|
@@ -34,6 +43,8 @@ function canonicalLanguage(name) {
|
|
|
34
43
|
}
|
|
35
44
|
|
|
36
45
|
class AdiaCode extends AdiaElement {
|
|
46
|
+
static formAssociated = true;
|
|
47
|
+
|
|
37
48
|
static properties = {
|
|
38
49
|
language: { type: String, default: '', reflect: true },
|
|
39
50
|
inline: { type: Boolean, default: false, reflect: true },
|
|
@@ -42,6 +53,14 @@ class AdiaCode extends AdiaElement {
|
|
|
42
53
|
editable: { type: Boolean, default: false, reflect: true },
|
|
43
54
|
bare: { type: Boolean, default: false, reflect: true },
|
|
44
55
|
placeholder: { type: String, default: '', reflect: true },
|
|
56
|
+
|
|
57
|
+
// Form-association (active only when `editable` is set; non-editable
|
|
58
|
+
// instances are FACE-declared for class consistency but contribute
|
|
59
|
+
// nothing to form submission).
|
|
60
|
+
name: { type: String, default: '', reflect: true },
|
|
61
|
+
required: { type: Boolean, default: false, reflect: true },
|
|
62
|
+
disabled: { type: Boolean, default: false, reflect: true },
|
|
63
|
+
readonly: { type: Boolean, default: false, reflect: true },
|
|
45
64
|
};
|
|
46
65
|
|
|
47
66
|
static template = () => null;
|
|
@@ -52,6 +71,8 @@ class AdiaCode extends AdiaElement {
|
|
|
52
71
|
#mountGen = 0; // bumps on every connect/disconnect — guards against stale mounts
|
|
53
72
|
#pendingMount = false; // true while bundle/lang pack is loading
|
|
54
73
|
#focusSnapshot = null; // doc text captured on focus; used to diff for `change` event
|
|
74
|
+
#initialDoc = ''; // captured at mount; used by formResetCallback
|
|
75
|
+
#editableCompartment = null; // CM Compartment — reconfigures readonly on disabled/readonly change
|
|
55
76
|
|
|
56
77
|
#onCopyClick = () => this.#copy();
|
|
57
78
|
|
|
@@ -137,7 +158,7 @@ class AdiaCode extends AdiaElement {
|
|
|
137
158
|
this.#pendingMount = true;
|
|
138
159
|
|
|
139
160
|
try {
|
|
140
|
-
const bundle = await import('
|
|
161
|
+
const bundle = await import('./code-editor.js');
|
|
141
162
|
|
|
142
163
|
// Load the language pack with a timeout. If it fails, continue with
|
|
143
164
|
// core-only (editor still renders; no syntax highlight). Emit an
|
|
@@ -156,7 +177,7 @@ class AdiaCode extends AdiaElement {
|
|
|
156
177
|
// we were loading. Discard.
|
|
157
178
|
if (gen !== this.#mountGen || !this.isConnected) return;
|
|
158
179
|
|
|
159
|
-
this.#attachEditor(bundle, extension);
|
|
180
|
+
this.#attachEditor(bundle, extension, lang);
|
|
160
181
|
} catch (err) {
|
|
161
182
|
this.dispatchEvent(new CustomEvent('language-load-error', {
|
|
162
183
|
bubbles: true, detail: { phase: 'core', error: err },
|
|
@@ -168,21 +189,24 @@ class AdiaCode extends AdiaElement {
|
|
|
168
189
|
}
|
|
169
190
|
}
|
|
170
191
|
|
|
171
|
-
#attachEditor(bundle, languageExtension) {
|
|
192
|
+
#attachEditor(bundle, languageExtension, lang) {
|
|
172
193
|
const {
|
|
173
194
|
EditorState, EditorView,
|
|
195
|
+
Compartment,
|
|
174
196
|
lineNumbers,
|
|
175
197
|
placeholder,
|
|
176
198
|
syntaxHighlighting,
|
|
177
199
|
history, historyKeymap, defaultKeymap, indentWithTab,
|
|
178
200
|
keymap,
|
|
179
201
|
adiaBaseTheme, adiaHighlightStyle,
|
|
202
|
+
lintGutter, jsonLinter,
|
|
180
203
|
} = bundle;
|
|
181
204
|
|
|
182
205
|
// Replace <pre><code> with a mount target. Keep <header> where it is.
|
|
183
206
|
const pre = this.querySelector(':scope > pre');
|
|
184
207
|
const prior = pre ? pre.querySelector('code')?.textContent ?? '' : '';
|
|
185
208
|
const initial = this.text || prior || '';
|
|
209
|
+
this.#initialDoc = initial;
|
|
186
210
|
|
|
187
211
|
const mount = document.createElement('div');
|
|
188
212
|
mount.setAttribute('data-cm-mount', '');
|
|
@@ -205,9 +229,11 @@ class AdiaCode extends AdiaElement {
|
|
|
205
229
|
]),
|
|
206
230
|
EditorView.updateListener.of((update) => {
|
|
207
231
|
if (!update.docChanged) return;
|
|
232
|
+
const value = update.state.doc.toString();
|
|
233
|
+
this.#syncFormValue(value);
|
|
208
234
|
this.dispatchEvent(new CustomEvent('input', {
|
|
209
235
|
bubbles: true,
|
|
210
|
-
detail: { value
|
|
236
|
+
detail: { value },
|
|
211
237
|
}));
|
|
212
238
|
}),
|
|
213
239
|
EditorView.domEventHandlers({
|
|
@@ -228,19 +254,126 @@ class AdiaCode extends AdiaElement {
|
|
|
228
254
|
},
|
|
229
255
|
}),
|
|
230
256
|
);
|
|
231
|
-
} else {
|
|
232
|
-
extensions.push(EditorView.editable.of(false));
|
|
233
|
-
extensions.push(EditorState.readOnly.of(true));
|
|
234
257
|
}
|
|
235
258
|
|
|
259
|
+
// Editable/readonly compartment — respects editable + disabled + readonly.
|
|
260
|
+
// Dynamic because `disabled` / `readonly` can change at runtime (e.g.
|
|
261
|
+
// `formDisabledCallback` from the owning form). Starts with the
|
|
262
|
+
// correct initial config; `#updateEditableState()` reconfigures on
|
|
263
|
+
// attribute change.
|
|
264
|
+
this.#editableCompartment = new Compartment();
|
|
265
|
+
extensions.push(
|
|
266
|
+
this.#editableCompartment.of(this.#editableExtensions(EditorView, EditorState)),
|
|
267
|
+
);
|
|
268
|
+
|
|
236
269
|
if (languageExtension) extensions.push(languageExtension);
|
|
237
270
|
if (this.lineNumbers) extensions.push(lineNumbers());
|
|
238
271
|
if (this.placeholder) extensions.push(placeholder(this.placeholder));
|
|
239
272
|
|
|
273
|
+
// Lint: editable-only, language-specific. JSON for now; extend per
|
|
274
|
+
// language as need arises. The gutter shows the aggregate marker
|
|
275
|
+
// per line; the diagnostics themselves produce the tooltip + error
|
|
276
|
+
// underline on the offending range.
|
|
277
|
+
if (this.editable && lang === 'json') {
|
|
278
|
+
extensions.push(jsonLinter, lintGutter());
|
|
279
|
+
}
|
|
280
|
+
|
|
240
281
|
this.#cmView = new EditorView({
|
|
241
282
|
parent: mount,
|
|
242
283
|
state: EditorState.create({ doc: initial, extensions }),
|
|
243
284
|
});
|
|
285
|
+
|
|
286
|
+
// Seed form value + constraints now that the editor is live.
|
|
287
|
+
if (this.editable) this.#syncFormValue(initial);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** Returns the CM extensions that enforce editable vs readonly state
|
|
291
|
+
* as a function of (editable && !disabled && !readonly). Called at
|
|
292
|
+
* mount time and on any runtime change via the compartment. */
|
|
293
|
+
#editableExtensions(EditorView, EditorState) {
|
|
294
|
+
const canEdit = this.editable && !this.disabled && !this.readonly;
|
|
295
|
+
if (canEdit) return [];
|
|
296
|
+
return [
|
|
297
|
+
EditorView.editable.of(false),
|
|
298
|
+
EditorState.readOnly.of(true),
|
|
299
|
+
];
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** Re-evaluate the editable/readonly state against current attributes.
|
|
303
|
+
* Called from `render()` on property change. No-op if the editor
|
|
304
|
+
* hasn't mounted yet. */
|
|
305
|
+
async #updateEditableState() {
|
|
306
|
+
if (!this.#cmView || !this.#editableCompartment) return;
|
|
307
|
+
const bundle = await import('./code-editor.js');
|
|
308
|
+
this.#cmView.dispatch({
|
|
309
|
+
effects: this.#editableCompartment.reconfigure(
|
|
310
|
+
this.#editableExtensions(bundle.EditorView, bundle.EditorState),
|
|
311
|
+
),
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// ── Form-association (FACE) ───────────────────────────────────────
|
|
316
|
+
|
|
317
|
+
get form() { return this.internals.form; }
|
|
318
|
+
get labels() { return this.internals.labels; }
|
|
319
|
+
get validity() { return this.internals.validity; }
|
|
320
|
+
get validationMessage() { return this.internals.validationMessage; }
|
|
321
|
+
get willValidate() { return this.internals.willValidate; }
|
|
322
|
+
|
|
323
|
+
checkValidity() { return this.internals.checkValidity(); }
|
|
324
|
+
reportValidity() { return this.internals.reportValidity(); }
|
|
325
|
+
|
|
326
|
+
/** Push the current editor value to the form + re-run constraints.
|
|
327
|
+
* No-op when `editable` is false. Called on every CM doc change and
|
|
328
|
+
* from the initial mount + `render()` when `text` is driven externally. */
|
|
329
|
+
#syncFormValue(val) {
|
|
330
|
+
if (!this.editable) return;
|
|
331
|
+
val = val ?? this.value ?? '';
|
|
332
|
+
this.internals.setFormValue(val);
|
|
333
|
+
this.#runConstraints(val);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
#runConstraints(val) {
|
|
337
|
+
val = val ?? '';
|
|
338
|
+
if (this.required && !val.trim()) {
|
|
339
|
+
this.internals.setValidity(
|
|
340
|
+
{ valueMissing: true },
|
|
341
|
+
this.getAttribute('data-msg-required') || 'This field is required.',
|
|
342
|
+
this,
|
|
343
|
+
);
|
|
344
|
+
this.setAttribute('aria-invalid', 'true');
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
this.internals.setValidity({});
|
|
348
|
+
this.removeAttribute('aria-invalid');
|
|
349
|
+
return true;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
formResetCallback() {
|
|
353
|
+
if (this.#cmView) {
|
|
354
|
+
this.#cmView.dispatch({
|
|
355
|
+
changes: { from: 0, to: this.#cmView.state.doc.length, insert: this.#initialDoc },
|
|
356
|
+
});
|
|
357
|
+
} else {
|
|
358
|
+
this.text = this.#initialDoc;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
formDisabledCallback(disabled) {
|
|
363
|
+
this.disabled = disabled;
|
|
364
|
+
// `disabled` is reflected; the render() hook picks up the change
|
|
365
|
+
// and calls #updateEditableState().
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
formStateRestoreCallback(state, _reason) {
|
|
369
|
+
if (typeof state !== 'string') return;
|
|
370
|
+
if (this.#cmView) {
|
|
371
|
+
this.#cmView.dispatch({
|
|
372
|
+
changes: { from: 0, to: this.#cmView.state.doc.length, insert: state },
|
|
373
|
+
});
|
|
374
|
+
} else {
|
|
375
|
+
this.text = state;
|
|
376
|
+
}
|
|
244
377
|
}
|
|
245
378
|
|
|
246
379
|
#emitSave(view) {
|
|
@@ -284,6 +417,9 @@ class AdiaCode extends AdiaElement {
|
|
|
284
417
|
const label = this.querySelector(':scope > header [slot="label"]');
|
|
285
418
|
if (label && this.language) label.textContent = this.language;
|
|
286
419
|
|
|
420
|
+
// React to disabled/readonly toggles on a live editor.
|
|
421
|
+
this.#updateEditableState();
|
|
422
|
+
|
|
287
423
|
// Keep the rendered code in sync with the reactive `text` property so
|
|
288
424
|
// callers can drive updates via setAttribute('text', ...).
|
|
289
425
|
if (!this.inline && this.text !== '') {
|
|
@@ -296,6 +432,8 @@ class AdiaCode extends AdiaElement {
|
|
|
296
432
|
this.#cmView.dispatch({
|
|
297
433
|
changes: { from: 0, to: current.length, insert: this.text },
|
|
298
434
|
});
|
|
435
|
+
// CM's updateListener catches the dispatch and re-syncs the
|
|
436
|
+
// form value; nothing extra to do here.
|
|
299
437
|
}
|
|
300
438
|
} else {
|
|
301
439
|
const code = this.querySelector(':scope > pre > code');
|
|
@@ -25,15 +25,57 @@ props:
|
|
|
25
25
|
description: Code text content
|
|
26
26
|
type: string
|
|
27
27
|
default: ""
|
|
28
|
+
editable:
|
|
29
|
+
description: Editable CodeMirror instance (vs read-only display)
|
|
30
|
+
type: boolean
|
|
31
|
+
default: false
|
|
32
|
+
bare:
|
|
33
|
+
description: Omit the header chrome (label + copy button); for embedded editor surfaces
|
|
34
|
+
type: boolean
|
|
35
|
+
default: false
|
|
36
|
+
placeholder:
|
|
37
|
+
description: Placeholder text shown when the editor is empty (editable mode only)
|
|
38
|
+
type: string
|
|
39
|
+
default: ""
|
|
40
|
+
name:
|
|
41
|
+
description: Form-control name — submitted with the enclosing form when `editable` is set
|
|
42
|
+
type: string
|
|
43
|
+
default: ""
|
|
44
|
+
required:
|
|
45
|
+
description: Form constraint — submission blocks if the editor is empty
|
|
46
|
+
type: boolean
|
|
47
|
+
default: false
|
|
48
|
+
disabled:
|
|
49
|
+
description: Disables input and excludes from form submission
|
|
50
|
+
type: boolean
|
|
51
|
+
default: false
|
|
52
|
+
readonly:
|
|
53
|
+
description: Blocks input but still submits with the form
|
|
54
|
+
type: boolean
|
|
55
|
+
default: false
|
|
28
56
|
events:
|
|
29
57
|
copy:
|
|
30
58
|
description: Fired when text is copied to clipboard
|
|
59
|
+
input:
|
|
60
|
+
description: Fired on every doc change (editable mode); detail.value is the current buffer
|
|
61
|
+
change:
|
|
62
|
+
description: Fired on blur if the buffer changed since focus (editable mode); detail.value is the final buffer
|
|
63
|
+
save:
|
|
64
|
+
description: Fired on Mod+S keybind (editable mode); detail.value is the current buffer. Cancelable.
|
|
65
|
+
language-load-error:
|
|
66
|
+
description: Fired when the language pack or CodeMirror bundle fails to load; detail.phase is 'core' or 'language'
|
|
31
67
|
slots:
|
|
32
68
|
default:
|
|
33
69
|
description: Raw text fallback when the text property is not set
|
|
34
70
|
states:
|
|
35
71
|
- name: idle
|
|
36
72
|
description: Default, ready for interaction.
|
|
73
|
+
- name: disabled
|
|
74
|
+
description: Editor is disabled — blocks input and excludes from form submission.
|
|
75
|
+
- name: readonly
|
|
76
|
+
description: Editor blocks input but still submits with the form.
|
|
77
|
+
- name: invalid
|
|
78
|
+
description: Editor has a validation error (e.g. `required` and empty). Reflects as `aria-invalid="true"`.
|
|
37
79
|
traits: []
|
|
38
80
|
tokens: {}
|
|
39
81
|
a2ui:
|