clip-pal 0.1.0

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 Jáchym Šolta
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,226 @@
1
+ # clip-pal 🖇️
2
+
3
+ A one-legged paperclip who walks along the side of your page as you scroll,
4
+ keeps you company, and acts out what he says. He is the mascot of
5
+ [ClipSpace](https://clipspace.djt-group.com); this is him as a React
6
+ component, so he can hold other pages together too.
7
+
8
+ - **One SVG, one wire.** The body and the leg are separate paths, so the leg
9
+ can step, wave, point, kick a ball and knock on the glass.
10
+ - **Twenty-one emotes.** Gestures that play once (`wave`, `nod`, `kick`,
11
+ `backflip`, …) and poses he bends into and holds (`heart`, `question`,
12
+ `key`, …) — the poses morph the wire itself with CSS `d: path()`.
13
+ - **Two ways to use him.** `ScrollPal` walks from anchor to anchor on wide
14
+ screens; `PalCompanion` stands in one place and talks now and then.
15
+ - **Everything is a knob.** Shape presets or your own wire, stroke width,
16
+ every colour, tempo, walking speed, sway, blink, bubble size, what he may
17
+ say and do — props or CSS variables, your pick. Hover comments via
18
+ `data-pal-say`, and a `ref` to make him say or do things on demand.
19
+ - **No dependencies** beyond React. ~25 KB of JS, one CSS file, themeable
20
+ with custom properties. Respects `prefers-reduced-motion`.
21
+
22
+ ## Install
23
+
24
+ ```bash
25
+ npm i clip-pal
26
+ ```
27
+
28
+ ```tsx
29
+ import { PalCompanion } from "clip-pal";
30
+ import "clip-pal/styles.css";
31
+
32
+ <PalCompanion
33
+ width={90}
34
+ lines={[
35
+ ["hey — i'm clip pal. i hold this whole thing together.", "wave"],
36
+ ["it looks like you're trying to leave big tech. want a hand?", "question"],
37
+ ]}
38
+ />
39
+ ```
40
+
41
+ Every line comes with the emote he acts out when he says it — a
42
+ `[text, emote]` pair. Keep them together: a question turns him into a
43
+ question mark, "no ads" gets a head shake.
44
+
45
+ ## The walking guide
46
+
47
+ ```tsx
48
+ import { ScrollPal, type PalStop } from "clip-pal";
49
+
50
+ const STOPS: PalStop[] = [
51
+ { id: "hero", anchor: "hero-title", place: "beside", lines: [["hello.", "wave"]] },
52
+ { id: "features", anchor: "features-h2", place: "gutter-left", lines: [["look at all this.", "look"]] },
53
+ { id: "footer", anchor: "footer-title", place: "gutter-right", lines: [["that's the lot.", "bow"]] },
54
+ ];
55
+
56
+ <ScrollPal stops={STOPS} idleLines={[["quiet in here.", "doze"]]} />
57
+ ```
58
+
59
+ - `id` — the element whose vertical span "owns" him. He belongs to whichever
60
+ stop's element the middle of the viewport is inside, so the switch point
61
+ is deterministic and the same in both scroll directions.
62
+ - `anchor` — the element he stands next to and rides along with as it
63
+ scrolls. `place` puts him right beside it, or parks him in the left or
64
+ right gutter at its height.
65
+ - He needs a gutter: the space between the screen edge and your content
66
+ column. By default he reads its width from the `--content-w` custom
67
+ property on `<html>` (rem or px; falls back to 72rem); pass
68
+ `contentWidth={1152}` or a function instead. When the gutter is narrower
69
+ than 46 px, or the viewport is under `minViewport` (1600), he is not
70
+ rendered — put a `PalCompanion` in the page for those screens.
71
+ - He is draggable. Drop him somewhere and he stays until the next scroll.
72
+ - After `idleAfterMs` (12 s) with no input he wanders to the far side of the
73
+ screen and says one of `idleLines`; any input sends him back.
74
+
75
+ ### Props
76
+
77
+ Every number that used to be hardcoded is a prop; the defaults are what the
78
+ ClipSpace site was tuned with.
79
+
80
+ | prop | default | |
81
+ |---|---|---|
82
+ | `stops` | — | the stops, top to bottom |
83
+ | `idleLines` | — | lines for when the page sits still; omit to disable wandering |
84
+ | `idle` | true | wander at all |
85
+ | `idleAfterMs` / `idleWanderMs` | 12000 / 20000 | stillness before he wanders, pause between wanders |
86
+ | `minViewport` | 1600 | narrowest viewport he appears on |
87
+ | `contentWidth` | reads `--content-w` | px, or `() => px` |
88
+ | `contentVar` | `"--content-w"` | the custom property to read |
89
+ | `minWidth` / `maxWidth` | 46 / 92 | his rendered width range; narrower gutters hide him |
90
+ | `gap` / `edge` | 12 / 8 | clearance from the content column / the screen edge |
91
+ | `side` | `"auto"` | `"left"` or `"right"` forces one gutter for every stop |
92
+ | `offsetY` | 0 | shift his resting height, px |
93
+ | `yLimitPad` | 150 | how close to the top/bottom edge he may stand |
94
+ | `speed` / `speedY` | 480 / 540 | walking and vertical catch-up speed, px/s |
95
+ | `ease` | 6.32 | ease rate toward the target — bigger snaps, smaller drifts |
96
+ | `leanMax` | 9 | how far he leans into a stride, degrees |
97
+ | `walkThreshold` | 27 | px/s above which he counts as walking |
98
+ | `retargetMs` | 330 | how often the anchors are re-measured |
99
+ | `typingMs` / `emoteDelayMs` | 850 / 500 | typing dots; beat before the trick |
100
+ | `bubbleMaxWidth` / `bubbleMinWidth` | 260 / 150 | px |
101
+ | `random` | true | random lines, or in order |
102
+ | `speak` | true | false makes him mime |
103
+ | `emotes` | true | false = never acts out; or a whitelist array |
104
+ | `hover` / `hoverCooldownMs` | true / 1200 | react to `data-pal-say` elements |
105
+ | `draggable` | true | |
106
+ | `shape` | `"gem"` | `"round"`, `"square"`, `"long"` or a `PalShape` |
107
+ | `strokeWidth` | 5 | wire thickness |
108
+ | `eyes` | `"dots"` | `"none"` |
109
+ | `colors` | — | `{ wire, face, ink, bubbleBg, bubbleBorder, bubbleText, bubbleDot }` |
110
+ | `motion` | — | `{ tempo, stepMs, swayMs, swayDeg, blinkMs, floatMs, floatPx }` |
111
+ | `zIndex` | 40 | |
112
+ | `onArrive(stopId)` / `onLeave(stopId)` / `onSpeak(text, emote)` / `onIdle()` | — | callbacks |
113
+
114
+ `PalCompanion`: `lines` (required), `width` (90), `height`, `showMs`
115
+ (10000), `gapMs` (20000), `firstMs` (1500), `typingMs`, `emoteDelayMs`,
116
+ `bubbleMaxWidth` ("15rem"), `random`, `speak`, `emotes`, `hover`,
117
+ `hoverCooldownMs`, `float` (true), `sway` (true), `visibleThreshold` (0.4),
118
+ `shape`, `strokeWidth`, `eyes`, `colors`, `motion`, `onSpeak`.
119
+
120
+ `PalSvg`: the bare drawing — `width`, `height`, `walking`, `emote`,
121
+ `emoteOut`, `shape`, `strokeWidth`, `eyes`, `colors`, `motion`. Use it to
122
+ drive him yourself; `gestureMs(name, tempo)` gives each gesture's duration,
123
+ and a pose holds until you render it again with `emoteOut`.
124
+
125
+ ### Control from outside
126
+
127
+ Both `ScrollPal` and `PalCompanion` take a `ref`:
128
+
129
+ ```tsx
130
+ const pal = useRef<ScrollPalHandle>(null);
131
+ <ScrollPal ref={pal} stops={STOPS} />
132
+ pal.current?.say("you clicked the thing.", "nod");
133
+ pal.current?.emote("backflip");
134
+ pal.current?.hush();
135
+ ```
136
+
137
+ ### Hover comments
138
+
139
+ Put `data-pal-say` on anything and he comments when the pointer lands on
140
+ it; `data-pal-emote` picks the trick.
141
+
142
+ ```html
143
+ <article data-pal-say="ooh, a card. fancy." data-pal-emote="spin">…</article>
144
+ ```
145
+
146
+ One `pointerover` listener on the document handles all of them, so add and
147
+ remove such elements freely. `hover={false}` turns it off.
148
+
149
+ ### Your own shape
150
+
151
+ ```tsx
152
+ <PalCompanion shape="round" strokeWidth={6} lines={LINES} />
153
+
154
+ <PalCompanion
155
+ shape={{
156
+ body: "M15 62 L15 14 C15 7 19 3 25 3 C31 3 35 7 35 14 L35 50 C35 55 32 58 28 58 C24 58 21 55 21 50 L21 20",
157
+ leg: "M15 63 C15 70 20 73 27 73 C34 73 41 69 41 61 L41 54",
158
+ hip: [15, 63],
159
+ eyes: [{ cx: 22, cy: 12, r: 2.4 }, { cx: 30, cy: 12, r: 2.4 }],
160
+ }}
161
+ lines={LINES}
162
+ />
163
+ ```
164
+
165
+ The viewBox is `0 0 50 80`. Keep the body as `M L C C L C C L` and the leg
166
+ as `M C C L` and every morphing pose still tweens from your shape; break
167
+ the sequence and he cuts to the pose instead (a dev warning says so).
168
+
169
+ ## Emotes
170
+
171
+ Gestures (play once, land back where they started):
172
+ `lean` `look` `knock` `kick` `crack` `dance` `wave` `hop` `spin` `nod`
173
+ `shake` `bow` `backflip` `shiver` `doze`
174
+
175
+ Poses (bend in, hold, unfold when he moves on):
176
+ `unbend` `curl` `heart` `question` `exclaim` `key`
177
+
178
+ ## Theming
179
+
180
+ Set on `:root` or any ancestor — or pass `colors` / `motion` props, which
181
+ set the same variables inline. Defaults are the ClipSpace desk set.
182
+
183
+ ```css
184
+ :root {
185
+ /* colours */
186
+ --pal-wire: #d9a441; /* the paperclip */
187
+ --pal-face: #f2ede0; /* eyes, ball, crack, rings, sleep bubbles */
188
+ --pal-ink: #131f1a; /* the ball's panel */
189
+ --pal-bubble-bg: #1b2a23;
190
+ --pal-bubble-border: rgba(242, 237, 224, 0.1);
191
+ --pal-bubble-text: #f2ede0;
192
+ --pal-bubble-dot: #9daa9f;
193
+ --pal-bubble-radius: 1rem;
194
+ --pal-bubble-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.3);
195
+ --pal-bubble-padding: 0.625rem 1rem;
196
+ --pal-bubble-font-size: 0.875rem;
197
+ --pal-font: inherit;
198
+ /* motion */
199
+ --pal-tempo: 1; /* multiplies every emote's duration */
200
+ --pal-step: 0.36s; /* one stride while walking */
201
+ --pal-sway: 5s; /* idle sway period */
202
+ --pal-sway-deg: 2.5deg; /* idle sway amplitude */
203
+ --pal-blink: 5s;
204
+ --pal-float: 6s; /* the companion's float */
205
+ --pal-float-px: 8px;
206
+ }
207
+ ```
208
+
209
+ ## Frameworks
210
+
211
+ The bundle is marked `"use client"`, so in Next.js you can import it
212
+ straight into a server component. Import the stylesheet once, in your root
213
+ layout.
214
+
215
+ ## Development
216
+
217
+ ```bash
218
+ npm i
219
+ npm run demo # every emote on a button grid + the walking guide
220
+ npm run build # dist/ via tsup
221
+ ```
222
+
223
+ ## License
224
+
225
+ MIT · made for [ClipSpace](https://clipspace.djt-group.com) by
226
+ [Jáchym Šolta](https://jachym.djt-group.com)