@barefootjs/client 0.33.1 → 0.33.3

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.
@@ -1737,8 +1737,7 @@ function hydrateCommentScope(comment) {
1737
1737
  if (hydratedScopes.has(proxyEl))
1738
1738
  return;
1739
1739
  commentScopeRegistry.set(proxyEl, { commentNode: comment, scopeId });
1740
- const parsed = parseProps(propsJson || null, `comment scope ${scopeId}`);
1741
- const props = parsed[name] ?? {};
1740
+ const props = parseProps(propsJson || null, `comment scope ${scopeId}`);
1742
1741
  runInit(proxyEl, def, props);
1743
1742
  }
1744
1743
  function nextElementSibling2(node) {
@@ -1775,25 +1774,25 @@ function mountRowRoot(el) {
1775
1774
  }
1776
1775
  return el;
1777
1776
  }
1778
- function createComponent(nameOrDef, props = {}, key, slot, mountAt) {
1779
- const element = materializeComponent(nameOrDef, props, key, slot, mountAt);
1777
+ function createComponent(nameOrDef, props = {}, key, slot, mountAt, keyAttrName2 = BF_KEY) {
1778
+ const element = materializeComponent(nameOrDef, props, key, slot, mountAt, keyAttrName2);
1780
1779
  if (mountAt && mountAt.parentNode && element !== mountAt) {
1781
1780
  mountAt.replaceWith(element);
1782
1781
  }
1783
1782
  return element;
1784
1783
  }
1785
- function materializeComponent(nameOrDef, props = {}, key, slot, mountAt) {
1784
+ function materializeComponent(nameOrDef, props = {}, key, slot, mountAt, keyAttrName2 = BF_KEY) {
1786
1785
  if (props == null)
1787
1786
  props = {};
1788
1787
  const rowMount = mountAt ? null : takeRowMountPoint();
1789
1788
  if (typeof nameOrDef !== "string") {
1790
- return createComponentFromDef(nameOrDef, props, key, mountAt, rowMount);
1789
+ return createComponentFromDef(nameOrDef, props, key, mountAt, rowMount, keyAttrName2);
1791
1790
  }
1792
1791
  const name = nameOrDef;
1793
1792
  const templateFn = getTemplate(name);
1794
1793
  if (!templateFn) {
1795
1794
  console.warn(`[BarefootJS] Template not found for component: ${name}`);
1796
- return createPlaceholder(name, key);
1795
+ return createPlaceholder(name, key, keyAttrName2);
1797
1796
  }
1798
1797
  const childrenDescriptor = Object.getOwnPropertyDescriptor(props, "children");
1799
1798
  const childrenIsGetter = childrenDescriptor != null && typeof childrenDescriptor.get === "function";
@@ -1818,13 +1817,16 @@ function materializeComponent(nameOrDef, props = {}, key, slot, mountAt) {
1818
1817
  });
1819
1818
  const def = getRegisteredDef(name);
1820
1819
  const isCommentWrapper = def?.comment === true;
1820
+ const isFragmentRoot = def?.fragmentRoot === true;
1821
1821
  const derivedScopeId = slot?.parent && slot.mount ? `${slot.parent}_${slot.mount}` : null;
1822
- const scopeId = isCommentWrapper ? null : derivedScopeId ?? `${def?.name ?? name}_${generateId()}`;
1822
+ const scopeId = isCommentWrapper && !isFragmentRoot ? null : derivedScopeId ?? `${def?.name ?? name}_${generateId()}`;
1823
1823
  const prevParentScopeId = _parentScopeId;
1824
1824
  if (scopeId) {
1825
1825
  _parentScopeId = scopeId;
1826
1826
  } else if (slot?.parent) {
1827
1827
  _parentScopeId = slot.parent;
1828
+ } else if (!_parentScopeId) {
1829
+ _parentScopeId = `${def?.name ?? name}_${generateId()}`;
1828
1830
  }
1829
1831
  let html;
1830
1832
  try {
@@ -1832,12 +1834,18 @@ function materializeComponent(nameOrDef, props = {}, key, slot, mountAt) {
1832
1834
  } finally {
1833
1835
  _parentScopeId = prevParentScopeId;
1834
1836
  }
1835
- const element = parseHTML(html.trim()).firstChild;
1837
+ const parsedFragment = parseHTML(html.trim());
1838
+ const roots = isFragmentRoot ? Array.from(parsedFragment.childNodes) : parsedFragment.firstChild ? [parsedFragment.firstChild] : [];
1839
+ const element = isFragmentRoot ? roots.find((node) => node.nodeType === Node.ELEMENT_NODE) : roots[0];
1840
+ if (isFragmentRoot && roots.length > 0 && !element) {
1841
+ console.warn(`[BarefootJS] Fragment-root component ${name} rendered no element root; ` + "a scope needs at least one element to attach to. Wrap the content in an element.");
1842
+ return createPlaceholder(name, key, keyAttrName2);
1843
+ }
1836
1844
  if (!element) {
1837
1845
  console.warn(`[BarefootJS] Template returned empty HTML for component: ${name}`);
1838
- return createPlaceholder(name, key);
1846
+ return createPlaceholder(name, key, keyAttrName2);
1839
1847
  }
1840
- if (scopeId) {
1848
+ if (scopeId && !isFragmentRoot) {
1841
1849
  element.setAttribute(BF_SCOPE, scopeId);
1842
1850
  }
1843
1851
  if (slot) {
@@ -1846,13 +1854,38 @@ function materializeComponent(nameOrDef, props = {}, key, slot, mountAt) {
1846
1854
  element.setAttribute(BF_AT, slot.mount);
1847
1855
  }
1848
1856
  if (key !== undefined) {
1849
- element.setAttribute(BF_KEY, String(key));
1857
+ element.setAttribute(keyAttrName2, String(key));
1858
+ }
1859
+ const fragmentComments = isFragmentRoot && scopeId ? {
1860
+ start: document.createComment(`${BF_SCOPE_COMMENT_PREFIX}${scopeId}`),
1861
+ end: document.createComment(`${BF_SCOPE_COMMENT_END_PREFIX}${scopeId}`)
1862
+ } : null;
1863
+ if (fragmentComments) {
1864
+ commentScopeRegistry.set(element, { commentNode: fragmentComments.start, scopeId });
1850
1865
  }
1851
1866
  const rootIsDeferredPlaceholder = element.hasAttribute(BF_PLACEHOLDER);
1867
+ let bareFragment = null;
1852
1868
  if (mountAt && !rootIsDeferredPlaceholder) {
1853
- mountAt.replaceWith(element);
1869
+ if (fragmentComments) {
1870
+ mountAt.replaceWith(fragmentComments.start, ...roots, fragmentComments.end);
1871
+ } else {
1872
+ mountAt.replaceWith(element);
1873
+ }
1854
1874
  } else if (rowMount && !rootIsDeferredPlaceholder) {
1855
- rowMount.container.insertBefore(element, rowMount.anchor);
1875
+ if (fragmentComments) {
1876
+ rowMount.container.insertBefore(fragmentComments.start, rowMount.anchor);
1877
+ rowMount.container.insertBefore(element, rowMount.anchor);
1878
+ rowMount.container.insertBefore(fragmentComments.end, rowMount.anchor);
1879
+ element.__bfScopeComments = fragmentComments;
1880
+ if (roots.length > 1) {
1881
+ console.warn(`[BarefootJS] Fragment-root component ${name} used as a loop row renders ` + `${roots.length} top-level nodes; only the first element is connected here. ` + "See https://github.com/piconic-ai/barefootjs/issues/2733");
1882
+ }
1883
+ } else {
1884
+ rowMount.container.insertBefore(element, rowMount.anchor);
1885
+ }
1886
+ } else if (fragmentComments && !rootIsDeferredPlaceholder) {
1887
+ bareFragment = document.createDocumentFragment();
1888
+ bareFragment.append(fragmentComments.start, ...roots, fragmentComments.end);
1856
1889
  }
1857
1890
  const prevScope = setCurrentScope(element);
1858
1891
  let placeholderWrapper = null;
@@ -1880,18 +1913,30 @@ function materializeComponent(nameOrDef, props = {}, key, slot, mountAt) {
1880
1913
  }
1881
1914
  setCurrentScope(prevScope);
1882
1915
  hydratedScopes.add(element);
1883
- return element;
1916
+ return bareFragment ?? element;
1917
+ }
1918
+ var FIRST_TAG_PATTERN = /<([a-zA-Z][^\s/>]*)/;
1919
+ var TAG_HEAD_PATTERN = /^(<[a-zA-Z][^\s/>]*)/;
1920
+ function spliceAttrsAfterFirstTag(html, attrs) {
1921
+ const firstElMatch = html.match(FIRST_TAG_PATTERN);
1922
+ if (!firstElMatch)
1923
+ return html;
1924
+ const insertPos = firstElMatch.index;
1925
+ return html.slice(0, insertPos) + html.slice(insertPos).replace(TAG_HEAD_PATTERN, `$1${attrs}`);
1884
1926
  }
1885
1927
  function renderChild(name, props, key, slotSuffix) {
1886
1928
  const templateFn = getTemplate(name);
1887
1929
  const suffix = slotSuffix ? `_${slotSuffix}` : "";
1888
- const displayName = getRegisteredDef(name)?.name ?? name;
1930
+ const def = getRegisteredDef(name);
1931
+ const displayName = def?.name ?? name;
1932
+ const isFragmentRoot = def?.fragmentRoot === true;
1889
1933
  const scopePrefix = _parentScopeId && slotSuffix ? _parentScopeId : `${displayName}_${generateId()}`;
1934
+ const scopeId = `${scopePrefix}${suffix}`;
1890
1935
  const keyAttr = key !== undefined ? ` ${BF_KEY}="${key}"` : "";
1891
1936
  const slotAttrs = _parentScopeId && slotSuffix ? ` ${BF_HOST}="${_parentScopeId}" ${BF_AT}="${slotSuffix}"` : "";
1892
- const bfsAttr = `${BF_SCOPE}="${scopePrefix}${suffix}"`;
1937
+ const bfsAttr = `${BF_SCOPE}="${scopeId}"`;
1893
1938
  if (!templateFn) {
1894
- return `<div ${bfsAttr}${slotAttrs}${keyAttr}></div>`;
1939
+ return isFragmentRoot ? `<!--${BF_SCOPE_COMMENT_PREFIX}${scopeId}--><div></div><!--${BF_SCOPE_COMMENT_END_PREFIX}${scopeId}-->` : `<div ${bfsAttr}${slotAttrs}${keyAttr}></div>`;
1895
1940
  }
1896
1941
  const prevParentScopeId = _parentScopeId;
1897
1942
  _parentScopeId = `${scopePrefix}${suffix}`;
@@ -1902,7 +1947,12 @@ function renderChild(name, props, key, slotSuffix) {
1902
1947
  _parentScopeId = prevParentScopeId;
1903
1948
  }
1904
1949
  let html = raw.trim().replace(PLACEHOLDER_ATTR_PATTERN, _parentScopeId ? ` bf-s="${_parentScopeId}"` : "");
1905
- const firstElMatch = html.match(/<(\w+)/);
1950
+ if (isFragmentRoot) {
1951
+ const hostSuffix = _parentScopeId && slotSuffix ? `|h=${_parentScopeId}|m=${slotSuffix}` : "";
1952
+ const keyedHtml = keyAttr ? spliceAttrsAfterFirstTag(html, keyAttr) : html;
1953
+ return `<!--${BF_SCOPE_COMMENT_PREFIX}${scopeId}${hostSuffix}-->${keyedHtml}<!--${BF_SCOPE_COMMENT_END_PREFIX}${scopeId}-->`;
1954
+ }
1955
+ const firstElMatch = html.match(FIRST_TAG_PATTERN);
1906
1956
  if (!firstElMatch)
1907
1957
  return html;
1908
1958
  const insertPos = firstElMatch.index;
@@ -1911,20 +1961,20 @@ function renderChild(name, props, key, slotSuffix) {
1911
1961
  if (ROOT_HAS_BFS_PATTERN.test(afterInsert)) {
1912
1962
  if (!extraAttrs)
1913
1963
  return html;
1914
- return html.slice(0, insertPos) + afterInsert.replace(/^(<\w+)/, `$1${extraAttrs}`);
1964
+ return html.slice(0, insertPos) + afterInsert.replace(TAG_HEAD_PATTERN, `$1${extraAttrs}`);
1915
1965
  }
1916
- return html.slice(0, insertPos) + afterInsert.replace(/^(<\w+)/, `$1 ${bfsAttr}${extraAttrs}`);
1966
+ return html.slice(0, insertPos) + afterInsert.replace(TAG_HEAD_PATTERN, `$1 ${bfsAttr}${extraAttrs}`);
1917
1967
  }
1918
1968
  var PLACEHOLDER_ATTR_PATTERN = new RegExp(`\\s+bf-s="${BF_PARENT_SCOPE_PLACEHOLDER}"`, "g");
1919
1969
  var ROOT_HAS_BFS_PATTERN = /^<\w+[^>]*\sbf-s="/;
1920
1970
  function generateId() {
1921
1971
  return Math.random().toString(36).slice(2, 8);
1922
1972
  }
1923
- function createPlaceholder(name, key) {
1973
+ function createPlaceholder(name, key, keyAttrName2 = BF_KEY) {
1924
1974
  const el = document.createElement("div");
1925
1975
  el.setAttribute(BF_SCOPE, `${name}_placeholder`);
1926
1976
  if (key !== undefined) {
1927
- el.setAttribute(BF_KEY, String(key));
1977
+ el.setAttribute(keyAttrName2, String(key));
1928
1978
  }
1929
1979
  el.textContent = `[${name}]`;
1930
1980
  el.style.cssText = "color: red; border: 1px dashed red; padding: 4px;";
@@ -1973,6 +2023,9 @@ function escapeTextOrMarkup(value) {
1973
2023
  return value[BF_MARKUP_BRAND];
1974
2024
  return escapeText(value);
1975
2025
  }
2026
+ function markupOrEmpty(value) {
2027
+ return value == null ? "" : value;
2028
+ }
1976
2029
  function escapeTextOrNode(value) {
1977
2030
  if (isBfMarkup(value))
1978
2031
  return value[BF_MARKUP_BRAND];
@@ -2034,7 +2087,7 @@ function insertGetterChildren(element, children) {
2034
2087
  element.appendChild(document.createTextNode(String(children)));
2035
2088
  }
2036
2089
  }
2037
- function createComponentFromDef(def, props, key, mountAt, rowMount) {
2090
+ function createComponentFromDef(def, props, key, mountAt, rowMount, keyAttrName2 = BF_KEY) {
2038
2091
  if (!def.template) {
2039
2092
  throw new Error("[BarefootJS] createComponent with ComponentDef requires a template function");
2040
2093
  }
@@ -2051,7 +2104,7 @@ function createComponentFromDef(def, props, key, mountAt, rowMount) {
2051
2104
  const scopeId = `${name}_${generateId()}`;
2052
2105
  element.setAttribute(BF_SCOPE, scopeId);
2053
2106
  if (key !== undefined) {
2054
- element.setAttribute(BF_KEY, String(key));
2107
+ element.setAttribute(keyAttrName2, String(key));
2055
2108
  }
2056
2109
  if (mountAt) {
2057
2110
  mountAt.replaceWith(element);
@@ -2243,6 +2296,11 @@ function upsertChildItem(primaryEl, name, slotId, props, key, anchorScope) {
2243
2296
  return null;
2244
2297
  }
2245
2298
  // src/runtime/map-array.ts
2299
+ function scopeIdOf(start) {
2300
+ const rest = (start.nodeValue ?? "").slice(BF_SCOPE_COMMENT_PREFIX.length);
2301
+ const pipe = rest.indexOf("|");
2302
+ return pipe >= 0 ? rest.slice(0, pipe) : rest;
2303
+ }
2246
2304
  function findLoopMarkers2(container, markerId) {
2247
2305
  let start = null;
2248
2306
  let end = null;
@@ -2280,31 +2338,55 @@ function findItemRanges(start, end) {
2280
2338
  const ranges = [];
2281
2339
  let current = null;
2282
2340
  let sawItemMarker = false;
2341
+ let pendingScopeStart = null;
2342
+ let openScopeComments = null;
2283
2343
  let node = start.nextSibling;
2284
2344
  while (node && node !== end) {
2285
- if (node.nodeType === Node.COMMENT_NODE && node.nodeValue === BF_LOOP_ITEM) {
2286
- sawItemMarker = true;
2287
- current = { startMarker: node, primaryEl: null, extras: [] };
2288
- ranges.push(current);
2345
+ if (node.nodeType === Node.COMMENT_NODE) {
2346
+ const value = node.nodeValue ?? "";
2347
+ if (value === BF_LOOP_ITEM) {
2348
+ sawItemMarker = true;
2349
+ current = { startMarker: node, primaryEl: null, extras: [], scopeComments: null };
2350
+ ranges.push(current);
2351
+ } else if (value.startsWith(BF_SCOPE_COMMENT_PREFIX)) {
2352
+ pendingScopeStart = node;
2353
+ } else if (openScopeComments && value === BF_SCOPE_COMMENT_END_PREFIX + scopeIdOf(openScopeComments.start)) {
2354
+ openScopeComments.end = node;
2355
+ openScopeComments = null;
2356
+ }
2289
2357
  } else if (node.nodeType === Node.ELEMENT_NODE) {
2290
2358
  const el = node;
2359
+ let scopeComments = null;
2360
+ if (pendingScopeStart) {
2361
+ scopeComments = { start: pendingScopeStart, end: null };
2362
+ openScopeComments = scopeComments;
2363
+ pendingScopeStart = null;
2364
+ }
2291
2365
  if (sawItemMarker) {
2292
- if (!current.primaryEl)
2366
+ if (!current.primaryEl) {
2293
2367
  current.primaryEl = el;
2294
- else
2368
+ current.scopeComments = scopeComments;
2369
+ } else
2295
2370
  current.extras.push(el);
2296
2371
  } else {
2297
- ranges.push({ startMarker: null, primaryEl: el, extras: [] });
2372
+ ranges.push({ startMarker: null, primaryEl: el, extras: [], scopeComments });
2298
2373
  }
2299
2374
  }
2300
2375
  node = node.nextSibling;
2301
2376
  }
2302
- return ranges.filter((r) => r.primaryEl !== null);
2377
+ return ranges.filter((r) => r.primaryEl !== null).map((r) => ({
2378
+ ...r,
2379
+ scopeComments: r.scopeComments?.end ? r.scopeComments : null
2380
+ }));
2303
2381
  }
2304
2382
  function insertScope(scope, target, anchor) {
2305
2383
  if (scope.startMarker)
2306
2384
  target.insertBefore(scope.startMarker, anchor);
2385
+ if (scope.scopeComments)
2386
+ target.insertBefore(scope.scopeComments.start, anchor);
2307
2387
  target.insertBefore(scope.primaryEl, anchor);
2388
+ if (scope.scopeComments)
2389
+ target.insertBefore(scope.scopeComments.end, anchor);
2308
2390
  for (const ex of scope.extras)
2309
2391
  target.insertBefore(ex, anchor);
2310
2392
  }
@@ -2343,19 +2425,24 @@ function longestIncreasingSubsequenceIndices(arr) {
2343
2425
  function removeScope(scope) {
2344
2426
  if (scope.startMarker?.parentNode)
2345
2427
  scope.startMarker.remove();
2428
+ if (scope.scopeComments?.start.parentNode)
2429
+ scope.scopeComments.start.remove();
2346
2430
  if (scope.primaryEl.parentNode)
2347
2431
  scope.primaryEl.remove();
2432
+ if (scope.scopeComments?.end.parentNode)
2433
+ scope.scopeComments.end.remove();
2348
2434
  for (const ex of scope.extras) {
2349
2435
  if (ex.parentNode)
2350
2436
  ex.remove();
2351
2437
  }
2352
2438
  }
2353
- function createItemScope(item, index, renderItem, existingPrimary, existingExtras, existingStart, rowMount) {
2439
+ function createItemScope(item, index, renderItem, existingPrimary, existingExtras, existingStart, existingScopeComments, rowMount) {
2354
2440
  let primaryEl;
2355
2441
  let dispose;
2356
2442
  let setItem;
2357
2443
  let extras = [];
2358
2444
  let startMarker = null;
2445
+ let scopeComments = null;
2359
2446
  createRoot((d) => {
2360
2447
  dispose = d;
2361
2448
  const [itemAccessor, itemSetter] = createSignal(item);
@@ -2378,6 +2465,7 @@ function createItemScope(item, index, renderItem, existingPrimary, existingExtra
2378
2465
  if (existingPrimary) {
2379
2466
  extras = existingExtras ?? [];
2380
2467
  startMarker = existingStart ?? null;
2468
+ scopeComments = existingScopeComments ?? null;
2381
2469
  } else {
2382
2470
  const stashed = primaryEl.__bfExtras;
2383
2471
  if (stashed && stashed.length > 0) {
@@ -2385,15 +2473,19 @@ function createItemScope(item, index, renderItem, existingPrimary, existingExtra
2385
2473
  startMarker = document.createComment(BF_LOOP_ITEM);
2386
2474
  }
2387
2475
  delete primaryEl.__bfExtras;
2476
+ const stashedScopeComments = primaryEl.__bfScopeComments;
2477
+ if (stashedScopeComments)
2478
+ scopeComments = stashedScopeComments;
2479
+ delete primaryEl.__bfScopeComments;
2388
2480
  }
2389
2481
  return;
2390
2482
  });
2391
2483
  if (rowMount && !existingPrimary && extras.length > 0 && primaryEl.parentNode) {
2392
2484
  primaryEl.remove();
2393
2485
  }
2394
- return { startMarker, primaryEl, extras, dispose, setItem };
2486
+ return { startMarker, primaryEl, extras, scopeComments, dispose, setItem };
2395
2487
  }
2396
- function mapArray(accessor, container, getKey, renderItem, markerId, bfId) {
2488
+ function mapArray(accessor, container, getKey, renderItem, markerId, bfId, keyAttrName2 = BF_KEY) {
2397
2489
  if (!container)
2398
2490
  return;
2399
2491
  const scopes = new Map;
@@ -2417,24 +2509,26 @@ function mapArray(accessor, container, getKey, renderItem, markerId, bfId) {
2417
2509
  const anchor = endMarker ?? null;
2418
2510
  if (!hydrated) {
2419
2511
  hydrated = true;
2420
- const existingRanges = startMarker ? findItemRanges(startMarker, endMarker) : Array.from(container.children).map((el) => ({ startMarker: null, primaryEl: el, extras: [] }));
2421
- const needsHydration = existingRanges.length > 0 && (!existingRanges[0]?.primaryEl.hasAttribute("data-key") || scopes.size === 0);
2512
+ const existingRanges = startMarker ? findItemRanges(startMarker, endMarker) : Array.from(container.children).map((el) => ({ startMarker: null, primaryEl: el, extras: [], scopeComments: null }));
2513
+ const needsHydration = existingRanges.length > 0;
2422
2514
  if (needsHydration) {
2423
2515
  for (let i2 = 0;i2 < existingRanges.length && i2 < items.length; i2++) {
2424
2516
  const range = existingRanges[i2];
2425
2517
  const item = items[i2];
2426
2518
  const key = getKey ? getKey(item, i2) : String(i2);
2427
- range.primaryEl.setAttribute(BF_KEY, key);
2428
- const scope = createItemScope(item, i2, renderItem, range.primaryEl, range.extras, range.startMarker);
2519
+ if (getKey && !range.primaryEl.getAttribute(keyAttrName2)) {
2520
+ range.primaryEl.setAttribute(keyAttrName2, key);
2521
+ }
2522
+ const scope = createItemScope(item, i2, renderItem, range.primaryEl, range.extras, range.startMarker, range.scopeComments);
2429
2523
  scopes.set(key, scope);
2430
2524
  hydratedScopes.add(range.primaryEl);
2431
2525
  }
2432
2526
  for (let i2 = existingRanges.length;i2 < items.length; i2++) {
2433
2527
  const item = items[i2];
2434
2528
  const key = getKey ? getKey(item, i2) : String(i2);
2435
- const scope = createItemScope(item, i2, renderItem, undefined, undefined, undefined, { container, anchor });
2436
- if (!scope.primaryEl.dataset.key)
2437
- scope.primaryEl.setAttribute(BF_KEY, key);
2529
+ const scope = createItemScope(item, i2, renderItem, undefined, undefined, undefined, undefined, { container, anchor });
2530
+ if (getKey && !scope.primaryEl.getAttribute(keyAttrName2))
2531
+ scope.primaryEl.setAttribute(keyAttrName2, key);
2438
2532
  scopes.set(key, scope);
2439
2533
  insertScope(scope, container, anchor);
2440
2534
  }
@@ -2442,8 +2536,12 @@ function mapArray(accessor, container, getKey, renderItem, markerId, bfId) {
2442
2536
  const range = existingRanges[i2];
2443
2537
  if (range.startMarker?.parentNode)
2444
2538
  range.startMarker.remove();
2539
+ if (range.scopeComments?.start.parentNode)
2540
+ range.scopeComments.start.remove();
2445
2541
  if (range.primaryEl.parentNode)
2446
2542
  range.primaryEl.remove();
2543
+ if (range.scopeComments?.end.parentNode)
2544
+ range.scopeComments.end.remove();
2447
2545
  for (const ex of range.extras) {
2448
2546
  if (ex.parentNode)
2449
2547
  ex.remove();
@@ -2453,14 +2551,15 @@ function mapArray(accessor, container, getKey, renderItem, markerId, bfId) {
2453
2551
  }
2454
2552
  }
2455
2553
  if (scopes.size === 0) {
2456
- const loopRanges = startMarker ? findItemRanges(startMarker, endMarker) : Array.from(container.children).map((el) => ({ startMarker: null, primaryEl: el, extras: [] }));
2554
+ const loopRanges = startMarker ? findItemRanges(startMarker, endMarker) : Array.from(container.children).map((el) => ({ startMarker: null, primaryEl: el, extras: [], scopeComments: null }));
2457
2555
  for (const range of loopRanges) {
2458
- const existingKey = range.primaryEl.dataset?.key;
2556
+ const existingKey = range.primaryEl.getAttribute(keyAttrName2);
2459
2557
  if (existingKey && !scopes.has(existingKey)) {
2460
2558
  scopes.set(existingKey, {
2461
2559
  startMarker: range.startMarker,
2462
2560
  primaryEl: range.primaryEl,
2463
2561
  extras: range.extras,
2562
+ scopeComments: range.scopeComments,
2464
2563
  dispose: () => {},
2465
2564
  setItem: () => {}
2466
2565
  });
@@ -2479,7 +2578,7 @@ function mapArray(accessor, container, getKey, renderItem, markerId, bfId) {
2479
2578
  } else {
2480
2579
  let expectedNodeCount = 0;
2481
2580
  for (const scope of scopes.values()) {
2482
- expectedNodeCount += 1 + scope.extras.length + (scope.startMarker ? 1 : 0);
2581
+ expectedNodeCount += 1 + scope.extras.length + (scope.startMarker ? 1 : 0) + (scope.scopeComments ? 2 : 0);
2483
2582
  }
2484
2583
  let actualNodeCount = 0;
2485
2584
  for (let node = container.firstChild;node; node = node.nextSibling)
@@ -2511,9 +2610,9 @@ function mapArray(accessor, container, getKey, renderItem, markerId, bfId) {
2511
2610
  existing.setItem(item);
2512
2611
  desiredOrder.push(existing);
2513
2612
  } else {
2514
- const scope = createItemScope(item, i2, renderItem, undefined, undefined, undefined, { container, anchor });
2515
- if (!scope.primaryEl.dataset.key)
2516
- scope.primaryEl.setAttribute(BF_KEY, key);
2613
+ const scope = createItemScope(item, i2, renderItem, undefined, undefined, undefined, undefined, { container, anchor });
2614
+ if (getKey && !scope.primaryEl.getAttribute(keyAttrName2))
2615
+ scope.primaryEl.setAttribute(keyAttrName2, key);
2517
2616
  scopes.set(key, scope);
2518
2617
  desiredOrder.push(scope);
2519
2618
  }
@@ -2550,7 +2649,7 @@ function mapArray(accessor, container, getKey, renderItem, markerId, bfId) {
2550
2649
  let j = i;
2551
2650
  while (j < desiredOrder.length && !stationary[j])
2552
2651
  j++;
2553
- const before = j < desiredOrder.length ? desiredOrder[j].startMarker ?? desiredOrder[j].primaryEl : anchor;
2652
+ const before = j < desiredOrder.length ? desiredOrder[j].startMarker ?? desiredOrder[j].scopeComments?.start ?? desiredOrder[j].primaryEl : anchor;
2554
2653
  if (j - i === 1) {
2555
2654
  insertScope(desiredOrder[i], container, before);
2556
2655
  } else {
@@ -2708,7 +2807,7 @@ function mapArrayAnchored(accessor, container, getKey, renderItem, markerId, bfI
2708
2807
  }, bfId);
2709
2808
  }
2710
2809
  // src/runtime/map-array-lazy.ts
2711
- function mapArrayLazy(accessor, container, getKey, plan, markerId, bfId) {
2810
+ function mapArrayLazy(accessor, container, getKey, plan, markerId, bfId, keyAttrName2 = BF_KEY) {
2712
2811
  if (!container)
2713
2812
  return;
2714
2813
  const entries = new Map;
@@ -2747,8 +2846,8 @@ function mapArrayLazy(accessor, container, getKey, plan, markerId, bfId) {
2747
2846
  last: null
2748
2847
  };
2749
2848
  entry.primaryEl = untrack(() => plan.createRow(entry, index));
2750
- if (!entry.primaryEl.dataset.key)
2751
- entry.primaryEl.setAttribute(BF_KEY, key);
2849
+ if (getKey && !entry.primaryEl.getAttribute(keyAttrName2))
2850
+ entry.primaryEl.setAttribute(keyAttrName2, key);
2752
2851
  markStranded();
2753
2852
  return entry;
2754
2853
  };
@@ -2770,7 +2869,7 @@ function mapArrayLazy(accessor, container, getKey, plan, markerId, bfId) {
2770
2869
  const shared = Math.min(doms.length, items.length);
2771
2870
  for (let i2 = 0;i2 < shared; i2++) {
2772
2871
  const el = doms[i2];
2773
- const ssrKey = el.getAttribute(BF_KEY);
2872
+ const ssrKey = el.getAttribute(keyAttrName2);
2774
2873
  const key = ssrKey !== null ? ssrKey : getKey ? getKey(items[i2], i2) : String(i2);
2775
2874
  const entry = { key, primaryEl: el, item: items[i2], refs: null, last: null };
2776
2875
  entries.set(key, entry);
@@ -3712,6 +3811,7 @@ export {
3712
3811
  onMount,
3713
3812
  onCleanup,
3714
3813
  mountRowRoot,
3814
+ markupOrEmpty,
3715
3815
  mapArrayLazy,
3716
3816
  mapArrayAnchored,
3717
3817
  mapArray,
@@ -20,7 +20,25 @@ export interface ComponentDef {
20
20
  init: InitFn;
21
21
  /** Template function for client-side component creation */
22
22
  template?: (props: Record<string, unknown>) => string;
23
- /** When true, use comment-based scope hydration (fragment roots) */
23
+ /**
24
+ * When true, this component's scope has no `bf-s`-carrying element of
25
+ * its own — a proxy element stands in for it. Set for TWO distinct
26
+ * shapes (see `fragmentRoot` below, which tells them apart):
27
+ * - a genuine fragment root (`<>...</>`), where the proxy is the
28
+ * fragment's own rendered content;
29
+ * - a root that is itself a single child component call, where the
30
+ * proxy is that child's own already-scoped element (#2649).
31
+ */
24
32
  comment?: boolean;
33
+ /**
34
+ * True only for the genuine-fragment-root shape of `comment` above
35
+ * (`ir.root.type === 'fragment'`) — never for the root-is-a-child-call
36
+ * shape. `materializeComponent` (component.ts) uses this to decide
37
+ * whether a CSR mount must generate its OWN scope id (fragment root: yes,
38
+ * so nested `renderChild()` calls get parent-prefixed naming matching
39
+ * SSR/hydrate) or leave the scope id null to avoid overwriting the
40
+ * child's own (#2649's shape, #2722).
41
+ */
42
+ fragmentRoot?: boolean;
25
43
  }
26
44
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/runtime/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;;;GAIG;AACH,MAAM,MAAM,MAAM,GAAG,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAA;AAE7E;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,sEAAsE;IACtE,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,kDAAkD;IAClD,IAAI,EAAE,MAAM,CAAA;IACZ,2DAA2D;IAC3D,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,MAAM,CAAA;IACrD,oEAAoE;IACpE,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/runtime/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;;;GAIG;AACH,MAAM,MAAM,MAAM,GAAG,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAA;AAE7E;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,sEAAsE;IACtE,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,kDAAkD;IAClD,IAAI,EAAE,MAAM,CAAA;IACZ,2DAA2D;IAC3D,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,MAAM,CAAA;IACrD;;;;;;;;OAQG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB;;;;;;;;OAQG;IACH,YAAY,CAAC,EAAE,OAAO,CAAA;CACvB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@barefootjs/client",
3
- "version": "0.33.1",
3
+ "version": "0.33.3",
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,7 +55,7 @@
55
55
  "directory": "packages/client"
56
56
  },
57
57
  "dependencies": {
58
- "@barefootjs/shared": "0.33.1"
58
+ "@barefootjs/shared": "0.33.3"
59
59
  },
60
60
  "peerDependencies": {
61
61
  "@barefootjs/jsx": ">=0.2.0"