@aznabee/freehand-ui 0.1.3 → 0.1.4
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 +2 -9
- package/dist/freehand-ui.js +2 -9
- package/dist/index.d.ts +5 -5
- package/dist/react.cjs +3 -10
- package/dist/react.d.ts +3 -3
- package/dist/react.js +3 -10
- 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
|
@@ -484,21 +484,14 @@ function isBroken(fraction, breaks) {
|
|
|
484
484
|
function createBorderBreaks(perimeter, config, random) {
|
|
485
485
|
if (!config || perimeter <= 0) return [];
|
|
486
486
|
const settings = config === true ? {} : config;
|
|
487
|
-
const count = Math.max(
|
|
488
|
-
1,
|
|
489
|
-
Math.round(settings.count ?? 2 + random() * 3)
|
|
490
|
-
);
|
|
487
|
+
const count = Math.max(1, Math.round(settings.count ?? 2 + random() * 3));
|
|
491
488
|
const min = Math.max(2, settings.min ?? 6);
|
|
492
489
|
const max = Math.max(min, settings.max ?? 30);
|
|
493
490
|
const slice = perimeter / count;
|
|
494
491
|
let budget = perimeter * 0.35;
|
|
495
492
|
const breaks = [];
|
|
496
493
|
for (let i = 0; i < count; i++) {
|
|
497
|
-
const length = Math.min(
|
|
498
|
-
min + random() * (max - min),
|
|
499
|
-
slice * 0.7,
|
|
500
|
-
budget
|
|
501
|
-
);
|
|
494
|
+
const length = Math.min(min + random() * (max - min), slice * 0.7, budget);
|
|
502
495
|
if (length < 2) break;
|
|
503
496
|
budget -= length;
|
|
504
497
|
const start = i * slice + random() * Math.max(0, slice - length);
|
package/dist/freehand-ui.js
CHANGED
|
@@ -459,21 +459,14 @@ function isBroken(fraction, breaks) {
|
|
|
459
459
|
function createBorderBreaks(perimeter, config, random) {
|
|
460
460
|
if (!config || perimeter <= 0) return [];
|
|
461
461
|
const settings = config === true ? {} : config;
|
|
462
|
-
const count = Math.max(
|
|
463
|
-
1,
|
|
464
|
-
Math.round(settings.count ?? 2 + random() * 3)
|
|
465
|
-
);
|
|
462
|
+
const count = Math.max(1, Math.round(settings.count ?? 2 + random() * 3));
|
|
466
463
|
const min = Math.max(2, settings.min ?? 6);
|
|
467
464
|
const max = Math.max(min, settings.max ?? 30);
|
|
468
465
|
const slice = perimeter / count;
|
|
469
466
|
let budget = perimeter * 0.35;
|
|
470
467
|
const breaks = [];
|
|
471
468
|
for (let i = 0; i < count; i++) {
|
|
472
|
-
const length = Math.min(
|
|
473
|
-
min + random() * (max - min),
|
|
474
|
-
slice * 0.7,
|
|
475
|
-
budget
|
|
476
|
-
);
|
|
469
|
+
const length = Math.min(min + random() * (max - min), slice * 0.7, budget);
|
|
477
470
|
if (length < 2) break;
|
|
478
471
|
budget -= length;
|
|
479
472
|
const start = i * slice + random() * Math.max(0, slice - length);
|
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
|
/**
|
|
@@ -114,5 +114,5 @@ export interface DoodleInstance {
|
|
|
114
114
|
*/
|
|
115
115
|
export function doodle(
|
|
116
116
|
target: string | Element,
|
|
117
|
-
options?: DoodleOptions
|
|
117
|
+
options?: DoodleOptions,
|
|
118
118
|
): DoodleInstance;
|
package/dist/react.cjs
CHANGED
|
@@ -489,21 +489,14 @@ function isBroken(fraction, breaks) {
|
|
|
489
489
|
function createBorderBreaks(perimeter, config, random) {
|
|
490
490
|
if (!config || perimeter <= 0) return [];
|
|
491
491
|
const settings = config === true ? {} : config;
|
|
492
|
-
const count = Math.max(
|
|
493
|
-
1,
|
|
494
|
-
Math.round(settings.count ?? 2 + random() * 3)
|
|
495
|
-
);
|
|
492
|
+
const count = Math.max(1, Math.round(settings.count ?? 2 + random() * 3));
|
|
496
493
|
const min = Math.max(2, settings.min ?? 6);
|
|
497
494
|
const max = Math.max(min, settings.max ?? 30);
|
|
498
495
|
const slice = perimeter / count;
|
|
499
496
|
let budget = perimeter * 0.35;
|
|
500
497
|
const breaks = [];
|
|
501
498
|
for (let i = 0; i < count; i++) {
|
|
502
|
-
const length = Math.min(
|
|
503
|
-
min + random() * (max - min),
|
|
504
|
-
slice * 0.7,
|
|
505
|
-
budget
|
|
506
|
-
);
|
|
499
|
+
const length = Math.min(min + random() * (max - min), slice * 0.7, budget);
|
|
507
500
|
if (length < 2) break;
|
|
508
501
|
budget -= length;
|
|
509
502
|
const start = i * slice + random() * Math.max(0, slice - length);
|
|
@@ -1795,7 +1788,7 @@ var Doodle = (0, import_react.forwardRef)(function Doodle2(props, forwardedRef)
|
|
|
1795
1788
|
ref: mergeRefs(innerRef, forwardedRef),
|
|
1796
1789
|
className,
|
|
1797
1790
|
// inline-flex shrink-wraps the child so the doodle hugs the component
|
|
1798
|
-
// rather than a full-width block. Flex specifically
|
|
1791
|
+
// rather than a full-width block. Flex specifically - inline-block would
|
|
1799
1792
|
// add baseline leading under an inline-level child like a <button>,
|
|
1800
1793
|
// leaving the frame taller than what it wraps. Override via `style`/`as`.
|
|
1801
1794
|
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
|
@@ -472,21 +472,14 @@ function isBroken(fraction, breaks) {
|
|
|
472
472
|
function createBorderBreaks(perimeter, config, random) {
|
|
473
473
|
if (!config || perimeter <= 0) return [];
|
|
474
474
|
const settings = config === true ? {} : config;
|
|
475
|
-
const count = Math.max(
|
|
476
|
-
1,
|
|
477
|
-
Math.round(settings.count ?? 2 + random() * 3)
|
|
478
|
-
);
|
|
475
|
+
const count = Math.max(1, Math.round(settings.count ?? 2 + random() * 3));
|
|
479
476
|
const min = Math.max(2, settings.min ?? 6);
|
|
480
477
|
const max = Math.max(min, settings.max ?? 30);
|
|
481
478
|
const slice = perimeter / count;
|
|
482
479
|
let budget = perimeter * 0.35;
|
|
483
480
|
const breaks = [];
|
|
484
481
|
for (let i = 0; i < count; i++) {
|
|
485
|
-
const length = Math.min(
|
|
486
|
-
min + random() * (max - min),
|
|
487
|
-
slice * 0.7,
|
|
488
|
-
budget
|
|
489
|
-
);
|
|
482
|
+
const length = Math.min(min + random() * (max - min), slice * 0.7, budget);
|
|
490
483
|
if (length < 2) break;
|
|
491
484
|
budget -= length;
|
|
492
485
|
const start = i * slice + random() * Math.max(0, slice - length);
|
|
@@ -1778,7 +1771,7 @@ var Doodle = forwardRef(function Doodle2(props, forwardedRef) {
|
|
|
1778
1771
|
ref: mergeRefs(innerRef, forwardedRef),
|
|
1779
1772
|
className,
|
|
1780
1773
|
// inline-flex shrink-wraps the child so the doodle hugs the component
|
|
1781
|
-
// rather than a full-width block. Flex specifically
|
|
1774
|
+
// rather than a full-width block. Flex specifically - inline-block would
|
|
1782
1775
|
// add baseline leading under an inline-level child like a <button>,
|
|
1783
1776
|
// leaving the frame taller than what it wraps. Override via `style`/`as`.
|
|
1784
1777
|
style: { display: "inline-flex", ...style }
|
package/package.json
CHANGED