penseat 0.1.0 → 0.3.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/index.mjs
CHANGED
|
@@ -42,7 +42,6 @@ const hasSrc = existsSync(join(cwd, "src"));
|
|
|
42
42
|
const base = hasSrc ? join(cwd, "src") : cwd;
|
|
43
43
|
|
|
44
44
|
const componentsDir = join(base, "components", "penseat");
|
|
45
|
-
const uiDir = join(componentsDir, "ui");
|
|
46
45
|
const libDir = join(base, "lib");
|
|
47
46
|
|
|
48
47
|
// ── Check if already installed ──
|
|
@@ -56,23 +55,16 @@ if (existsSync(join(componentsDir, "penseat.tsx"))) {
|
|
|
56
55
|
console.log(` ${dim("Installing to")} ${cyan(componentsDir.replace(cwd + "/", ""))}`);
|
|
57
56
|
console.log();
|
|
58
57
|
|
|
59
|
-
mkdirSync(
|
|
58
|
+
mkdirSync(componentsDir, { recursive: true });
|
|
60
59
|
mkdirSync(libDir, { recursive: true });
|
|
61
60
|
|
|
62
61
|
const files = [
|
|
63
62
|
{ from: "components/penseat.tsx", to: join(componentsDir, "penseat.tsx") },
|
|
64
63
|
{ from: "components/penseat-bar.tsx", to: join(componentsDir, "penseat-bar.tsx") },
|
|
65
64
|
{ from: "components/drawing-canvas.tsx", to: join(componentsDir, "drawing-canvas.tsx") },
|
|
66
|
-
{ from: "components/ui/button.tsx", to: join(componentsDir, "ui", "button.tsx") },
|
|
67
65
|
{ from: "lib/capture.ts", to: join(libDir, "capture.ts") },
|
|
68
66
|
];
|
|
69
67
|
|
|
70
|
-
// Only copy utils.ts if it doesn't exist (shadcn projects already have it)
|
|
71
|
-
const utilsTarget = join(libDir, "utils.ts");
|
|
72
|
-
if (!existsSync(utilsTarget)) {
|
|
73
|
-
files.push({ from: "lib/utils.ts", to: utilsTarget });
|
|
74
|
-
}
|
|
75
|
-
|
|
76
68
|
for (const f of files) {
|
|
77
69
|
copyFileSync(join(TEMPLATES, f.from), f.to);
|
|
78
70
|
}
|
|
@@ -82,11 +74,6 @@ console.log(` ${green("+")} Copied ${files.length} files`);
|
|
|
82
74
|
// ── Install dependencies ──
|
|
83
75
|
const needed = [
|
|
84
76
|
"html2canvas-pro",
|
|
85
|
-
"lucide-react",
|
|
86
|
-
"@base-ui/react",
|
|
87
|
-
"class-variance-authority",
|
|
88
|
-
"clsx",
|
|
89
|
-
"tailwind-merge",
|
|
90
77
|
];
|
|
91
78
|
|
|
92
79
|
const missing = needed.filter((d) => !deps[d]);
|
package/package.json
CHANGED
|
@@ -601,8 +601,10 @@ const DrawingCanvas = forwardRef<DrawingCanvasHandle, DrawingCanvasProps>(
|
|
|
601
601
|
<canvas
|
|
602
602
|
ref={canvasRef}
|
|
603
603
|
data-penseat="canvas"
|
|
604
|
-
className="absolute top-0 left-0"
|
|
605
604
|
style={{
|
|
605
|
+
position: "absolute",
|
|
606
|
+
top: 0,
|
|
607
|
+
left: 0,
|
|
606
608
|
pointerEvents: active ? "auto" : "none",
|
|
607
609
|
cursor: active
|
|
608
610
|
? color === ERASER_COLOR
|
|
@@ -616,8 +618,13 @@ const DrawingCanvas = forwardRef<DrawingCanvasHandle, DrawingCanvasProps>(
|
|
|
616
618
|
{active && isEraser && (
|
|
617
619
|
<div
|
|
618
620
|
ref={eraserCursorRef}
|
|
619
|
-
className="fixed z-[99999] pointer-events-none -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-white"
|
|
620
621
|
style={{
|
|
622
|
+
position: "fixed",
|
|
623
|
+
zIndex: 99999,
|
|
624
|
+
pointerEvents: "none",
|
|
625
|
+
transform: "translate(-50%, -50%)",
|
|
626
|
+
borderRadius: "9999px",
|
|
627
|
+
border: "2px solid #ffffff",
|
|
621
628
|
width: ERASER_RADIUS * 2,
|
|
622
629
|
height: ERASER_RADIUS * 2,
|
|
623
630
|
mixBlendMode: "difference",
|
|
@@ -2,8 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
import { useState, useCallback, useRef, useEffect, createContext, useContext, type ReactNode } from "react";
|
|
4
4
|
import { createPortal } from "react-dom";
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
// Inline SVG icons (from Lucide) — zero dependencies
|
|
6
|
+
const iconProps = { width: 16, height: 16, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round" as const, strokeLinejoin: "round" as const };
|
|
7
|
+
const Undo2Icon = <svg {...iconProps}><path d="M9 14 4 9l5-5"/><path d="M4 9h10.5a5.5 5.5 0 0 1 5.5 5.5a5.5 5.5 0 0 1-5.5 5.5H11"/></svg>;
|
|
8
|
+
const Trash2Icon = <svg {...iconProps}><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/><line x1="10" x2="10" y1="11" y2="17"/><line x1="14" x2="14" y1="11" y2="17"/></svg>;
|
|
9
|
+
const XIcon = <svg {...iconProps} width={20} height={20}><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>;
|
|
10
|
+
const CopyIcon = <svg {...iconProps}><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>;
|
|
11
|
+
const CheckIcon = <svg {...iconProps} strokeWidth={3}><path d="M20 6 9 17l-5-5"/></svg>;
|
|
7
12
|
|
|
8
13
|
const TOOLTIP_DELAY = 360;
|
|
9
14
|
const TOOLTIP_GAP = 10;
|
|
@@ -25,13 +30,26 @@ const CMD_ICON = (
|
|
|
25
30
|
function Keycap({ children }: { children: ReactNode }) {
|
|
26
31
|
const content = children === "command" ? CMD_ICON : children === "shift" ? SHIFT_ICON : children;
|
|
27
32
|
return (
|
|
28
|
-
<kbd
|
|
33
|
+
<kbd style={{
|
|
34
|
+
display: 'inline-flex',
|
|
35
|
+
alignItems: 'center',
|
|
36
|
+
justifyContent: 'center',
|
|
37
|
+
width: '20px',
|
|
38
|
+
height: '20px',
|
|
39
|
+
borderRadius: '4px',
|
|
40
|
+
backgroundColor: '#3f3f46',
|
|
41
|
+
border: '1px solid #52525b',
|
|
42
|
+
fontSize: '13px',
|
|
43
|
+
fontFamily: 'monospace',
|
|
44
|
+
lineHeight: 1,
|
|
45
|
+
color: '#d4d4d8',
|
|
46
|
+
}}>
|
|
29
47
|
{content}
|
|
30
48
|
</kbd>
|
|
31
49
|
);
|
|
32
50
|
}
|
|
33
51
|
|
|
34
|
-
function WithTooltip({ label, shortcut, children, pressed }: { label: string; shortcut?: string[]; children: ReactNode; pressed?: boolean }) {
|
|
52
|
+
function WithTooltip({ label, shortcut, children, pressed, forceShow }: { label: string; shortcut?: string[]; children: ReactNode; pressed?: boolean; forceShow?: boolean }) {
|
|
35
53
|
const below = useContext(TooltipBelowCtx);
|
|
36
54
|
const [hoverShow, setHoverShow] = useState(false);
|
|
37
55
|
const [pos, setPos] = useState<{ x: number; top: number; bottom: number } | null>(null);
|
|
@@ -45,6 +63,11 @@ function WithTooltip({ label, shortcut, children, pressed }: { label: string; sh
|
|
|
45
63
|
}
|
|
46
64
|
}, []);
|
|
47
65
|
|
|
66
|
+
// Force-show: compute position on mount when forceShow is true
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (forceShow) computePos();
|
|
69
|
+
}, [forceShow, computePos]);
|
|
70
|
+
|
|
48
71
|
const onEnter = () => {
|
|
49
72
|
timerRef.current = setTimeout(() => {
|
|
50
73
|
computePos();
|
|
@@ -61,17 +84,24 @@ function WithTooltip({ label, shortcut, children, pressed }: { label: string; sh
|
|
|
61
84
|
return (
|
|
62
85
|
<div
|
|
63
86
|
ref={wrapRef}
|
|
64
|
-
|
|
65
|
-
|
|
87
|
+
style={{
|
|
88
|
+
display: 'flex',
|
|
89
|
+
alignItems: 'center',
|
|
90
|
+
alignSelf: 'stretch',
|
|
91
|
+
transition: 'transform 100ms',
|
|
92
|
+
transform: pressed ? 'scale(0.95)' : undefined,
|
|
93
|
+
}}
|
|
66
94
|
onPointerEnter={onEnter}
|
|
67
95
|
onPointerLeave={onLeave}
|
|
68
96
|
>
|
|
69
97
|
{children}
|
|
70
|
-
{hoverShow && pos && createPortal(
|
|
98
|
+
{(hoverShow || forceShow) && pos && createPortal(
|
|
71
99
|
<div
|
|
72
100
|
data-penseat="tooltip"
|
|
73
|
-
className="fixed z-[99999] pointer-events-none"
|
|
74
101
|
style={{
|
|
102
|
+
position: 'fixed',
|
|
103
|
+
zIndex: 99999,
|
|
104
|
+
pointerEvents: 'none',
|
|
75
105
|
left: pos.x,
|
|
76
106
|
top: below ? pos.bottom + TOOLTIP_GAP : pos.top - TOOLTIP_GAP,
|
|
77
107
|
translate: below ? "-50% 0" : "-50% -100%",
|
|
@@ -79,10 +109,20 @@ function WithTooltip({ label, shortcut, children, pressed }: { label: string; sh
|
|
|
79
109
|
animation: below ? "tooltip-in-below 150ms ease-out" : "tooltip-in-above 150ms ease-out",
|
|
80
110
|
}}
|
|
81
111
|
>
|
|
82
|
-
<div
|
|
83
|
-
|
|
112
|
+
<div style={{
|
|
113
|
+
display: 'flex',
|
|
114
|
+
alignItems: 'center',
|
|
115
|
+
gap: '8px',
|
|
116
|
+
borderRadius: '8px',
|
|
117
|
+
backgroundColor: '#27272A',
|
|
118
|
+
paddingLeft: '12px',
|
|
119
|
+
paddingTop: '6px',
|
|
120
|
+
paddingBottom: '6px',
|
|
121
|
+
paddingRight: shortcut && shortcut.length > 0 ? '6px' : '12px',
|
|
122
|
+
}}>
|
|
123
|
+
<span style={{ fontSize: '12px', fontFamily: 'monospace', color: '#e4e4e7' }}>{label}</span>
|
|
84
124
|
{shortcut && shortcut.length > 0 && (
|
|
85
|
-
<div
|
|
125
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '2px' }}>
|
|
86
126
|
{shortcut.map((key, i) => (
|
|
87
127
|
<Keycap key={i}>{key}</Keycap>
|
|
88
128
|
))}
|
|
@@ -131,15 +171,27 @@ interface PenseatBarProps {
|
|
|
131
171
|
}
|
|
132
172
|
|
|
133
173
|
function EraserDot({ active, onClick }: { active: boolean; onClick: () => void }) {
|
|
174
|
+
const [hovered, setHovered] = useState(false);
|
|
134
175
|
return (
|
|
135
176
|
<div
|
|
136
177
|
onClick={onClick}
|
|
137
|
-
|
|
178
|
+
onPointerEnter={() => setHovered(true)}
|
|
179
|
+
onPointerLeave={() => setHovered(false)}
|
|
138
180
|
style={{
|
|
181
|
+
width: '20px',
|
|
182
|
+
height: '20px',
|
|
183
|
+
borderRadius: '9999px',
|
|
184
|
+
cursor: 'pointer',
|
|
185
|
+
transition: 'transform 150ms',
|
|
186
|
+
transform: hovered ? 'scale(1.1)' : 'scale(1)',
|
|
187
|
+
flexShrink: 0,
|
|
188
|
+
display: 'flex',
|
|
189
|
+
alignItems: 'center',
|
|
190
|
+
justifyContent: 'center',
|
|
139
191
|
boxShadow: active ? "0 0 0 2px #18181b, 0 0 0 3px #a1a1aa" : "none",
|
|
140
192
|
}}
|
|
141
193
|
>
|
|
142
|
-
<svg viewBox="0 0 20 20"
|
|
194
|
+
<svg viewBox="0 0 20 20" style={{ width: '20px', height: '20px' }}>
|
|
143
195
|
<defs>
|
|
144
196
|
<clipPath id="eraser-clip">
|
|
145
197
|
<circle cx="10" cy="10" r="8.5" />
|
|
@@ -158,7 +210,7 @@ function EraserDot({ active, onClick }: { active: boolean; onClick: () => void }
|
|
|
158
210
|
|
|
159
211
|
function ColorDots({ colors, active, onChange, pressedId }: { colors: typeof COLORS; active: string; onChange: (c: string) => void; pressedId: string | null }) {
|
|
160
212
|
return (
|
|
161
|
-
<div
|
|
213
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', padding: '0 12px', flexShrink: 0 }}>
|
|
162
214
|
{colors.map((c, i) => {
|
|
163
215
|
if (c.name === "Eraser") {
|
|
164
216
|
return (
|
|
@@ -168,41 +220,98 @@ function ColorDots({ colors, active, onChange, pressedId }: { colors: typeof COL
|
|
|
168
220
|
);
|
|
169
221
|
}
|
|
170
222
|
return (
|
|
171
|
-
<
|
|
172
|
-
<div
|
|
173
|
-
onClick={() => onChange(c.value)}
|
|
174
|
-
className="size-5 rounded-full cursor-pointer transition-transform hover:scale-110 shrink-0"
|
|
175
|
-
style={{
|
|
176
|
-
backgroundColor: c.value,
|
|
177
|
-
boxShadow: active === c.value ? `0 0 0 2px #18181b, 0 0 0 3px ${c.value}` : "none",
|
|
178
|
-
}}
|
|
179
|
-
/>
|
|
180
|
-
</WithTooltip>
|
|
223
|
+
<ColorDot key={c.value} color={c} index={i} active={active === c.value} onChange={onChange} pressedId={pressedId} />
|
|
181
224
|
);
|
|
182
225
|
})}
|
|
183
226
|
</div>
|
|
184
227
|
);
|
|
185
228
|
}
|
|
186
229
|
|
|
230
|
+
function ColorDot({ color: c, index: i, active, onChange, pressedId }: { color: typeof COLORS[number]; index: number; active: boolean; onChange: (c: string) => void; pressedId: string | null }) {
|
|
231
|
+
const [hovered, setHovered] = useState(false);
|
|
232
|
+
return (
|
|
233
|
+
<WithTooltip label={c.name} shortcut={[String(i + 1)]} pressed={pressedId === `color-${i + 1}`}>
|
|
234
|
+
<div
|
|
235
|
+
onClick={() => onChange(c.value)}
|
|
236
|
+
onPointerEnter={() => setHovered(true)}
|
|
237
|
+
onPointerLeave={() => setHovered(false)}
|
|
238
|
+
style={{
|
|
239
|
+
width: '20px',
|
|
240
|
+
height: '20px',
|
|
241
|
+
borderRadius: '9999px',
|
|
242
|
+
cursor: 'pointer',
|
|
243
|
+
transition: 'transform 150ms',
|
|
244
|
+
transform: hovered ? 'scale(1.1)' : 'scale(1)',
|
|
245
|
+
flexShrink: 0,
|
|
246
|
+
backgroundColor: c.value,
|
|
247
|
+
boxShadow: active ? `0 0 0 2px #18181b, 0 0 0 3px ${c.value}` : "none",
|
|
248
|
+
}}
|
|
249
|
+
/>
|
|
250
|
+
</WithTooltip>
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function ActionButton({ icon, ariaLabel, onClick, pressed }: { icon: ReactNode; ariaLabel: string; onClick: () => void; pressed: boolean }) {
|
|
255
|
+
return (
|
|
256
|
+
<button
|
|
257
|
+
tabIndex={-1}
|
|
258
|
+
data-penseat-btn
|
|
259
|
+
aria-label={ariaLabel}
|
|
260
|
+
onClick={onClick}
|
|
261
|
+
style={{
|
|
262
|
+
display: 'flex',
|
|
263
|
+
alignItems: 'center',
|
|
264
|
+
justifyContent: 'center',
|
|
265
|
+
width: '32px',
|
|
266
|
+
height: '32px',
|
|
267
|
+
flexShrink: 0,
|
|
268
|
+
cursor: 'pointer',
|
|
269
|
+
transition: 'all 150ms',
|
|
270
|
+
transform: pressed ? 'scale(0.9)' : undefined,
|
|
271
|
+
color: pressed ? '#f4f4f5' : '#a1a1aa',
|
|
272
|
+
backgroundColor: pressed ? 'rgba(63,63,70,1)' : 'rgba(63,63,70,0)',
|
|
273
|
+
border: 'none',
|
|
274
|
+
outline: 'none',
|
|
275
|
+
borderRadius: '6px',
|
|
276
|
+
padding: 0,
|
|
277
|
+
}}
|
|
278
|
+
>
|
|
279
|
+
{icon}
|
|
280
|
+
</button>
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
187
284
|
function Actions({ onUndo, onClear, pressedId }: { onUndo: () => void; onClear: () => void; pressedId: string | null }) {
|
|
188
285
|
return (
|
|
189
|
-
<div
|
|
286
|
+
<div style={{
|
|
287
|
+
display: 'flex',
|
|
288
|
+
alignItems: 'center',
|
|
289
|
+
gap: '4px',
|
|
290
|
+
padding: '0 4px',
|
|
291
|
+
opacity: 1,
|
|
292
|
+
}}>
|
|
190
293
|
<WithTooltip label="Undo" shortcut={["command", "Z"]} pressed={pressedId === "undo"}>
|
|
191
|
-
<
|
|
192
|
-
|
|
193
|
-
|
|
294
|
+
<ActionButton
|
|
295
|
+
icon={Undo2Icon}
|
|
296
|
+
ariaLabel="Undo"
|
|
297
|
+
onClick={onUndo}
|
|
298
|
+
pressed={pressedId === "undo"}
|
|
299
|
+
/>
|
|
194
300
|
</WithTooltip>
|
|
195
301
|
<WithTooltip label="Clear all" shortcut={["X"]} pressed={pressedId === "clear"}>
|
|
196
|
-
<
|
|
197
|
-
|
|
198
|
-
|
|
302
|
+
<ActionButton
|
|
303
|
+
icon={Trash2Icon}
|
|
304
|
+
ariaLabel="Clear all"
|
|
305
|
+
onClick={onClear}
|
|
306
|
+
pressed={pressedId === "clear"}
|
|
307
|
+
/>
|
|
199
308
|
</WithTooltip>
|
|
200
309
|
</div>
|
|
201
310
|
);
|
|
202
311
|
}
|
|
203
312
|
|
|
204
313
|
const PEN_ICON = (
|
|
205
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
|
|
314
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" style={{ width: '20px', height: '20px' }} fill="none">
|
|
206
315
|
<path fillRule="evenodd" clipRule="evenodd" d="M20.1507 2.76256C19.4673 2.07914 18.3593 2.07915 17.6759 2.76256L16.0006 4.43796L16.0001 4.4375L14.9395 5.49816L18.9091 9.46783C19.202 9.76072 19.202 10.2356 18.9091 10.5285L16.9698 12.4678C16.6769 12.7607 16.6769 13.2356 16.9698 13.5285C17.2627 13.8214 17.7375 13.8214 18.0304 13.5285L19.9698 11.5892C20.8485 10.7105 20.8485 9.28585 19.9698 8.40717L19.5612 7.99862L21.2365 6.32322C21.9199 5.63981 21.9199 4.53177 21.2365 3.84835L20.1507 2.76256ZM17.6159 9.94413L14.0552 6.38347L8.49985 11.9392L12.0605 15.4999L17.6159 9.94413ZM3.83613 16.6032L7.43923 12.9999L10.9999 16.5605L7.39683 20.1639C7.06636 20.4943 6.65711 20.7351 6.20775 20.8635L3.20606 21.7211C2.94416 21.796 2.66229 21.7229 2.46969 21.5303C2.27709 21.3377 2.20405 21.0559 2.27888 20.794L3.1365 17.7923C3.26489 17.3429 3.50567 16.9337 3.83613 16.6032Z" fill="currentColor" />
|
|
207
316
|
</svg>
|
|
208
317
|
);
|
|
@@ -224,18 +333,37 @@ function AnchorIcon({ expanded, onToggle, onPointerDown, wasDragRef, pressed, ri
|
|
|
224
333
|
data-penseat="trigger"
|
|
225
334
|
onClick={(e) => { (e.currentTarget as HTMLElement).blur(); if (!wasDragRef.current) onToggle(); }}
|
|
226
335
|
onPointerDown={expanded ? undefined : onPointerDown}
|
|
227
|
-
className="relative flex items-center justify-center shrink-0 cursor-pointer transition-colors [&:hover]:bg-zinc-800 rounded-full"
|
|
228
336
|
style={{
|
|
337
|
+
position: 'relative',
|
|
338
|
+
display: 'flex',
|
|
339
|
+
alignItems: 'center',
|
|
340
|
+
justifyContent: 'center',
|
|
341
|
+
flexShrink: 0,
|
|
342
|
+
cursor: 'pointer',
|
|
343
|
+
backgroundColor: 'rgba(63,63,70,0)',
|
|
344
|
+
borderRadius: '9999px',
|
|
345
|
+
border: 'none',
|
|
346
|
+
outline: 'none',
|
|
347
|
+
padding: 0,
|
|
229
348
|
width: BAR_HEIGHT - 4,
|
|
230
349
|
height: BAR_HEIGHT - 4,
|
|
231
350
|
transform: pressed ? "scale(0.9)" : undefined,
|
|
232
|
-
transition: "transform 100ms",
|
|
351
|
+
transition: "transform 100ms, background-color 150ms",
|
|
233
352
|
}}
|
|
234
353
|
>
|
|
235
354
|
{/* Pen icon — visible when collapsed */}
|
|
236
355
|
<span
|
|
237
|
-
className="absolute inset-0 flex items-center justify-center text-white transition-all duration-200 ease-in-out"
|
|
238
356
|
style={{
|
|
357
|
+
position: 'absolute',
|
|
358
|
+
top: 0,
|
|
359
|
+
left: 0,
|
|
360
|
+
right: 0,
|
|
361
|
+
bottom: 0,
|
|
362
|
+
display: 'flex',
|
|
363
|
+
alignItems: 'center',
|
|
364
|
+
justifyContent: 'center',
|
|
365
|
+
color: '#ffffff',
|
|
366
|
+
transition: 'all 200ms ease-in-out',
|
|
239
367
|
opacity: expanded ? 0 : 1,
|
|
240
368
|
transform: expanded ? "scale(0.5)" : "scale(1)",
|
|
241
369
|
filter: expanded ? "blur(4px)" : "blur(0px)",
|
|
@@ -247,8 +375,17 @@ function AnchorIcon({ expanded, onToggle, onPointerDown, wasDragRef, pressed, ri
|
|
|
247
375
|
</span>
|
|
248
376
|
{/* Close icon — spins in from the toolbar direction */}
|
|
249
377
|
<span
|
|
250
|
-
className="absolute inset-0 flex items-center justify-center text-zinc-400 transition-all duration-300 ease-in-out"
|
|
251
378
|
style={{
|
|
379
|
+
position: 'absolute',
|
|
380
|
+
top: 0,
|
|
381
|
+
left: 0,
|
|
382
|
+
right: 0,
|
|
383
|
+
bottom: 0,
|
|
384
|
+
display: 'flex',
|
|
385
|
+
alignItems: 'center',
|
|
386
|
+
justifyContent: 'center',
|
|
387
|
+
color: '#a1a1aa',
|
|
388
|
+
transition: 'all 300ms ease-in-out',
|
|
252
389
|
opacity: expanded ? 1 : 0,
|
|
253
390
|
transform: expanded ? "scale(1) rotate(0deg)" : `scale(0.5) rotate(${-spinDeg}deg)`,
|
|
254
391
|
filter: expanded ? "blur(0px)" : "blur(4px)",
|
|
@@ -256,44 +393,90 @@ function AnchorIcon({ expanded, onToggle, onPointerDown, wasDragRef, pressed, ri
|
|
|
256
393
|
transitionDelay: expanded ? "0ms" : "60ms",
|
|
257
394
|
}}
|
|
258
395
|
>
|
|
259
|
-
|
|
396
|
+
{XIcon}
|
|
260
397
|
</span>
|
|
261
398
|
</button>
|
|
262
399
|
);
|
|
263
400
|
}
|
|
264
401
|
|
|
265
402
|
function DoneBtn({ onClick, disabled, capturing, copied, pressed, shake }: { onClick: () => void; disabled: boolean; capturing: boolean; copied: boolean; pressed: boolean; shake?: boolean }) {
|
|
403
|
+
const isDown = pressed && !shake;
|
|
266
404
|
const anim = copied ? "copy-bounce 600ms ease-out" : shake ? "nope-shake 320ms ease-out" : undefined;
|
|
267
405
|
return (
|
|
268
|
-
<div
|
|
269
|
-
|
|
270
|
-
|
|
406
|
+
<div style={{
|
|
407
|
+
display: 'flex',
|
|
408
|
+
alignItems: 'center',
|
|
409
|
+
alignSelf: 'stretch',
|
|
410
|
+
animation: anim,
|
|
411
|
+
}}>
|
|
412
|
+
<WithTooltip label={copied ? "Copied!" : "Copy"} shortcut={copied ? undefined : ["command", "C"]} pressed={pressed} forceShow={copied}>
|
|
413
|
+
<button
|
|
271
414
|
tabIndex={-1}
|
|
272
|
-
|
|
273
|
-
size="icon"
|
|
415
|
+
data-penseat-btn
|
|
274
416
|
onClick={onClick}
|
|
275
417
|
disabled={disabled && !copied}
|
|
276
418
|
aria-label="Done"
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
419
|
+
style={{
|
|
420
|
+
display: 'flex',
|
|
421
|
+
alignItems: 'center',
|
|
422
|
+
justifyContent: 'center',
|
|
423
|
+
width: '32px',
|
|
424
|
+
height: '32px',
|
|
425
|
+
flexShrink: 0,
|
|
426
|
+
cursor: 'pointer',
|
|
427
|
+
transition: 'all 200ms',
|
|
428
|
+
transform: isDown ? 'scale(0.9)' : undefined,
|
|
429
|
+
border: 'none',
|
|
430
|
+
outline: 'none',
|
|
431
|
+
padding: 0,
|
|
432
|
+
borderRadius: '6px',
|
|
433
|
+
backgroundColor: isDown ? 'rgba(63,63,70,1)' : 'rgba(63,63,70,0)',
|
|
434
|
+
color: isDown ? '#f4f4f5' : '#a1a1aa',
|
|
435
|
+
opacity: (disabled && !copied) ? 0.5 : 1,
|
|
436
|
+
}}
|
|
285
437
|
>
|
|
286
438
|
{capturing ? (
|
|
287
|
-
<span
|
|
439
|
+
<span style={{
|
|
440
|
+
width: '12px',
|
|
441
|
+
height: '12px',
|
|
442
|
+
borderRadius: '9999px',
|
|
443
|
+
border: '2px solid rgba(161,161,170,0.3)',
|
|
444
|
+
borderTopColor: '#a1a1aa',
|
|
445
|
+
animation: 'penseat-spin 1s linear infinite',
|
|
446
|
+
}} />
|
|
288
447
|
) : (
|
|
289
|
-
<span
|
|
290
|
-
<
|
|
291
|
-
|
|
448
|
+
<span style={{ position: 'relative', width: '16px', height: '16px' }}>
|
|
449
|
+
<span style={{
|
|
450
|
+
position: 'absolute',
|
|
451
|
+
top: 0,
|
|
452
|
+
left: 0,
|
|
453
|
+
right: 0,
|
|
454
|
+
bottom: 0,
|
|
455
|
+
display: 'flex',
|
|
456
|
+
alignItems: 'center',
|
|
457
|
+
justifyContent: 'center',
|
|
458
|
+
transition: 'all 180ms ease-out',
|
|
459
|
+
opacity: copied ? 0 : 1,
|
|
460
|
+
transform: copied ? "scale(0.5)" : "scale(1)",
|
|
461
|
+
}}>{CopyIcon}</span>
|
|
462
|
+
<span style={{
|
|
463
|
+
position: 'absolute',
|
|
464
|
+
top: 0,
|
|
465
|
+
left: 0,
|
|
466
|
+
right: 0,
|
|
467
|
+
bottom: 0,
|
|
468
|
+
display: 'flex',
|
|
469
|
+
alignItems: 'center',
|
|
470
|
+
justifyContent: 'center',
|
|
471
|
+
transition: 'all 180ms ease-out',
|
|
472
|
+
opacity: copied ? 1 : 0,
|
|
473
|
+
transform: copied ? "scale(1)" : "scale(0.5)",
|
|
474
|
+
}}>{CheckIcon}</span>
|
|
292
475
|
</span>
|
|
293
476
|
)}
|
|
294
|
-
</
|
|
477
|
+
</button>
|
|
295
478
|
</WithTooltip>
|
|
296
|
-
{(shake || copied) && (
|
|
479
|
+
{(shake || copied || capturing) && (
|
|
297
480
|
<style>{`
|
|
298
481
|
@keyframes nope-shake {
|
|
299
482
|
0% { transform: translateX(0); }
|
|
@@ -309,6 +492,20 @@ function DoneBtn({ onClick, disabled, capturing, copied, pressed, shake }: { onC
|
|
|
309
492
|
70% { transform: translateY(-2px); }
|
|
310
493
|
100% { transform: translateY(0); }
|
|
311
494
|
}
|
|
495
|
+
@keyframes penseat-spin {
|
|
496
|
+
from { transform: rotate(0deg); }
|
|
497
|
+
to { transform: rotate(360deg); }
|
|
498
|
+
}
|
|
499
|
+
[data-penseat-btn]:active {
|
|
500
|
+
transform: scale(0.9) !important;
|
|
501
|
+
}
|
|
502
|
+
[data-penseat-btn]:hover {
|
|
503
|
+
background-color: rgba(63,63,70,1) !important;
|
|
504
|
+
color: #f4f4f5 !important;
|
|
505
|
+
}
|
|
506
|
+
[data-penseat="trigger"]:hover {
|
|
507
|
+
background-color: rgba(63,63,70,1) !important;
|
|
508
|
+
}
|
|
312
509
|
`}</style>
|
|
313
510
|
)}
|
|
314
511
|
</div>
|
|
@@ -616,8 +813,17 @@ export default function PenseatBar({
|
|
|
616
813
|
{isDragging && previewTarget && dragPos && (
|
|
617
814
|
<svg
|
|
618
815
|
data-penseat="projectile"
|
|
619
|
-
|
|
620
|
-
|
|
816
|
+
style={{
|
|
817
|
+
position: 'fixed',
|
|
818
|
+
top: 0,
|
|
819
|
+
left: 0,
|
|
820
|
+
right: 0,
|
|
821
|
+
bottom: 0,
|
|
822
|
+
zIndex: 9998,
|
|
823
|
+
pointerEvents: 'none',
|
|
824
|
+
width: '100%',
|
|
825
|
+
height: '100%',
|
|
826
|
+
}}
|
|
621
827
|
>
|
|
622
828
|
<line
|
|
623
829
|
x1={dragPos.x}
|
|
@@ -643,8 +849,18 @@ export default function PenseatBar({
|
|
|
643
849
|
<div
|
|
644
850
|
data-penseat="bar"
|
|
645
851
|
onKeyDown={(e) => { if (e.key === " " || e.key === "Enter") e.preventDefault(); }}
|
|
646
|
-
className={`fixed z-[9999] flex items-center overflow-hidden rounded-full bg-zinc-800 border-[1.5px] border-white/25 shadow-[0_0_0_0.5px_rgba(255,255,255,0.06),0_1px_3px_rgba(0,0,0,0.15),inset_0_0.5px_0_rgba(255,255,255,0.06)] ${isDragging ? "opacity-80 scale-105" : ""} ${isFlying ? "scale-95" : ""}`}
|
|
647
852
|
style={{
|
|
853
|
+
position: 'fixed',
|
|
854
|
+
zIndex: 9999,
|
|
855
|
+
display: 'flex',
|
|
856
|
+
alignItems: 'center',
|
|
857
|
+
overflow: 'hidden',
|
|
858
|
+
borderRadius: '9999px',
|
|
859
|
+
backgroundColor: '#27272a',
|
|
860
|
+
border: '1.5px solid rgba(255,255,255,0.25)',
|
|
861
|
+
boxShadow: '0 0 0 0.5px rgba(255,255,255,0.06), 0 1px 3px rgba(0,0,0,0.15), inset 0 0.5px 0 rgba(255,255,255,0.06)',
|
|
862
|
+
opacity: isDragging ? 0.8 : 1,
|
|
863
|
+
transform: isDragging ? 'scale(1.05)' : isFlying ? 'scale(0.95)' : undefined,
|
|
648
864
|
...posStyle,
|
|
649
865
|
height: BAR_HEIGHT,
|
|
650
866
|
width: expanded ? contentWidth + (BAR_HEIGHT - 4) + 4 : BAR_HEIGHT,
|
|
@@ -663,11 +879,18 @@ export default function PenseatBar({
|
|
|
663
879
|
{/* Toolbar content — always in DOM, revealed/hidden by width animation */}
|
|
664
880
|
<div
|
|
665
881
|
ref={contentRef}
|
|
666
|
-
|
|
667
|
-
|
|
882
|
+
style={{
|
|
883
|
+
display: 'flex',
|
|
884
|
+
flexDirection: 'row',
|
|
885
|
+
alignItems: 'stretch',
|
|
886
|
+
alignSelf: 'stretch',
|
|
887
|
+
flexShrink: 0,
|
|
888
|
+
paddingRight: '8px',
|
|
889
|
+
pointerEvents: expanded ? 'auto' : 'none',
|
|
890
|
+
}}
|
|
668
891
|
>
|
|
669
892
|
<ColorDots colors={COLORS} active={color} onChange={onColorChange} pressedId={pressedId} />
|
|
670
|
-
<Actions onUndo={onUndo} onClear={onClear} pressedId={pressedId} />
|
|
893
|
+
<Actions onUndo={onUndo} onClear={onClear} pressedId={pressedId} />
|
|
671
894
|
<DoneBtn onClick={onDone} disabled={capturing || !!copied} capturing={capturing} copied={!!copied} pressed={pressedId === "copy"} shake={shake} />
|
|
672
895
|
</div>
|
|
673
896
|
</div>
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
3
|
-
import { Button as ButtonPrimitive } from "@base-ui/react/button"
|
|
4
|
-
import { cva, type VariantProps } from "class-variance-authority"
|
|
5
|
-
|
|
6
|
-
import { cn } from "@/lib/utils"
|
|
7
|
-
|
|
8
|
-
const buttonVariants = cva(
|
|
9
|
-
"group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
10
|
-
{
|
|
11
|
-
variants: {
|
|
12
|
-
variant: {
|
|
13
|
-
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
|
|
14
|
-
outline:
|
|
15
|
-
"border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
|
|
16
|
-
secondary:
|
|
17
|
-
"bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
|
|
18
|
-
ghost:
|
|
19
|
-
"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
|
|
20
|
-
destructive:
|
|
21
|
-
"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
|
|
22
|
-
link: "text-primary underline-offset-4 hover:underline",
|
|
23
|
-
},
|
|
24
|
-
size: {
|
|
25
|
-
default:
|
|
26
|
-
"h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
|
27
|
-
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
|
|
28
|
-
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
|
|
29
|
-
lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3",
|
|
30
|
-
icon: "size-8",
|
|
31
|
-
"icon-xs":
|
|
32
|
-
"size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
|
|
33
|
-
"icon-sm":
|
|
34
|
-
"size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
|
|
35
|
-
"icon-lg": "size-9",
|
|
36
|
-
},
|
|
37
|
-
},
|
|
38
|
-
defaultVariants: {
|
|
39
|
-
variant: "default",
|
|
40
|
-
size: "default",
|
|
41
|
-
},
|
|
42
|
-
}
|
|
43
|
-
)
|
|
44
|
-
|
|
45
|
-
function Button({
|
|
46
|
-
className,
|
|
47
|
-
variant = "default",
|
|
48
|
-
size = "default",
|
|
49
|
-
...props
|
|
50
|
-
}: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) {
|
|
51
|
-
return (
|
|
52
|
-
<ButtonPrimitive
|
|
53
|
-
data-slot="button"
|
|
54
|
-
className={cn(buttonVariants({ variant, size, className }))}
|
|
55
|
-
{...props}
|
|
56
|
-
/>
|
|
57
|
-
)
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export { Button, buttonVariants }
|