@ohhwells/bridge 0.1.16 → 0.1.18
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 +198 -70
- package/dist/index.cjs +93 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +89 -11
- package/dist/index.js.map +1 -1
- package/dist/styles.css +112 -88
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -4,138 +4,266 @@ The OhhWells canvas editor bridge — a standalone npm package that enables inli
|
|
|
4
4
|
|
|
5
5
|
## What it does
|
|
6
6
|
|
|
7
|
-
When a studio owner opens their site in the OhhWells
|
|
7
|
+
When a studio owner opens their site in the OhhWells canvas editor, the bridge:
|
|
8
8
|
|
|
9
9
|
- Connects the iframe (the live site) to the parent canvas editor via `postMessage`
|
|
10
10
|
- Enables click-to-edit for text, images, and background images
|
|
11
11
|
- Handles draft saving and content hydration
|
|
12
|
-
-
|
|
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)
|
|
13
14
|
- Injects scoped styles that never leak into the host template
|
|
14
15
|
|
|
16
|
+
---
|
|
17
|
+
|
|
15
18
|
## Installation
|
|
16
19
|
|
|
17
20
|
```bash
|
|
18
21
|
npm install @ohhwells/bridge
|
|
19
22
|
```
|
|
20
23
|
|
|
21
|
-
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Template setup (required steps)
|
|
27
|
+
|
|
28
|
+
### 1. Import styles
|
|
29
|
+
|
|
30
|
+
In your root layout file, import the bridge stylesheet **once**:
|
|
22
31
|
|
|
23
32
|
```ts
|
|
24
33
|
import '@ohhwells/bridge/styles'
|
|
25
34
|
```
|
|
26
35
|
|
|
27
|
-
|
|
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
|
+
{/* Inline script — shows the loader immediately on the client before React hydrates */}
|
|
53
|
+
<script
|
|
54
|
+
dangerouslySetInnerHTML={{
|
|
55
|
+
__html: `(function(){try{
|
|
56
|
+
var p=location.hostname.split(".");
|
|
57
|
+
var fromHost=p.length>=3&&p[0]!=="www"?p[0]:"";
|
|
58
|
+
var fromQuery=new URLSearchParams(location.search).get("subdomain")||"";
|
|
59
|
+
if(!fromHost&&!fromQuery)return;
|
|
60
|
+
var e=document.getElementById("ohw-loader");
|
|
61
|
+
if(e)e.style.display="flex"
|
|
62
|
+
}catch(e){}})();`,
|
|
63
|
+
}}
|
|
64
|
+
/>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
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.
|
|
28
68
|
|
|
29
|
-
|
|
69
|
+
### 3. Mount `OhhwellsBridge`
|
|
70
|
+
|
|
71
|
+
Add `<OhhwellsBridge />` inside a `<Suspense>` boundary in your root layout. It must be in `<Suspense>` because it calls `useSearchParams()` internally.
|
|
30
72
|
|
|
31
73
|
```tsx
|
|
74
|
+
import { Suspense } from 'react'
|
|
32
75
|
import { OhhwellsBridge } from '@ohhwells/bridge'
|
|
33
76
|
|
|
34
77
|
export default function RootLayout({ children }) {
|
|
35
78
|
return (
|
|
36
|
-
<html>
|
|
79
|
+
<html lang="en">
|
|
37
80
|
<body>
|
|
81
|
+
{/* loader + inline script here (see step 2) */}
|
|
82
|
+
|
|
83
|
+
<Suspense>
|
|
84
|
+
<OhhwellsBridge />
|
|
85
|
+
</Suspense>
|
|
86
|
+
|
|
87
|
+
{/* rest of your layout */}
|
|
38
88
|
{children}
|
|
39
|
-
<OhhwellsBridge />
|
|
40
89
|
</body>
|
|
41
90
|
</html>
|
|
42
91
|
)
|
|
43
92
|
}
|
|
44
93
|
```
|
|
45
94
|
|
|
46
|
-
The bridge activates automatically when the page is loaded inside the OhhWells canvas editor. It
|
|
95
|
+
The bridge activates automatically when the page is loaded inside the OhhWells canvas editor. It does nothing in production (live site) mode.
|
|
47
96
|
|
|
48
|
-
|
|
97
|
+
### 4. Mark sections
|
|
49
98
|
|
|
50
|
-
|
|
99
|
+
Every top-level section on each page must have a unique `data-ohw-section` attribute. This is what the canvas editor uses to:
|
|
100
|
+
- Show the "Add Section" insert line between sections
|
|
101
|
+
- Persist widget insertions (saving which section a widget was inserted after)
|
|
51
102
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
103
|
+
```tsx
|
|
104
|
+
// Good — section element is the direct content root
|
|
105
|
+
<section data-ohw-section="hero">
|
|
106
|
+
...
|
|
107
|
+
</section>
|
|
108
|
+
|
|
109
|
+
// Also fine — any element type works
|
|
110
|
+
<div data-ohw-section="testimonials">
|
|
111
|
+
...
|
|
112
|
+
</div>
|
|
113
|
+
```
|
|
58
114
|
|
|
59
|
-
|
|
115
|
+
**Naming rules:**
|
|
116
|
+
- Use `kebab-case`
|
|
117
|
+
- Must be unique across the entire page
|
|
118
|
+
- Must be stable — if a section is renamed, any saved widget insertions referencing that name will break
|
|
60
119
|
|
|
61
|
-
|
|
120
|
+
**Example — a page with multiple sections:**
|
|
62
121
|
|
|
63
|
-
|
|
122
|
+
```tsx
|
|
123
|
+
export default function HomePage() {
|
|
124
|
+
return (
|
|
125
|
+
<>
|
|
126
|
+
<section data-ohw-section="hero">...</section>
|
|
127
|
+
<section data-ohw-section="lagree-intro">...</section>
|
|
128
|
+
<section data-ohw-section="classes-strip">...</section>
|
|
129
|
+
<section data-ohw-section="testimonials">...</section>
|
|
130
|
+
<section data-ohw-section="plan-form">...</section>
|
|
131
|
+
</>
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
```
|
|
64
135
|
|
|
65
|
-
|
|
136
|
+
### 5. Mark editable elements
|
|
66
137
|
|
|
67
|
-
|
|
68
|
-
|---|---|---|
|
|
69
|
-
| `background` | `#ffffff` | `#020617` |
|
|
70
|
-
| `foreground` | `#020617` | `#f8fafc` |
|
|
71
|
-
| `primary` | `#0f172a` | `#f8fafc` |
|
|
72
|
-
| `primary-foreground` | `#f8fafc` | `#0f172a` |
|
|
73
|
-
| `secondary` | `#f1f5f9` | `#1e293b` |
|
|
74
|
-
| `muted` | `#f1f5f9` | `#1e293b` |
|
|
75
|
-
| `muted-foreground` | `#64748b` | `#94a3b8` |
|
|
76
|
-
| `accent` | `#f1f5f9` | `#1e293b` |
|
|
77
|
-
| `border` | `#e2e8f0` | `#334155` |
|
|
78
|
-
| `destructive` | `#dc2626` | `#7f1d1d` |
|
|
79
|
-
| `success` | `#16a34a` | `#22c55e` |
|
|
138
|
+
Add `data-ohw-editable` and `data-ohw-key` to any element the studio owner should be able to edit:
|
|
80
139
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
140
|
+
```tsx
|
|
141
|
+
{/* Editable rich text (bold, italic, etc.) */}
|
|
142
|
+
<h1
|
|
143
|
+
data-ohw-editable="text"
|
|
144
|
+
data-ohw-key="hero-heading"
|
|
145
|
+
>
|
|
146
|
+
Welcome to the studio
|
|
147
|
+
</h1>
|
|
148
|
+
|
|
149
|
+
{/* Editable plain text (no formatting) */}
|
|
150
|
+
<p
|
|
151
|
+
data-ohw-editable="plain"
|
|
152
|
+
data-ohw-key="hero-subtitle"
|
|
153
|
+
>
|
|
154
|
+
Book your first class
|
|
155
|
+
</p>
|
|
156
|
+
|
|
157
|
+
{/* Editable image */}
|
|
158
|
+
<img
|
|
159
|
+
data-ohw-editable="image"
|
|
160
|
+
data-ohw-key="hero-image"
|
|
161
|
+
src="/hero.jpg"
|
|
162
|
+
alt="Hero"
|
|
163
|
+
/>
|
|
164
|
+
|
|
165
|
+
{/* Editable background image */}
|
|
166
|
+
<div
|
|
167
|
+
data-ohw-editable="bg-image"
|
|
168
|
+
data-ohw-key="hero-bg"
|
|
169
|
+
style={{ backgroundImage: 'url(/bg.jpg)' }}
|
|
170
|
+
/>
|
|
171
|
+
```
|
|
100
172
|
|
|
101
|
-
|
|
173
|
+
**Key naming rules:**
|
|
174
|
+
- Must be globally unique across all pages
|
|
175
|
+
- Use `kebab-case`
|
|
176
|
+
- Must be stable — changing a key orphans any saved content for that element
|
|
102
177
|
|
|
103
|
-
|
|
178
|
+
---
|
|
104
179
|
|
|
105
|
-
##
|
|
180
|
+
## Editable states (advanced)
|
|
106
181
|
|
|
107
|
-
|
|
182
|
+
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`:
|
|
108
183
|
|
|
109
|
-
```
|
|
110
|
-
<div
|
|
184
|
+
```tsx
|
|
185
|
+
<div
|
|
186
|
+
data-ohw-editable-state="default,success,error"
|
|
187
|
+
data-ohw-key="contact-form"
|
|
188
|
+
>
|
|
189
|
+
<div data-ohw-state-view="default">
|
|
190
|
+
{/* default form UI */}
|
|
191
|
+
</div>
|
|
192
|
+
<div data-ohw-state-view="success">
|
|
193
|
+
{/* success message */}
|
|
194
|
+
</div>
|
|
195
|
+
<div data-ohw-state-view="error">
|
|
196
|
+
{/* error message */}
|
|
197
|
+
</div>
|
|
198
|
+
</div>
|
|
111
199
|
```
|
|
112
200
|
|
|
113
|
-
|
|
201
|
+
The bridge shows a state toggle in the canvas editor to switch between states.
|
|
114
202
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## Local development workflow
|
|
206
|
+
|
|
207
|
+
When iterating on the bridge package itself:
|
|
118
208
|
|
|
119
|
-
|
|
209
|
+
```bash
|
|
210
|
+
# 1. Build the package
|
|
211
|
+
cd ohhwells-bridge
|
|
120
212
|
npm run build
|
|
121
213
|
|
|
122
|
-
#
|
|
123
|
-
npm
|
|
214
|
+
# 2. Link it globally (one-time setup)
|
|
215
|
+
npm link
|
|
216
|
+
|
|
217
|
+
# 3. In your template repo, use the local build instead of the npm version
|
|
218
|
+
cd rebound-template
|
|
219
|
+
npm link @ohhwells/bridge
|
|
124
220
|
```
|
|
125
221
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
-
|
|
130
|
-
|
|
222
|
+
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:
|
|
223
|
+
|
|
224
|
+
```bash
|
|
225
|
+
cd rebound-template
|
|
226
|
+
npm unlink @ohhwells/bridge
|
|
227
|
+
npm install
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
---
|
|
131
231
|
|
|
132
232
|
## Publishing
|
|
133
233
|
|
|
234
|
+
The package publishes automatically via GitHub Actions on push to `main` (production tag) or `staging` (next tag). To publish manually:
|
|
235
|
+
|
|
134
236
|
```bash
|
|
135
237
|
npm run build
|
|
136
238
|
npm publish --access public
|
|
137
239
|
```
|
|
138
240
|
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
## Design tokens
|
|
244
|
+
|
|
245
|
+
The package ships the full OhhWells design token set, scoped to `[data-ohw-bridge-root]` so styles never leak into the host template.
|
|
246
|
+
|
|
247
|
+
### Semantic colors (CSS variables on `[data-ohw-bridge-root]`)
|
|
248
|
+
|
|
249
|
+
| Token | Light | Dark |
|
|
250
|
+
|---|---|---|
|
|
251
|
+
| `primary` | `#0f172a` | `#f8fafc` |
|
|
252
|
+
| `primary-foreground` | `#f8fafc` | `#0f172a` |
|
|
253
|
+
| `background` | `#ffffff` | `#020617` |
|
|
254
|
+
| `foreground` | `#020617` | `#f8fafc` |
|
|
255
|
+
| `muted` | `#f1f5f9` | `#1e293b` |
|
|
256
|
+
| `muted-foreground` | `#64748b` | `#94a3b8` |
|
|
257
|
+
| `border` | `#e2e8f0` | `#334155` |
|
|
258
|
+
| `destructive` | `#dc2626` | `#7f1d1d` |
|
|
259
|
+
| `success` | `#16a34a` | `#22c55e` |
|
|
260
|
+
|
|
261
|
+
### Brand palette
|
|
262
|
+
|
|
263
|
+
Re:Bound brand colors: `bone`, `ivory`, `sand`, `linen`, `stone`, `clay`, `umber`, `umber-deep`, `espresso`, `clove`, `ink`, `ash`, `carbon`.
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
139
267
|
## License
|
|
140
268
|
|
|
141
269
|
MIT
|
package/dist/index.cjs
CHANGED
|
@@ -3472,8 +3472,30 @@ function isEditSessionActive() {
|
|
|
3472
3472
|
return new URLSearchParams(q).get("mode") === "edit";
|
|
3473
3473
|
}
|
|
3474
3474
|
|
|
3475
|
-
// src/
|
|
3475
|
+
// src/ui/badge.tsx
|
|
3476
3476
|
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
3477
|
+
var badgeVariants = cva(
|
|
3478
|
+
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
|
3479
|
+
{
|
|
3480
|
+
variants: {
|
|
3481
|
+
variant: {
|
|
3482
|
+
default: "border-transparent bg-primary text-primary-foreground",
|
|
3483
|
+
secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
|
3484
|
+
destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
|
3485
|
+
outline: "text-foreground"
|
|
3486
|
+
}
|
|
3487
|
+
},
|
|
3488
|
+
defaultVariants: {
|
|
3489
|
+
variant: "default"
|
|
3490
|
+
}
|
|
3491
|
+
}
|
|
3492
|
+
);
|
|
3493
|
+
function Badge({ className, variant, ...props }) {
|
|
3494
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: cn(badgeVariants({ variant }), className), ...props });
|
|
3495
|
+
}
|
|
3496
|
+
|
|
3497
|
+
// src/OhhwellsBridge.tsx
|
|
3498
|
+
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
3477
3499
|
var PRIMARY = "#0885FE";
|
|
3478
3500
|
var IMAGE_FADE_MS = 300;
|
|
3479
3501
|
function runOpacityFade(el, onDone) {
|
|
@@ -3681,7 +3703,7 @@ var TOOLBAR_GROUPS = [
|
|
|
3681
3703
|
];
|
|
3682
3704
|
function GlowFrame({ rect, elRef }) {
|
|
3683
3705
|
const GAP = 6;
|
|
3684
|
-
return /* @__PURE__ */ (0,
|
|
3706
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
3685
3707
|
"div",
|
|
3686
3708
|
{
|
|
3687
3709
|
ref: elRef,
|
|
@@ -3732,7 +3754,7 @@ function FloatingToolbar({
|
|
|
3732
3754
|
activeCommands
|
|
3733
3755
|
}) {
|
|
3734
3756
|
const { top, left, transform } = calcToolbarPos(rect, parentScroll);
|
|
3735
|
-
return /* @__PURE__ */ (0,
|
|
3757
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
3736
3758
|
"div",
|
|
3737
3759
|
{
|
|
3738
3760
|
ref: elRef,
|
|
@@ -3755,11 +3777,11 @@ function FloatingToolbar({
|
|
|
3755
3777
|
pointerEvents: "auto",
|
|
3756
3778
|
whiteSpace: "nowrap"
|
|
3757
3779
|
},
|
|
3758
|
-
children: TOOLBAR_GROUPS.map((btns, gi) => /* @__PURE__ */ (0,
|
|
3759
|
-
gi > 0 && /* @__PURE__ */ (0,
|
|
3780
|
+
children: TOOLBAR_GROUPS.map((btns, gi) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_react.default.Fragment, { children: [
|
|
3781
|
+
gi > 0 && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { style: { display: "block", width: 1, height: 24, background: "#E7E5E4", flexShrink: 0 } }),
|
|
3760
3782
|
btns.map((btn) => {
|
|
3761
3783
|
const isActive = activeCommands.has(btn.cmd);
|
|
3762
|
-
return /* @__PURE__ */ (0,
|
|
3784
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
3763
3785
|
"button",
|
|
3764
3786
|
{
|
|
3765
3787
|
title: btn.title,
|
|
@@ -3785,7 +3807,7 @@ function FloatingToolbar({
|
|
|
3785
3807
|
flexShrink: 0,
|
|
3786
3808
|
padding: 6
|
|
3787
3809
|
},
|
|
3788
|
-
children: /* @__PURE__ */ (0,
|
|
3810
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
3789
3811
|
"svg",
|
|
3790
3812
|
{
|
|
3791
3813
|
width: "16",
|
|
@@ -3818,7 +3840,7 @@ function StateToggle({
|
|
|
3818
3840
|
states,
|
|
3819
3841
|
onStateChange
|
|
3820
3842
|
}) {
|
|
3821
|
-
return /* @__PURE__ */ (0,
|
|
3843
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
3822
3844
|
ToggleGroup,
|
|
3823
3845
|
{
|
|
3824
3846
|
"data-ohw-state-toggle": "",
|
|
@@ -3832,7 +3854,7 @@ function StateToggle({
|
|
|
3832
3854
|
left: rect.right - 8,
|
|
3833
3855
|
transform: "translateX(-100%)"
|
|
3834
3856
|
},
|
|
3835
|
-
children: states.map((state) => /* @__PURE__ */ (0,
|
|
3857
|
+
children: states.map((state) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ToggleGroupItem, { value: state, size: "sm", children: state }, state))
|
|
3836
3858
|
}
|
|
3837
3859
|
);
|
|
3838
3860
|
}
|
|
@@ -3886,6 +3908,7 @@ function OhhwellsBridge() {
|
|
|
3886
3908
|
const glowElRef = (0, import_react.useRef)(null);
|
|
3887
3909
|
const hoveredImageRef = (0, import_react.useRef)(null);
|
|
3888
3910
|
const hoveredImageHasTextOverlapRef = (0, import_react.useRef)(false);
|
|
3911
|
+
const hoveredGapRef = (0, import_react.useRef)(null);
|
|
3889
3912
|
const imageUnhoverTimerRef = (0, import_react.useRef)(null);
|
|
3890
3913
|
const imageShowTimerRef = (0, import_react.useRef)(null);
|
|
3891
3914
|
const editStylesRef = (0, import_react.useRef)(null);
|
|
@@ -3901,6 +3924,7 @@ function OhhwellsBridge() {
|
|
|
3901
3924
|
const [toggleState, setToggleState] = (0, import_react.useState)(null);
|
|
3902
3925
|
const [maxBadge, setMaxBadge] = (0, import_react.useState)(null);
|
|
3903
3926
|
const [activeCommands, setActiveCommands] = (0, import_react.useState)(/* @__PURE__ */ new Set());
|
|
3927
|
+
const [sectionGap, setSectionGap] = (0, import_react.useState)(null);
|
|
3904
3928
|
(0, import_react.useEffect)(() => {
|
|
3905
3929
|
const update = () => {
|
|
3906
3930
|
const el = activeElRef.current;
|
|
@@ -3909,6 +3933,12 @@ function OhhwellsBridge() {
|
|
|
3909
3933
|
if (!prev || !activeStateElRef.current) return prev;
|
|
3910
3934
|
return { ...prev, rect: activeStateElRef.current.getBoundingClientRect() };
|
|
3911
3935
|
});
|
|
3936
|
+
setSectionGap((prev) => {
|
|
3937
|
+
if (!prev) return prev;
|
|
3938
|
+
const anchor = document.querySelector(`[data-ohw-section="${prev.insertAfter}"]`);
|
|
3939
|
+
if (!anchor) return prev;
|
|
3940
|
+
return { ...prev, y: anchor.getBoundingClientRect().bottom };
|
|
3941
|
+
});
|
|
3912
3942
|
};
|
|
3913
3943
|
const vvp = window.visualViewport;
|
|
3914
3944
|
if (!vvp) return;
|
|
@@ -4413,10 +4443,32 @@ function OhhwellsBridge() {
|
|
|
4413
4443
|
}
|
|
4414
4444
|
}
|
|
4415
4445
|
};
|
|
4446
|
+
const probeSectionGapAt = (clientX, clientY, fromParentViewport = false) => {
|
|
4447
|
+
const { y } = toProbeCoords(clientX, clientY, fromParentViewport);
|
|
4448
|
+
const sections = Array.from(document.querySelectorAll("[data-ohw-section]")).sort((a, b) => a.getBoundingClientRect().top - b.getBoundingClientRect().top);
|
|
4449
|
+
const ZONE = 20;
|
|
4450
|
+
for (let i = 0; i < sections.length; i++) {
|
|
4451
|
+
const a = sections[i];
|
|
4452
|
+
const b = sections[i + 1] ?? null;
|
|
4453
|
+
const boundaryY = a.getBoundingClientRect().bottom;
|
|
4454
|
+
if (Math.abs(y - boundaryY) <= ZONE) {
|
|
4455
|
+
const insertAfter = a.dataset.ohwSection ?? "";
|
|
4456
|
+
const insertBefore = b?.dataset.ohwSection ?? null;
|
|
4457
|
+
hoveredGapRef.current = { insertAfter, insertBefore };
|
|
4458
|
+
setSectionGap({ insertAfter, insertBefore, y: boundaryY });
|
|
4459
|
+
return;
|
|
4460
|
+
}
|
|
4461
|
+
}
|
|
4462
|
+
if (hoveredGapRef.current) {
|
|
4463
|
+
hoveredGapRef.current = null;
|
|
4464
|
+
setSectionGap(null);
|
|
4465
|
+
}
|
|
4466
|
+
};
|
|
4416
4467
|
const handleMouseMove = (e) => {
|
|
4417
4468
|
const { clientX, clientY } = e;
|
|
4418
4469
|
probeImageAt(clientX, clientY);
|
|
4419
4470
|
probeHoverCardsAt(clientX, clientY);
|
|
4471
|
+
probeSectionGapAt(clientX, clientY);
|
|
4420
4472
|
};
|
|
4421
4473
|
const handlePointerSync = (e) => {
|
|
4422
4474
|
if (e.data?.type !== "ow:pointer-sync") return;
|
|
@@ -4424,6 +4476,7 @@ function OhhwellsBridge() {
|
|
|
4424
4476
|
if (typeof clientX !== "number" || typeof clientY !== "number") return;
|
|
4425
4477
|
probeImageAt(clientX, clientY);
|
|
4426
4478
|
probeHoverCardsAt(clientX, clientY);
|
|
4479
|
+
probeSectionGapAt(clientX, clientY);
|
|
4427
4480
|
};
|
|
4428
4481
|
const handleDragOver = (e) => {
|
|
4429
4482
|
e.preventDefault();
|
|
@@ -4829,12 +4882,12 @@ function OhhwellsBridge() {
|
|
|
4829
4882
|
setToggleState((prev) => prev ? { ...prev, activeState: state } : null);
|
|
4830
4883
|
}, [deactivate]);
|
|
4831
4884
|
return bridgeRoot ? (0, import_react_dom.createPortal)(
|
|
4832
|
-
/* @__PURE__ */ (0,
|
|
4833
|
-
toolbarRect && /* @__PURE__ */ (0,
|
|
4834
|
-
/* @__PURE__ */ (0,
|
|
4835
|
-
/* @__PURE__ */ (0,
|
|
4885
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
|
|
4886
|
+
toolbarRect && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
|
|
4887
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(GlowFrame, { rect: toolbarRect, elRef: glowElRef }),
|
|
4888
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(FloatingToolbar, { rect: toolbarRect, parentScroll: parentScrollRef.current, elRef: toolbarElRef, onCommand: handleCommand, activeCommands })
|
|
4836
4889
|
] }),
|
|
4837
|
-
maxBadge && /* @__PURE__ */ (0,
|
|
4890
|
+
maxBadge && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
|
|
4838
4891
|
"div",
|
|
4839
4892
|
{
|
|
4840
4893
|
"data-ohw-max-badge": "",
|
|
@@ -4860,7 +4913,7 @@ function OhhwellsBridge() {
|
|
|
4860
4913
|
]
|
|
4861
4914
|
}
|
|
4862
4915
|
),
|
|
4863
|
-
toggleState && /* @__PURE__ */ (0,
|
|
4916
|
+
toggleState && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
4864
4917
|
StateToggle,
|
|
4865
4918
|
{
|
|
4866
4919
|
rect: toggleState.rect,
|
|
@@ -4868,6 +4921,31 @@ function OhhwellsBridge() {
|
|
|
4868
4921
|
states: toggleState.states,
|
|
4869
4922
|
onStateChange: handleStateChange
|
|
4870
4923
|
}
|
|
4924
|
+
),
|
|
4925
|
+
sectionGap && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
|
|
4926
|
+
"div",
|
|
4927
|
+
{
|
|
4928
|
+
"data-ohw-section-insert-line": "",
|
|
4929
|
+
className: "fixed left-0 w-full z-2147483646 flex items-center pointer-events-none",
|
|
4930
|
+
style: { top: sectionGap.y, transform: "translateY(-50%)" },
|
|
4931
|
+
children: [
|
|
4932
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "flex-1 bg-primary", style: { height: 3 } }),
|
|
4933
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
4934
|
+
Badge,
|
|
4935
|
+
{
|
|
4936
|
+
className: "px-8 py-1 bg-primary hover:bg-primary text-primary-foreground text-xs font-medium shrink-0 rounded-full cursor-pointer pointer-events-auto",
|
|
4937
|
+
onClick: () => {
|
|
4938
|
+
window.parent.postMessage(
|
|
4939
|
+
{ type: "ow:add-section", insertAfter: sectionGap.insertAfter, insertBefore: sectionGap.insertBefore },
|
|
4940
|
+
"*"
|
|
4941
|
+
);
|
|
4942
|
+
},
|
|
4943
|
+
children: "Add Section"
|
|
4944
|
+
}
|
|
4945
|
+
),
|
|
4946
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "flex-1 bg-primary", style: { height: 3 } })
|
|
4947
|
+
]
|
|
4948
|
+
}
|
|
4871
4949
|
)
|
|
4872
4950
|
] }),
|
|
4873
4951
|
bridgeRoot
|