@barefootjs/cli 0.1.3 → 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.
@@ -19,7 +19,7 @@
19
19
  - [Backend Freedom](./core-concepts/backend-freedom.md) — How adapters let the same JSX run on any server
20
20
  - [MPA-style Development](./core-concepts/mpa-style.md) — Server-rendering by default, JS only where marked
21
21
  - [Fine-grained Reactivity](./core-concepts/reactivity.md) — Signals, effects, memos — no virtual DOM needed
22
- - [AI-native Development](./core-concepts/ai-native.md) — Testable IR, CLI discovery, AI-assisted workflows
22
+ - [AI-native Development](./core-concepts/ai-native.md) — Testable IR, CLI discovery, agent skill (Claude Code / Codex), AI-assisted workflows
23
23
  - [How It Works](./core-concepts/how-it-works.mdx) — Two-phase compilation, hydration markers, clean overrides
24
24
 
25
25
  ### 4. [Reactivity](./reactivity.md)
@@ -31,6 +31,7 @@
31
31
  - [`onCleanup`](./reactivity/on-cleanup.md) — Register cleanup for effects and lifecycle
32
32
  - [`untrack`](./reactivity/untrack.md) — Read signals without tracking dependencies
33
33
  - [Props Reactivity](./reactivity/props-reactivity.md) — Gotchas with destructuring
34
+ - [Shared State Patterns](./reactivity/shared-state.md) — Cross-file state sharing: consolidation, custom events, server props
34
35
 
35
36
  ### 5. [Templates & Rendering](./rendering.md)
36
37
 
