@aznabee/freehand-ui 0.1.1
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/LICENSE +21 -0
- package/README.md +261 -0
- package/dist/freehand-ui.cjs +1513 -0
- package/dist/freehand-ui.js +1491 -0
- package/dist/index.d.ts +89 -0
- package/dist/react.cjs +1634 -0
- package/dist/react.d.ts +66 -0
- package/dist/react.js +1622 -0
- package/package.json +67 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Lakshay Gupta
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
# freehand-ui
|
|
2
|
+
|
|
3
|
+
Wrap any existing web element with a beautiful, responsive, hand-drawn doodle layer.
|
|
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
|
|
14
|
+
|
|
15
|
+
```javascript
|
|
16
|
+
import { doodle } from "@aznabee/freehand-ui";
|
|
17
|
+
|
|
18
|
+
doodle(document.querySelector(".card"));
|
|
19
|
+
// or
|
|
20
|
+
doodle(".card");
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
This immediately draws a hand-drawn border around the element and keeps it aligned as the page resizes, scrolls, or reflows.
|
|
24
|
+
|
|
25
|
+
## React / Next.js
|
|
26
|
+
|
|
27
|
+
```jsx
|
|
28
|
+
import Doodle from "@aznabee/freehand-ui/react";
|
|
29
|
+
|
|
30
|
+
<Doodle note="start chat" strokeWidth={2}>
|
|
31
|
+
<Button />
|
|
32
|
+
</Doodle>;
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Every option below works as a prop. The component ships `"use client"`, so it
|
|
36
|
+
drops straight into the Next.js App Router without a wrapper of your own, and
|
|
37
|
+
it renders on the server without warnings.
|
|
38
|
+
|
|
39
|
+
The doodle is drawn around **the wrapped component as a whole** — children are
|
|
40
|
+
never decorated individually. To decorate descendants instead, opt in with
|
|
41
|
+
`childSelector`:
|
|
42
|
+
|
|
43
|
+
```jsx
|
|
44
|
+
<Doodle childSelector=".card">
|
|
45
|
+
{items.map((item) => <Card key={item.id} className="card" {...item} />)}
|
|
46
|
+
</Doodle>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Wrapper element
|
|
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
|
+
```
|
|
64
|
+
|
|
65
|
+
Border radius is picked up automatically: if the wrapper has none of its own,
|
|
66
|
+
it inherits the radius of the child it hugs, so a wrapped pill button is still
|
|
67
|
+
drawn as a pill.
|
|
68
|
+
|
|
69
|
+
### Hook
|
|
70
|
+
|
|
71
|
+
For an element you already hold a ref to, skip the wrapper:
|
|
72
|
+
|
|
73
|
+
```jsx
|
|
74
|
+
import { useDoodle } from "@aznabee/freehand-ui/react";
|
|
75
|
+
|
|
76
|
+
function Card() {
|
|
77
|
+
const ref = useRef(null);
|
|
78
|
+
useDoodle(ref, { note: "new", decorations: true });
|
|
79
|
+
return <div ref={ref}>…</div>;
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Notes
|
|
84
|
+
|
|
85
|
+
- Inline object props (`note={{ text: "hi" }}`) are compared by value, so a
|
|
86
|
+
re-render does not tear down and redraw the overlay with a new random seed.
|
|
87
|
+
- `disabled` skips drawing entirely — useful behind a reduced-motion check or a
|
|
88
|
+
feature flag.
|
|
89
|
+
- TypeScript definitions ship with the package for both entry points.
|
|
90
|
+
|
|
91
|
+
## Configuration
|
|
92
|
+
|
|
93
|
+
```javascript
|
|
94
|
+
doodle(".card", {
|
|
95
|
+
border: true,
|
|
96
|
+
color: "#ffffff",
|
|
97
|
+
strokeWidth: 1.5,
|
|
98
|
+
roughness: 1.5,
|
|
99
|
+
padding: 8,
|
|
100
|
+
radius: 20, // optional override; auto-detected from CSS when omitted
|
|
101
|
+
opacity: 0.9,
|
|
102
|
+
addBreaks: false, // lift the pen at random points around the outline
|
|
103
|
+
});
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Broken outlines
|
|
107
|
+
|
|
108
|
+
`addBreaks` cuts randomly sized gaps into the border, so it reads as a few
|
|
109
|
+
confident dashes rather than one closed loop:
|
|
110
|
+
|
|
111
|
+
```javascript
|
|
112
|
+
doodle(".cta", { addBreaks: true });
|
|
113
|
+
|
|
114
|
+
doodle(".cta", { addBreaks: 5 }); // five gaps
|
|
115
|
+
|
|
116
|
+
doodle(".cta", {
|
|
117
|
+
addBreaks: {
|
|
118
|
+
count: 4, // omit for 2–5 gaps
|
|
119
|
+
min: 6, // shortest gap, px
|
|
120
|
+
max: 30, // longest gap, px
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Gaps are spread one per section of the perimeter so they never clump, and their
|
|
126
|
+
total is capped at a third of the outline so the frame still reads as a frame.
|
|
127
|
+
On small elements the gaps shrink to fit.
|
|
128
|
+
|
|
129
|
+
## Handwritten annotations
|
|
130
|
+
|
|
131
|
+
```javascript
|
|
132
|
+
doodle(".cta", {
|
|
133
|
+
note: "start chat ♡",
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
doodle(".cta", {
|
|
137
|
+
note: { text: "try me", position: "bottom-right", underline: false },
|
|
138
|
+
});
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Positions: `top`, `top-right`, `right`, `bottom-right`, `bottom`, `bottom-left`, `left`, `top-left`. Same names apply to `arrow.from`/`arrow.to`.
|
|
142
|
+
|
|
143
|
+
The position is a preference, not a command. Notes are measured from their real
|
|
144
|
+
glyph metrics and laid out so they never sit on top of the element — if the
|
|
145
|
+
preferred side would run off screen, the note flips to a side that fits. The
|
|
146
|
+
underline is drawn to the measured text width; set `underline: false` for plain
|
|
147
|
+
handwriting.
|
|
148
|
+
|
|
149
|
+
## Arrows
|
|
150
|
+
|
|
151
|
+
```javascript
|
|
152
|
+
doodle(".cta", { arrow: true });
|
|
153
|
+
|
|
154
|
+
doodle(".cta", {
|
|
155
|
+
arrow: {
|
|
156
|
+
from: "top-right",
|
|
157
|
+
to: "center",
|
|
158
|
+
style: "curved", // curved | straight | dotted
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
When the element also has a `note`, the arrow ignores `from` and launches from
|
|
164
|
+
the note itself. `to: "edge"` (the default) lands the tip just *outside* the
|
|
165
|
+
frame; `to: "center"` is the one setting that deliberately points into the
|
|
166
|
+
element.
|
|
167
|
+
|
|
168
|
+
## Decorations
|
|
169
|
+
|
|
170
|
+
```javascript
|
|
171
|
+
doodle(".card", { decorations: true });
|
|
172
|
+
|
|
173
|
+
doodle(".card", {
|
|
174
|
+
decorations: {
|
|
175
|
+
count: 2, // border marks, hard-capped at 2
|
|
176
|
+
style: "corners", // corners | sides — omit to pick automatically
|
|
177
|
+
types: ["arcs", "twinkle", "heart"],
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
A frame stays readable with **at most two marks around it**, so `count` is
|
|
183
|
+
capped at 2 and the default composition is deliberately fixed:
|
|
184
|
+
|
|
185
|
+
- **one corner accent** — `arcs` or `emphasis` — hugging a corner, aimed outward
|
|
186
|
+
- **one corner star** — `twinkle` or `star` — on the corner farthest from it,
|
|
187
|
+
and sitting noticeably further off the border than the accent
|
|
188
|
+
- **one handwriting accent** — `heart` or `sparkle` — beside the note, not the
|
|
189
|
+
component
|
|
190
|
+
|
|
191
|
+
Corner marks follow the *rounded* corner rather than the bounding box, so they
|
|
192
|
+
hug a pill's cap as closely as a card's corner, and any mark too large for the
|
|
193
|
+
gap it landed in is pushed further out. Everything keeps off the element, off
|
|
194
|
+
the note, and off the arrow's sweep.
|
|
195
|
+
|
|
196
|
+
### Handwriting accents
|
|
197
|
+
|
|
198
|
+
`heart` and `sparkle` belong to text: they are drawn at the far end of the
|
|
199
|
+
note, on the side away from the element. With no `note` on the element they are
|
|
200
|
+
skipped rather than moved to the border.
|
|
201
|
+
|
|
202
|
+
### Small components
|
|
203
|
+
|
|
204
|
+
Chips and icon buttons have no corner worth pointing at, so they get a mirrored
|
|
205
|
+
pair of two-stroke `emphasis` marks — one either side — instead of corner
|
|
206
|
+
marks. This is chosen automatically when the element is under 48px on its short
|
|
207
|
+
side or under 130px wide; `style: "sides"` forces it at any size and
|
|
208
|
+
`style: "corners"` opts out.
|
|
209
|
+
|
|
210
|
+
The remaining types — `star`, `smiley`, `dots`, `stroke`, `steam` — are still
|
|
211
|
+
available and fill a corner slot when no accent or star was requested.
|
|
212
|
+
|
|
213
|
+
## Child selector mode
|
|
214
|
+
|
|
215
|
+
Decorate multiple children from a single parent overlay:
|
|
216
|
+
|
|
217
|
+
```javascript
|
|
218
|
+
doodle("#hero", {
|
|
219
|
+
children: ".doodle-item",
|
|
220
|
+
});
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
## Lifecycle
|
|
224
|
+
|
|
225
|
+
```javascript
|
|
226
|
+
const instance = doodle(".card");
|
|
227
|
+
|
|
228
|
+
instance.update(); // recalculate geometry and redraw
|
|
229
|
+
instance.destroy(); // remove overlay and disconnect observers
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Calling `doodle()` twice on the same element replaces the previous overlay — no duplicates.
|
|
233
|
+
|
|
234
|
+
## Examples
|
|
235
|
+
|
|
236
|
+
Run the dev server after building:
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
npm install
|
|
240
|
+
npm run build
|
|
241
|
+
npm run dev
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Then open:
|
|
245
|
+
|
|
246
|
+
- http://localhost:5173/examples/basic/
|
|
247
|
+
- http://localhost:5173/examples/annotations/
|
|
248
|
+
- http://localhost:5173/examples/children/
|
|
249
|
+
- http://localhost:5173/examples/decorations/
|
|
250
|
+
|
|
251
|
+
## Design principles
|
|
252
|
+
|
|
253
|
+
- **Framework agnostic** — plain JavaScript at the core; React is an optional entry point
|
|
254
|
+
- **SVG based** — hand-drawn paths, not CSS borders
|
|
255
|
+
- **Non-invasive** — `pointer-events: none`, no layout changes
|
|
256
|
+
- **Responsive** — `ResizeObserver`, scroll listeners, and `requestAnimationFrame`
|
|
257
|
+
- **Lightweight** — zero runtime dependencies
|
|
258
|
+
|
|
259
|
+
## License
|
|
260
|
+
|
|
261
|
+
MIT
|