@ohhwells/bridge 0.1.28 → 0.1.29-next.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,304 +1,362 @@
1
- # @ohhwells/bridge
2
-
3
- The OhhWells canvas editor bridge — a standalone npm package that enables inline text and image editing for any site deployed on the OhhWells platform.
4
-
5
- ## What it does
6
-
7
- When a studio owner opens their site in the OhhWells canvas editor, the bridge:
8
-
9
- - Connects the iframe (the live site) to the parent canvas editor via `postMessage`
10
- - Enables click-to-edit for text, images, and background images
11
- - Handles draft saving and content hydration
12
- - Shows the "Add Section" insert line between sections in the canvas editor
13
- - Provides state toggle UI for editing hidden content (hover states, form views)
14
- - Injects scoped styles that never leak into the host template
15
-
16
- ---
17
-
18
- ## Installation
19
-
20
- ```bash
21
- npm install @ohhwells/bridge
22
- ```
23
-
24
- ---
25
-
26
- ## Template setup (required steps)
27
-
28
- ### 1. Import styles
29
-
30
- In your root layout file, import the bridge stylesheet **once**:
31
-
32
- ```ts
33
- import "@ohhwells/bridge/styles";
34
- ```
35
-
36
- ### 2. Add the loader surface
37
-
38
- The loader is a full-screen spinner shown while the bridge fetches personalised content. It hides itself once content is ready. Add this **before** your main content in the `<body>`:
39
-
40
- ```tsx
41
- import { OHW_LOADER_STYLE, OhwLoaderSpinner } from "@ohhwells/bridge";
42
-
43
- // Inside <body>:
44
- <div
45
- id="ohw-loader"
46
- suppressHydrationWarning
47
- style={{ ...OHW_LOADER_STYLE, display: "none" }}
48
- >
49
- <OhwLoaderSpinner />
50
- </div>;
51
-
52
- {
53
- /* Inline script — shows the loader immediately on the client before React hydrates */
54
- }
55
- <script
56
- dangerouslySetInnerHTML={{
57
- __html: `(function(){try{
58
- var p=location.hostname.split(".");
59
- var fromHost=p.length>=3&&p[0]!=="www"?p[0]:"";
60
- var fromQuery=new URLSearchParams(location.search).get("subdomain")||"";
61
- if(!fromHost&&!fromQuery)return;
62
- var e=document.getElementById("ohw-loader");
63
- if(e)e.style.display="flex"
64
- }catch(e){}})();`,
65
- }}
66
- />;
67
- ```
68
-
69
- The inline script detects whether the page is being loaded under a subdomain (personalised content mode) and shows the loader before React has a chance to hydrate, preventing a flash of the default content.
70
-
71
- ### 3. Mount `OhhwellsBridge`
72
-
73
- Add `<OhhwellsBridge />` inside a `<Suspense>` boundary in your root layout. It must be in `<Suspense>` because it calls `useSearchParams()` internally.
74
-
75
- ```tsx
76
- import { Suspense } from "react";
77
- import { OhhwellsBridge } from "@ohhwells/bridge";
78
-
79
- export default function RootLayout({ children }) {
80
- return (
81
- <html lang="en">
82
- <body>
83
- {/* loader + inline script here (see step 2) */}
84
-
85
- <Suspense>
86
- <OhhwellsBridge />
87
- </Suspense>
88
-
89
- {/* rest of your layout */}
90
- {children}
91
- </body>
92
- </html>
93
- );
94
- }
95
- ```
96
-
97
- The bridge activates automatically when the page is loaded inside the OhhWells canvas editor. It does nothing in production (live site) mode.
98
-
99
- ### 4. Mark sections
100
-
101
- Every top-level section on each page must have a unique `data-ohw-section` attribute. This is what the canvas editor uses to:
102
-
103
- - Show the "Add Section" insert line between sections
104
- - Persist widget insertions (saving which section a widget was inserted after)
105
-
106
- ```tsx
107
- // Good — section element is the direct content root
108
- <section data-ohw-section="hero">
109
- ...
110
- </section>
111
-
112
- // Also fine — any element type works
113
- <div data-ohw-section="testimonials">
114
- ...
115
- </div>
116
- ```
117
-
118
- **Naming rules:**
119
-
120
- - Use `kebab-case`
121
- - Must be unique across the entire page
122
- - Must be stable — if a section is renamed, any saved widget insertions referencing that name will break
123
-
124
- **Example — a page with multiple sections:**
125
-
126
- ```tsx
127
- export default function HomePage() {
128
- return (
129
- <>
130
- <section data-ohw-section="hero">...</section>
131
- <section data-ohw-section="lagree-intro">...</section>
132
- <section data-ohw-section="classes-strip">...</section>
133
- <section data-ohw-section="testimonials">...</section>
134
- <section data-ohw-section="plan-form">...</section>
135
- </>
136
- );
137
- }
138
- ```
139
-
140
- ### 5. Mark editable elements
141
-
142
- Add `data-ohw-editable` and `data-ohw-key` to any element the studio owner should be able to edit:
143
-
144
- ```tsx
145
- {
146
- /* Editable rich text (bold, italic, etc.) */
147
- }
148
- <h1 data-ohw-editable="text" data-ohw-key="hero-heading">
149
- Welcome to the studio
150
- </h1>;
151
-
152
- {
153
- /* Editable plain text (no formatting) */
154
- }
155
- <p data-ohw-editable="plain" data-ohw-key="hero-subtitle">
156
- Book your first class
157
- </p>;
158
-
159
- {
160
- /* Editable image */
161
- }
162
- <img
163
- data-ohw-editable="image"
164
- data-ohw-key="hero-image"
165
- src="/hero.jpg"
166
- alt="Hero"
167
- />;
168
-
169
- {
170
- /* Editable background image */
171
- }
172
- <div
173
- data-ohw-editable="bg-image"
174
- data-ohw-key="hero-bg"
175
- style={{ backgroundImage: "url(/bg.jpg)" }}
176
- />;
177
- ```
178
-
179
- **Key naming rules:**
180
-
181
- - Must be globally unique across all pages
182
- - Use `kebab-case`
183
- - Must be stable — changing a key orphans any saved content for that element
184
-
185
- ---
186
-
187
- ## Editable states (advanced)
188
-
189
- For elements with multiple display states (e.g. a contact form with default/success/error views), wrap each state in a `data-ohw-state-view` and mark the container with `data-ohw-editable-state`:
190
-
191
- ```tsx
192
- <div
193
- data-ohw-editable-state="default,success,error"
194
- data-ohw-key="contact-form"
195
- >
196
- <div data-ohw-state-view="default">{/* default form UI */}</div>
197
- <div data-ohw-state-view="success">{/* success message */}</div>
198
- <div data-ohw-state-view="error">{/* error message */}</div>
199
- </div>
200
- ```
201
-
202
- The bridge shows a state toggle in the canvas editor to switch between states.
203
-
204
- ---
205
-
206
- ## Local development workflow
207
-
208
- When iterating on the bridge package itself:
209
-
210
- ```bash
211
- # 1. Build the package
212
- cd ohhwells-bridge
213
- npm run build
214
-
215
- # 2. Link it globally (one-time setup)
216
- npm link
217
-
218
- # 3. In your template repo, use the local build instead of the npm version
219
- cd rebound-template
220
- npm link @ohhwells/bridge
221
- ```
222
-
223
- After that, every `npm run build` in `ohhwells-bridge` is picked up by the template immediately (no re-link needed). To go back to the npm version:
224
-
225
- ```bash
226
- cd rebound-template
227
- npm unlink @ohhwells/bridge
228
- npm install
229
- ```
230
-
231
- ---
232
-
233
- ## Publishing
234
-
235
- The package publishes automatically via GitHub Actions on push to `main` (production tag) or `staging` (next tag). To publish manually:
236
-
237
- ```bash
238
- npm run build
239
- npm publish --access public
240
- ```
241
-
242
- ---
243
-
244
- ## Link popover (Canvas Editor)
245
-
246
- `LinkPopover` is rendered inside `OhhwellsBridge` (iframe portal) when editing link destinations. It implements the Figma shadcn kit panel (nodes 8365-7616 / 8382-4161) — anchored popover, not a centered dialog.
247
-
248
- ```tsx
249
- // Used internally by OhhwellsBridge — external consumers rarely mount this directly.
250
- import { LinkPopover } from "@ohhwells/bridge";
251
- ```
252
-
253
- The iframe bridge reports page sections on `ow:ready`:
254
-
255
- ```json
256
- { "type": "ow:ready", "version": "1", "nodes": [...], "sections": [{ "id": "hero", "label": "Hero" }] }
257
- ```
258
-
259
- Templates must mark sections with `data-ohw-section` (and optional `data-ohw-section-label`).
260
-
261
- ### Local link-editor testing (dev fixtures)
262
-
263
- Without the canvas editor parent, add `ohw-fixtures=1` to the edit URL. This preloads Re:Bound pages and sections into the link popover:
264
-
265
- ```
266
- http://localhost:3000 /?mode=edit&ohw-fixtures=1
267
- ```
268
-
269
- 1. Click a nav link label (e.g. **Pricing**) → toolbar → chain icon
270
- 2. Pick **About** in Destination → **Choose a section** → **Personal training**
271
- 3. **Save** — href becomes `/about#personal-training`
272
- 4. Open `http://localhost:3001/about#personal-training` (without `mode=edit`) to verify scroll
273
-
274
- In edit mode link clicks are blocked; check `href` in DevTools or test on a normal page load.
275
-
276
- ---
277
-
278
- ## Design tokens
279
-
280
- The package ships the full OhhWells design token set, scoped to `[data-ohw-bridge-root]` so styles never leak into the host template.
281
-
282
- ### Semantic colors (CSS variables on `[data-ohw-bridge-root]`)
283
-
284
- | Token | Light | Dark |
285
- | -------------------- | --------- | --------- |
286
- | `primary` | `#0f172a` | `#f8fafc` |
287
- | `primary-foreground` | `#f8fafc` | `#0f172a` |
288
- | `background` | `#ffffff` | `#020617` |
289
- | `foreground` | `#020617` | `#f8fafc` |
290
- | `muted` | `#f1f5f9` | `#1e293b` |
291
- | `muted-foreground` | `#64748b` | `#94a3b8` |
292
- | `border` | `#e2e8f0` | `#334155` |
293
- | `destructive` | `#dc2626` | `#7f1d1d` |
294
- | `success` | `#16a34a` | `#22c55e` |
295
-
296
- ### Brand palette
297
-
298
- Re:Bound brand colors: `bone`, `ivory`, `sand`, `linen`, `stone`, `clay`, `umber`, `umber-deep`, `espresso`, `clove`, `ink`, `ash`, `carbon`.
299
-
300
- ---
301
-
302
- ## License
303
-
304
- MIT
1
+ # @ohhwells/bridge
2
+
3
+ The OhhWells canvas editor bridge — a standalone npm package that enables inline text and image editing for any site deployed on the OhhWells platform.
4
+
5
+ ## What it does
6
+
7
+ When a studio owner opens their site in the OhhWells canvas editor, the bridge:
8
+
9
+ - Connects the iframe (the live site) to the parent canvas editor via `postMessage`
10
+ - Enables click-to-edit for text, images, and background images
11
+ - Handles draft saving and content hydration
12
+ - Shows the "Add Section" insert line between sections in the canvas editor
13
+ - Provides state toggle UI for editing hidden content (hover states, form views)
14
+ - Injects scoped styles that never leak into the host template
15
+
16
+ ---
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ npm install @ohhwells/bridge
22
+ ```
23
+
24
+ ---
25
+
26
+ ## Template setup (required steps)
27
+
28
+ ### 1. Import styles
29
+
30
+ In your root layout file, import the bridge stylesheet **once**:
31
+
32
+ ```ts
33
+ import "@ohhwells/bridge/styles";
34
+ ```
35
+
36
+ ### 2. Add the loader surface
37
+
38
+ The loader is a full-screen spinner shown while the bridge fetches personalised content. It hides itself once content is ready. Add this **before** your main content in the `<body>`:
39
+
40
+ ```tsx
41
+ import { OHW_LOADER_STYLE, OhwLoaderSpinner } from "@ohhwells/bridge";
42
+
43
+ // Inside <body>:
44
+ <div
45
+ id="ohw-loader"
46
+ suppressHydrationWarning
47
+ style={{ ...OHW_LOADER_STYLE, display: "none" }}
48
+ >
49
+ <OhwLoaderSpinner />
50
+ </div>;
51
+
52
+ {
53
+ /* Inline script — shows the loader immediately on the client before React hydrates */
54
+ }
55
+ <script
56
+ dangerouslySetInnerHTML={{
57
+ __html: `(function(){try{
58
+ var p=location.hostname.split(".");
59
+ var fromHost=p.length>=3&&p[0]!=="www"?p[0]:"";
60
+ var fromQuery=new URLSearchParams(location.search).get("subdomain")||"";
61
+ if(!fromHost&&!fromQuery)return;
62
+ var e=document.getElementById("ohw-loader");
63
+ if(e)e.style.display="flex"
64
+ }catch(e){}})();`,
65
+ }}
66
+ />;
67
+ ```
68
+
69
+ The inline script detects whether the page is being loaded under a subdomain (personalised content mode) and shows the loader before React has a chance to hydrate, preventing a flash of the default content.
70
+
71
+ ### 3. Mount `OhhwellsBridge`
72
+
73
+ Add `<OhhwellsBridge />` inside a `<Suspense>` boundary in your root layout. It must be in `<Suspense>` because it calls `useSearchParams()` internally.
74
+
75
+ ```tsx
76
+ import { Suspense } from "react";
77
+ import { OhhwellsBridge } from "@ohhwells/bridge";
78
+
79
+ export default function RootLayout({ children }) {
80
+ return (
81
+ <html lang="en">
82
+ <body>
83
+ {/* loader + inline script here (see step 2) */}
84
+
85
+ <Suspense>
86
+ <OhhwellsBridge />
87
+ </Suspense>
88
+
89
+ {/* rest of your layout */}
90
+ {children}
91
+ </body>
92
+ </html>
93
+ );
94
+ }
95
+ ```
96
+
97
+ The bridge activates automatically when the page is loaded inside the OhhWells canvas editor. It does nothing in production (live site) mode.
98
+
99
+ ### 4. Mark sections
100
+
101
+ Every top-level section on each page must have a unique `data-ohw-section` attribute. This is what the canvas editor uses to:
102
+
103
+ - Show the "Add Section" insert line between sections
104
+ - Persist widget insertions (saving which section a widget was inserted after)
105
+
106
+ ```tsx
107
+ // Good — section element is the direct content root
108
+ <section data-ohw-section="hero">
109
+ ...
110
+ </section>
111
+
112
+ // Also fine — any element type works
113
+ <div data-ohw-section="testimonials">
114
+ ...
115
+ </div>
116
+ ```
117
+
118
+ **Naming rules:**
119
+
120
+ - Use `kebab-case`
121
+ - Must be unique across the entire page
122
+ - Must be stable — if a section is renamed, any saved widget insertions referencing that name will break
123
+
124
+ **Example — a page with multiple sections:**
125
+
126
+ ```tsx
127
+ export default function HomePage() {
128
+ return (
129
+ <>
130
+ <section data-ohw-section="hero">...</section>
131
+ <section data-ohw-section="lagree-intro">...</section>
132
+ <section data-ohw-section="classes-strip">...</section>
133
+ <section data-ohw-section="testimonials">...</section>
134
+ <section data-ohw-section="plan-form">...</section>
135
+ </>
136
+ );
137
+ }
138
+ ```
139
+
140
+ ### 5. Mark editable elements
141
+
142
+ Add `data-ohw-editable` and `data-ohw-key` to any element the studio owner should be able to edit:
143
+
144
+ ```tsx
145
+ {
146
+ /* Editable rich text (bold, italic, etc.) */
147
+ }
148
+ <h1 data-ohw-editable="text" data-ohw-key="hero-heading">
149
+ Welcome to the studio
150
+ </h1>;
151
+
152
+ {
153
+ /* Editable plain text (no formatting) */
154
+ }
155
+ <p data-ohw-editable="plain" data-ohw-key="hero-subtitle">
156
+ Book your first class
157
+ </p>;
158
+
159
+ {
160
+ /* Editable image */
161
+ }
162
+ <img
163
+ data-ohw-editable="image"
164
+ data-ohw-key="hero-image"
165
+ src="/hero.jpg"
166
+ alt="Hero"
167
+ />;
168
+
169
+ {
170
+ /* Editable background image */
171
+ }
172
+ <div
173
+ data-ohw-editable="bg-image"
174
+ data-ohw-key="hero-bg"
175
+ style={{ backgroundImage: "url(/bg.jpg)" }}
176
+ />;
177
+ ```
178
+
179
+ **Key naming rules:**
180
+
181
+ - Must be globally unique across all pages
182
+ - Use `kebab-case`
183
+ - Must be stable — changing a key orphans any saved content for that element
184
+
185
+ ---
186
+
187
+ ## SchedulingWidget (vibe-coder placement)
188
+
189
+ `SchedulingWidget` lets a template builder embed a scheduling/booking section directly in their template JSX, without going through the "Add Section" flow. The canvas editor treats it exactly like a dynamically-inserted scheduling widget the studio owner can connect a schedule, switch it, and clear it.
190
+
191
+ ### Basic usage
192
+
193
+ ```tsx
194
+ import { SchedulingWidget } from '@ohhwells/bridge'
195
+
196
+ export default function ClassesPage() {
197
+ return (
198
+ <>
199
+ <PageHeader ... />
200
+ <ClassLibrary ... />
201
+ <SchedulingWidget />
202
+ <WordmarkBand ... />
203
+ </>
204
+ )
205
+ }
206
+ ```
207
+
208
+ No props are required. The widget auto-generates a stable identity via React's `useId()` so the bridge can track which schedule is connected to it across saves.
209
+
210
+ ### Props
211
+
212
+ | Prop | Type | Default | Description |
213
+ |---|---|---|---|
214
+ | `insertAfter` | `string` | auto | Stable identifier used as the tracker key. Only set this manually if you need two `SchedulingWidget`s on the same page. |
215
+ | `initialScheduleId` | `string \| null` | `undefined` | Set by the bridge internally after hydration. Do not pass this yourself. |
216
+ | `notifyOnConnect` | `boolean` | `false` | Set by the bridge internally. Do not pass this yourself. |
217
+
218
+ ### How it works
219
+
220
+ 1. The widget renders immediately with a loading skeleton.
221
+ 2. It sends `ow:request-schedule-config` to the bridge (running in the same window).
222
+ 3. The bridge looks up the tracker for a saved `scheduleId` for this widget:
223
+ - **Found** responds with `ow:schedule-config { scheduleId }` widget loads that schedule.
224
+ - **Not found (first time)** → bridge adopts the widget (adds it to the tracker, notifies the canvas editor), responds with `scheduleId: null` → widget shows empty state with an "Add Schedule" button.
225
+ 4. In the canvas editor, the studio owner clicks "Add Schedule" → selects a schedule → the widget updates and the connection is saved.
226
+ 5. On the live site, the widget fetches the saved schedule by ID and renders it.
227
+
228
+ ### Multiple widgets on one page
229
+
230
+ Each `SchedulingWidget` must have a unique `insertAfter` to be tracked independently:
231
+
232
+ ```tsx
233
+ <SchedulingWidget insertAfter="classes-morning" />
234
+ <SchedulingWidget insertAfter="classes-evening" />
235
+ ```
236
+
237
+ If you omit `insertAfter` on both, `useId()` auto-generates different stable IDs for each, so they are tracked separately anyway.
238
+
239
+ ### Empty state on live site
240
+
241
+ If the studio owner has never connected a schedule to this widget, it renders nothing on the live site (no empty placeholder is shown to visitors).
242
+
243
+ ---
244
+
245
+ ## Editable states (advanced)
246
+
247
+ For elements with multiple display states (e.g. a contact form with default/success/error views), wrap each state in a `data-ohw-state-view` and mark the container with `data-ohw-editable-state`:
248
+
249
+ ```tsx
250
+ <div
251
+ data-ohw-editable-state="default,success,error"
252
+ data-ohw-key="contact-form"
253
+ >
254
+ <div data-ohw-state-view="default">{/* default form UI */}</div>
255
+ <div data-ohw-state-view="success">{/* success message */}</div>
256
+ <div data-ohw-state-view="error">{/* error message */}</div>
257
+ </div>
258
+ ```
259
+
260
+ The bridge shows a state toggle in the canvas editor to switch between states.
261
+
262
+ ---
263
+
264
+ ## Local development workflow
265
+
266
+ When iterating on the bridge package itself:
267
+
268
+ ```bash
269
+ # 1. Build the package
270
+ cd ohhwells-bridge
271
+ npm run build
272
+
273
+ # 2. Link it globally (one-time setup)
274
+ npm link
275
+
276
+ # 3. In your template repo, use the local build instead of the npm version
277
+ cd rebound-template
278
+ npm link @ohhwells/bridge
279
+ ```
280
+
281
+ After that, every `npm run build` in `ohhwells-bridge` is picked up by the template immediately (no re-link needed). To go back to the npm version:
282
+
283
+ ```bash
284
+ cd rebound-template
285
+ npm unlink @ohhwells/bridge
286
+ npm install
287
+ ```
288
+
289
+ ---
290
+
291
+ ## Publishing
292
+
293
+ The package publishes automatically via GitHub Actions on push to `main` (production tag) or `staging` (next tag). To publish manually:
294
+
295
+ ```bash
296
+ npm run build
297
+ npm publish --access public
298
+ ```
299
+
300
+ ---
301
+
302
+ ## Link popover (Canvas Editor)
303
+
304
+ `LinkPopover` is rendered inside `OhhwellsBridge` (iframe portal) when editing link destinations. It implements the Figma shadcn kit panel (nodes 8365-7616 / 8382-4161) — anchored popover, not a centered dialog.
305
+
306
+ ```tsx
307
+ // Used internally by OhhwellsBridge — external consumers rarely mount this directly.
308
+ import { LinkPopover } from "@ohhwells/bridge";
309
+ ```
310
+
311
+ The iframe bridge reports page sections on `ow:ready`:
312
+
313
+ ```json
314
+ { "type": "ow:ready", "version": "1", "nodes": [...], "sections": [{ "id": "hero", "label": "Hero" }] }
315
+ ```
316
+
317
+ Templates must mark sections with `data-ohw-section` (and optional `data-ohw-section-label`).
318
+
319
+ ### Local link-editor testing (dev fixtures)
320
+
321
+ Without the canvas editor parent, add `ohw-fixtures=1` to the edit URL. This preloads Re:Bound pages and sections into the link popover:
322
+
323
+ ```
324
+ http://localhost:3000 /?mode=edit&ohw-fixtures=1
325
+ ```
326
+
327
+ 1. Click a nav link label (e.g. **Pricing**) → toolbar → chain icon
328
+ 2. Pick **About** in Destination → **Choose a section** → **Personal training**
329
+ 3. **Save** — href becomes `/about#personal-training`
330
+ 4. Open `http://localhost:3001/about#personal-training` (without `mode=edit`) to verify scroll
331
+
332
+ In edit mode link clicks are blocked; check `href` in DevTools or test on a normal page load.
333
+
334
+ ---
335
+
336
+ ## Design tokens
337
+
338
+ The package ships the full OhhWells design token set, scoped to `[data-ohw-bridge-root]` so styles never leak into the host template.
339
+
340
+ ### Semantic colors (CSS variables on `[data-ohw-bridge-root]`)
341
+
342
+ | Token | Light | Dark |
343
+ | -------------------- | --------- | --------- |
344
+ | `primary` | `#0f172a` | `#f8fafc` |
345
+ | `primary-foreground` | `#f8fafc` | `#0f172a` |
346
+ | `background` | `#ffffff` | `#020617` |
347
+ | `foreground` | `#020617` | `#f8fafc` |
348
+ | `muted` | `#f1f5f9` | `#1e293b` |
349
+ | `muted-foreground` | `#64748b` | `#94a3b8` |
350
+ | `border` | `#e2e8f0` | `#334155` |
351
+ | `destructive` | `#dc2626` | `#7f1d1d` |
352
+ | `success` | `#16a34a` | `#22c55e` |
353
+
354
+ ### Brand palette
355
+
356
+ Re:Bound brand colors: `bone`, `ivory`, `sand`, `linen`, `stone`, `clay`, `umber`, `umber-deep`, `espresso`, `clove`, `ink`, `ash`, `carbon`.
357
+
358
+ ---
359
+
360
+ ## License
361
+
362
+ MIT