@barefootjs/client 0.1.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.
Files changed (108) hide show
  1. package/dist/build.d.ts +56 -0
  2. package/dist/build.d.ts.map +1 -0
  3. package/dist/build.js +76 -0
  4. package/dist/context.d.ts +25 -0
  5. package/dist/context.d.ts.map +1 -0
  6. package/dist/csr-adapter.d.ts +26 -0
  7. package/dist/csr-adapter.d.ts.map +1 -0
  8. package/dist/forward-props.d.ts +17 -0
  9. package/dist/forward-props.d.ts.map +1 -0
  10. package/dist/index.d.ts +8 -0
  11. package/dist/index.d.ts.map +1 -0
  12. package/dist/index.js +154 -0
  13. package/dist/reactive.d.ts +150 -0
  14. package/dist/reactive.d.ts.map +1 -0
  15. package/dist/reactive.js +215 -0
  16. package/dist/runtime/apply-rest-attrs.d.ts +16 -0
  17. package/dist/runtime/apply-rest-attrs.d.ts.map +1 -0
  18. package/dist/runtime/branch-slot.d.ts +22 -0
  19. package/dist/runtime/branch-slot.d.ts.map +1 -0
  20. package/dist/runtime/client-marker.d.ts +21 -0
  21. package/dist/runtime/client-marker.d.ts.map +1 -0
  22. package/dist/runtime/component.d.ts +99 -0
  23. package/dist/runtime/component.d.ts.map +1 -0
  24. package/dist/runtime/context.d.ts +40 -0
  25. package/dist/runtime/context.d.ts.map +1 -0
  26. package/dist/runtime/hydrate.d.ts +100 -0
  27. package/dist/runtime/hydrate.d.ts.map +1 -0
  28. package/dist/runtime/hydration-state.d.ts +13 -0
  29. package/dist/runtime/hydration-state.d.ts.map +1 -0
  30. package/dist/runtime/index.d.ts +27 -0
  31. package/dist/runtime/index.d.ts.map +1 -0
  32. package/dist/runtime/index.js +2093 -0
  33. package/dist/runtime/insert.d.ts +75 -0
  34. package/dist/runtime/insert.d.ts.map +1 -0
  35. package/dist/runtime/list.d.ts +21 -0
  36. package/dist/runtime/list.d.ts.map +1 -0
  37. package/dist/runtime/map-array.d.ts +32 -0
  38. package/dist/runtime/map-array.d.ts.map +1 -0
  39. package/dist/runtime/portal.d.ts +96 -0
  40. package/dist/runtime/portal.d.ts.map +1 -0
  41. package/dist/runtime/qsa-item.d.ts +52 -0
  42. package/dist/runtime/qsa-item.d.ts.map +1 -0
  43. package/dist/runtime/query.d.ts +86 -0
  44. package/dist/runtime/query.d.ts.map +1 -0
  45. package/dist/runtime/reconcile-elements.d.ts +44 -0
  46. package/dist/runtime/reconcile-elements.d.ts.map +1 -0
  47. package/dist/runtime/registry.d.ts +53 -0
  48. package/dist/runtime/registry.d.ts.map +1 -0
  49. package/dist/runtime/render.d.ts +35 -0
  50. package/dist/runtime/render.d.ts.map +1 -0
  51. package/dist/runtime/scope.d.ts +28 -0
  52. package/dist/runtime/scope.d.ts.map +1 -0
  53. package/dist/runtime/slot-resolver.d.ts +36 -0
  54. package/dist/runtime/slot-resolver.d.ts.map +1 -0
  55. package/dist/runtime/spread-attrs.d.ts +19 -0
  56. package/dist/runtime/spread-attrs.d.ts.map +1 -0
  57. package/dist/runtime/standalone.js +2278 -0
  58. package/dist/runtime/streaming.d.ts +36 -0
  59. package/dist/runtime/streaming.d.ts.map +1 -0
  60. package/dist/runtime/style.d.ts +17 -0
  61. package/dist/runtime/style.d.ts.map +1 -0
  62. package/dist/runtime/template.d.ts +39 -0
  63. package/dist/runtime/template.d.ts.map +1 -0
  64. package/dist/runtime/types.d.ts +26 -0
  65. package/dist/runtime/types.d.ts.map +1 -0
  66. package/dist/shims.d.ts +21 -0
  67. package/dist/shims.d.ts.map +1 -0
  68. package/dist/slot.d.ts +14 -0
  69. package/dist/slot.d.ts.map +1 -0
  70. package/dist/split-props.d.ts +26 -0
  71. package/dist/split-props.d.ts.map +1 -0
  72. package/dist/unwrap.d.ts +16 -0
  73. package/dist/unwrap.d.ts.map +1 -0
  74. package/package.json +71 -0
  75. package/src/build.ts +92 -0
  76. package/src/context.ts +33 -0
  77. package/src/csr-adapter.ts +134 -0
  78. package/src/forward-props.ts +43 -0
  79. package/src/index.ts +42 -0
  80. package/src/reactive.ts +411 -0
  81. package/src/runtime/apply-rest-attrs.ts +109 -0
  82. package/src/runtime/branch-slot.ts +32 -0
  83. package/src/runtime/client-marker.ts +46 -0
  84. package/src/runtime/component.ts +501 -0
  85. package/src/runtime/context.ts +111 -0
  86. package/src/runtime/hydrate.ts +311 -0
  87. package/src/runtime/hydration-state.ts +13 -0
  88. package/src/runtime/index.ts +96 -0
  89. package/src/runtime/insert.ts +407 -0
  90. package/src/runtime/list.ts +47 -0
  91. package/src/runtime/map-array.ts +381 -0
  92. package/src/runtime/portal.ts +174 -0
  93. package/src/runtime/qsa-item.ts +128 -0
  94. package/src/runtime/query.ts +632 -0
  95. package/src/runtime/reconcile-elements.ts +391 -0
  96. package/src/runtime/registry.ts +160 -0
  97. package/src/runtime/render.ts +105 -0
  98. package/src/runtime/scope.ts +46 -0
  99. package/src/runtime/slot-resolver.ts +66 -0
  100. package/src/runtime/spread-attrs.ts +88 -0
  101. package/src/runtime/streaming.ts +65 -0
  102. package/src/runtime/style.ts +27 -0
  103. package/src/runtime/template.ts +53 -0
  104. package/src/runtime/types.ts +27 -0
  105. package/src/shims.ts +54 -0
  106. package/src/slot.ts +23 -0
  107. package/src/split-props.ts +86 -0
  108. package/src/unwrap.ts +18 -0
