@ohhwells/bridge 0.1.27 → 0.1.29-next.20
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 +304 -304
- package/dist/index.cjs +20 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +20 -10
- package/dist/index.js.map +1 -1
- package/dist/styles.css +8 -8
- package/package.json +48 -48
package/README.md
CHANGED
|
@@ -1,304 +1,304 @@
|
|
|
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
|
+
## 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
|
package/dist/index.cjs
CHANGED
|
@@ -4311,7 +4311,10 @@ function isValidUrl(urlString) {
|
|
|
4311
4311
|
const hostname = url.hostname;
|
|
4312
4312
|
if (!hostname || hostname.length === 0) return false;
|
|
4313
4313
|
if (hostname.includes(" ")) return false;
|
|
4314
|
-
|
|
4314
|
+
if (hostname === "localhost") return true;
|
|
4315
|
+
if (!hostname.includes(".")) return false;
|
|
4316
|
+
if (!/[a-z]/i.test(hostname)) return false;
|
|
4317
|
+
return true;
|
|
4315
4318
|
} catch {
|
|
4316
4319
|
return false;
|
|
4317
4320
|
}
|
|
@@ -4322,12 +4325,19 @@ function normalizeUrl(value) {
|
|
|
4322
4325
|
return `https://${trimmed}`;
|
|
4323
4326
|
}
|
|
4324
4327
|
function validateUrlInput(value, pages, allPages) {
|
|
4325
|
-
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
if (
|
|
4328
|
+
const trimmed = value.trim();
|
|
4329
|
+
if (!trimmed) return "";
|
|
4330
|
+
const exactPage = allPages.find((p) => p.title.toLowerCase() === trimmed.toLowerCase());
|
|
4331
|
+
if (exactPage) return "";
|
|
4332
|
+
const hasPrefixMatch = pages.some((p) => p.title.toLowerCase().startsWith(trimmed.toLowerCase()));
|
|
4333
|
+
if (hasPrefixMatch) {
|
|
4334
|
+
return "Select a page from the list or enter a valid URL";
|
|
4335
|
+
}
|
|
4336
|
+
const hasScheme = /^https?:\/\//i.test(trimmed);
|
|
4337
|
+
if (!hasScheme && !trimmed.includes(".")) {
|
|
4338
|
+
return "Please enter a valid URL (e.g., https://example.com)";
|
|
4339
|
+
}
|
|
4340
|
+
if (!isValidUrl(normalizeUrl(trimmed))) {
|
|
4331
4341
|
return "Please enter a valid URL (e.g., https://example.com)";
|
|
4332
4342
|
}
|
|
4333
4343
|
return "";
|
|
@@ -4870,9 +4880,8 @@ function useLinkModalState({
|
|
|
4870
4880
|
setUrlError("");
|
|
4871
4881
|
const filtered = value.trim() ? availablePages.filter((p) => p.title.toLowerCase().startsWith(value.toLowerCase())) : availablePages;
|
|
4872
4882
|
setDropdownOpen(filtered.length > 0);
|
|
4873
|
-
if (value.trim()
|
|
4874
|
-
|
|
4875
|
-
if (error) setUrlError(error);
|
|
4883
|
+
if (value.trim()) {
|
|
4884
|
+
setUrlError(validateUrlInput(value, availablePages, pages));
|
|
4876
4885
|
}
|
|
4877
4886
|
};
|
|
4878
4887
|
const handlePageSelect = (page) => {
|
|
@@ -5426,6 +5435,7 @@ function applyLinkByKey(key, val) {
|
|
|
5426
5435
|
document.querySelectorAll(`[data-ohw-href-key="${key}"]`).forEach((el) => {
|
|
5427
5436
|
applyLinkHref(el, val);
|
|
5428
5437
|
});
|
|
5438
|
+
window.dispatchEvent(new CustomEvent("ohw:link-href", { detail: { key, href: val } }));
|
|
5429
5439
|
}
|
|
5430
5440
|
function isInsideLinkEditor(target) {
|
|
5431
5441
|
return Boolean(
|