@aznabee/freehand-ui 0.1.3 → 0.1.5
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 +192 -179
- package/dist/freehand-ui.cjs +45 -12
- package/dist/freehand-ui.js +45 -12
- package/dist/index.d.ts +10 -5
- package/dist/react.cjs +46 -13
- package/dist/react.d.ts +3 -3
- package/dist/react.js +46 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,96 +1,54 @@
|
|
|
1
|
-
|
|
1
|
+

|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
`freehand-ui` adds a subtle SVG overlay — thin white pen strokes, handwritten notes, arrows, and sparse decorations — without changing your HTML layout or intercepting pointer events.
|
|
6
|
-
|
|
7
|
-
## Install
|
|
8
|
-
|
|
9
|
-
```bash
|
|
10
|
-
npm install @aznabee/freehand-ui
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
## Basic usage
|
|
3
|
+
# Freehand-ui
|
|
14
4
|
|
|
15
|
-
|
|
16
|
-
import { doodle } from "@aznabee/freehand-ui";
|
|
5
|
+
Wrap any existing web element with a beautiful, responsive, hand-drawn doodle layer.
|
|
17
6
|
|
|
18
|
-
|
|
19
|
-
// or
|
|
20
|
-
doodle(".card");
|
|
21
|
-
```
|
|
7
|
+
## What it does
|
|
22
8
|
|
|
23
|
-
|
|
9
|
+
`freehand-ui` adds a subtle SVG overlay on top of your UI - thin pen strokes, handwritten notes, arrows, and small decorations. It does **not** change your HTML layout or block clicks (`pointer-events: none`).
|
|
24
10
|
|
|
25
|
-
|
|
11
|
+
**Good for:** call-to-action buttons, feature cards, onboarding hints, playful marketing UI.
|
|
26
12
|
|
|
27
|
-
|
|
28
|
-
|
|
13
|
+
| Feature | One-liner |
|
|
14
|
+
| --------------- | ------------------------------------------------------------------- |
|
|
15
|
+
| **Border** | Hand-drawn frame that follows the element's shape and border-radius |
|
|
16
|
+
| **Notes** | Caveat handwriting beside the element |
|
|
17
|
+
| **Arrows** | Curved, straight, dotted, or looped pointers into the element |
|
|
18
|
+
| **Decorations** | Corner accents, stars, hearts, and more |
|
|
19
|
+
| **Responsive** | Stays aligned on resize, scroll, and reflow |
|
|
29
20
|
|
|
30
|
-
|
|
31
|
-
<Button />
|
|
32
|
-
</Doodle>;
|
|
33
|
-
```
|
|
21
|
+
**Live on [Aznabee.com](https://aznabee.com)** - see it in production on the real product.
|
|
34
22
|
|
|
35
|
-
|
|
36
|
-
drops straight into the Next.js App Router without a wrapper of your own, and
|
|
37
|
-
it renders on the server without warnings.
|
|
23
|
+
## 
|
|
38
24
|
|
|
39
|
-
|
|
40
|
-
never decorated individually. To decorate descendants instead, opt in with
|
|
41
|
-
`childSelector`:
|
|
25
|
+
## Install
|
|
42
26
|
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
{items.map((item) => <Card key={item.id} className="card" {...item} />)}
|
|
46
|
-
</Doodle>
|
|
27
|
+
```bash
|
|
28
|
+
npm install @aznabee/freehand-ui
|
|
47
29
|
```
|
|
48
30
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
`<Doodle>` renders a `<span style="display:inline-flex">` that shrink-wraps its
|
|
52
|
-
child, so the frame hugs your component instead of stretching to the full width
|
|
53
|
-
of its container. `inline-flex` specifically: an `inline-block` wrapper adds
|
|
54
|
-
baseline leading beneath an inline-level child like a `<button>`, which would
|
|
55
|
-
leave the frame visibly taller than the button.
|
|
56
|
-
|
|
57
|
-
Override it when you need different layout — any extra props go to the DOM node:
|
|
58
|
-
|
|
59
|
-
```jsx
|
|
60
|
-
<Doodle as="div" style={{ display: "block" }} className="w-full" note="hi">
|
|
61
|
-
<Card />
|
|
62
|
-
</Doodle>
|
|
63
|
-
```
|
|
31
|
+
---
|
|
64
32
|
|
|
65
|
-
|
|
66
|
-
it inherits the radius of the child it hugs, so a wrapped pill button is still
|
|
67
|
-
drawn as a pill.
|
|
33
|
+
## Vanilla JavaScript
|
|
68
34
|
|
|
69
|
-
|
|
35
|
+
Import from the main entry point and call `doodle()` on any element or CSS selector.
|
|
70
36
|
|
|
71
|
-
|
|
37
|
+
### Quick start
|
|
72
38
|
|
|
73
|
-
```
|
|
74
|
-
import {
|
|
39
|
+
```javascript
|
|
40
|
+
import { doodle } from "@aznabee/freehand-ui";
|
|
75
41
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
return <div ref={ref}>…</div>;
|
|
80
|
-
}
|
|
42
|
+
doodle(document.querySelector(".card"));
|
|
43
|
+
// or
|
|
44
|
+
doodle(".card");
|
|
81
45
|
```
|
|
82
46
|
|
|
83
|
-
|
|
47
|
+
This draws a hand-drawn border around the element and keeps it aligned as the page resizes, scrolls, or reflows.
|
|
84
48
|
|
|
85
|
-
|
|
86
|
-
re-render does not tear down and redraw the overlay with a new random seed.
|
|
87
|
-
- The handwriting font is fetched automatically — see [Handwriting
|
|
88
|
-
font](#handwriting-font) to use `next/font` or self-host instead.
|
|
89
|
-
- `disabled` skips drawing entirely — useful behind a reduced-motion check or a
|
|
90
|
-
feature flag.
|
|
91
|
-
- TypeScript definitions ship with the package for both entry points.
|
|
49
|
+
### Configuration
|
|
92
50
|
|
|
93
|
-
|
|
51
|
+
Pass a second argument to `doodle()`:
|
|
94
52
|
|
|
95
53
|
```javascript
|
|
96
54
|
doodle(".card", {
|
|
@@ -98,75 +56,54 @@ doodle(".card", {
|
|
|
98
56
|
color: "#ffffff",
|
|
99
57
|
strokeWidth: 1.5,
|
|
100
58
|
roughness: 1.5,
|
|
101
|
-
padding: 0,
|
|
102
|
-
radius: 20,
|
|
59
|
+
padding: 0, // gap between the element's edge and the frame
|
|
60
|
+
radius: 20, // optional override; auto-detected from CSS when omitted
|
|
103
61
|
opacity: 0.9,
|
|
104
|
-
addBreaks: false,
|
|
105
|
-
fontFamily: null,
|
|
62
|
+
addBreaks: false, // lift the pen at random points around the outline
|
|
63
|
+
fontFamily: null, // override the handwriting stack
|
|
106
64
|
autoLoadFont: true,
|
|
107
65
|
});
|
|
108
66
|
```
|
|
109
67
|
|
|
110
|
-
The frame traces the element's own edge by default. Raise `padding` to stand it
|
|
111
|
-
off — `padding: 8` leaves a comfortable margin around a card.
|
|
68
|
+
The frame traces the element's own edge by default. Raise `padding` to stand it off - `padding: 8` leaves a comfortable margin around a card.
|
|
112
69
|
|
|
113
|
-
|
|
70
|
+
### Handwriting font
|
|
114
71
|
|
|
115
|
-
Notes are set in [Caveat](https://fonts.google.com/specimen/Caveat). The library
|
|
116
|
-
loads it for you the first time a note is drawn, so the handwriting looks right
|
|
117
|
-
in a plain app with no font setup of its own — without it the text falls back to
|
|
118
|
-
the generic `cursive` family, which on macOS is a calligraphic serif rather than
|
|
119
|
-
anything handwritten.
|
|
72
|
+
Notes are set in [Caveat](https://fonts.google.com/specimen/Caveat). The library loads it the first time a note is drawn, so handwriting looks right in a plain app with no font setup. Without it, text falls back to the generic `cursive` family (on macOS that's a calligraphic serif, not handwriting).
|
|
120
73
|
|
|
121
|
-
It skips the request when the page already provides the family, and only ever
|
|
122
|
-
requests it once. To take over:
|
|
74
|
+
It skips the request when the page already provides the family, and only ever requests it once. To take over:
|
|
123
75
|
|
|
124
76
|
```javascript
|
|
125
77
|
// self-hosted, or already loaded elsewhere on the page
|
|
126
78
|
doodle(".card", { note: "hi", autoLoadFont: false });
|
|
127
79
|
|
|
128
|
-
// your own family
|
|
80
|
+
// your own family
|
|
129
81
|
doodle(".card", { note: "hi", fontFamily: "var(--font-caveat)" });
|
|
130
82
|
```
|
|
131
83
|
|
|
132
|
-
|
|
133
|
-
import { Caveat } from "next/font/google";
|
|
134
|
-
const caveat = Caveat({ subsets: ["latin"], variable: "--font-caveat" });
|
|
84
|
+
Setting `autoLoadFont: false` without a `fontFamily` falls back to system handwriting faces (Bradley Hand, Segoe Script, Comic Sans MS). Notes are re-measured when a font finishes loading, so the underline always matches the final glyph widths.
|
|
135
85
|
|
|
136
|
-
|
|
137
|
-
<Button />
|
|
138
|
-
</Doodle>;
|
|
139
|
-
```
|
|
86
|
+
### Broken outlines
|
|
140
87
|
|
|
141
|
-
|
|
142
|
-
handwriting faces the system has (Bradley Hand, Segoe Script, Comic Sans MS).
|
|
143
|
-
Notes are re-measured when a font finishes loading, so the underline always
|
|
144
|
-
matches the final glyph widths.
|
|
145
|
-
|
|
146
|
-
## Broken outlines
|
|
147
|
-
|
|
148
|
-
`addBreaks` cuts randomly sized gaps into the border, so it reads as a few
|
|
149
|
-
confident dashes rather than one closed loop:
|
|
88
|
+
`addBreaks` cuts randomly sized gaps into the border so it reads as a few confident dashes rather than one closed loop:
|
|
150
89
|
|
|
151
90
|
```javascript
|
|
152
91
|
doodle(".cta", { addBreaks: true });
|
|
153
92
|
|
|
154
|
-
doodle(".cta", { addBreaks: 5 });
|
|
93
|
+
doodle(".cta", { addBreaks: 5 }); // five gaps
|
|
155
94
|
|
|
156
95
|
doodle(".cta", {
|
|
157
96
|
addBreaks: {
|
|
158
|
-
count: 4,
|
|
159
|
-
min: 6,
|
|
160
|
-
max: 30,
|
|
97
|
+
count: 4, // omit for 2–5 gaps
|
|
98
|
+
min: 6, // shortest gap, px
|
|
99
|
+
max: 30, // longest gap, px
|
|
161
100
|
},
|
|
162
101
|
});
|
|
163
102
|
```
|
|
164
103
|
|
|
165
|
-
Gaps are spread one per section of the perimeter so they never clump, and their
|
|
166
|
-
total is capped at a third of the outline so the frame still reads as a frame.
|
|
167
|
-
On small elements the gaps shrink to fit.
|
|
104
|
+
Gaps are spread one per section of the perimeter so they never clump, and their total is capped at a third of the outline so the frame still reads as a frame. On small elements the gaps shrink to fit.
|
|
168
105
|
|
|
169
|
-
|
|
106
|
+
### Handwritten annotations
|
|
170
107
|
|
|
171
108
|
```javascript
|
|
172
109
|
doodle(".cta", {
|
|
@@ -178,25 +115,17 @@ doodle(".cta", {
|
|
|
178
115
|
});
|
|
179
116
|
```
|
|
180
117
|
|
|
181
|
-
Positions
|
|
118
|
+
**Positions:** `top`, `top-right`, `right`, `bottom-right`, `bottom`, `bottom-left`, `left`, `top-left`. Same names apply to `arrow.from` / `arrow.to`.
|
|
182
119
|
|
|
183
|
-
Notes are measured from
|
|
184
|
-
on top of the element. The underline is drawn to the measured text width; set
|
|
185
|
-
`underline: false` for plain handwriting.
|
|
120
|
+
Notes are measured from real glyph metrics and laid out so they never sit on top of the element. The underline is drawn to the measured text width; set `underline: false` for plain handwriting.
|
|
186
121
|
|
|
187
|
-
|
|
188
|
-
the viewport it slides horizontally by exactly the amount it overhangs, so a
|
|
189
|
-
narrowing viewport walks it gradually inward instead of snapping it to the
|
|
190
|
-
opposite side. The only limit is the element itself: a note level with it stops
|
|
191
|
-
once they meet, rather than sliding across it.
|
|
122
|
+
### Placement behavior
|
|
192
123
|
|
|
193
|
-
|
|
194
|
-
scrolling
|
|
195
|
-
|
|
196
|
-
against the viewport edge has nowhere to slide, so it may sit partly off screen
|
|
197
|
-
— use a different `position` or an `offset` there.
|
|
124
|
+
- **The side you ask for is the side you get.** When a note runs past the viewport edge it slides horizontally by exactly the amount it overhangs, so a narrowing viewport walks it gradually inward instead of snapping to the opposite side.
|
|
125
|
+
- **Vertical placement is fixed** relative to the element and does not react to scrolling - a note stays pinned to what it annotates.
|
|
126
|
+
- If an element is jammed against the viewport edge, a note may sit partly off screen - use a different `position` or an `offset` there.
|
|
198
127
|
|
|
199
|
-
Nudge a note
|
|
128
|
+
Nudge a note with `offset`:
|
|
200
129
|
|
|
201
130
|
```javascript
|
|
202
131
|
doodle(".cta", {
|
|
@@ -204,12 +133,9 @@ doodle(".cta", {
|
|
|
204
133
|
});
|
|
205
134
|
```
|
|
206
135
|
|
|
207
|
-
The offset moves the note from the spot `position` chose
|
|
208
|
-
judged from there — so a large offset slides back into the viewport rather than
|
|
209
|
-
carrying the note off screen. Vertical offsets are always applied verbatim,
|
|
210
|
-
since vertical placement never reacts to the viewport.
|
|
136
|
+
The offset moves the note from the spot `position` chose. A large horizontal offset slides back into the viewport rather than carrying the note off screen.
|
|
211
137
|
|
|
212
|
-
|
|
138
|
+
### Arrows
|
|
213
139
|
|
|
214
140
|
```javascript
|
|
215
141
|
doodle(".cta", { arrow: true });
|
|
@@ -218,17 +144,20 @@ doodle(".cta", {
|
|
|
218
144
|
arrow: {
|
|
219
145
|
from: "top-right", // note | any position name
|
|
220
146
|
to: "center",
|
|
221
|
-
style: "curved",
|
|
147
|
+
style: "curved", // curved | straight | dotted | looped
|
|
222
148
|
},
|
|
223
149
|
});
|
|
224
150
|
```
|
|
225
151
|
|
|
226
|
-
|
|
227
|
-
|
|
152
|
+

|
|
153
|
+
|
|
154
|
+
**Styles:** `curved` (default), `straight`, `dotted`, `looped` - `looped` adds a flourish that doubles back before reaching the tip.
|
|
155
|
+
|
|
156
|
+
**`from`** decides where the arrow leaves:
|
|
228
157
|
|
|
229
|
-
|
|
230
|
-
note
|
|
231
|
-
the element
|
|
158
|
+
- **With a note** - names a side of the note (where the pen lifts off the handwriting), then travels to the element.
|
|
159
|
+
- **`"note"`** (default) - picks the side of the note facing the element.
|
|
160
|
+
- **Without a note** - `from` names a side of the element itself.
|
|
232
161
|
|
|
233
162
|
```javascript
|
|
234
163
|
doodle(".cta", {
|
|
@@ -237,62 +166,43 @@ doodle(".cta", {
|
|
|
237
166
|
});
|
|
238
167
|
```
|
|
239
168
|
|
|
240
|
-
`"
|
|
241
|
-
callout always departs from its note, so moving the note with `offset` takes the
|
|
242
|
-
arrow with it rather than leaving it stranded by the element.
|
|
169
|
+
**`to`:** `"edge"` (default) lands the tip just outside the frame; `"center"` points into the element.
|
|
243
170
|
|
|
244
|
-
|
|
245
|
-
naming a side of the element itself.
|
|
246
|
-
|
|
247
|
-
`to: "edge"` (the default) lands the tip just *outside* the frame; `to: "center"`
|
|
248
|
-
is the one setting that deliberately points into the element.
|
|
249
|
-
|
|
250
|
-
## Decorations
|
|
171
|
+
### Decorations
|
|
251
172
|
|
|
252
173
|
```javascript
|
|
253
174
|
doodle(".card", { decorations: true });
|
|
254
175
|
|
|
255
176
|
doodle(".card", {
|
|
256
177
|
decorations: {
|
|
257
|
-
count: 2,
|
|
258
|
-
style: "corners",
|
|
178
|
+
count: 2, // border marks, hard-capped at 2
|
|
179
|
+
style: "corners", // corners | sides - omit to pick automatically
|
|
259
180
|
types: ["arcs", "twinkle", "heart"],
|
|
260
181
|
},
|
|
261
182
|
});
|
|
262
183
|
```
|
|
263
184
|
|
|
264
|
-
|
|
265
|
-
capped at 2 and the default composition is deliberately fixed:
|
|
185
|
+

|
|
266
186
|
|
|
267
|
-
|
|
268
|
-
- **one corner star** — `twinkle` or `star` — on the corner farthest from it,
|
|
269
|
-
and sitting noticeably further off the border than the accent
|
|
270
|
-
- **one handwriting accent** — `heart` or `sparkle` — beside the note, not the
|
|
271
|
-
component
|
|
187
|
+
A frame stays readable with **at most two marks around it**. Default composition:
|
|
272
188
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
189
|
+
- **one corner accent** - `arcs` or `emphasis` - hugging a corner, aimed outward
|
|
190
|
+
- **one corner star** - `twinkle` or `star` - on the corner farthest from it
|
|
191
|
+
- **one handwriting accent** - `heart` or `sparkle` - beside the note, not the component
|
|
192
|
+
|
|
193
|
+
Corner marks follow the _rounded_ corner rather than the bounding box. Marks too large for their gap are pushed further out.
|
|
277
194
|
|
|
278
195
|
### Handwriting accents
|
|
279
196
|
|
|
280
|
-
`heart` and `sparkle`
|
|
281
|
-
note, on the side away from the element. With no `note` on the element they are
|
|
282
|
-
skipped rather than moved to the border.
|
|
197
|
+
`heart` and `sparkle` are drawn at the far end of the note, on the side away from the element. With no `note` they are skipped rather than moved to the border.
|
|
283
198
|
|
|
284
199
|
### Small components
|
|
285
200
|
|
|
286
|
-
Chips and icon buttons
|
|
287
|
-
pair of two-stroke `emphasis` marks — one either side — instead of corner
|
|
288
|
-
marks. This is chosen automatically when the element is under 48px on its short
|
|
289
|
-
side or under 130px wide; `style: "sides"` forces it at any size and
|
|
290
|
-
`style: "corners"` opts out.
|
|
201
|
+
Chips and icon buttons get a mirrored pair of two-stroke `emphasis` marks - one either side - instead of corner marks. This is automatic when the element is under 48px on its short side or under 130px wide. `style: "sides"` forces it at any size; `style: "corners"` opts out.
|
|
291
202
|
|
|
292
|
-
|
|
293
|
-
available and fill a corner slot when no accent or star was requested.
|
|
203
|
+
Other types - `star`, `smiley`, `dots`, `stroke`, `steam` - fill a corner slot when no accent or star was requested.
|
|
294
204
|
|
|
295
|
-
|
|
205
|
+
### Child selector mode
|
|
296
206
|
|
|
297
207
|
Decorate multiple children from a single parent overlay:
|
|
298
208
|
|
|
@@ -302,16 +212,113 @@ doodle("#hero", {
|
|
|
302
212
|
});
|
|
303
213
|
```
|
|
304
214
|
|
|
305
|
-
|
|
215
|
+
### Lifecycle
|
|
306
216
|
|
|
307
217
|
```javascript
|
|
308
218
|
const instance = doodle(".card");
|
|
309
219
|
|
|
310
|
-
instance.update();
|
|
220
|
+
instance.update(); // recalculate geometry and redraw
|
|
311
221
|
instance.destroy(); // remove overlay and disconnect observers
|
|
312
222
|
```
|
|
313
223
|
|
|
314
|
-
Calling `doodle()` twice on the same element replaces the previous overlay
|
|
224
|
+
Calling `doodle()` twice on the same element replaces the previous overlay - no duplicates.
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
## React / Next.js
|
|
229
|
+
|
|
230
|
+
Import from `@aznabee/freehand-ui/react`. Every option in the [Vanilla JavaScript](#vanilla-javascript) section works as a prop.
|
|
231
|
+
|
|
232
|
+
### Component
|
|
233
|
+
|
|
234
|
+
```jsx
|
|
235
|
+
import Doodle from "@aznabee/freehand-ui/react";
|
|
236
|
+
|
|
237
|
+
<Doodle note="start chat" strokeWidth={2}>
|
|
238
|
+
<Button />
|
|
239
|
+
</Doodle>;
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Ships `"use client"` - drops into the Next.js App Router without a wrapper, and renders on the server without warnings.
|
|
243
|
+
|
|
244
|
+
The doodle is drawn around **the wrapped component as a whole** - children are never decorated individually. To decorate descendants instead, opt in with `childSelector`:
|
|
245
|
+
|
|
246
|
+
```jsx
|
|
247
|
+
<Doodle childSelector=".card">
|
|
248
|
+
{items.map((item) => (
|
|
249
|
+
<Card key={item.id} className="card" {...item} />
|
|
250
|
+
))}
|
|
251
|
+
</Doodle>
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### Configuration
|
|
255
|
+
|
|
256
|
+
Same options as `doodle()`, passed as props:
|
|
257
|
+
|
|
258
|
+
```jsx
|
|
259
|
+
<Doodle
|
|
260
|
+
border
|
|
261
|
+
color="#ffffff"
|
|
262
|
+
strokeWidth={1.5}
|
|
263
|
+
roughness={1.5}
|
|
264
|
+
padding={0}
|
|
265
|
+
radius={20}
|
|
266
|
+
opacity={0.9}
|
|
267
|
+
addBreaks={false}
|
|
268
|
+
autoLoadFont
|
|
269
|
+
>
|
|
270
|
+
<Card />
|
|
271
|
+
</Doodle>
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### Wrapper element
|
|
275
|
+
|
|
276
|
+
`<Doodle>` renders a `<span style="display:inline-flex">` that shrink-wraps its child, so the frame hugs your component instead of stretching to the full width of its container. `inline-flex` specifically: an `inline-block` wrapper adds baseline leading beneath an inline-level child like a `<button>`, which would leave the frame visibly taller than the button.
|
|
277
|
+
|
|
278
|
+
Override when you need different layout - extra props go to the DOM node:
|
|
279
|
+
|
|
280
|
+
```jsx
|
|
281
|
+
<Doodle as="div" style={{ display: "block" }} className="w-full" note="hi">
|
|
282
|
+
<Card />
|
|
283
|
+
</Doodle>
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Border radius is picked up automatically: if the wrapper has none of its own, it inherits the radius of the child it hugs, so a wrapped pill button is still drawn as a pill.
|
|
287
|
+
|
|
288
|
+
### Hook
|
|
289
|
+
|
|
290
|
+
For an element you already hold a ref to, skip the wrapper:
|
|
291
|
+
|
|
292
|
+
```jsx
|
|
293
|
+
import { useDoodle } from "@aznabee/freehand-ui/react";
|
|
294
|
+
|
|
295
|
+
function Card() {
|
|
296
|
+
const ref = useRef(null);
|
|
297
|
+
useDoodle(ref, { note: "new", decorations: true });
|
|
298
|
+
return <div ref={ref}>…</div>;
|
|
299
|
+
}
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### Handwriting font
|
|
303
|
+
|
|
304
|
+
With `next/font`:
|
|
305
|
+
|
|
306
|
+
```jsx
|
|
307
|
+
import { Caveat } from "next/font/google";
|
|
308
|
+
const caveat = Caveat({ subsets: ["latin"], variable: "--font-caveat" });
|
|
309
|
+
|
|
310
|
+
<Doodle note="start chat" fontFamily="var(--font-caveat)" autoLoadFont={false}>
|
|
311
|
+
<Button />
|
|
312
|
+
</Doodle>;
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
### React notes
|
|
316
|
+
|
|
317
|
+
- Inline object props (`note={{ text: "hi" }}`) are compared by value, so a re-render does not tear down and redraw the overlay with a new random seed.
|
|
318
|
+
- `disabled` skips drawing entirely - useful behind a reduced-motion check or a feature flag.
|
|
319
|
+
- TypeScript definitions ship with the package for both entry points.
|
|
320
|
+
|
|
321
|
+
---
|
|
315
322
|
|
|
316
323
|
## Examples
|
|
317
324
|
|
|
@@ -327,16 +334,22 @@ Then open:
|
|
|
327
334
|
|
|
328
335
|
- http://localhost:5173/examples/basic/
|
|
329
336
|
- http://localhost:5173/examples/annotations/
|
|
337
|
+
- http://localhost:5173/examples/arrows/
|
|
330
338
|
- http://localhost:5173/examples/children/
|
|
331
339
|
- http://localhost:5173/examples/decorations/
|
|
340
|
+
- http://localhost:5173/examples/react/
|
|
341
|
+
|
|
342
|
+
---
|
|
332
343
|
|
|
333
344
|
## Design principles
|
|
334
345
|
|
|
335
|
-
- **Framework agnostic**
|
|
336
|
-
- **SVG based**
|
|
337
|
-
- **Non-invasive**
|
|
338
|
-
- **Responsive**
|
|
339
|
-
- **Lightweight**
|
|
346
|
+
- **Framework agnostic** - plain JavaScript at the core; React is an optional entry point
|
|
347
|
+
- **SVG based** - hand-drawn paths, not CSS borders
|
|
348
|
+
- **Non-invasive** - `pointer-events: none`, no layout changes
|
|
349
|
+
- **Responsive** - `ResizeObserver`, scroll listeners, and `requestAnimationFrame`
|
|
350
|
+
- **Lightweight** - zero runtime dependencies
|
|
351
|
+
|
|
352
|
+
---
|
|
340
353
|
|
|
341
354
|
## License
|
|
342
355
|
|
package/dist/freehand-ui.cjs
CHANGED
|
@@ -41,7 +41,12 @@ var DEFAULT_OPTIONS = {
|
|
|
41
41
|
/** Override the handwriting stack, e.g. a next/font CSS variable. */
|
|
42
42
|
fontFamily: null,
|
|
43
43
|
/** Fetch Caveat when the page has not provided it. */
|
|
44
|
-
autoLoadFont: true
|
|
44
|
+
autoLoadFont: true,
|
|
45
|
+
/**
|
|
46
|
+
* Overlay stacking order. Omit to mirror the target element's z-index so
|
|
47
|
+
* doodles stay above the annotated element but below modals and other UI.
|
|
48
|
+
*/
|
|
49
|
+
zIndex: null
|
|
45
50
|
};
|
|
46
51
|
function resolveElement(target) {
|
|
47
52
|
if (!target) return null;
|
|
@@ -134,6 +139,40 @@ function getScrollableAncestors(element) {
|
|
|
134
139
|
ancestors.push(window);
|
|
135
140
|
return ancestors;
|
|
136
141
|
}
|
|
142
|
+
function resolveOverlayZIndex(element, override) {
|
|
143
|
+
if (override != null) return override;
|
|
144
|
+
const { zIndex, position } = getComputedStyle(element);
|
|
145
|
+
if (zIndex !== "auto") {
|
|
146
|
+
const parsed = Number.parseInt(zIndex, 10);
|
|
147
|
+
if (Number.isFinite(parsed)) return parsed + 1;
|
|
148
|
+
}
|
|
149
|
+
let node = element.parentElement;
|
|
150
|
+
while (node && node !== document.documentElement) {
|
|
151
|
+
const style = getComputedStyle(node);
|
|
152
|
+
if (style.zIndex !== "auto" && style.position !== "static") {
|
|
153
|
+
const parsed = Number.parseInt(style.zIndex, 10);
|
|
154
|
+
if (Number.isFinite(parsed)) return parsed + 1;
|
|
155
|
+
}
|
|
156
|
+
node = node.parentElement;
|
|
157
|
+
}
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
function insertOverlaySvg(svg, element) {
|
|
161
|
+
const parent = element.parentNode;
|
|
162
|
+
if (!parent) {
|
|
163
|
+
document.body.appendChild(svg);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
parent.insertBefore(svg, element.nextSibling);
|
|
167
|
+
}
|
|
168
|
+
function syncOverlayStacking(svg, element, zIndexOverride) {
|
|
169
|
+
const zIndex = resolveOverlayZIndex(element, zIndexOverride);
|
|
170
|
+
if (zIndex == null) {
|
|
171
|
+
svg.style.zIndex = "";
|
|
172
|
+
} else {
|
|
173
|
+
svg.style.zIndex = String(zIndex);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
137
176
|
|
|
138
177
|
// src/geometry.js
|
|
139
178
|
function averageCornerRadius(element) {
|
|
@@ -484,21 +523,14 @@ function isBroken(fraction, breaks) {
|
|
|
484
523
|
function createBorderBreaks(perimeter, config, random) {
|
|
485
524
|
if (!config || perimeter <= 0) return [];
|
|
486
525
|
const settings = config === true ? {} : config;
|
|
487
|
-
const count = Math.max(
|
|
488
|
-
1,
|
|
489
|
-
Math.round(settings.count ?? 2 + random() * 3)
|
|
490
|
-
);
|
|
526
|
+
const count = Math.max(1, Math.round(settings.count ?? 2 + random() * 3));
|
|
491
527
|
const min = Math.max(2, settings.min ?? 6);
|
|
492
528
|
const max = Math.max(min, settings.max ?? 30);
|
|
493
529
|
const slice = perimeter / count;
|
|
494
530
|
let budget = perimeter * 0.35;
|
|
495
531
|
const breaks = [];
|
|
496
532
|
for (let i = 0; i < count; i++) {
|
|
497
|
-
const length = Math.min(
|
|
498
|
-
min + random() * (max - min),
|
|
499
|
-
slice * 0.7,
|
|
500
|
-
budget
|
|
501
|
-
);
|
|
533
|
+
const length = Math.min(min + random() * (max - min), slice * 0.7, budget);
|
|
502
534
|
if (length < 2) break;
|
|
503
535
|
budget -= length;
|
|
504
536
|
const start = i * slice + random() * Math.max(0, slice - length);
|
|
@@ -966,7 +998,6 @@ function createOverlaySvg(left, top, width, height) {
|
|
|
966
998
|
svg.style.height = `${height}px`;
|
|
967
999
|
svg.style.pointerEvents = "none";
|
|
968
1000
|
svg.style.overflow = "visible";
|
|
969
|
-
svg.style.zIndex = "2147483646";
|
|
970
1001
|
return svg;
|
|
971
1002
|
}
|
|
972
1003
|
|
|
@@ -1496,7 +1527,8 @@ var DoodleOverlay = class {
|
|
|
1496
1527
|
}
|
|
1497
1528
|
mount() {
|
|
1498
1529
|
this.svg = createOverlaySvg(0, 0, 0, 0);
|
|
1499
|
-
|
|
1530
|
+
insertOverlaySvg(this.svg, this.element);
|
|
1531
|
+
syncOverlayStacking(this.svg, this.element, this.options.zIndex);
|
|
1500
1532
|
this.resizeObserver = new ResizeObserver(this.onResize);
|
|
1501
1533
|
this.resizeObserver.observe(this.element);
|
|
1502
1534
|
if (this.options.children) {
|
|
@@ -1554,6 +1586,7 @@ var DoodleOverlay = class {
|
|
|
1554
1586
|
);
|
|
1555
1587
|
const overlayRect = unionRects(absoluteRects, margin);
|
|
1556
1588
|
clearSvg(this.svg);
|
|
1589
|
+
syncOverlayStacking(this.svg, this.element, this.options.zIndex);
|
|
1557
1590
|
this.svg.setAttribute("width", String(overlayRect.width));
|
|
1558
1591
|
this.svg.setAttribute("height", String(overlayRect.height));
|
|
1559
1592
|
this.svg.setAttribute("viewBox", `0 0 ${overlayRect.width} ${overlayRect.height}`);
|
package/dist/freehand-ui.js
CHANGED
|
@@ -16,7 +16,12 @@ var DEFAULT_OPTIONS = {
|
|
|
16
16
|
/** Override the handwriting stack, e.g. a next/font CSS variable. */
|
|
17
17
|
fontFamily: null,
|
|
18
18
|
/** Fetch Caveat when the page has not provided it. */
|
|
19
|
-
autoLoadFont: true
|
|
19
|
+
autoLoadFont: true,
|
|
20
|
+
/**
|
|
21
|
+
* Overlay stacking order. Omit to mirror the target element's z-index so
|
|
22
|
+
* doodles stay above the annotated element but below modals and other UI.
|
|
23
|
+
*/
|
|
24
|
+
zIndex: null
|
|
20
25
|
};
|
|
21
26
|
function resolveElement(target) {
|
|
22
27
|
if (!target) return null;
|
|
@@ -109,6 +114,40 @@ function getScrollableAncestors(element) {
|
|
|
109
114
|
ancestors.push(window);
|
|
110
115
|
return ancestors;
|
|
111
116
|
}
|
|
117
|
+
function resolveOverlayZIndex(element, override) {
|
|
118
|
+
if (override != null) return override;
|
|
119
|
+
const { zIndex, position } = getComputedStyle(element);
|
|
120
|
+
if (zIndex !== "auto") {
|
|
121
|
+
const parsed = Number.parseInt(zIndex, 10);
|
|
122
|
+
if (Number.isFinite(parsed)) return parsed + 1;
|
|
123
|
+
}
|
|
124
|
+
let node = element.parentElement;
|
|
125
|
+
while (node && node !== document.documentElement) {
|
|
126
|
+
const style = getComputedStyle(node);
|
|
127
|
+
if (style.zIndex !== "auto" && style.position !== "static") {
|
|
128
|
+
const parsed = Number.parseInt(style.zIndex, 10);
|
|
129
|
+
if (Number.isFinite(parsed)) return parsed + 1;
|
|
130
|
+
}
|
|
131
|
+
node = node.parentElement;
|
|
132
|
+
}
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
function insertOverlaySvg(svg, element) {
|
|
136
|
+
const parent = element.parentNode;
|
|
137
|
+
if (!parent) {
|
|
138
|
+
document.body.appendChild(svg);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
parent.insertBefore(svg, element.nextSibling);
|
|
142
|
+
}
|
|
143
|
+
function syncOverlayStacking(svg, element, zIndexOverride) {
|
|
144
|
+
const zIndex = resolveOverlayZIndex(element, zIndexOverride);
|
|
145
|
+
if (zIndex == null) {
|
|
146
|
+
svg.style.zIndex = "";
|
|
147
|
+
} else {
|
|
148
|
+
svg.style.zIndex = String(zIndex);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
112
151
|
|
|
113
152
|
// src/geometry.js
|
|
114
153
|
function averageCornerRadius(element) {
|
|
@@ -459,21 +498,14 @@ function isBroken(fraction, breaks) {
|
|
|
459
498
|
function createBorderBreaks(perimeter, config, random) {
|
|
460
499
|
if (!config || perimeter <= 0) return [];
|
|
461
500
|
const settings = config === true ? {} : config;
|
|
462
|
-
const count = Math.max(
|
|
463
|
-
1,
|
|
464
|
-
Math.round(settings.count ?? 2 + random() * 3)
|
|
465
|
-
);
|
|
501
|
+
const count = Math.max(1, Math.round(settings.count ?? 2 + random() * 3));
|
|
466
502
|
const min = Math.max(2, settings.min ?? 6);
|
|
467
503
|
const max = Math.max(min, settings.max ?? 30);
|
|
468
504
|
const slice = perimeter / count;
|
|
469
505
|
let budget = perimeter * 0.35;
|
|
470
506
|
const breaks = [];
|
|
471
507
|
for (let i = 0; i < count; i++) {
|
|
472
|
-
const length = Math.min(
|
|
473
|
-
min + random() * (max - min),
|
|
474
|
-
slice * 0.7,
|
|
475
|
-
budget
|
|
476
|
-
);
|
|
508
|
+
const length = Math.min(min + random() * (max - min), slice * 0.7, budget);
|
|
477
509
|
if (length < 2) break;
|
|
478
510
|
budget -= length;
|
|
479
511
|
const start = i * slice + random() * Math.max(0, slice - length);
|
|
@@ -941,7 +973,6 @@ function createOverlaySvg(left, top, width, height) {
|
|
|
941
973
|
svg.style.height = `${height}px`;
|
|
942
974
|
svg.style.pointerEvents = "none";
|
|
943
975
|
svg.style.overflow = "visible";
|
|
944
|
-
svg.style.zIndex = "2147483646";
|
|
945
976
|
return svg;
|
|
946
977
|
}
|
|
947
978
|
|
|
@@ -1471,7 +1502,8 @@ var DoodleOverlay = class {
|
|
|
1471
1502
|
}
|
|
1472
1503
|
mount() {
|
|
1473
1504
|
this.svg = createOverlaySvg(0, 0, 0, 0);
|
|
1474
|
-
|
|
1505
|
+
insertOverlaySvg(this.svg, this.element);
|
|
1506
|
+
syncOverlayStacking(this.svg, this.element, this.options.zIndex);
|
|
1475
1507
|
this.resizeObserver = new ResizeObserver(this.onResize);
|
|
1476
1508
|
this.resizeObserver.observe(this.element);
|
|
1477
1509
|
if (this.options.children) {
|
|
@@ -1529,6 +1561,7 @@ var DoodleOverlay = class {
|
|
|
1529
1561
|
);
|
|
1530
1562
|
const overlayRect = unionRects(absoluteRects, margin);
|
|
1531
1563
|
clearSvg(this.svg);
|
|
1564
|
+
syncOverlayStacking(this.svg, this.element, this.options.zIndex);
|
|
1532
1565
|
this.svg.setAttribute("width", String(overlayRect.width));
|
|
1533
1566
|
this.svg.setAttribute("height", String(overlayRect.height));
|
|
1534
1567
|
this.svg.setAttribute("viewBox", `0 0 ${overlayRect.width} ${overlayRect.height}`);
|
package/dist/index.d.ts
CHANGED
|
@@ -24,7 +24,7 @@ export type DecorationType =
|
|
|
24
24
|
export interface NoteOptions {
|
|
25
25
|
text: string;
|
|
26
26
|
/**
|
|
27
|
-
* Which side of the element the note sits on. Always honoured
|
|
27
|
+
* Which side of the element the note sits on. Always honoured - a note that
|
|
28
28
|
* would run off screen slides horizontally to stay in view rather than
|
|
29
29
|
* moving to another side.
|
|
30
30
|
*/
|
|
@@ -41,7 +41,7 @@ export interface NoteOptions {
|
|
|
41
41
|
|
|
42
42
|
export interface ArrowOptions {
|
|
43
43
|
/**
|
|
44
|
-
* Where the arrow leaves. With a `note` this names a side of *the note*
|
|
44
|
+
* Where the arrow leaves. With a `note` this names a side of *the note* -
|
|
45
45
|
* the arrow always departs from the handwriting and travels to the element,
|
|
46
46
|
* however far an `offset` has moved it. `"note"` (the default) picks the side
|
|
47
47
|
* facing the element. Without a note it names a side of the element instead.
|
|
@@ -88,8 +88,8 @@ export interface DoodleOptions {
|
|
|
88
88
|
/** Lift the pen at random points around the outline. */
|
|
89
89
|
addBreaks?: boolean | number | BreakOptions;
|
|
90
90
|
/**
|
|
91
|
-
* Font stack for handwritten notes. Point this at your own family
|
|
92
|
-
* `next/font` CSS variable, for instance
|
|
91
|
+
* Font stack for handwritten notes. Point this at your own family - a
|
|
92
|
+
* `next/font` CSS variable, for instance - to take over from the built-in one.
|
|
93
93
|
*/
|
|
94
94
|
fontFamily?: string | null;
|
|
95
95
|
/**
|
|
@@ -99,6 +99,11 @@ export interface DoodleOptions {
|
|
|
99
99
|
* handwriting fallbacks.
|
|
100
100
|
*/
|
|
101
101
|
autoLoadFont?: boolean;
|
|
102
|
+
/**
|
|
103
|
+
* Overlay stacking order. Omit to follow the target element's z-index so
|
|
104
|
+
* doodles stay above the annotated element but below modals and other UI.
|
|
105
|
+
*/
|
|
106
|
+
zIndex?: number | null;
|
|
102
107
|
}
|
|
103
108
|
|
|
104
109
|
export interface DoodleInstance {
|
|
@@ -114,5 +119,5 @@ export interface DoodleInstance {
|
|
|
114
119
|
*/
|
|
115
120
|
export function doodle(
|
|
116
121
|
target: string | Element,
|
|
117
|
-
options?: DoodleOptions
|
|
122
|
+
options?: DoodleOptions,
|
|
118
123
|
): DoodleInstance;
|
package/dist/react.cjs
CHANGED
|
@@ -46,7 +46,12 @@ var DEFAULT_OPTIONS = {
|
|
|
46
46
|
/** Override the handwriting stack, e.g. a next/font CSS variable. */
|
|
47
47
|
fontFamily: null,
|
|
48
48
|
/** Fetch Caveat when the page has not provided it. */
|
|
49
|
-
autoLoadFont: true
|
|
49
|
+
autoLoadFont: true,
|
|
50
|
+
/**
|
|
51
|
+
* Overlay stacking order. Omit to mirror the target element's z-index so
|
|
52
|
+
* doodles stay above the annotated element but below modals and other UI.
|
|
53
|
+
*/
|
|
54
|
+
zIndex: null
|
|
50
55
|
};
|
|
51
56
|
function resolveElement(target) {
|
|
52
57
|
if (!target) return null;
|
|
@@ -139,6 +144,40 @@ function getScrollableAncestors(element) {
|
|
|
139
144
|
ancestors.push(window);
|
|
140
145
|
return ancestors;
|
|
141
146
|
}
|
|
147
|
+
function resolveOverlayZIndex(element, override) {
|
|
148
|
+
if (override != null) return override;
|
|
149
|
+
const { zIndex, position } = getComputedStyle(element);
|
|
150
|
+
if (zIndex !== "auto") {
|
|
151
|
+
const parsed = Number.parseInt(zIndex, 10);
|
|
152
|
+
if (Number.isFinite(parsed)) return parsed + 1;
|
|
153
|
+
}
|
|
154
|
+
let node = element.parentElement;
|
|
155
|
+
while (node && node !== document.documentElement) {
|
|
156
|
+
const style = getComputedStyle(node);
|
|
157
|
+
if (style.zIndex !== "auto" && style.position !== "static") {
|
|
158
|
+
const parsed = Number.parseInt(style.zIndex, 10);
|
|
159
|
+
if (Number.isFinite(parsed)) return parsed + 1;
|
|
160
|
+
}
|
|
161
|
+
node = node.parentElement;
|
|
162
|
+
}
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
function insertOverlaySvg(svg, element) {
|
|
166
|
+
const parent = element.parentNode;
|
|
167
|
+
if (!parent) {
|
|
168
|
+
document.body.appendChild(svg);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
parent.insertBefore(svg, element.nextSibling);
|
|
172
|
+
}
|
|
173
|
+
function syncOverlayStacking(svg, element, zIndexOverride) {
|
|
174
|
+
const zIndex = resolveOverlayZIndex(element, zIndexOverride);
|
|
175
|
+
if (zIndex == null) {
|
|
176
|
+
svg.style.zIndex = "";
|
|
177
|
+
} else {
|
|
178
|
+
svg.style.zIndex = String(zIndex);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
142
181
|
|
|
143
182
|
// src/geometry.js
|
|
144
183
|
function averageCornerRadius(element) {
|
|
@@ -489,21 +528,14 @@ function isBroken(fraction, breaks) {
|
|
|
489
528
|
function createBorderBreaks(perimeter, config, random) {
|
|
490
529
|
if (!config || perimeter <= 0) return [];
|
|
491
530
|
const settings = config === true ? {} : config;
|
|
492
|
-
const count = Math.max(
|
|
493
|
-
1,
|
|
494
|
-
Math.round(settings.count ?? 2 + random() * 3)
|
|
495
|
-
);
|
|
531
|
+
const count = Math.max(1, Math.round(settings.count ?? 2 + random() * 3));
|
|
496
532
|
const min = Math.max(2, settings.min ?? 6);
|
|
497
533
|
const max = Math.max(min, settings.max ?? 30);
|
|
498
534
|
const slice = perimeter / count;
|
|
499
535
|
let budget = perimeter * 0.35;
|
|
500
536
|
const breaks = [];
|
|
501
537
|
for (let i = 0; i < count; i++) {
|
|
502
|
-
const length = Math.min(
|
|
503
|
-
min + random() * (max - min),
|
|
504
|
-
slice * 0.7,
|
|
505
|
-
budget
|
|
506
|
-
);
|
|
538
|
+
const length = Math.min(min + random() * (max - min), slice * 0.7, budget);
|
|
507
539
|
if (length < 2) break;
|
|
508
540
|
budget -= length;
|
|
509
541
|
const start = i * slice + random() * Math.max(0, slice - length);
|
|
@@ -971,7 +1003,6 @@ function createOverlaySvg(left, top, width, height) {
|
|
|
971
1003
|
svg.style.height = `${height}px`;
|
|
972
1004
|
svg.style.pointerEvents = "none";
|
|
973
1005
|
svg.style.overflow = "visible";
|
|
974
|
-
svg.style.zIndex = "2147483646";
|
|
975
1006
|
return svg;
|
|
976
1007
|
}
|
|
977
1008
|
|
|
@@ -1501,7 +1532,8 @@ var DoodleOverlay = class {
|
|
|
1501
1532
|
}
|
|
1502
1533
|
mount() {
|
|
1503
1534
|
this.svg = createOverlaySvg(0, 0, 0, 0);
|
|
1504
|
-
|
|
1535
|
+
insertOverlaySvg(this.svg, this.element);
|
|
1536
|
+
syncOverlayStacking(this.svg, this.element, this.options.zIndex);
|
|
1505
1537
|
this.resizeObserver = new ResizeObserver(this.onResize);
|
|
1506
1538
|
this.resizeObserver.observe(this.element);
|
|
1507
1539
|
if (this.options.children) {
|
|
@@ -1559,6 +1591,7 @@ var DoodleOverlay = class {
|
|
|
1559
1591
|
);
|
|
1560
1592
|
const overlayRect = unionRects(absoluteRects, margin);
|
|
1561
1593
|
clearSvg(this.svg);
|
|
1594
|
+
syncOverlayStacking(this.svg, this.element, this.options.zIndex);
|
|
1562
1595
|
this.svg.setAttribute("width", String(overlayRect.width));
|
|
1563
1596
|
this.svg.setAttribute("height", String(overlayRect.height));
|
|
1564
1597
|
this.svg.setAttribute("viewBox", `0 0 ${overlayRect.width} ${overlayRect.height}`);
|
|
@@ -1795,7 +1828,7 @@ var Doodle = (0, import_react.forwardRef)(function Doodle2(props, forwardedRef)
|
|
|
1795
1828
|
ref: mergeRefs(innerRef, forwardedRef),
|
|
1796
1829
|
className,
|
|
1797
1830
|
// inline-flex shrink-wraps the child so the doodle hugs the component
|
|
1798
|
-
// rather than a full-width block. Flex specifically
|
|
1831
|
+
// rather than a full-width block. Flex specifically - inline-block would
|
|
1799
1832
|
// add baseline leading under an inline-level child like a <button>,
|
|
1800
1833
|
// leaving the frame taller than what it wraps. Override via `style`/`as`.
|
|
1801
1834
|
style: { display: "inline-flex", ...style }
|
package/dist/react.d.ts
CHANGED
|
@@ -24,7 +24,7 @@ export interface DoodleOwnProps extends DoodleOptionProps {
|
|
|
24
24
|
children?: ReactNode;
|
|
25
25
|
/** Wrapper element to render. Default `"span"`. */
|
|
26
26
|
as?: ElementType;
|
|
27
|
-
/** Skip drawing entirely
|
|
27
|
+
/** Skip drawing entirely - useful for reduced-motion or feature flags. */
|
|
28
28
|
disabled?: boolean;
|
|
29
29
|
/**
|
|
30
30
|
* Selector for decorating descendants instead of the wrapper as a whole.
|
|
@@ -52,7 +52,7 @@ export type DoodleProps<T extends ElementType = "span"> = DoodleOwnProps &
|
|
|
52
52
|
* decorated individually unless you opt in with `childSelector`.
|
|
53
53
|
*/
|
|
54
54
|
declare const Doodle: <T extends ElementType = "span">(
|
|
55
|
-
props: DoodleProps<T
|
|
55
|
+
props: DoodleProps<T>,
|
|
56
56
|
) => JSX.Element;
|
|
57
57
|
|
|
58
58
|
export default Doodle;
|
|
@@ -62,5 +62,5 @@ export { Doodle };
|
|
|
62
62
|
export function useDoodle(
|
|
63
63
|
ref: RefObject<Element | null>,
|
|
64
64
|
options?: DoodleOptions,
|
|
65
|
-
enabled?: boolean
|
|
65
|
+
enabled?: boolean,
|
|
66
66
|
): void;
|
package/dist/react.js
CHANGED
|
@@ -29,7 +29,12 @@ var DEFAULT_OPTIONS = {
|
|
|
29
29
|
/** Override the handwriting stack, e.g. a next/font CSS variable. */
|
|
30
30
|
fontFamily: null,
|
|
31
31
|
/** Fetch Caveat when the page has not provided it. */
|
|
32
|
-
autoLoadFont: true
|
|
32
|
+
autoLoadFont: true,
|
|
33
|
+
/**
|
|
34
|
+
* Overlay stacking order. Omit to mirror the target element's z-index so
|
|
35
|
+
* doodles stay above the annotated element but below modals and other UI.
|
|
36
|
+
*/
|
|
37
|
+
zIndex: null
|
|
33
38
|
};
|
|
34
39
|
function resolveElement(target) {
|
|
35
40
|
if (!target) return null;
|
|
@@ -122,6 +127,40 @@ function getScrollableAncestors(element) {
|
|
|
122
127
|
ancestors.push(window);
|
|
123
128
|
return ancestors;
|
|
124
129
|
}
|
|
130
|
+
function resolveOverlayZIndex(element, override) {
|
|
131
|
+
if (override != null) return override;
|
|
132
|
+
const { zIndex, position } = getComputedStyle(element);
|
|
133
|
+
if (zIndex !== "auto") {
|
|
134
|
+
const parsed = Number.parseInt(zIndex, 10);
|
|
135
|
+
if (Number.isFinite(parsed)) return parsed + 1;
|
|
136
|
+
}
|
|
137
|
+
let node = element.parentElement;
|
|
138
|
+
while (node && node !== document.documentElement) {
|
|
139
|
+
const style = getComputedStyle(node);
|
|
140
|
+
if (style.zIndex !== "auto" && style.position !== "static") {
|
|
141
|
+
const parsed = Number.parseInt(style.zIndex, 10);
|
|
142
|
+
if (Number.isFinite(parsed)) return parsed + 1;
|
|
143
|
+
}
|
|
144
|
+
node = node.parentElement;
|
|
145
|
+
}
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
function insertOverlaySvg(svg, element) {
|
|
149
|
+
const parent = element.parentNode;
|
|
150
|
+
if (!parent) {
|
|
151
|
+
document.body.appendChild(svg);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
parent.insertBefore(svg, element.nextSibling);
|
|
155
|
+
}
|
|
156
|
+
function syncOverlayStacking(svg, element, zIndexOverride) {
|
|
157
|
+
const zIndex = resolveOverlayZIndex(element, zIndexOverride);
|
|
158
|
+
if (zIndex == null) {
|
|
159
|
+
svg.style.zIndex = "";
|
|
160
|
+
} else {
|
|
161
|
+
svg.style.zIndex = String(zIndex);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
125
164
|
|
|
126
165
|
// src/geometry.js
|
|
127
166
|
function averageCornerRadius(element) {
|
|
@@ -472,21 +511,14 @@ function isBroken(fraction, breaks) {
|
|
|
472
511
|
function createBorderBreaks(perimeter, config, random) {
|
|
473
512
|
if (!config || perimeter <= 0) return [];
|
|
474
513
|
const settings = config === true ? {} : config;
|
|
475
|
-
const count = Math.max(
|
|
476
|
-
1,
|
|
477
|
-
Math.round(settings.count ?? 2 + random() * 3)
|
|
478
|
-
);
|
|
514
|
+
const count = Math.max(1, Math.round(settings.count ?? 2 + random() * 3));
|
|
479
515
|
const min = Math.max(2, settings.min ?? 6);
|
|
480
516
|
const max = Math.max(min, settings.max ?? 30);
|
|
481
517
|
const slice = perimeter / count;
|
|
482
518
|
let budget = perimeter * 0.35;
|
|
483
519
|
const breaks = [];
|
|
484
520
|
for (let i = 0; i < count; i++) {
|
|
485
|
-
const length = Math.min(
|
|
486
|
-
min + random() * (max - min),
|
|
487
|
-
slice * 0.7,
|
|
488
|
-
budget
|
|
489
|
-
);
|
|
521
|
+
const length = Math.min(min + random() * (max - min), slice * 0.7, budget);
|
|
490
522
|
if (length < 2) break;
|
|
491
523
|
budget -= length;
|
|
492
524
|
const start = i * slice + random() * Math.max(0, slice - length);
|
|
@@ -954,7 +986,6 @@ function createOverlaySvg(left, top, width, height) {
|
|
|
954
986
|
svg.style.height = `${height}px`;
|
|
955
987
|
svg.style.pointerEvents = "none";
|
|
956
988
|
svg.style.overflow = "visible";
|
|
957
|
-
svg.style.zIndex = "2147483646";
|
|
958
989
|
return svg;
|
|
959
990
|
}
|
|
960
991
|
|
|
@@ -1484,7 +1515,8 @@ var DoodleOverlay = class {
|
|
|
1484
1515
|
}
|
|
1485
1516
|
mount() {
|
|
1486
1517
|
this.svg = createOverlaySvg(0, 0, 0, 0);
|
|
1487
|
-
|
|
1518
|
+
insertOverlaySvg(this.svg, this.element);
|
|
1519
|
+
syncOverlayStacking(this.svg, this.element, this.options.zIndex);
|
|
1488
1520
|
this.resizeObserver = new ResizeObserver(this.onResize);
|
|
1489
1521
|
this.resizeObserver.observe(this.element);
|
|
1490
1522
|
if (this.options.children) {
|
|
@@ -1542,6 +1574,7 @@ var DoodleOverlay = class {
|
|
|
1542
1574
|
);
|
|
1543
1575
|
const overlayRect = unionRects(absoluteRects, margin);
|
|
1544
1576
|
clearSvg(this.svg);
|
|
1577
|
+
syncOverlayStacking(this.svg, this.element, this.options.zIndex);
|
|
1545
1578
|
this.svg.setAttribute("width", String(overlayRect.width));
|
|
1546
1579
|
this.svg.setAttribute("height", String(overlayRect.height));
|
|
1547
1580
|
this.svg.setAttribute("viewBox", `0 0 ${overlayRect.width} ${overlayRect.height}`);
|
|
@@ -1778,7 +1811,7 @@ var Doodle = forwardRef(function Doodle2(props, forwardedRef) {
|
|
|
1778
1811
|
ref: mergeRefs(innerRef, forwardedRef),
|
|
1779
1812
|
className,
|
|
1780
1813
|
// inline-flex shrink-wraps the child so the doodle hugs the component
|
|
1781
|
-
// rather than a full-width block. Flex specifically
|
|
1814
|
+
// rather than a full-width block. Flex specifically - inline-block would
|
|
1782
1815
|
// add baseline leading under an inline-level child like a <button>,
|
|
1783
1816
|
// leaving the frame taller than what it wraps. Override via `style`/`as`.
|
|
1784
1817
|
style: { display: "inline-flex", ...style }
|
package/package.json
CHANGED