@@ -234,3 +234,10 @@ const theme = useContext(ThemeContext)
234
234
  ```
235
235
 
236
236
  Use for optional contexts with a sensible fallback.
237
+
238
+
239
+ ## Cross-File Context Does Not Work
240
+
241
+ `createContext()` and all components that use it must be in the **same file**. Importing a context from another file does not work — the compiler does not currently support cross-file context sharing.
242
+
243
+ This is why compound components (Accordion, Dialog, Select, Tabs) define all sub-components in a single file. For sharing state across components in separate files, see [Shared State Patterns](../reactivity/shared-state.md).
@@ -66,6 +66,25 @@ When nothing in the registry fits, `bf gen component <name> <comps...>` scaffold
66
66
 
67
67
  **Visual preview**: hosted previews live at [ui.barefootjs.dev/components/&lt;name&gt;](https://ui.barefootjs.dev). Standalone `bf preview` for npm-installed projects is tracked in [#885](https://github.com/piconic-ai/barefootjs/issues/885).
68
68
 
69
+ ## Agent Skill
70
+
71
+ BarefootJS publishes an agent skill that gives AI deep knowledge of the compiler, IR, CLI, and component system. Install it and the agent can build, test, and debug BarefootJS projects autonomously.
72
+
73
+ **Claude Code:**
74
+
75
+ ```sh
76
+ /plugin marketplace add piconic-ai/barefootjs
77
+ /plugin install barefootjs@barefootjs
78
+ ```
79
+
80
+ **Codex:**
81
+
82
+ ```
83
+ install the barefootjs skill from piconic-ai/barefootjs
84
+ ```
85
+
86
+ With the skill active, the agent understands `bf` CLI commands, IR test patterns, signal graphs, adapter output, and error codes — no manual prompting needed.
87
+
69
88
  ## Agent Loop
70
89
 
71
90
  The same workflow driven by an AI agent — say, "add a settings form":
@@ -18,6 +18,7 @@
18
18
  - [onCleanup](https://barefootjs.dev/docs/reactivity/on-cleanup.md): Registers a cleanup function that runs when the owning effect re-runs or the component is destroyed.
19
19
  - [onMount](https://barefootjs.dev/docs/reactivity/on-mount.md): Runs a callback once when the component initializes, without tracking signal dependencies.
20
20
  - [Props Reactivity](https://barefootjs.dev/docs/reactivity/props-reactivity.md): How prop access patterns determine whether reactive updates propagate in BarefootJS components.
21
+ - [Shared State Patterns](https://barefootjs.dev/docs/reactivity/shared-state.md): Patterns for sharing reactive state between components in separate files, and when to use each.
21
22
  - [untrack](https://barefootjs.dev/docs/reactivity/untrack.md): Executes a function without tracking signal dependencies in the current reactive context.
22
23
 
23
24
  ## Rendering
@@ -147,6 +147,7 @@ This runs `bf build`, generates the final `uno.css`, and calls `wrangler deploy`
147
147
 
148
148
  ## Next steps
149
149
 
150
+ - **[Agent Skill](./core-concepts/ai-native.md#agent-skill)** — Install the BarefootJS skill for Claude Code or Codex and let the agent build components, write IR tests, and debug signals for you.
150
151
  - **[Core Concepts](./core-concepts.md)** — the four design principles behind BarefootJS: backend freedom, MPA-style rendering, fine-grained reactivity, and AI-native workflows.
151
152
  - **[`createSignal`](./reactivity/create-signal.md)** and **[`createMemo`](./reactivity/create-memo.md)** — the reactivity primitives you just used.
152
153
  - **[Client Directive](./rendering/client-directive.md)** — exactly what `"use client"` does and when to reach for it.
@@ -0,0 +1,233 @@
1
+ ---
2
+ title: Shared State Patterns
3
+ description: Patterns for sharing reactive state between components in separate files, and when to use each.
4
+ ---
5
+
6
+ # Shared State Patterns
7
+
8
+ The [Context API](../components/context-api.md) shares state between components in the **same file** (compound components like Accordion, Dialog, Tabs). But the compilation model — one `.tsx` file produces one `.client.js` bundle — means context **cannot cross file boundaries**.
9
+
10
+ This guide covers patterns for sharing reactive state between components in **separate files**.
11
+
12
+
13
+ ## Why Context Doesn't Work Across Files
14
+
15
+ Each `.client.js` bundle is independent. When the compiler processes two files that both reference the same `createContext()`, each bundle gets its own call — producing a unique `Symbol` id. The provider's context and the consumer's context are different objects:
16
+
17
+ ```
18
+ PlaybackProvider.client.js → createContext() → Symbol(#1)
19
+ Player.client.js → createContext() → Symbol(#2)
20
+ ↑ different identity
21
+ ```
22
+
23
+ `useContext` in Player walks the DOM looking for `Symbol(#2)`, but PlaybackProvider set `Symbol(#1)`. No match — the value is never found.
24
+
25
+
26
+ ## Pattern 1: Consolidate Into One File
27
+
28
+ The simplest solution. If components are tightly coupled, put them in the same file:
29
+
30
+ ```tsx
31
+ "use client"
32
+ import { createContext, useContext, createSignal, createEffect } from '@barefootjs/client'
33
+
34
+ interface PlaybackContextValue {
35
+ elapsedMs: () => number
36
+ playing: () => boolean
37
+ seek: (ms: number) => void
38
+ toggle: () => void
39
+ }
40
+
41
+ const PlaybackContext = createContext<PlaybackContextValue>()
42
+
43
+ export function PlaybackProvider(props: { children?: unknown }) {
44
+ const [elapsedMs, setElapsedMs] = createSignal(0)
45
+ const [playing, setPlaying] = createSignal(false)
46
+
47
+ const seek = (ms: number) => setElapsedMs(ms)
48
+ const toggle = () => setPlaying(p => !p)
49
+
50
+ return (
51
+ <PlaybackContext.Provider value={{ elapsedMs, playing, seek, toggle }}>
52
+ {props.children}
53
+ </PlaybackContext.Provider>
54
+ )
55
+ }
56
+
57
+ export function Player(props: { children?: unknown }) {
58
+ const handleMount = (el: HTMLElement) => {
59
+ const ctx = useContext(PlaybackContext)
60
+ createEffect(() => {
61
+ el.textContent = `${Math.floor(ctx.elapsedMs() / 1000)}s`
62
+ })
63
+ }
64
+ return <div ref={handleMount}>{props.children}</div>
65
+ }
66
+
67
+ export function TimelineBar(props: { duration: number }) {
68
+ const handleMount = (el: HTMLElement) => {
69
+ const ctx = useContext(PlaybackContext)
70
+ el.addEventListener('click', (e) => {
71
+ const rect = el.getBoundingClientRect()
72
+ const ratio = (e.clientX - rect.left) / rect.width
73
+ ctx.seek(ratio * props.duration)
74
+ })
75
+ }
76
+ return <div ref={handleMount} className="timeline" />
77
+ }
78
+ ```
79
+
80
+ **When to use:** Components share a tight contract and are always used together (like Select + SelectTrigger + SelectContent).
81
+
82
+ | Pros | Cons |
83
+ |------|------|
84
+ | Full reactivity via signals | All components in one file |
85
+ | Type-safe | File grows with component count |
86
+ | Works with Context API | Not suitable for loosely coupled components |
87
+
88
+
89
+ ## Pattern 2: Custom DOM Events
90
+
91
+ Use the browser's native event system. One component dispatches events, others listen. No shared module state needed.
92
+
93
+ Player (`components/Player.tsx`):
94
+
95
+ ```tsx
96
+ "use client"
97
+ import { createSignal, createEffect, onCleanup } from '@barefootjs/client'
98
+
99
+ export function Player() {
100
+ const [elapsedMs, setElapsedMs] = createSignal(0)
101
+
102
+ const handleMount = (el: HTMLElement) => {
103
+ createEffect(() => {
104
+ el.dispatchEvent(new CustomEvent('playback:timeupdate', {
105
+ bubbles: true,
106
+ detail: { elapsedMs: elapsedMs() },
107
+ }))
108
+ })
109
+
110
+ const onSeek = ((e: CustomEvent) => {
111
+ setElapsedMs(e.detail.ms)
112
+ }) as EventListener
113
+
114
+ document.addEventListener('playback:seek', onSeek)
115
+ onCleanup(() => document.removeEventListener('playback:seek', onSeek))
116
+ }
117
+
118
+ return <div ref={handleMount} />
119
+ }
120
+ ```
121
+
122
+ TimelineBar (`components/TimelineBar.tsx`):
123
+
124
+ ```tsx
125
+ "use client"
126
+ import { onCleanup } from '@barefootjs/client'
127
+
128
+ export function TimelineBar(props: { duration: number }) {
129
+ const handleMount = (el: HTMLElement) => {
130
+ const onTimeUpdate = ((e: CustomEvent) => {
131
+ const ratio = e.detail.elapsedMs / props.duration
132
+ el.style.setProperty('--progress', String(ratio))
133
+ }) as EventListener
134
+
135
+ document.addEventListener('playback:timeupdate', onTimeUpdate)
136
+ onCleanup(() => document.removeEventListener('playback:timeupdate', onTimeUpdate))
137
+
138
+ el.addEventListener('click', (e) => {
139
+ const rect = el.getBoundingClientRect()
140
+ const ratio = (e.clientX - rect.left) / rect.width
141
+ el.dispatchEvent(new CustomEvent('playback:seek', {
142
+ bubbles: true,
143
+ detail: { ms: ratio * props.duration },
144
+ }))
145
+ })
146
+ }
147
+
148
+ return <div ref={handleMount} className="timeline" />
149
+ }
150
+ ```
151
+
152
+ **When to use:** Components are in separate files and communicate through a shared parent in the DOM.
153
+
154
+ | Pros | Cons |
155
+ |------|------|
156
+ | Works across any file boundary | Not reactive — imperative dispatch/listen |
157
+ | No shared imports needed | No type safety on event payloads (use a shared type file to mitigate) |
158
+ | Familiar browser API | Requires manual cleanup |
159
+ | SSR-safe (listeners only run on client) | Event naming conventions needed |
160
+
161
+ ### Type-safe event helpers
162
+
163
+ Define event types in a shared `src/` file to get type safety without sharing runtime state:
164
+
165
+ ```ts
166
+ export interface PlaybackTimeUpdateDetail {
167
+ elapsedMs: number
168
+ }
169
+
170
+ export interface PlaybackSeekDetail {
171
+ ms: number
172
+ }
173
+
174
+ export function dispatchTimeUpdate(el: Element, detail: PlaybackTimeUpdateDetail) {
175
+ el.dispatchEvent(new CustomEvent('playback:timeupdate', { bubbles: true, detail }))
176
+ }
177
+
178
+ export function dispatchSeek(el: Element, detail: PlaybackSeekDetail) {
179
+ el.dispatchEvent(new CustomEvent('playback:seek', { bubbles: true, detail }))
180
+ }
181
+ ```
182
+
183
+ `src/` utility files are inlined at compile time. Type definitions and `CustomEvent` constructors don't carry mutable state, so inlining is safe — each bundle gets its own copy of the helper functions, but they produce identical events.
184
+
185
+
186
+ ## Pattern 3: Server-Mediated Props
187
+
188
+ For state that originates on the server (database, session, URL params), pass it as props from the server route. No client-side sharing needed:
189
+
190
+ ```tsx
191
+ import { Player } from '@/components/Player'
192
+ import { TimelineBar } from '@/components/TimelineBar'
193
+
194
+ app.get('/player/:id', async (c) => {
195
+ const track = await db.getTrack(c.req.param('id'))
196
+ return c.render(
197
+ <main>
198
+ <Player trackId={track.id} initialPosition={track.lastPosition} />
199
+ <TimelineBar duration={track.durationMs} />
200
+ </main>
201
+ )
202
+ })
203
+ ```
204
+
205
+ Each component hydrates independently with its own signals. User interactions that need to cross components use Pattern 2 (custom events).
206
+
207
+ **When to use:** Initial state comes from the server; components only need to sync on user-driven actions.
208
+
209
+ | Pros | Cons |
210
+ |------|------|
211
+ | SSR-friendly — state is in HTML | Real-time sync still needs custom events |
212
+ | No shared client state | Server round-trip for state changes |
213
+ | Each component is independently testable | |
214
+
215
+
216
+ ## Choosing a Pattern
217
+
218
+ ```
219
+ Is all state server-derived?
220
+ └─ Yes → Pattern 3 (Server-Mediated Props)
221
+ └─ No
222
+ ├─ Are components tightly coupled (always used together)?
223
+ │ └─ Yes → Pattern 1 (Consolidate Into One File)
224
+ └─ No → Pattern 2 (Custom DOM Events)
225
+ ```
226
+
227
+ | | Context (same file) | Consolidate | Custom Events | Server Props |
228
+ |---|---|---|---|---|
229
+ | Cross-file | No | No (one file) | Yes | Yes |
230
+ | Reactive | Yes | Yes | No | No (initial only) |
231
+ | Type-safe | Yes | Yes | With helpers | Yes |
232
+ | SSR-safe | Yes | Yes | Yes | Yes |
233
+ | Testable | IR test | IR test | E2E | IR test + E2E |
@@ -26,3 +26,4 @@ import { createSignal, createEffect, createMemo, onMount, onCleanup, untrack } f
26
26
  ## Guides
27
27
 
28
28
  - [Props Reactivity](./reactivity/props-reactivity.md) — How props stay reactive, and when destructuring breaks it
29
+ - [Shared State Patterns](./reactivity/shared-state.md) — Sharing state across components in separate files