@adia-ai/web-components 0.6.7 → 0.6.9
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/CHANGELOG.md +22 -0
- package/USAGE.md +215 -0
- package/components/action-list/action-list.css +1 -1
- package/components/card/card.css +43 -43
- package/components/chart/chart.css +1 -1
- package/components/chat-thread/chat-thread.css +6 -6
- package/components/code/code.css +17 -17
- package/components/command/command.css +7 -7
- package/components/grid/grid.css +6 -6
- package/components/pane/pane.css +10 -10
- package/components/stack/stack.css +1 -1
- package/core/template.js +236 -7
- package/core/template.test.js +294 -0
- package/package.json +1 -1
package/components/code/code.css
CHANGED
|
@@ -90,7 +90,7 @@
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
/* Header — chrome band above the code block */
|
|
93
|
-
> header {
|
|
93
|
+
& > header {
|
|
94
94
|
display: flex;
|
|
95
95
|
flex-direction: row;
|
|
96
96
|
align-items: center;
|
|
@@ -103,14 +103,14 @@
|
|
|
103
103
|
color: var(--code-header-fg);
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
> header [slot="label"] {
|
|
106
|
+
& > header [slot="label"] {
|
|
107
107
|
font-weight: 500;
|
|
108
108
|
text-transform: uppercase;
|
|
109
109
|
letter-spacing: 0.05em;
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
/* Copy button — ghost style */
|
|
113
|
-
> header [slot="copy"] {
|
|
113
|
+
& > header [slot="copy"] {
|
|
114
114
|
all: unset;
|
|
115
115
|
cursor: pointer;
|
|
116
116
|
font-size: var(--code-header-font);
|
|
@@ -121,7 +121,7 @@
|
|
|
121
121
|
background var(--code-duration) var(--code-easing),
|
|
122
122
|
color var(--code-duration) var(--code-easing);
|
|
123
123
|
}
|
|
124
|
-
> header [slot="copy"]:hover {
|
|
124
|
+
& > header [slot="copy"]:hover {
|
|
125
125
|
background: var(--code-copy-hover-bg);
|
|
126
126
|
color: var(--code-copy-hover-fg);
|
|
127
127
|
}
|
|
@@ -129,7 +129,7 @@
|
|
|
129
129
|
/* Pre > Code — reset any ambient page styling (margins, borders,
|
|
130
130
|
radii, backgrounds) so the <code-ui> chrome is the single source
|
|
131
131
|
of visual framing. */
|
|
132
|
-
> pre {
|
|
132
|
+
& > pre {
|
|
133
133
|
margin: 0;
|
|
134
134
|
border: none;
|
|
135
135
|
border-radius: 0;
|
|
@@ -141,11 +141,11 @@
|
|
|
141
141
|
line-height: 1.5;
|
|
142
142
|
scrollbar-width: none;
|
|
143
143
|
}
|
|
144
|
-
> pre::-webkit-scrollbar {
|
|
144
|
+
& > pre::-webkit-scrollbar {
|
|
145
145
|
display: none;
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
-
> pre > code {
|
|
148
|
+
& > pre > code {
|
|
149
149
|
margin: 0;
|
|
150
150
|
padding: 0;
|
|
151
151
|
border: none;
|
|
@@ -162,10 +162,10 @@
|
|
|
162
162
|
a `[data-line-state]` row whose bg picks up the state token. The
|
|
163
163
|
CodeMirror path doesn't use these markers; line decorations there go
|
|
164
164
|
through CM extensions instead. */
|
|
165
|
-
> pre > code[data-line-state-mode] {
|
|
165
|
+
& > pre > code[data-line-state-mode] {
|
|
166
166
|
display: block;
|
|
167
167
|
}
|
|
168
|
-
> pre > code[data-line-state-mode] > [data-line-state] {
|
|
168
|
+
& > pre > code[data-line-state-mode] > [data-line-state] {
|
|
169
169
|
display: grid;
|
|
170
170
|
grid-template-columns: 1fr;
|
|
171
171
|
/* Bleed the row tint to the pre's padding edges so it reads as a
|
|
@@ -173,33 +173,33 @@
|
|
|
173
173
|
margin-inline: calc(-1 * var(--code-px));
|
|
174
174
|
padding-inline: var(--code-px);
|
|
175
175
|
}
|
|
176
|
-
> pre > code[data-line-numbers] > [data-line-state] {
|
|
176
|
+
& > pre > code[data-line-numbers] > [data-line-state] {
|
|
177
177
|
grid-template-columns: auto 1fr;
|
|
178
178
|
column-gap: var(--a-space-3);
|
|
179
179
|
}
|
|
180
|
-
> pre > code[data-line-state-mode] [data-line-num] {
|
|
180
|
+
& > pre > code[data-line-state-mode] [data-line-num] {
|
|
181
181
|
color: var(--a-fg-subtle);
|
|
182
182
|
text-align: end;
|
|
183
183
|
user-select: none;
|
|
184
184
|
min-width: 1.5ch;
|
|
185
185
|
}
|
|
186
|
-
> pre > code[data-line-state-mode] [data-line-body] {
|
|
186
|
+
& > pre > code[data-line-state-mode] [data-line-body] {
|
|
187
187
|
white-space: pre;
|
|
188
188
|
}
|
|
189
189
|
/* Empty lines still need height so the diff column counts line up. */
|
|
190
|
-
> pre > code[data-line-state-mode] [data-line-body]:empty::before {
|
|
190
|
+
& > pre > code[data-line-state-mode] [data-line-body]:empty::before {
|
|
191
191
|
content: " ";
|
|
192
192
|
}
|
|
193
|
-
> pre > code [data-line-state="added"] {
|
|
193
|
+
& > pre > code [data-line-state="added"] {
|
|
194
194
|
background: var(--a-success-muted);
|
|
195
195
|
}
|
|
196
|
-
> pre > code [data-line-state="removed"] {
|
|
196
|
+
& > pre > code [data-line-state="removed"] {
|
|
197
197
|
background: var(--a-danger-muted);
|
|
198
198
|
}
|
|
199
199
|
|
|
200
200
|
/* Footer — optional chrome band below the code block
|
|
201
201
|
(line counts, byte size, language family, etc.) */
|
|
202
|
-
> footer {
|
|
202
|
+
& > footer {
|
|
203
203
|
display: flex;
|
|
204
204
|
flex-direction: row;
|
|
205
205
|
align-items: center;
|
|
@@ -288,7 +288,7 @@
|
|
|
288
288
|
CM's CSS-in-JS stylesheets load first + at lower specificity, so
|
|
289
289
|
these rules override without !important. */
|
|
290
290
|
|
|
291
|
-
> [data-cm-mount] {
|
|
291
|
+
& > [data-cm-mount] {
|
|
292
292
|
display: block;
|
|
293
293
|
overflow: hidden;
|
|
294
294
|
}
|
|
@@ -87,7 +87,7 @@
|
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
/* ── Search header ── */
|
|
90
|
-
> header {
|
|
90
|
+
& > header {
|
|
91
91
|
display: flex;
|
|
92
92
|
align-items: center;
|
|
93
93
|
gap: var(--command-gap);
|
|
@@ -96,13 +96,13 @@
|
|
|
96
96
|
flex-shrink: 0;
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
> header icon-ui {
|
|
99
|
+
& > header icon-ui {
|
|
100
100
|
flex-shrink: 0;
|
|
101
101
|
color: var(--command-fg-muted);
|
|
102
102
|
--a-icon-size: 1rem;
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
> header input {
|
|
105
|
+
& > header input {
|
|
106
106
|
flex: 1;
|
|
107
107
|
min-width: 0;
|
|
108
108
|
border: none;
|
|
@@ -115,16 +115,16 @@
|
|
|
115
115
|
padding: 0;
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
> header input::placeholder {
|
|
118
|
+
& > header input::placeholder {
|
|
119
119
|
color: var(--command-fg-muted);
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
/* Suppress focus ring on the input — the palette itself is the focused surface */
|
|
123
|
-
> header input:focus-visible {
|
|
123
|
+
& > header input:focus-visible {
|
|
124
124
|
outline: none;
|
|
125
125
|
box-shadow: none;
|
|
126
126
|
}
|
|
127
|
-
> header:focus-within {
|
|
127
|
+
& > header:focus-within {
|
|
128
128
|
outline: none;
|
|
129
129
|
box-shadow: none;
|
|
130
130
|
}
|
|
@@ -217,7 +217,7 @@
|
|
|
217
217
|
}
|
|
218
218
|
|
|
219
219
|
/* ── Footer ── */
|
|
220
|
-
> footer {
|
|
220
|
+
& > footer {
|
|
221
221
|
display: flex;
|
|
222
222
|
align-items: center;
|
|
223
223
|
gap: var(--command-px);
|
package/components/grid/grid.css
CHANGED
|
@@ -33,10 +33,10 @@
|
|
|
33
33
|
:scope[columns="auto-fit"] { grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr)); }
|
|
34
34
|
|
|
35
35
|
/* Column span — children can span multiple columns */
|
|
36
|
-
> [span="2"] { grid-column: span 2; }
|
|
37
|
-
> [span="3"] { grid-column: span 3; }
|
|
38
|
-
> [span="4"] { grid-column: span 4; }
|
|
39
|
-
> [span="5"] { grid-column: span 5; }
|
|
40
|
-
> [span="6"] { grid-column: span 6; }
|
|
41
|
-
> [span="full"] { grid-column: 1 / -1; }
|
|
36
|
+
& > [span="2"] { grid-column: span 2; }
|
|
37
|
+
& > [span="3"] { grid-column: span 3; }
|
|
38
|
+
& > [span="4"] { grid-column: span 4; }
|
|
39
|
+
& > [span="5"] { grid-column: span 5; }
|
|
40
|
+
& > [span="6"] { grid-column: span 6; }
|
|
41
|
+
& > [span="full"] { grid-column: 1 / -1; }
|
|
42
42
|
}
|
package/components/pane/pane.css
CHANGED
|
@@ -81,7 +81,7 @@
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
/* ── Pane header ── */
|
|
84
|
-
> header {
|
|
84
|
+
& > header {
|
|
85
85
|
box-sizing: border-box;
|
|
86
86
|
display: flex;
|
|
87
87
|
align-items: center;
|
|
@@ -98,17 +98,17 @@
|
|
|
98
98
|
transition: background var(--pane-duration) var(--pane-easing);
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
> header:hover {
|
|
101
|
+
& > header:hover {
|
|
102
102
|
background: var(--pane-header-bg-hover);
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
> header:focus-visible {
|
|
105
|
+
& > header:focus-visible {
|
|
106
106
|
outline: none;
|
|
107
107
|
box-shadow: var(--a-focus-ring) inset;
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
/* Collapse indicator — stamped by JS as icon-ui */
|
|
111
|
-
> header > [slot="chevron"] {
|
|
111
|
+
& > header > [slot="chevron"] {
|
|
112
112
|
--a-icon-size: var(--a-caret-size);
|
|
113
113
|
flex-shrink: 0;
|
|
114
114
|
margin-inline-start: auto;
|
|
@@ -122,7 +122,7 @@
|
|
|
122
122
|
}
|
|
123
123
|
|
|
124
124
|
/* ── Sections ── */
|
|
125
|
-
> section {
|
|
125
|
+
& > section {
|
|
126
126
|
flex: 1;
|
|
127
127
|
min-height: 0;
|
|
128
128
|
overflow-y: auto;
|
|
@@ -130,13 +130,13 @@
|
|
|
130
130
|
padding: var(--pane-section-py) var(--pane-section-px);
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
> section::-webkit-scrollbar {
|
|
133
|
+
& > section::-webkit-scrollbar {
|
|
134
134
|
display: none;
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
/* Section header — section's padding is the single source of horizontal inset;
|
|
138
138
|
content inside the section must not add its own padding. */
|
|
139
|
-
> section > header {
|
|
139
|
+
& > section > header {
|
|
140
140
|
font-size: var(--pane-section-header-size);
|
|
141
141
|
color: var(--pane-section-header-fg);
|
|
142
142
|
padding-bottom: var(--pane-col-gap);
|
|
@@ -147,12 +147,12 @@
|
|
|
147
147
|
|
|
148
148
|
/* Defensive: a data-col directly under a section inherits the section's inset;
|
|
149
149
|
adding its own padding would compound and misalign with the section header. */
|
|
150
|
-
> section > [data-col] {
|
|
150
|
+
& > section > [data-col] {
|
|
151
151
|
padding: 0;
|
|
152
152
|
}
|
|
153
153
|
|
|
154
154
|
/* Section divider between sections */
|
|
155
|
-
> section + section {
|
|
155
|
+
& > section + section {
|
|
156
156
|
border-top: 1px solid var(--pane-border);
|
|
157
157
|
}
|
|
158
158
|
|
|
@@ -165,7 +165,7 @@
|
|
|
165
165
|
}
|
|
166
166
|
|
|
167
167
|
/* ── Footer ── */
|
|
168
|
-
> footer {
|
|
168
|
+
& > footer {
|
|
169
169
|
min-height: var(--pane-bar-height);
|
|
170
170
|
display: flex;
|
|
171
171
|
align-items: center;
|
package/core/template.js
CHANGED
|
@@ -91,6 +91,106 @@ function getTemplate(strings) {
|
|
|
91
91
|
return tpl;
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
// §-TBD (FB-55, P1): the HTML parser lowercases attribute names inside
|
|
95
|
+
// <template>.innerHTML per HTML5 §13.2.5.32, so `.className=${expr}`
|
|
96
|
+
// arrives at scan() as `.classname`. The `name.slice(1)` strips the dot
|
|
97
|
+
// and the binding writes to a lowercase enumerable expando instead of
|
|
98
|
+
// invoking the camelCase property setter. Net: classes never apply, no
|
|
99
|
+
// warning, no error.
|
|
100
|
+
//
|
|
101
|
+
// The trap affects every camelCase DOM property surface — `.className`,
|
|
102
|
+
// `.innerText`, `.tabIndex`, `.ariaLabel`, `.contentEditable`,
|
|
103
|
+
// `.readOnly`, `.maxLength`, `.minLength`, `.colSpan`, `.rowSpan` — AND
|
|
104
|
+
// every camelCase property declared via `UIElement.static properties`
|
|
105
|
+
// (e.g. `.minL`, `.maxChroma`, `.hueDriftMax`, `.colorScheme`,
|
|
106
|
+
// `.strokeWidth`, `.collapseKeepLeading`). Even the canonical example
|
|
107
|
+
// `<my-panel .minL=${minL}>` from packages/web-components/README.md:212
|
|
108
|
+
// silently failed pre-fix.
|
|
109
|
+
//
|
|
110
|
+
// Two-layer fix:
|
|
111
|
+
//
|
|
112
|
+
// 1. Static PROP_CASE_FIX map covers built-in DOM camelCase property
|
|
113
|
+
// names (zero-cost lookup, no prototype walk per binding for the
|
|
114
|
+
// common case).
|
|
115
|
+
//
|
|
116
|
+
// 2. Prototype-walk fallback finds the canonical camelCase property
|
|
117
|
+
// name on the element's prototype chain via case-insensitive
|
|
118
|
+
// match. Cost is O(prototype-chain-depth × prototype-key-count)
|
|
119
|
+
// per UNIQUE template binding (cached in parts[i].name, not
|
|
120
|
+
// re-walked per update() tick). Custom-element prototype chains
|
|
121
|
+
// are short (subclass → UIElement → HTMLElement → Element → Node
|
|
122
|
+
// → EventTarget); cost is negligible.
|
|
123
|
+
//
|
|
124
|
+
// Backward-compatible: when the prototype walk finds no case-insensitive
|
|
125
|
+
// match (genuine lowercase-expando case — rare but possible), the
|
|
126
|
+
// original lowercase name is preserved. No consumer that was
|
|
127
|
+
// deliberately writing to a lowercase expando regresses.
|
|
128
|
+
//
|
|
129
|
+
// Verification: 8-test matrix in core/template.test.js's FB-55 describe
|
|
130
|
+
// block. RESPONSE-55 documents the trap end-to-end.
|
|
131
|
+
const PROP_CASE_FIX = {
|
|
132
|
+
classname: 'className',
|
|
133
|
+
innertext: 'innerText',
|
|
134
|
+
innerhtml: 'innerHTML',
|
|
135
|
+
outerhtml: 'outerHTML',
|
|
136
|
+
textcontent: 'textContent',
|
|
137
|
+
tabindex: 'tabIndex',
|
|
138
|
+
arialabel: 'ariaLabel',
|
|
139
|
+
ariadescribedby: 'ariaDescribedBy',
|
|
140
|
+
arialabelledby: 'ariaLabelledBy',
|
|
141
|
+
ariarole: 'role',
|
|
142
|
+
contenteditable: 'contentEditable',
|
|
143
|
+
readonly: 'readOnly',
|
|
144
|
+
maxlength: 'maxLength',
|
|
145
|
+
minlength: 'minLength',
|
|
146
|
+
colspan: 'colSpan',
|
|
147
|
+
rowspan: 'rowSpan',
|
|
148
|
+
cellpadding: 'cellPadding',
|
|
149
|
+
cellspacing: 'cellSpacing',
|
|
150
|
+
usemap: 'useMap',
|
|
151
|
+
ismap: 'isMap',
|
|
152
|
+
accesskey: 'accessKey',
|
|
153
|
+
defaultchecked: 'defaultChecked',
|
|
154
|
+
defaultvalue: 'defaultValue',
|
|
155
|
+
defaultselected: 'defaultSelected',
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
// Resolve a lowercase property name to its canonical camelCase form on
|
|
159
|
+
// the given element. Returns the original `lower` name when no
|
|
160
|
+
// case-insensitive match exists (preserves expando semantics). Walks
|
|
161
|
+
// instance own-properties first (UIElement installs camelCase props via
|
|
162
|
+
// `Object.defineProperty(this, ...)` in the constructor, so they live
|
|
163
|
+
// on the instance, not the prototype), then prototype chain. Skips
|
|
164
|
+
// Object.prototype to avoid noisy matches on `toString` etc.
|
|
165
|
+
//
|
|
166
|
+
// IMPORTANT: at scan() time the custom element has typically NOT yet
|
|
167
|
+
// been constructed (template fragment is cloned + scanned before
|
|
168
|
+
// insertion → upgrade), so instance-defined props aren't visible yet.
|
|
169
|
+
// PROP_CASE_FIX covers built-in DOM camelCase props at scan() time;
|
|
170
|
+
// the per-element walk runs ALSO at applyValue() time (lazy resolve,
|
|
171
|
+
// cached on the part after first hit) to catch UIElement instance props
|
|
172
|
+
// once the element has been upgraded.
|
|
173
|
+
function resolvePropName(el, lower) {
|
|
174
|
+
if (PROP_CASE_FIX[lower]) return PROP_CASE_FIX[lower];
|
|
175
|
+
// Instance own-properties (UIElement installProps lives here).
|
|
176
|
+
const ownNames = Object.getOwnPropertyNames(el);
|
|
177
|
+
for (let i = 0; i < ownNames.length; i++) {
|
|
178
|
+
const n = ownNames[i];
|
|
179
|
+
if (n !== lower && n.toLowerCase() === lower) return n;
|
|
180
|
+
}
|
|
181
|
+
// Prototype chain (built-in DOM camelCase + class-defined accessors).
|
|
182
|
+
let proto = Object.getPrototypeOf(el);
|
|
183
|
+
while (proto && proto !== Object.prototype) {
|
|
184
|
+
const names = Object.getOwnPropertyNames(proto);
|
|
185
|
+
for (let i = 0; i < names.length; i++) {
|
|
186
|
+
const n = names[i];
|
|
187
|
+
if (n !== lower && n.toLowerCase() === lower) return n;
|
|
188
|
+
}
|
|
189
|
+
proto = Object.getPrototypeOf(proto);
|
|
190
|
+
}
|
|
191
|
+
return lower;
|
|
192
|
+
}
|
|
193
|
+
|
|
94
194
|
export function stamp(result, container) {
|
|
95
195
|
let inst = container._i;
|
|
96
196
|
if (!inst || inst.s !== result.strings) {
|
|
@@ -104,12 +204,118 @@ export function stamp(result, container) {
|
|
|
104
204
|
function mount(result, container) {
|
|
105
205
|
const { strings } = result;
|
|
106
206
|
const tpl = getTemplate(strings);
|
|
107
|
-
|
|
207
|
+
let f = tpl.content.cloneNode(true);
|
|
208
|
+
|
|
209
|
+
// §-TBD (FB-57, P1): when stamping into an SVG or MathML context, the
|
|
210
|
+
// fragment's Elements arrive HTML-namespaced because `tpl.innerHTML =
|
|
211
|
+
// m` is parsed at document level without foreign-content context. SVG
|
|
212
|
+
// and MathML layout are namespace-strict — HTMLUnknownElement nodes
|
|
213
|
+
// inside an SVG render to zero-bound-rect invisible. Re-namespace the
|
|
214
|
+
// fragment before insertion, respecting `<foreignObject>` and
|
|
215
|
+
// `<annotation-xml>` HTML boundaries per the HTML5 foreign-content
|
|
216
|
+
// insertion-mode spec.
|
|
217
|
+
//
|
|
218
|
+
// Discriminator runs once per mount() call (no per-update cost).
|
|
219
|
+
// Re-namespacing walks the fragment recursively (O(node-count)),
|
|
220
|
+
// cheap for typical interpolation arrays (16 lines + 11 stops in
|
|
221
|
+
// the canonical curve-preview case = 27 createElementNS calls).
|
|
222
|
+
//
|
|
223
|
+
// Implementation site choice: mount() rather than getTemplate(),
|
|
224
|
+
// because:
|
|
225
|
+
// - container reference is in hand only at mount()
|
|
226
|
+
// - the same cached template may be stamped into either context;
|
|
227
|
+
// namespacing per-cache-entry would double the WeakMap size
|
|
228
|
+
// and require a cache-key extension
|
|
229
|
+
// - cloning is already happening here; re-namespacing piggybacks
|
|
230
|
+
// on the existing fragment walk
|
|
231
|
+
//
|
|
232
|
+
// RESPONSE-57 documents the trap end-to-end. Regression tests pin
|
|
233
|
+
// the inline-SVG path (must keep working), nested-SVG path (would
|
|
234
|
+
// fail pre-fix), foreignObject boundary (HTML inside SVG stays
|
|
235
|
+
// HTML), and MathML parallel.
|
|
236
|
+
const ns = foreignContentNS(container);
|
|
237
|
+
if (ns) f = renamespaceFragment(f, ns);
|
|
238
|
+
|
|
108
239
|
const parts = scan(f, result.values.length);
|
|
109
240
|
container.replaceChildren(f);
|
|
110
241
|
return { s: strings, p: parts };
|
|
111
242
|
}
|
|
112
243
|
|
|
244
|
+
const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
245
|
+
const MATHML_NS = 'http://www.w3.org/1998/Math/MathML';
|
|
246
|
+
const XHTML_NS = 'http://www.w3.org/1999/xhtml';
|
|
247
|
+
|
|
248
|
+
// Determine whether `container` is inside an SVG or MathML subtree, and
|
|
249
|
+
// return the namespace to re-namespace stamped fragments into. Returns
|
|
250
|
+
// null when the container is in HTML context (the no-op case — vast
|
|
251
|
+
// majority of templates). Respects `<foreignObject>` (SVG) and
|
|
252
|
+
// `<annotation-xml encoding=text/html|application/xhtml+xml>` (MathML)
|
|
253
|
+
// HTML boundaries per HTML5 §12.2.5 foreign-content insertion mode.
|
|
254
|
+
function foreignContentNS(container) {
|
|
255
|
+
if (!container || container.nodeType !== 1) return null;
|
|
256
|
+
// Direct check — container's own namespace is a fast-path.
|
|
257
|
+
if (container.namespaceURI === SVG_NS) return SVG_NS;
|
|
258
|
+
if (container.namespaceURI === MATHML_NS) return MATHML_NS;
|
|
259
|
+
// Walk ancestors to find a foreign-content root. `closest()` matches
|
|
260
|
+
// the element itself + ancestors, so this catches the wrap()-span
|
|
261
|
+
// case (HTML span inside an SVG subtree). The first match wins —
|
|
262
|
+
// foreignObject/annotation-xml inside SVG short-circuits before we
|
|
263
|
+
// see the outer <svg>, correctly returning null (HTML context).
|
|
264
|
+
if (typeof container.closest !== 'function') return null;
|
|
265
|
+
const anchor = container.closest('svg, math, foreignObject, annotation-xml');
|
|
266
|
+
if (!anchor) return null;
|
|
267
|
+
const tag = anchor.localName;
|
|
268
|
+
if (tag === 'foreignobject' || tag === 'foreignObject') return null;
|
|
269
|
+
if (tag === 'annotation-xml') {
|
|
270
|
+
const enc = (anchor.getAttribute('encoding') || '').toLowerCase();
|
|
271
|
+
if (enc === 'text/html' || enc === 'application/xhtml+xml') return null;
|
|
272
|
+
return MATHML_NS;
|
|
273
|
+
}
|
|
274
|
+
if (tag === 'svg') return SVG_NS;
|
|
275
|
+
if (tag === 'math') return MATHML_NS;
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Recursively transfer fragment children into a new fragment with each
|
|
280
|
+
// Element recreated in the target namespace. Text/comment nodes clone
|
|
281
|
+
// unchanged. `<foreignObject>` and `<annotation-xml encoding=text/html>`
|
|
282
|
+
// stay in the target namespace themselves, but their descendants revert
|
|
283
|
+
// to HTML (matches the browser parser's foreign-content insertion mode).
|
|
284
|
+
function renamespaceFragment(fragment, ns) {
|
|
285
|
+
const out = document.createDocumentFragment();
|
|
286
|
+
transferChildren(fragment, out, ns);
|
|
287
|
+
return out;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function transferChildren(src, dstParent, ns) {
|
|
291
|
+
for (const child of [...src.childNodes]) {
|
|
292
|
+
const t = child.nodeType;
|
|
293
|
+
if (t === 1 /* Element */) {
|
|
294
|
+
const dst = document.createElementNS(ns, child.localName);
|
|
295
|
+
for (const a of child.attributes) {
|
|
296
|
+
if (a.namespaceURI) dst.setAttributeNS(a.namespaceURI, a.name, a.value);
|
|
297
|
+
else dst.setAttribute(a.name, a.value);
|
|
298
|
+
}
|
|
299
|
+
dstParent.appendChild(dst);
|
|
300
|
+
// Boundary handling: <foreignObject> (SVG) and
|
|
301
|
+
// <annotation-xml encoding=text/html> (MathML) revert to HTML.
|
|
302
|
+
const local = child.localName;
|
|
303
|
+
let childNs = ns;
|
|
304
|
+
if (ns === SVG_NS && (local === 'foreignObject' || local === 'foreignobject')) {
|
|
305
|
+
childNs = XHTML_NS;
|
|
306
|
+
} else if (ns === MATHML_NS && local === 'annotation-xml') {
|
|
307
|
+
const enc = (child.getAttribute('encoding') || '').toLowerCase();
|
|
308
|
+
if (enc === 'text/html' || enc === 'application/xhtml+xml') childNs = XHTML_NS;
|
|
309
|
+
}
|
|
310
|
+
transferChildren(child, dst, childNs);
|
|
311
|
+
} else {
|
|
312
|
+
// Text, comment, etc. — clone unchanged. Comments are load-bearing
|
|
313
|
+
// (the `<!--p:N-->` placeholders scan() looks for).
|
|
314
|
+
dstParent.appendChild(child.cloneNode(true));
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
113
319
|
function scan(fragment, count) {
|
|
114
320
|
const parts = new Array(count);
|
|
115
321
|
const w = document.createTreeWalker(fragment, 129);
|
|
@@ -146,14 +352,21 @@ function scan(fragment, count) {
|
|
|
146
352
|
// canonical style-object path; for everything else keep
|
|
147
353
|
// the generic property-assignment recipe.
|
|
148
354
|
const a = attr.name;
|
|
355
|
+
// §-TBD (FB-55 #2, P1): updated post-fix. The v0.5.5 §184
|
|
356
|
+
// recommendation had `.className=${expression}` first;
|
|
357
|
+
// that path now works (§-TBD PROP_CASE_FIX above) but
|
|
358
|
+
// `class="${expression}"` is the more discoverable form
|
|
359
|
+
// that doesn't require knowing the camelCase property
|
|
360
|
+
// name. Promoting it first. The `.classList=` line is
|
|
361
|
+
// removed entirely — classList is a read-only getter, no
|
|
362
|
+
// PROP_CASE_FIX entry can make it assignable.
|
|
149
363
|
const specific =
|
|
150
364
|
a === 'class'
|
|
151
|
-
? `
|
|
152
|
-
`
|
|
153
|
-
` .classList=\${{foo: true, bar: false}} ← if/when implemented; verify in USAGE.md\n`
|
|
365
|
+
? ` class="\${expression}" ← full replacement (whole class string is the expression)\n` +
|
|
366
|
+
` .className=\${expression} ← writes to the className property (resolved camelCase since FB-55 fix)\n`
|
|
154
367
|
: a === 'style'
|
|
155
|
-
? `
|
|
156
|
-
` style
|
|
368
|
+
? ` style="\${expression}" ← full replacement of the style string\n` +
|
|
369
|
+
` .style.cssText=\${expression} ← write CSS text via the style.cssText accessor\n`
|
|
157
370
|
: ` ${a}="\${expression}" ← full replacement (whole attr is the placeholder)\n` +
|
|
158
371
|
` .${a}=\${expression} ← property assignment (preferred for objects/functions)\n`;
|
|
159
372
|
// eslint-disable-next-line no-console
|
|
@@ -173,8 +386,12 @@ function scan(fragment, count) {
|
|
|
173
386
|
n.removeAttribute(name);
|
|
174
387
|
parts[i] = { t: 'e', n, name: name.slice(1), c: undefined, _fx: null };
|
|
175
388
|
} else if (name[0] === '.') {
|
|
389
|
+
// §-TBD (FB-55, P1): resolve the parser-lowercased property
|
|
390
|
+
// name back to its canonical camelCase via PROP_CASE_FIX +
|
|
391
|
+
// prototype walk. See PROP_CASE_FIX comment above for context.
|
|
392
|
+
// Lazy-resolve in applyValue() too — see _resolved guard there.
|
|
176
393
|
n.removeAttribute(name);
|
|
177
|
-
parts[i] = { t: 'p', n, name: name.slice(1), c: undefined, _fx: null };
|
|
394
|
+
parts[i] = { t: 'p', n, name: resolvePropName(n, name.slice(1)), c: undefined, _fx: null, _resolved: false };
|
|
178
395
|
} else if (name[0] === '?') {
|
|
179
396
|
// §250 (v0.5.11, FEEDBACK-27): Lit-style boolean attribute syntax
|
|
180
397
|
// (`?attr=${bool}`) is NOT supported. Without this branch, the
|
|
@@ -256,6 +473,18 @@ function applyValue(p, v) {
|
|
|
256
473
|
if (v == null || v === false) p.n.removeAttribute(p.name);
|
|
257
474
|
else p.n.setAttribute(p.name, v === true ? '' : v);
|
|
258
475
|
} else if (p.t === 'p') {
|
|
476
|
+
// §-TBD (FB-55, P1): two-phase property-name resolution. PROP_CASE_FIX
|
|
477
|
+
// resolved at scan() time for zero-cost built-in DOM camelCase (the
|
|
478
|
+
// common case). For UIElement custom-element props installed on the
|
|
479
|
+
// INSTANCE via the constructor's installProps(), the property doesn't
|
|
480
|
+
// exist at scan() time (element not yet upgraded), so a lazy lookup
|
|
481
|
+
// here catches them. Resolution is cached back into p.name on first
|
|
482
|
+
// hit — subsequent updates skip the walk.
|
|
483
|
+
if (!p._resolved) {
|
|
484
|
+
const resolved = resolvePropName(p.n, p.name);
|
|
485
|
+
if (resolved !== p.name) p.name = resolved;
|
|
486
|
+
p._resolved = true;
|
|
487
|
+
}
|
|
259
488
|
p.n[p.name] = v;
|
|
260
489
|
} else if (p.t === 'e') {
|
|
261
490
|
if (isHandler(p.c)) p.n.removeEventListener(p.name, p.c);
|