@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 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