@@ -0,0 +1,501 @@
1
+ /**
2
+ * BarefootJS - Component Creation
3
+ *
4
+ * Functions for dynamically creating component instances at runtime.
5
+ * Used by reconcileList() when rendering components in loops.
6
+ */
7
+
8
+ import { getTemplate } from './template'
9
+ import { getComponentInit } from './registry'
10
+ import { getRegisteredDef } from './hydrate'
11
+ import { hydratedScopes } from './hydration-state'
12
+ import { untrack } from '@barefootjs/client/reactive'
13
+ import { setCurrentScope } from './context'
14
+ import { BF_SCOPE, BF_KEY, BF_HOST, BF_AT, BF_PARENT_SCOPE_PLACEHOLDER } from '@barefootjs/shared'
15
+ import type { ComponentDef } from './types'
16
+
17
+ // Parent scope ID context for renderChild() inside insert() branch templates.
18
+ // When set, renderChild uses the parent's scope ID as prefix instead of a random ID,
19
+ // producing scope IDs consistent with SSR (e.g., "ParentName_abc_s5" instead of
20
+ // "Button_random_s5"). This enables $cSingle's getDualScopeIds check to pass.
21
+ // Set by insert() before calling branch.template(), cleared after.
22
+ let _parentScopeId: string | null = null
23
+
24
+ export function setParentScopeId(id: string | null): void {
25
+ _parentScopeId = id
26
+ }
27
+
28
+ // WeakMap to store props update functions for each component element
29
+ // This allows reconcileList to update props when an element is reused
30
+ const propsUpdateMap = new WeakMap<HTMLElement, (props: Record<string, unknown>) => void>()
31
+
32
+ // WeakMap to store the current props for each component element
33
+ // Used to pass props to existing elements when they are reused
34
+ const propsMap = new WeakMap<HTMLElement, Record<string, unknown>>()
35
+
36
+
37
+ /**
38
+ * Create a component instance with DOM element and initialized state.
39
+ *
40
+ * This function:
41
+ * 1. Gets the template function for the component
42
+ * 2. Generates HTML from props using the template
43
+ * 3. Creates DOM element from HTML
44
+ * 4. Sets scope ID and key attributes
45
+ * 5. Initializes the component (attaches event handlers, sets up effects)
46
+ *
47
+ * @param name - Component name (e.g., 'TodoItem')
48
+ * @param props - Props to pass to the component
49
+ * @param key - Optional key for list reconciliation
50
+ * @returns Created DOM element
51
+ *
52
+ * @example
53
+ * const el = createComponent('TodoItem', {
54
+ * todo: { id: 1, text: 'Buy milk', done: false },
55
+ * onDelete: () => handleDelete(1)
56
+ * }, 1)
57
+ */
58
+ /**
59
+ * Create a component instance from a string name (SSR mode, uses registry)
60
+ * or from a ComponentDef (CSR mode, no registry needed).
61
+ */
62
+ /**
63
+ * Slot-relationship metadata stamped onto a freshly-created component as
64
+ * `bf-h` / `bf-m`. Top-level CSR mounts pass no `slot` — they own their
65
+ * own hydration lifecycle and `initChild` re-binds callbacks freely on
66
+ * each reconcile.
67
+ */
68
+ export interface CreateComponentSlotInfo {
69
+ /** Host scope id (this child's `bf-h` value). */
70
+ parent: string
71
+ /** Slot id in the host (this child's `bf-m` value). */
72
+ mount: string
73
+ }
74
+
75
+ export function createComponent(
76
+ nameOrDef: string | ComponentDef,
77
+ props: Record<string, unknown>,
78
+ key?: string | number,
79
+ slot?: CreateComponentSlotInfo,
80
+ ): HTMLElement {
81
+ // ComponentDef mode: use def directly instead of registry lookup
82
+ if (typeof nameOrDef !== 'string') {
83
+ return createComponentFromDef(nameOrDef, props, key)
84
+ }
85
+
86
+ const name = nameOrDef
87
+
88
+ // 1. Get template function
89
+ const templateFn = getTemplate(name)
90
+ if (!templateFn) {
91
+ console.warn(`[BarefootJS] Template not found for component: ${name}`)
92
+ return createPlaceholder(name, key)
93
+ }
94
+
95
+ // 2. Check for getter children.
96
+ // Children defined via a getter are evaluated AFTER initFn so that context
97
+ // providers set up by the parent are available when children are created.
98
+ const childrenDescriptor = Object.getOwnPropertyDescriptor(props, 'children')
99
+ const childrenIsGetter = childrenDescriptor != null && typeof childrenDescriptor.get === 'function'
100
+
101
+ // 3. Evaluate props for template HTML generation, skipping the children getter.
102
+ // Use untrack() so signal reads don't contaminate the caller's effect tracking.
103
+ const unwrappedProps = untrack(() => {
104
+ const result: Record<string, unknown> = {}
105
+ for (const k of Object.keys(props)) {
106
+ if (k === 'children' && childrenIsGetter) {
107
+ result.children = '' // Deferred — will be inserted after initFn
108
+ continue
109
+ }
110
+ const descriptor = Object.getOwnPropertyDescriptor(props, k)
111
+ if (descriptor && typeof descriptor.get === 'function') {
112
+ result[k] = descriptor.get()
113
+ } else {
114
+ result[k] = props[k]
115
+ }
116
+ }
117
+ // Template functions expect children as an HTML string, not an array.
118
+ if (Array.isArray(result.children) && !hasDomElements(result.children)) {
119
+ result.children = (result.children as unknown[])
120
+ .flat()
121
+ .map(c => c == null ? '' : String(c))
122
+ .join('')
123
+ }
124
+ return result
125
+ })
126
+
127
+ // 4. Generate HTML from props.
128
+ //
129
+ // Thread `slot.parent` into `_parentScopeId` so any hoisted-children
130
+ // placeholder (#1320) resolves to the calling site's scope.
131
+ const prevParentScopeId = _parentScopeId
132
+ if (slot?.parent) {
133
+ _parentScopeId = slot.parent
134
+ }
135
+ let html: string
136
+ try {
137
+ html = templateFn(unwrappedProps)
138
+ } finally {
139
+ _parentScopeId = prevParentScopeId
140
+ }
141
+
142
+ // 5. Create DOM element
143
+ const element = parseHTML(html.trim()).firstChild as HTMLElement
144
+
145
+ if (!element) {
146
+ console.warn(`[BarefootJS] Template returned empty HTML for component: ${name}`)
147
+ return createPlaceholder(name, key)
148
+ }
149
+
150
+ // 6. Set scope ID and key attributes.
151
+ //
152
+ // `comment: true` components (synthesized inline-JSX-callback wrappers
153
+ // from #1211) render as transparent shells — the parsed `firstChild` is
154
+ // already the inner component's root with its own bf-s. Don't overwrite
155
+ // it, or `$c(__scope, 's0')` from the wrapper's init resolves to null.
156
+ const def = getRegisteredDef(name)
157
+ const isCommentWrapper = def?.comment === true
158
+ if (!isCommentWrapper) {
159
+ element.setAttribute(BF_SCOPE, `${name}_${generateId()}`)
160
+ }
161
+ if (slot) {
162
+ if (slot.parent) element.setAttribute(BF_HOST, slot.parent)
163
+ element.setAttribute(BF_AT, slot.mount)
164
+ }
165
+ if (key !== undefined) {
166
+ element.setAttribute(BF_KEY, String(key))
167
+ }
168
+
169
+ // 7. Set currentScope so provideContext/useContext are element-scoped.
170
+ // This allows context providers in initFn to store context on this element.
171
+ const prevScope = setCurrentScope(element)
172
+
173
+ // 8. Initialize the component (context providers set up here).
174
+ const initFn = getComponentInit(name)
175
+ if (initFn) {
176
+ // Pass original props (with getters) for reactivity
177
+ initFn(element, props)
178
+ }
179
+
180
+ // 9. Evaluate getter children and insert them.
181
+ // Children are evaluated NOW (after initFn) so that context provided by
182
+ // the parent is in the global store when children call useContext().
183
+ if (childrenIsGetter) {
184
+ const children = untrack(() => childrenDescriptor!.get!())
185
+ if (children != null) {
186
+ insertGetterChildren(element, children)
187
+ }
188
+ }
189
+
190
+ // 10. Restore previous scope
191
+ setCurrentScope(prevScope)
192
+
193
+ // 11. Mark element as initialized
194
+ hydratedScopes.add(element)
195
+
196
+ // 12. Store props and register update function for element reuse in reconcileList
197
+ propsMap.set(element, props)
198
+ registerPropsUpdate(element, name, props)
199
+
200
+ return element
201
+ }
202
+
203
+ /**
204
+ * Get the props stored for a component element.
205
+ * Used by reconcileList to pass props to an existing element.
206
+ */
207
+ export function getComponentProps(element: HTMLElement): Record<string, unknown> | undefined {
208
+ return propsMap.get(element)
209
+ }
210
+
211
+ /**
212
+ * Register a props update function for a component element.
213
+ * When called, this function re-initializes the component with new props.
214
+ */
215
+ function registerPropsUpdate(
216
+ element: HTMLElement,
217
+ name: string,
218
+ _initialProps: Record<string, unknown>
219
+ ): void {
220
+ // Register update function that will be called by reconcileList
221
+ propsUpdateMap.set(element, (newProps: Record<string, unknown>) => {
222
+ // Re-initialize the component with new props
223
+ // This allows the component to capture new values (e.g., todo with editing: true)
224
+ // and set up new effects that reference the new values
225
+ const init = getComponentInit(name)
226
+ if (init) {
227
+ init(element, newProps)
228
+ }
229
+ })
230
+ }
231
+
232
+ /**
233
+ * Get the props update function for an element.
234
+ * Used by reconcileList to update props when reusing an element.
235
+ */
236
+ export function getPropsUpdateFn(element: HTMLElement): ((props: Record<string, unknown>) => void) | undefined {
237
+ return propsUpdateMap.get(element)
238
+ }
239
+
240
+
241
+ /**
242
+ * Render a child component's template to an HTML string.
243
+ * Used by compiler-generated template functions when a stateless component
244
+ * appears inside a conditional branch or loop template.
245
+ *
246
+ * If the component has a registered template, it renders the HTML and injects
247
+ * a bf-s scope attribute. Otherwise, falls back to an empty placeholder.
248
+ *
249
+ * @param name - Component name (e.g., 'Spinner')
250
+ * @param props - Props to pass to the template
251
+ * @param key - Optional key for list reconciliation
252
+ * @returns HTML string with scope marker
253
+ */
254
+ export function renderChild(
255
+ name: string,
256
+ props: Record<string, unknown>,
257
+ key?: string | number,
258
+ slotSuffix?: string
259
+ ): string {
260
+ const templateFn = getTemplate(name)
261
+ const suffix = slotSuffix ? `_${slotSuffix}` : ''
262
+ // When inside an insert() branch template with a known parent scope,
263
+ // use the parent scope ID so child scope IDs match the SSR convention
264
+ // (e.g., ~ParentName_parentHash_s5 instead of ~Button_randomHash_s5).
265
+ // This enables $cSingle's getDualScopeIds verification to pass.
266
+ const scopePrefix = (_parentScopeId && slotSuffix)
267
+ ? _parentScopeId
268
+ : `${name}_${generateId()}`
269
+ const keyAttr = key !== undefined ? ` ${BF_KEY}="${key}"` : ''
270
+ // Slot-relationship markers — only emitted when both host and slot are
271
+ // known; top-level renders without parent context omit them.
272
+ const slotAttrs = (_parentScopeId && slotSuffix)
273
+ ? ` ${BF_HOST}="${_parentScopeId}" ${BF_AT}="${slotSuffix}"`
274
+ : ''
275
+ const bfsAttr = `${BF_SCOPE}="${scopePrefix}${suffix}"`
276
+
277
+ if (!templateFn) {
278
+ return `<div ${bfsAttr}${slotAttrs}${keyAttr}></div>`
279
+ }
280
+
281
+ // The placeholder substitution is anchored to the exact `bf-s="…"`
282
+ // shape so user content that contains the sentinel as text survives
283
+ // unchanged. When `_parentScopeId` is null (top-level render) the
284
+ // attribute strips rather than emitting `bf-s=""`. (#1320)
285
+ let html = templateFn(props).trim().replace(
286
+ PLACEHOLDER_ATTR_PATTERN,
287
+ _parentScopeId ? ` bf-s="${_parentScopeId}"` : '',
288
+ )
289
+ // Templates may start with comment markers (e.g. <!--bf-cond-start:...-->)
290
+ // so we find the first element tag rather than assuming index 0.
291
+ const firstElMatch = html.match(/<(\w+)/)
292
+ if (!firstElMatch) return html
293
+ const insertPos = firstElMatch.index!
294
+ // Dedupe `bf-s` only when the template body's root already carries
295
+ // one (the body was itself a renderChild call). Still inject
296
+ // `slotAttrs` / `keyAttr` — `data-key` is the reconciliation
297
+ // contract `mapArray` reads, and `bf-h` / `bf-m` mark child
298
+ // membership in the parent scope. (#1320)
299
+ const afterInsert = html.slice(insertPos)
300
+ const extraAttrs = `${slotAttrs}${keyAttr}`
301
+ if (ROOT_HAS_BFS_PATTERN.test(afterInsert)) {
302
+ if (!extraAttrs) return html
303
+ return html.slice(0, insertPos) +
304
+ afterInsert.replace(/^(<\w+)/, `$1${extraAttrs}`)
305
+ }
306
+ return html.slice(0, insertPos) +
307
+ afterInsert.replace(/^(<\w+)/, `$1 ${bfsAttr}${extraAttrs}`)
308
+ }
309
+
310
+ // The leading `\s+` is part of the match so dropping the attribute
311
+ // doesn't leave a dangling space; the compiler always emits the
312
+ // placeholder preceded by whitespace from an enclosing tag.
313
+ const PLACEHOLDER_ATTR_PATTERN = new RegExp(`\\s+bf-s="${BF_PARENT_SCOPE_PLACEHOLDER}"`, 'g')
314
+ const ROOT_HAS_BFS_PATTERN = /^<\w+[^>]*\sbf-s="/
315
+
316
+ /**
317
+ * Generate a random ID for scope identification
318
+ */
319
+ function generateId(): string {
320
+ return Math.random().toString(36).slice(2, 8)
321
+ }
322
+
323
+ /**
324
+ * Create a placeholder element when template is not found
325
+ */
326
+ function createPlaceholder(name: string, key?: string | number): HTMLElement {
327
+ const el = document.createElement('div')
328
+ el.setAttribute(BF_SCOPE, `${name}_placeholder`)
329
+ if (key !== undefined) {
330
+ el.setAttribute(BF_KEY, String(key))
331
+ }
332
+ el.textContent = `[${name}]`
333
+ el.style.cssText = 'color: red; border: 1px dashed red; padding: 4px;'
334
+ return el
335
+ }
336
+
337
+ /**
338
+ * Unwrap getter props to plain values for template rendering.
339
+ * Template functions need actual values, not getter functions.
340
+ *
341
+ * @param props - Props object (may contain getters)
342
+ * @returns Plain object with unwrapped values
343
+ */
344
+ function unwrapPropsForTemplate(props: Record<string, unknown>): Record<string, unknown> {
345
+ const result: Record<string, unknown> = {}
346
+
347
+ for (const key of Object.keys(props)) {
348
+ const descriptor = Object.getOwnPropertyDescriptor(props, key)
349
+
350
+ if (descriptor && typeof descriptor.get === 'function') {
351
+ // It's a getter - call it to get the value
352
+ result[key] = descriptor.get()
353
+ } else {
354
+ // Regular property
355
+ result[key] = props[key]
356
+ }
357
+ }
358
+
359
+ // Template functions expect children as an HTML string, not an array.
360
+ // Join non-DOM array children to avoid Array.toString() inserting commas.
361
+ if (Array.isArray(result.children) && !hasDomElements(result.children)) {
362
+ result.children = (result.children as unknown[])
363
+ .flat()
364
+ .map(c => c == null ? '' : String(c))
365
+ .join('')
366
+ }
367
+
368
+ return result
369
+ }
370
+
371
+ /**
372
+ * Escape ">" inside HTML attribute values to prevent broken parsing.
373
+ * UnoCSS classes like has-[>svg]:shrink-0 contain ">" which terminates
374
+ * the opening tag when parsed via innerHTML. The browser decodes &gt;
375
+ * back to ">" in the DOM attribute value, preserving CSS matching.
376
+ */
377
+ /**
378
+ * Escape ">" inside HTML attribute values to prevent broken parsing.
379
+ * UnoCSS classes like has-[>svg]:shrink-0 contain ">" which terminates
380
+ * the opening tag when parsed via innerHTML. The browser decodes &gt;
381
+ * back to ">" in the DOM attribute value, preserving CSS matching.
382
+ */
383
+ export function escapeAttrGt(html: string): string {
384
+ return html.replace(/"[^"]*"/g, match => match.replace(/>/g, '&gt;'))
385
+ }
386
+
387
+ const SVG_NS = 'http://www.w3.org/2000/svg'
388
+
389
+ /**
390
+ * Parse an HTML string into a DocumentFragment, safely escaping ">" in
391
+ * attribute values. All code that sets innerHTML on dynamic HTML should
392
+ * use this instead of raw innerHTML assignment.
393
+ *
394
+ * When `parent` is provided and lives in the SVG namespace, the markup
395
+ * is parsed under SVG foreign-content context by wrapping it in
396
+ * `<svg>...</svg>`; the wrapper's children are moved into the returned
397
+ * fragment so callers see the same shape as the HTML path. Without
398
+ * this, dynamically-inserted SVG elements (e.g., a `<path>` in a
399
+ * conditional drag preview) end up as `HTMLUnknownElement` in the
400
+ * xhtml namespace and the SVG renderer ignores them. Surfaced by the
401
+ * Graph/DAG Editor block (#135).
402
+ */
403
+ export function parseHTML(html: string, parent?: Element | null): DocumentFragment {
404
+ const tpl = document.createElement('template')
405
+ const escaped = escapeAttrGt(html)
406
+ if (parent && parent.namespaceURI === SVG_NS) {
407
+ tpl.innerHTML = `<svg>${escaped}</svg>`
408
+ const wrapper = tpl.content.firstElementChild
409
+ const frag = document.createDocumentFragment()
410
+ if (wrapper) {
411
+ while (wrapper.firstChild) frag.appendChild(wrapper.firstChild)
412
+ }
413
+ return frag
414
+ }
415
+ tpl.innerHTML = escaped
416
+ return tpl.content
417
+ }
418
+
419
+ /**
420
+ * Check if a value contains DOM elements (HTMLElement instances).
421
+ */
422
+ function hasDomElements(value: unknown): boolean {
423
+ if (value instanceof Element) return true
424
+ if (Array.isArray(value)) return value.some(hasDomElements)
425
+ return false
426
+ }
427
+
428
+
429
+ /**
430
+ * Insert getter children into an element.
431
+ * Unlike insertDomChildren, strings are parsed as HTML (not text nodes) because
432
+ * getter children may return HTML strings from compiler-generated template literals
433
+ * (e.g. `<span class="...">Required</span>`).
434
+ * Arrays may contain a mix of DOM elements and HTML strings.
435
+ */
436
+ function insertGetterChildren(element: HTMLElement, children: unknown): void {
437
+ if (children instanceof Element) {
438
+ element.appendChild(children)
439
+ } else if (Array.isArray(children)) {
440
+ for (const child of (children as unknown[]).flat()) {
441
+ if (child instanceof Element) {
442
+ element.appendChild(child)
443
+ } else if (typeof child === 'string' && child.length > 0) {
444
+ element.appendChild(parseHTML(child.trim()))
445
+ } else if (typeof child === 'number') {
446
+ element.appendChild(document.createTextNode(String(child)))
447
+ }
448
+ }
449
+ } else if (typeof children === 'string' && (children as string).length > 0) {
450
+ element.appendChild(parseHTML((children as string).trim()))
451
+ } else if (typeof children === 'number') {
452
+ element.appendChild(document.createTextNode(String(children)))
453
+ }
454
+ }
455
+
456
+ /**
457
+ * Create a component instance from a ComponentDef (CSR mode).
458
+ * Does not use the component registry — the def is passed directly.
459
+ */
460
+ function createComponentFromDef(
461
+ def: ComponentDef,
462
+ props: Record<string, unknown>,
463
+ key?: string | number
464
+ ): HTMLElement {
465
+ if (!def.template) {
466
+ throw new Error('[BarefootJS] createComponent with ComponentDef requires a template function')
467
+ }
468
+
469
+ // Generate HTML from template
470
+ const unwrappedProps = unwrapPropsForTemplate(props)
471
+ const html = def.template(unwrappedProps)
472
+
473
+ // Create DOM element
474
+ const element = parseHTML(html.trim()).firstChild as HTMLElement
475
+
476
+ if (!element) {
477
+ const el = document.createElement('div')
478
+ el.textContent = '[ComponentDef]'
479
+ el.style.cssText = 'color: red; border: 1px dashed red; padding: 4px;'
480
+ return el
481
+ }
482
+
483
+ // Set scope ID and key
484
+ const name = def.name || def.init.name?.replace(/^init/, '') || 'Component'
485
+ const scopeId = `${name}_${generateId()}`
486
+ element.setAttribute(BF_SCOPE, scopeId)
487
+ if (key !== undefined) {
488
+ element.setAttribute(BF_KEY, String(key))
489
+ }
490
+
491
+ // Initialize
492
+ def.init(element, props)
493
+
494
+ // Mark as initialized
495
+ hydratedScopes.add(element)
496
+
497
+ // Store props for element reuse
498
+ propsMap.set(element, props)
499
+
500
+ return element
501
+ }
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Context API: DOM-bound runtime portion.
3
+ *
4
+ * `useContext` and `provideContext` walk the DOM (scope-based) to locate
5
+ * the nearest provider. Portal elements (with bf-po attribute) follow
6
+ * the logical owner chain.
7
+ *
8
+ * A global store is kept as a fallback for non-scoped usage.
9
+ */
10
+
11
+ import { BF_PORTAL_OWNER, BF_SCOPE } from '@barefootjs/shared'
12
+ import type { Context } from '../context'
13
+
14
+ export { createContext, type Context } from '../context'
15
+
16
+ /** Global fallback store for contexts without a DOM scope. */
17
+ const contextStore = new Map<symbol, unknown>()
18
+
19
+ /** Property key for context data stored on DOM elements. */
20
+ const CONTEXT_KEY = '__bfCtx'
21
+
22
+ /** Current scope element, set by initChild during component initialization. */
23
+ let currentScope: Element | null = null
24
+
25
+ /**
26
+ * Set the current scope element for context operations.
27
+ * Called by initChild to scope provideContext/useContext to the correct element.
28
+ * Returns the previous scope for restoration.
29
+ */
30
+ export function setCurrentScope(scope: Element | null): Element | null {
31
+ const prev = currentScope
32
+ currentScope = scope
33
+ return prev
34
+ }
35
+
36
+ /**
37
+ * Read the current value of a context.
38
+ *
39
+ * Walks up the DOM tree from the current scope element to find
40
+ * the nearest ancestor that provided this context. Falls back to
41
+ * the global store, then to the context's default value, then to
42
+ * `undefined`.
43
+ *
44
+ * Returning `undefined` (rather than throwing) when no provider is
45
+ * available lets templates evaluate safely before init has run
46
+ * `provideContext` — init's `createEffect` repaints once the
47
+ * provider is set up. See piconic-ai/barefootjs#1156.
48
+ */
49
+ export function useContext<T>(context: Context<T>): T {
50
+ // Walk DOM ancestors from current scope to find nearest provider.
51
+ // For portal elements (bf-po attribute), follow the logical owner
52
+ // chain back to the original parent scope.
53
+ if (currentScope) {
54
+ let el: Element | null = currentScope
55
+ while (el) {
56
+ const ctxMap = (el as any)[CONTEXT_KEY] as Map<symbol, unknown> | undefined
57
+ if (ctxMap?.has(context.id)) {
58
+ return ctxMap.get(context.id) as T
59
+ }
60
+ // Follow portal owner chain: if this element has bf-po, jump to the owner scope
61
+ const portalOwnerId: string | null = el.getAttribute(BF_PORTAL_OWNER)
62
+ if (portalOwnerId) {
63
+ const ownerEl: Element | null = document.querySelector(`[${BF_SCOPE}="${portalOwnerId}"]`)
64
+ if (ownerEl && ownerEl !== el) {
65
+ el = ownerEl
66
+ continue
67
+ }
68
+ }
69
+ el = el.parentElement
70
+ }
71
+ }
72
+ if (contextStore.has(context.id)) {
73
+ return contextStore.get(context.id) as T
74
+ }
75
+ return context.defaultValue as T
76
+ }
77
+
78
+ /**
79
+ * Provide a value for a context.
80
+ *
81
+ * Stores the value on the current scope DOM element so that child
82
+ * components can find it via useContext's DOM ancestor walk.
83
+ * Also sets the global store as fallback.
84
+ */
85
+ export function provideContext<T>(context: Context<T>, value: T): void {
86
+ if (currentScope) {
87
+ let ctxMap = (currentScope as any)[CONTEXT_KEY] as Map<symbol, unknown> | undefined
88
+ if (!ctxMap) {
89
+ ctxMap = new Map()
90
+ ;(currentScope as any)[CONTEXT_KEY] = ctxMap
91
+ }
92
+ ctxMap.set(context.id, value)
93
+
94
+ // Propagate context to child scope elements so portal-moved children
95
+ // can find it via DOM ancestor walk. At provideContext time, children
96
+ // are still in their original SSR positions (portals haven't moved them yet).
97
+ const childScopes = currentScope.querySelectorAll(`[${BF_SCOPE}]`)
98
+ for (const child of childScopes) {
99
+ let childCtxMap = (child as any)[CONTEXT_KEY] as Map<symbol, unknown> | undefined
100
+ if (!childCtxMap) {
101
+ childCtxMap = new Map()
102
+ ;(child as any)[CONTEXT_KEY] = childCtxMap
103
+ }
104
+ // Only set if not already provided (don't override nested providers)
105
+ if (!childCtxMap.has(context.id)) {
106
+ childCtxMap.set(context.id, value)
107
+ }
108
+ }
109
+ }
110
+ contextStore.set(context.id, value)
111
+ }