@barefootjs/client 0.1.2 → 0.2.0

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.
@@ -1 +1 @@
1
- {"version":3,"file":"apply-rest-attrs.d.ts","sourceRoot":"","sources":["../../src/runtime/apply-rest-attrs.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAyBH;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAC5B,EAAE,EAAE,OAAO,EACX,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,WAAW,EAAE,MAAM,EAAE,GACpB,IAAI,CAkEN"}
1
+ {"version":3,"file":"apply-rest-attrs.d.ts","sourceRoot":"","sources":["../../src/runtime/apply-rest-attrs.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAiBH;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAC5B,EAAE,EAAE,OAAO,EACX,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,WAAW,EAAE,MAAM,EAAE,GACpB,IAAI,CA4DN"}
@@ -123,6 +123,156 @@ var BF_KEY = "data-key";
123
123
  var BF_ASYNC = "bf-async";
124
124
  var BF_ASYNC_RESOLVE = "bf-async-resolve";
125
125
  var BF_PARENT_SCOPE_PLACEHOLDER = "__BF_PARENT_SCOPE__";
126
+ // ../shared/src/dom-prop.ts
127
+ var BOOLEAN_ATTRS = new Set([
128
+ "checked",
129
+ "disabled",
130
+ "readonly",
131
+ "selected",
132
+ "required",
133
+ "hidden",
134
+ "autofocus",
135
+ "autoplay",
136
+ "controls",
137
+ "loop",
138
+ "muted",
139
+ "open",
140
+ "multiple",
141
+ "novalidate",
142
+ "formnovalidate"
143
+ ]);
144
+ var SVG_CAMEL_TO_KEBAB = {
145
+ strokeWidth: "stroke-width",
146
+ strokeLinecap: "stroke-linecap",
147
+ strokeLinejoin: "stroke-linejoin",
148
+ strokeDasharray: "stroke-dasharray",
149
+ strokeDashoffset: "stroke-dashoffset",
150
+ strokeMiterlimit: "stroke-miterlimit",
151
+ strokeOpacity: "stroke-opacity",
152
+ fillOpacity: "fill-opacity",
153
+ fillRule: "fill-rule",
154
+ stopColor: "stop-color",
155
+ stopOpacity: "stop-opacity",
156
+ textAnchor: "text-anchor",
157
+ dominantBaseline: "dominant-baseline",
158
+ alignmentBaseline: "alignment-baseline",
159
+ fontFamily: "font-family",
160
+ fontSize: "font-size",
161
+ fontWeight: "font-weight",
162
+ fontStyle: "font-style",
163
+ letterSpacing: "letter-spacing",
164
+ wordSpacing: "word-spacing",
165
+ pointerEvents: "pointer-events",
166
+ vectorEffect: "vector-effect",
167
+ colorInterpolation: "color-interpolation",
168
+ clipPath: "clip-path",
169
+ clipRule: "clip-rule",
170
+ markerStart: "marker-start",
171
+ markerMid: "marker-mid",
172
+ markerEnd: "marker-end"
173
+ };
174
+ var SVG_XML_CAMEL_ATTRS = new Set([
175
+ "allowReorder",
176
+ "attributeName",
177
+ "attributeType",
178
+ "autoReverse",
179
+ "baseFrequency",
180
+ "baseProfile",
181
+ "calcMode",
182
+ "clipPathUnits",
183
+ "contentScriptType",
184
+ "contentStyleType",
185
+ "diffuseConstant",
186
+ "edgeMode",
187
+ "externalResourcesRequired",
188
+ "filterRes",
189
+ "filterUnits",
190
+ "glyphRef",
191
+ "gradientTransform",
192
+ "gradientUnits",
193
+ "kernelMatrix",
194
+ "kernelUnitLength",
195
+ "keyPoints",
196
+ "keySplines",
197
+ "keyTimes",
198
+ "lengthAdjust",
199
+ "limitingConeAngle",
200
+ "markerHeight",
201
+ "markerUnits",
202
+ "markerWidth",
203
+ "maskContentUnits",
204
+ "maskUnits",
205
+ "numOctaves",
206
+ "pathLength",
207
+ "patternContentUnits",
208
+ "patternTransform",
209
+ "patternUnits",
210
+ "pointsAtX",
211
+ "pointsAtY",
212
+ "pointsAtZ",
213
+ "preserveAlpha",
214
+ "preserveAspectRatio",
215
+ "primitiveUnits",
216
+ "refX",
217
+ "refY",
218
+ "repeatCount",
219
+ "repeatDur",
220
+ "requiredExtensions",
221
+ "requiredFeatures",
222
+ "specularConstant",
223
+ "specularExponent",
224
+ "spreadMethod",
225
+ "startOffset",
226
+ "stdDeviation",
227
+ "stitchTiles",
228
+ "surfaceScale",
229
+ "systemLanguage",
230
+ "tableValues",
231
+ "targetX",
232
+ "targetY",
233
+ "textLength",
234
+ "viewBox",
235
+ "viewTarget",
236
+ "xChannelSelector",
237
+ "yChannelSelector",
238
+ "zoomAndPan"
239
+ ]);
240
+ function isEventProp(key) {
241
+ return key.length > 2 && key[0] === "o" && key[1] === "n" && key[2] >= "A" && key[2] <= "Z";
242
+ }
243
+ function classifyDOMProp(key) {
244
+ if (key === "children")
245
+ return { kind: "skip", attrName: key };
246
+ if (key === "ref")
247
+ return { kind: "ref", attrName: key };
248
+ if (isEventProp(key))
249
+ return { kind: "event", attrName: key };
250
+ const attrName = toHTMLAttrNameRuntime(key);
251
+ if (attrName === "style")
252
+ return { kind: "style", attrName };
253
+ if (attrName === "value")
254
+ return { kind: "property", attrName };
255
+ if (attrName === "checked")
256
+ return { kind: "property", attrName };
257
+ if (BOOLEAN_ATTRS.has(attrName.toLowerCase()))
258
+ return { kind: "boolean", attrName };
259
+ return { kind: "attr", attrName };
260
+ }
261
+ function toHTMLAttrNameRuntime(key) {
262
+ if (key === "className")
263
+ return "class";
264
+ if (key === "htmlFor")
265
+ return "for";
266
+ const svgKebab = SVG_CAMEL_TO_KEBAB[key];
267
+ if (svgKebab !== undefined)
268
+ return svgKebab;
269
+ if (SVG_XML_CAMEL_ATTRS.has(key))
270
+ return key;
271
+ if (key.startsWith("data") || key.startsWith("aria")) {
272
+ return key.replace(/([A-Z])/g, "-$1").toLowerCase();
273
+ }
274
+ return key;
275
+ }
126
276
  // src/context.ts
127
277
  function createContext(defaultValue) {
128
278
  return {
@@ -1580,13 +1730,6 @@ function styleToCss(value) {
1580
1730
  }
1581
1731
 
1582
1732
  // src/runtime/apply-rest-attrs.ts
1583
- function toAttrName(key) {
1584
- if (key === "className")
1585
- return "class";
1586
- if (key === "htmlFor")
1587
- return "for";
1588
- return key.replace(/([A-Z])/g, "-$1").toLowerCase();
1589
- }
1590
1733
  var jsxToDomEventMap = { doubleclick: "dblclick" };
1591
1734
  function toEventName(jsxPropName) {
1592
1735
  const raw = (jsxPropName[2].toLowerCase() + jsxPropName.slice(3)).toLowerCase();
@@ -1594,127 +1737,61 @@ function toEventName(jsxPropName) {
1594
1737
  }
1595
1738
  function applyRestAttrs(el, source, excludeKeys) {
1596
1739
  const exclude = new Set(excludeKeys);
1740
+ const classified = [];
1597
1741
  for (const key of Object.keys(source)) {
1598
1742
  if (exclude.has(key))
1599
1743
  continue;
1600
- if (key === "ref") {
1744
+ classified.push({ key, c: classifyDOMProp(key) });
1745
+ }
1746
+ for (const { key, c } of classified) {
1747
+ if (c.kind === "ref") {
1601
1748
  const ref = source[key];
1602
1749
  if (typeof ref === "function")
1603
1750
  ref(el);
1604
- continue;
1605
- }
1606
- if (key.startsWith("on") && key.length > 2 && key[2] === key[2].toUpperCase()) {
1751
+ } else if (c.kind === "event") {
1607
1752
  const handler = source[key];
1608
1753
  if (typeof handler === "function") {
1609
1754
  el.addEventListener(toEventName(key), handler);
1610
1755
  }
1611
1756
  }
1612
1757
  }
1758
+ const attrEntries = classified.filter(({ c }) => c.kind !== "ref" && c.kind !== "event" && c.kind !== "skip");
1613
1759
  createEffect2(() => {
1614
- for (const key of Object.keys(source)) {
1615
- if (exclude.has(key))
1616
- continue;
1617
- if (key === "ref")
1618
- continue;
1619
- if (key === "children")
1620
- continue;
1621
- if (key.startsWith("on") && key.length > 2 && key[2] === key[2].toUpperCase())
1622
- continue;
1760
+ for (const { key, c } of attrEntries) {
1623
1761
  const value = source[key];
1624
- const attr = toAttrName(key);
1625
1762
  if (value != null && value !== false) {
1626
- if (attr === "value" && "value" in el) {
1763
+ if (c.kind === "property" && c.attrName === "value" && "value" in el) {
1627
1764
  const strVal = String(value);
1628
1765
  if (el.value !== strVal)
1629
1766
  el.value = strVal;
1630
- } else if (attr === "checked" && "checked" in el) {
1767
+ } else if (c.kind === "property" && c.attrName === "checked" && "checked" in el) {
1631
1768
  el.checked = !!value;
1632
- } else if (attr === "style") {
1769
+ } else if (c.kind === "boolean") {
1770
+ el.setAttribute(c.attrName, "");
1771
+ } else if (c.kind === "style") {
1633
1772
  const css = styleToCss(value);
1634
1773
  if (css == null)
1635
1774
  el.removeAttribute("style");
1636
1775
  else
1637
1776
  el.setAttribute("style", css);
1638
1777
  } else {
1639
- el.setAttribute(attr, String(value));
1778
+ el.setAttribute(c.attrName, String(value));
1640
1779
  }
1641
1780
  } else {
1642
- if (attr === "checked" && "checked" in el) {
1781
+ if (c.kind === "property" && c.attrName === "value" && "value" in el) {
1782
+ el.value = "";
1783
+ } else if (c.kind === "property" && c.attrName === "checked" && "checked" in el) {
1643
1784
  el.checked = false;
1785
+ } else if (c.kind === "boolean") {
1786
+ el.removeAttribute(c.attrName);
1644
1787
  } else {
1645
- el.removeAttribute(attr);
1788
+ el.removeAttribute(c.attrName);
1646
1789
  }
1647
1790
  }
1648
1791
  }
1649
1792
  });
1650
1793
  }
1651
1794
  // src/runtime/spread-attrs.ts
1652
- var SVG_CAMEL_CASE_ATTRS = new Set([
1653
- "allowReorder",
1654
- "attributeName",
1655
- "attributeType",
1656
- "autoReverse",
1657
- "baseFrequency",
1658
- "baseProfile",
1659
- "calcMode",
1660
- "clipPathUnits",
1661
- "contentScriptType",
1662
- "contentStyleType",
1663
- "diffuseConstant",
1664
- "edgeMode",
1665
- "externalResourcesRequired",
1666
- "filterRes",
1667
- "filterUnits",
1668
- "glyphRef",
1669
- "gradientTransform",
1670
- "gradientUnits",
1671
- "kernelMatrix",
1672
- "kernelUnitLength",
1673
- "keyPoints",
1674
- "keySplines",
1675
- "keyTimes",
1676
- "lengthAdjust",
1677
- "limitingConeAngle",
1678
- "markerHeight",
1679
- "markerUnits",
1680
- "markerWidth",
1681
- "maskContentUnits",
1682
- "maskUnits",
1683
- "numOctaves",
1684
- "pathLength",
1685
- "patternContentUnits",
1686
- "patternTransform",
1687
- "patternUnits",
1688
- "pointsAtX",
1689
- "pointsAtY",
1690
- "pointsAtZ",
1691
- "preserveAlpha",
1692
- "preserveAspectRatio",
1693
- "primitiveUnits",
1694
- "refX",
1695
- "refY",
1696
- "repeatCount",
1697
- "repeatDur",
1698
- "requiredExtensions",
1699
- "requiredFeatures",
1700
- "specularConstant",
1701
- "specularExponent",
1702
- "spreadMethod",
1703
- "startOffset",
1704
- "stdDeviation",
1705
- "stitchTiles",
1706
- "surfaceScale",
1707
- "systemLanguage",
1708
- "tableValues",
1709
- "targetX",
1710
- "targetY",
1711
- "textLength",
1712
- "viewBox",
1713
- "viewTarget",
1714
- "xChannelSelector",
1715
- "yChannelSelector",
1716
- "zoomAndPan"
1717
- ]);
1718
1795
  function spreadAttrs(obj) {
1719
1796
  if (!obj || typeof obj !== "object")
1720
1797
  return "";
@@ -1722,18 +1799,20 @@ function spreadAttrs(obj) {
1722
1799
  for (const [key, value] of Object.entries(obj)) {
1723
1800
  if (value == null || value === false)
1724
1801
  continue;
1725
- if (key.startsWith("on") && key.length > 2 && key[2] === key[2].toUpperCase())
1726
- continue;
1727
- if (key === "children")
1802
+ const c = classifyDOMProp(key);
1803
+ if (c.kind === "event" || c.kind === "skip" || c.kind === "ref")
1728
1804
  continue;
1729
- if (key === "style") {
1805
+ if (c.kind === "style") {
1730
1806
  const css = styleToCss(value);
1731
1807
  if (css != null)
1732
1808
  parts.push(`style="${css}"`);
1733
1809
  continue;
1734
1810
  }
1735
- const attr = key === "className" ? "class" : key === "htmlFor" ? "for" : SVG_CAMEL_CASE_ATTRS.has(key) ? key : key.replace(/([A-Z])/g, "-$1").toLowerCase();
1736
- parts.push(value === true ? attr : `${attr}="${value}"`);
1811
+ if (c.kind === "boolean" && value === true) {
1812
+ parts.push(c.attrName);
1813
+ } else {
1814
+ parts.push(`${c.attrName}="${value}"`);
1815
+ }
1737
1816
  }
1738
1817
  return parts.join(" ");
1739
1818
  }
@@ -7,13 +7,10 @@
7
7
  */
8
8
  /**
9
9
  * Convert an object to an HTML attribute string.
10
- * Aligned with applyRestAttrs conventions: skips null/undefined/false,
11
- * event handlers, maps className→class and htmlFor→for. The `style`
12
- * prop is routed through `styleToCss` so object literals serialize to
13
- * a real CSS string (matching the reactive `applyRestAttrs` path).
14
- *
15
- * SVG attributes listed in `SVG_CAMEL_CASE_ATTRS` are preserved
16
- * verbatim — the SVG XML spec is case-sensitive for those names.
10
+ * Uses the shared classifyDOMProp classifier to determine how each prop
11
+ * maps to the DOM. Skips null/undefined/false, event handlers, ref, and
12
+ * children. The `style` prop is routed through `styleToCss` so object
13
+ * literals serialize to a real CSS string.
17
14
  */
18
15
  export declare function spreadAttrs(obj: Record<string, unknown>): string;
19
16
  //# sourceMappingURL=spread-attrs.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"spread-attrs.d.ts","sourceRoot":"","sources":["../../src/runtime/spread-attrs.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AA+CH;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAwBhE"}
1
+ {"version":3,"file":"spread-attrs.d.ts","sourceRoot":"","sources":["../../src/runtime/spread-attrs.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAKH;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAmBhE"}
@@ -314,6 +314,156 @@ var BF_KEY = "data-key";
314
314
  var BF_ASYNC = "bf-async";
315
315
  var BF_ASYNC_RESOLVE = "bf-async-resolve";
316
316
  var BF_PARENT_SCOPE_PLACEHOLDER = "__BF_PARENT_SCOPE__";
317
+ // ../shared/src/dom-prop.ts
318
+ var BOOLEAN_ATTRS = new Set([
319
+ "checked",
320
+ "disabled",
321
+ "readonly",
322
+ "selected",
323
+ "required",
324
+ "hidden",
325
+ "autofocus",
326
+ "autoplay",
327
+ "controls",
328
+ "loop",
329
+ "muted",
330
+ "open",
331
+ "multiple",
332
+ "novalidate",
333
+ "formnovalidate"
334
+ ]);
335
+ var SVG_CAMEL_TO_KEBAB = {
336
+ strokeWidth: "stroke-width",
337
+ strokeLinecap: "stroke-linecap",
338
+ strokeLinejoin: "stroke-linejoin",
339
+ strokeDasharray: "stroke-dasharray",
340
+ strokeDashoffset: "stroke-dashoffset",
341
+ strokeMiterlimit: "stroke-miterlimit",
342
+ strokeOpacity: "stroke-opacity",
343
+ fillOpacity: "fill-opacity",
344
+ fillRule: "fill-rule",
345
+ stopColor: "stop-color",
346
+ stopOpacity: "stop-opacity",
347
+ textAnchor: "text-anchor",
348
+ dominantBaseline: "dominant-baseline",
349
+ alignmentBaseline: "alignment-baseline",
350
+ fontFamily: "font-family",
351
+ fontSize: "font-size",
352
+ fontWeight: "font-weight",
353
+ fontStyle: "font-style",
354
+ letterSpacing: "letter-spacing",
355
+ wordSpacing: "word-spacing",
356
+ pointerEvents: "pointer-events",
357
+ vectorEffect: "vector-effect",
358
+ colorInterpolation: "color-interpolation",
359
+ clipPath: "clip-path",
360
+ clipRule: "clip-rule",
361
+ markerStart: "marker-start",
362
+ markerMid: "marker-mid",
363
+ markerEnd: "marker-end"
364
+ };
365
+ var SVG_XML_CAMEL_ATTRS = new Set([
366
+ "allowReorder",
367
+ "attributeName",
368
+ "attributeType",
369
+ "autoReverse",
370
+ "baseFrequency",
371
+ "baseProfile",
372
+ "calcMode",
373
+ "clipPathUnits",
374
+ "contentScriptType",
375
+ "contentStyleType",
376
+ "diffuseConstant",
377
+ "edgeMode",
378
+ "externalResourcesRequired",
379
+ "filterRes",
380
+ "filterUnits",
381
+ "glyphRef",
382
+ "gradientTransform",
383
+ "gradientUnits",
384
+ "kernelMatrix",
385
+ "kernelUnitLength",
386
+ "keyPoints",
387
+ "keySplines",
388
+ "keyTimes",
389
+ "lengthAdjust",
390
+ "limitingConeAngle",
391
+ "markerHeight",
392
+ "markerUnits",
393
+ "markerWidth",
394
+ "maskContentUnits",
395
+ "maskUnits",
396
+ "numOctaves",
397
+ "pathLength",
398
+ "patternContentUnits",
399
+ "patternTransform",
400
+ "patternUnits",
401
+ "pointsAtX",
402
+ "pointsAtY",
403
+ "pointsAtZ",
404
+ "preserveAlpha",
405
+ "preserveAspectRatio",
406
+ "primitiveUnits",
407
+ "refX",
408
+ "refY",
409
+ "repeatCount",
410
+ "repeatDur",
411
+ "requiredExtensions",
412
+ "requiredFeatures",
413
+ "specularConstant",
414
+ "specularExponent",
415
+ "spreadMethod",
416
+ "startOffset",
417
+ "stdDeviation",
418
+ "stitchTiles",
419
+ "surfaceScale",
420
+ "systemLanguage",
421
+ "tableValues",
422
+ "targetX",
423
+ "targetY",
424
+ "textLength",
425
+ "viewBox",
426
+ "viewTarget",
427
+ "xChannelSelector",
428
+ "yChannelSelector",
429
+ "zoomAndPan"
430
+ ]);
431
+ function isEventProp(key) {
432
+ return key.length > 2 && key[0] === "o" && key[1] === "n" && key[2] >= "A" && key[2] <= "Z";
433
+ }
434
+ function classifyDOMProp(key) {
435
+ if (key === "children")
436
+ return { kind: "skip", attrName: key };
437
+ if (key === "ref")
438
+ return { kind: "ref", attrName: key };
439
+ if (isEventProp(key))
440
+ return { kind: "event", attrName: key };
441
+ const attrName = toHTMLAttrNameRuntime(key);
442
+ if (attrName === "style")
443
+ return { kind: "style", attrName };
444
+ if (attrName === "value")
445
+ return { kind: "property", attrName };
446
+ if (attrName === "checked")
447
+ return { kind: "property", attrName };
448
+ if (BOOLEAN_ATTRS.has(attrName.toLowerCase()))
449
+ return { kind: "boolean", attrName };
450
+ return { kind: "attr", attrName };
451
+ }
452
+ function toHTMLAttrNameRuntime(key) {
453
+ if (key === "className")
454
+ return "class";
455
+ if (key === "htmlFor")
456
+ return "for";
457
+ const svgKebab = SVG_CAMEL_TO_KEBAB[key];
458
+ if (svgKebab !== undefined)
459
+ return svgKebab;
460
+ if (SVG_XML_CAMEL_ATTRS.has(key))
461
+ return key;
462
+ if (key.startsWith("data") || key.startsWith("aria")) {
463
+ return key.replace(/([A-Z])/g, "-$1").toLowerCase();
464
+ }
465
+ return key;
466
+ }
317
467
  // src/context.ts
318
468
  function createContext(defaultValue) {
319
469
  return {
@@ -1766,13 +1916,6 @@ function styleToCss(value) {
1766
1916
  }
1767
1917
 
1768
1918
  // src/runtime/apply-rest-attrs.ts
1769
- function toAttrName(key) {
1770
- if (key === "className")
1771
- return "class";
1772
- if (key === "htmlFor")
1773
- return "for";
1774
- return key.replace(/([A-Z])/g, "-$1").toLowerCase();
1775
- }
1776
1919
  var jsxToDomEventMap = { doubleclick: "dblclick" };
1777
1920
  function toEventName(jsxPropName) {
1778
1921
  const raw = (jsxPropName[2].toLowerCase() + jsxPropName.slice(3)).toLowerCase();
@@ -1780,127 +1923,61 @@ function toEventName(jsxPropName) {
1780
1923
  }
1781
1924
  function applyRestAttrs(el, source, excludeKeys) {
1782
1925
  const exclude = new Set(excludeKeys);
1926
+ const classified = [];
1783
1927
  for (const key of Object.keys(source)) {
1784
1928
  if (exclude.has(key))
1785
1929
  continue;
1786
- if (key === "ref") {
1930
+ classified.push({ key, c: classifyDOMProp(key) });
1931
+ }
1932
+ for (const { key, c } of classified) {
1933
+ if (c.kind === "ref") {
1787
1934
  const ref = source[key];
1788
1935
  if (typeof ref === "function")
1789
1936
  ref(el);
1790
- continue;
1791
- }
1792
- if (key.startsWith("on") && key.length > 2 && key[2] === key[2].toUpperCase()) {
1937
+ } else if (c.kind === "event") {
1793
1938
  const handler = source[key];
1794
1939
  if (typeof handler === "function") {
1795
1940
  el.addEventListener(toEventName(key), handler);
1796
1941
  }
1797
1942
  }
1798
1943
  }
1944
+ const attrEntries = classified.filter(({ c }) => c.kind !== "ref" && c.kind !== "event" && c.kind !== "skip");
1799
1945
  createEffect(() => {
1800
- for (const key of Object.keys(source)) {
1801
- if (exclude.has(key))
1802
- continue;
1803
- if (key === "ref")
1804
- continue;
1805
- if (key === "children")
1806
- continue;
1807
- if (key.startsWith("on") && key.length > 2 && key[2] === key[2].toUpperCase())
1808
- continue;
1946
+ for (const { key, c } of attrEntries) {
1809
1947
  const value = source[key];
1810
- const attr = toAttrName(key);
1811
1948
  if (value != null && value !== false) {
1812
- if (attr === "value" && "value" in el) {
1949
+ if (c.kind === "property" && c.attrName === "value" && "value" in el) {
1813
1950
  const strVal = String(value);
1814
1951
  if (el.value !== strVal)
1815
1952
  el.value = strVal;
1816
- } else if (attr === "checked" && "checked" in el) {
1953
+ } else if (c.kind === "property" && c.attrName === "checked" && "checked" in el) {
1817
1954
  el.checked = !!value;
1818
- } else if (attr === "style") {
1955
+ } else if (c.kind === "boolean") {
1956
+ el.setAttribute(c.attrName, "");
1957
+ } else if (c.kind === "style") {
1819
1958
  const css = styleToCss(value);
1820
1959
  if (css == null)
1821
1960
  el.removeAttribute("style");
1822
1961
  else
1823
1962
  el.setAttribute("style", css);
1824
1963
  } else {
1825
- el.setAttribute(attr, String(value));
1964
+ el.setAttribute(c.attrName, String(value));
1826
1965
  }
1827
1966
  } else {
1828
- if (attr === "checked" && "checked" in el) {
1967
+ if (c.kind === "property" && c.attrName === "value" && "value" in el) {
1968
+ el.value = "";
1969
+ } else if (c.kind === "property" && c.attrName === "checked" && "checked" in el) {
1829
1970
  el.checked = false;
1971
+ } else if (c.kind === "boolean") {
1972
+ el.removeAttribute(c.attrName);
1830
1973
  } else {
1831
- el.removeAttribute(attr);
1974
+ el.removeAttribute(c.attrName);
1832
1975
  }
1833
1976
  }
1834
1977
  }
1835
1978
  });
1836
1979
  }
1837
1980
  // src/runtime/spread-attrs.ts
1838
- var SVG_CAMEL_CASE_ATTRS = new Set([
1839
- "allowReorder",
1840
- "attributeName",
1841
- "attributeType",
1842
- "autoReverse",
1843
- "baseFrequency",
1844
- "baseProfile",
1845
- "calcMode",
1846
- "clipPathUnits",
1847
- "contentScriptType",
1848
- "contentStyleType",
1849
- "diffuseConstant",
1850
- "edgeMode",
1851
- "externalResourcesRequired",
1852
- "filterRes",
1853
- "filterUnits",
1854
- "glyphRef",
1855
- "gradientTransform",
1856
- "gradientUnits",
1857
- "kernelMatrix",
1858
- "kernelUnitLength",
1859
- "keyPoints",
1860
- "keySplines",
1861
- "keyTimes",
1862
- "lengthAdjust",
1863
- "limitingConeAngle",
1864
- "markerHeight",
1865
- "markerUnits",
1866
- "markerWidth",
1867
- "maskContentUnits",
1868
- "maskUnits",
1869
- "numOctaves",
1870
- "pathLength",
1871
- "patternContentUnits",
1872
- "patternTransform",
1873
- "patternUnits",
1874
- "pointsAtX",
1875
- "pointsAtY",
1876
- "pointsAtZ",
1877
- "preserveAlpha",
1878
- "preserveAspectRatio",
1879
- "primitiveUnits",
1880
- "refX",
1881
- "refY",
1882
- "repeatCount",
1883
- "repeatDur",
1884
- "requiredExtensions",
1885
- "requiredFeatures",
1886
- "specularConstant",
1887
- "specularExponent",
1888
- "spreadMethod",
1889
- "startOffset",
1890
- "stdDeviation",
1891
- "stitchTiles",
1892
- "surfaceScale",
1893
- "systemLanguage",
1894
- "tableValues",
1895
- "targetX",
1896
- "targetY",
1897
- "textLength",
1898
- "viewBox",
1899
- "viewTarget",
1900
- "xChannelSelector",
1901
- "yChannelSelector",
1902
- "zoomAndPan"
1903
- ]);
1904
1981
  function spreadAttrs(obj) {
1905
1982
  if (!obj || typeof obj !== "object")
1906
1983
  return "";
@@ -1908,18 +1985,20 @@ function spreadAttrs(obj) {
1908
1985
  for (const [key, value] of Object.entries(obj)) {
1909
1986
  if (value == null || value === false)
1910
1987
  continue;
1911
- if (key.startsWith("on") && key.length > 2 && key[2] === key[2].toUpperCase())
1912
- continue;
1913
- if (key === "children")
1988
+ const c = classifyDOMProp(key);
1989
+ if (c.kind === "event" || c.kind === "skip" || c.kind === "ref")
1914
1990
  continue;
1915
- if (key === "style") {
1991
+ if (c.kind === "style") {
1916
1992
  const css = styleToCss(value);
1917
1993
  if (css != null)
1918
1994
  parts.push(`style="${css}"`);
1919
1995
  continue;
1920
1996
  }
1921
- const attr = key === "className" ? "class" : key === "htmlFor" ? "for" : SVG_CAMEL_CASE_ATTRS.has(key) ? key : key.replace(/([A-Z])/g, "-$1").toLowerCase();
1922
- parts.push(value === true ? attr : `${attr}="${value}"`);
1997
+ if (c.kind === "boolean" && value === true) {
1998
+ parts.push(c.attrName);
1999
+ } else {
2000
+ parts.push(`${c.attrName}="${value}"`);
2001
+ }
1923
2002
  }
1924
2003
  return parts.join(" ");
1925
2004
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barefootjs/client",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "BarefootJS client package: reactive primitives (SSR-safe) plus browser runtime under the `/runtime` subpath (compiler target)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -55,10 +55,10 @@
55
55
  "directory": "packages/client"
56
56
  },
57
57
  "dependencies": {
58
- "@barefootjs/shared": "0.1.2"
58
+ "@barefootjs/shared": "0.2.0"
59
59
  },
60
60
  "peerDependencies": {
61
- "@barefootjs/jsx": "0.1.2"
61
+ "@barefootjs/jsx": "0.2.0"
62
62
  },
63
63
  "peerDependenciesMeta": {
64
64
  "@barefootjs/jsx": {
@@ -66,7 +66,7 @@
66
66
  }
67
67
  },
68
68
  "devDependencies": {
69
- "@barefootjs/jsx": "0.1.2",
69
+ "@barefootjs/jsx": "0.2.0",
70
70
  "@happy-dom/global-registrator": "^20.0.11",
71
71
  "typescript": "^5.0.0"
72
72
  }
@@ -6,16 +6,9 @@
6
6
  */
7
7
 
8
8
  import { createEffect } from '@barefootjs/client/reactive'
9
+ import { classifyDOMProp, type DOMPropClassification } from '@barefootjs/shared'
9
10
  import { styleToCss } from './style'
10
11
 
11
- /** Map of JSX prop names to HTML attribute names */
12
- function toAttrName(key: string): string {
13
- if (key === 'className') return 'class'
14
- if (key === 'htmlFor') return 'for'
15
- // Convert camelCase to kebab-case for data-* and aria-* style attributes
16
- return key.replace(/([A-Z])/g, '-$1').toLowerCase()
17
- }
18
-
19
12
  /**
20
13
  * Convert a JSX event prop name to a DOM event name for addEventListener.
21
14
  * Handles: camelCase → lowercase, plus special mappings (doubleclick → dblclick).
@@ -23,7 +16,6 @@ function toAttrName(key: string): string {
23
16
  */
24
17
  const jsxToDomEventMap: Record<string, string> = { doubleclick: 'dblclick' }
25
18
  function toEventName(jsxPropName: string): string {
26
- // onKeyDown → 'k' + 'eyDown' → 'keydown'
27
19
  const raw = (jsxPropName[2].toLowerCase() + jsxPropName.slice(3)).toLowerCase()
28
20
  return jsxToDomEventMap[raw] ?? raw
29
21
  }
@@ -43,15 +35,19 @@ export function applyRestAttrs(
43
35
  ): void {
44
36
  const exclude = new Set(excludeKeys)
45
37
 
46
- // Wire up event handlers and ref callbacks once (not reactively)
38
+ // Precompute classifications once keys are stable for rest props.
39
+ const classified: Array<{ key: string; c: DOMPropClassification }> = []
47
40
  for (const key of Object.keys(source)) {
48
41
  if (exclude.has(key)) continue
49
- if (key === 'ref') {
42
+ classified.push({ key, c: classifyDOMProp(key) })
43
+ }
44
+
45
+ // Wire up event handlers and ref callbacks once (not reactively)
46
+ for (const { key, c } of classified) {
47
+ if (c.kind === 'ref') {
50
48
  const ref = source[key]
51
49
  if (typeof ref === 'function') (ref as (el: Element) => void)(el)
52
- continue
53
- }
54
- if (key.startsWith('on') && key.length > 2 && key[2] === key[2].toUpperCase()) {
50
+ } else if (c.kind === 'event') {
55
51
  const handler = source[key]
56
52
  if (typeof handler === 'function') {
57
53
  el.addEventListener(toEventName(key), handler as EventListener)
@@ -59,49 +55,39 @@ export function applyRestAttrs(
59
55
  }
60
56
  }
61
57
 
62
- createEffect(() => {
63
- for (const key of Object.keys(source)) {
64
- if (exclude.has(key)) continue
65
-
66
- // Event handlers and ref are wired up above, not as attributes
67
- if (key === 'ref') continue
68
- // `children` is a JSX construct rendered inside the element, never
69
- // a DOM attribute. Without this exclusion, parent components that
70
- // pass `children` through `{...props}` end up with
71
- // `children="<p ...>...</p>"` written as a literal attribute on
72
- // the wrapper div. The matching `spreadAttrs` (SSR-string) path
73
- // already skips `children` for the same reason.
74
- if (key === 'children') continue
75
- if (key.startsWith('on') && key.length > 2 && key[2] === key[2].toUpperCase()) continue
58
+ // Filter to only attr-like entries for the reactive loop
59
+ const attrEntries = classified.filter(
60
+ ({ c }) => c.kind !== 'ref' && c.kind !== 'event' && c.kind !== 'skip',
61
+ )
76
62
 
63
+ createEffect(() => {
64
+ for (const { key, c } of attrEntries) {
77
65
  const value = source[key]
78
- const attr = toAttrName(key)
79
66
 
80
67
  if (value != null && value !== false) {
81
- // Use DOM property for value/checked (setAttribute sets the default, not current)
82
- if (attr === 'value' && 'value' in el) {
68
+ if (c.kind === 'property' && c.attrName === 'value' && 'value' in el) {
83
69
  const strVal = String(value)
84
70
  if ((el as HTMLInputElement).value !== strVal) (el as HTMLInputElement).value = strVal
85
- } else if (attr === 'checked' && 'checked' in el) {
71
+ } else if (c.kind === 'property' && c.attrName === 'checked' && 'checked' in el) {
86
72
  (el as HTMLInputElement).checked = !!value
87
- } else if (attr === 'style') {
88
- // Route the `style` prop through `styleToCss` so object literals
89
- // (`{'--err': errorHue()}`) and inline strings (`'color:red'`)
90
- // both reach the DOM as a real CSS string instead of
91
- // `[object Object]`. Mirrors the compiler's
92
- // `setAttribute('style', styleToCss(...))` path used when the
93
- // attribute is bound directly on a JSX element.
73
+ } else if (c.kind === 'boolean') {
74
+ el.setAttribute(c.attrName, '')
75
+ } else if (c.kind === 'style') {
94
76
  const css = styleToCss(value)
95
77
  if (css == null) el.removeAttribute('style')
96
78
  else el.setAttribute('style', css)
97
79
  } else {
98
- el.setAttribute(attr, String(value))
80
+ el.setAttribute(c.attrName, String(value))
99
81
  }
100
82
  } else {
101
- if (attr === 'checked' && 'checked' in el) {
83
+ if (c.kind === 'property' && c.attrName === 'value' && 'value' in el) {
84
+ (el as HTMLInputElement).value = ''
85
+ } else if (c.kind === 'property' && c.attrName === 'checked' && 'checked' in el) {
102
86
  (el as HTMLInputElement).checked = false
87
+ } else if (c.kind === 'boolean') {
88
+ el.removeAttribute(c.attrName)
103
89
  } else {
104
- el.removeAttribute(attr)
90
+ el.removeAttribute(c.attrName)
105
91
  }
106
92
  }
107
93
  }
@@ -6,83 +6,33 @@
6
6
  * a static string for server/template rendering of computed local spreads.
7
7
  */
8
8
 
9
+ import { classifyDOMProp } from '@barefootjs/shared'
9
10
  import { styleToCss } from './style'
10
11
 
11
- /**
12
- * SVG attributes that are case-sensitive and MUST stay in camelCase.
13
- *
14
- * The default JSX-prop → HTML-attribute rewrite lower-cases camelCase
15
- * (`fooBar` → `foo-bar`), which is correct for HTML attrs and for the
16
- * CSS-style SVG presentation attrs (`strokeWidth` → `stroke-width`).
17
- * The XML-namespaced SVG attrs below, though, are case-sensitive in
18
- * the spec: `viewBox` lower-cased to `view-box` makes the browser
19
- * treat it as an unknown user attribute and the SVG no longer renders
20
- * (pointer events stop hitting the inner geometry — surfaced as the
21
- * Form Builder e2e regression in #1244's merge-emit follow-up).
22
- *
23
- * Coordinates with the compile-time `SVG_CAMEL_TO_KEBAB` table in
24
- * `packages/jsx/src/ir-to-client-js/utils.ts`: presentation attrs
25
- * (`clipPath`, `strokeWidth`, …) live there and must NOT appear here,
26
- * or the same JSX prop would lower to `clip-path` via the explicit-
27
- * attr path and stay `clipPath` via the spread path — a silent
28
- * divergence. The list below is XML attribute names that have no
29
- * kebab-case mirror (`viewBox`, `clipPathUnits`, …).
30
- *
31
- * The list mirrors React DOM's `DOMProperty` case-preserving entries
32
- * (only the attributes that appear on actual SVG elements; ARIA and
33
- * XLink namespaces are unrelated and handled by their `aria-*` /
34
- * `xlink:*` literal prefix).
35
- */
36
- const SVG_CAMEL_CASE_ATTRS: ReadonlySet<string> = new Set([
37
- 'allowReorder', 'attributeName', 'attributeType', 'autoReverse',
38
- 'baseFrequency', 'baseProfile', 'calcMode', 'clipPathUnits',
39
- 'contentScriptType', 'contentStyleType', 'diffuseConstant', 'edgeMode',
40
- 'externalResourcesRequired', 'filterRes', 'filterUnits', 'glyphRef',
41
- 'gradientTransform', 'gradientUnits', 'kernelMatrix', 'kernelUnitLength',
42
- 'keyPoints', 'keySplines', 'keyTimes', 'lengthAdjust', 'limitingConeAngle',
43
- 'markerHeight', 'markerUnits', 'markerWidth', 'maskContentUnits',
44
- 'maskUnits', 'numOctaves', 'pathLength', 'patternContentUnits',
45
- 'patternTransform', 'patternUnits', 'pointsAtX', 'pointsAtY', 'pointsAtZ',
46
- 'preserveAlpha', 'preserveAspectRatio', 'primitiveUnits', 'refX', 'refY',
47
- 'repeatCount', 'repeatDur', 'requiredExtensions', 'requiredFeatures',
48
- 'specularConstant', 'specularExponent', 'spreadMethod', 'startOffset',
49
- 'stdDeviation', 'stitchTiles', 'surfaceScale', 'systemLanguage',
50
- 'tableValues', 'targetX', 'targetY', 'textLength', 'viewBox', 'viewTarget',
51
- 'xChannelSelector', 'yChannelSelector', 'zoomAndPan',
52
- ])
53
-
54
12
  /**
55
13
  * Convert an object to an HTML attribute string.
56
- * Aligned with applyRestAttrs conventions: skips null/undefined/false,
57
- * event handlers, maps className→class and htmlFor→for. The `style`
58
- * prop is routed through `styleToCss` so object literals serialize to
59
- * a real CSS string (matching the reactive `applyRestAttrs` path).
60
- *
61
- * SVG attributes listed in `SVG_CAMEL_CASE_ATTRS` are preserved
62
- * verbatim — the SVG XML spec is case-sensitive for those names.
14
+ * Uses the shared classifyDOMProp classifier to determine how each prop
15
+ * maps to the DOM. Skips null/undefined/false, event handlers, ref, and
16
+ * children. The `style` prop is routed through `styleToCss` so object
17
+ * literals serialize to a real CSS string.
63
18
  */
64
19
  export function spreadAttrs(obj: Record<string, unknown>): string {
65
20
  if (!obj || typeof obj !== 'object') return ''
66
21
  const parts: string[] = []
67
22
  for (const [key, value] of Object.entries(obj)) {
68
23
  if (value == null || value === false) continue
69
- // Skip event handlers
70
- if (key.startsWith('on') && key.length > 2 && key[2] === key[2].toUpperCase()) continue
71
- // Skip children prop
72
- if (key === 'children') continue
73
- if (key === 'style') {
24
+ const c = classifyDOMProp(key)
25
+ if (c.kind === 'event' || c.kind === 'skip' || c.kind === 'ref') continue
26
+ if (c.kind === 'style') {
74
27
  const css = styleToCss(value)
75
28
  if (css != null) parts.push(`style="${css}"`)
76
29
  continue
77
30
  }
78
- // Map JSX prop names to HTML attribute names. Case-sensitive SVG
79
- // attrs keep their camelCase per the spec; HTML / CSS-style SVG
80
- // presentation attrs lower-case to kebab-case.
81
- const attr = key === 'className' ? 'class'
82
- : key === 'htmlFor' ? 'for'
83
- : SVG_CAMEL_CASE_ATTRS.has(key) ? key
84
- : key.replace(/([A-Z])/g, '-$1').toLowerCase()
85
- parts.push(value === true ? attr : `${attr}="${value}"`)
31
+ if (c.kind === 'boolean' && value === true) {
32
+ parts.push(c.attrName)
33
+ } else {
34
+ parts.push(`${c.attrName}="${value}"`)
35
+ }
86
36
  }
87
37
  return parts.join(' ')
88
38
  }