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(uiDir, { recursive: true });
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "penseat",
3
- "version": "0.1.0",
3
+ "version": "0.3.1",
4
4
  "description": "Draw on your screen, copy to clipboard. A devtool for annotating and sharing.",
5
5
  "bin": {
6
6
  "penseat": "./index.mjs"
@@ -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
- import { Undo2, Trash2, X, Copy, Check } from "lucide-react";
6
- import { Button } from "./ui/button";
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 className="inline-flex items-center justify-center size-5 rounded bg-zinc-700 border border-zinc-600 text-[13px] font-mono leading-none text-zinc-300">
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
- className="flex items-center self-stretch transition-transform duration-100"
65
- style={{ transform: pressed ? "scale(0.95)" : undefined }}
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 className={`flex items-center gap-2 rounded-lg bg-[#27272A] pl-3 py-1.5 ${shortcut && shortcut.length > 0 ? "pr-1.5" : "pr-3"}`}>
83
- <span className="text-xs font-mono text-zinc-200">{label}</span>
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 className="flex items-center gap-0.5">
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
- className="size-5 rounded-full cursor-pointer transition-transform hover:scale-110 shrink-0 flex items-center justify-center"
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" className="size-5">
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 className="flex items-center gap-2 px-3 shrink-0">
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
- <WithTooltip key={c.value} label={c.name} shortcut={[String(i + 1)]} pressed={pressedId === `color-${i + 1}`}>
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 className="flex items-center gap-1 px-1 animate-in fade-in duration-200">
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
- <Button tabIndex={-1} variant="ghost" size="icon" aria-label="Undo" className={`size-8 text-zinc-400 hover:text-zinc-100 hover:bg-zinc-800 shrink-0 cursor-pointer active:scale-[0.9] active:translate-y-0 transition-all ${pressedId === "undo" ? "bg-zinc-800 text-zinc-100" : ""}`} onClick={onUndo}>
192
- <Undo2 className="size-4" />
193
- </Button>
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
- <Button tabIndex={-1} variant="ghost" size="icon" aria-label="Clear all" className={`size-8 text-zinc-400 hover:text-zinc-100 hover:bg-zinc-800 shrink-0 cursor-pointer active:scale-[0.9] active:translate-y-0 transition-all ${pressedId === "clear" ? "bg-zinc-800 text-zinc-100" : ""}`} onClick={onClear}>
197
- <Trash2 className="size-4" />
198
- </Button>
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" className="size-5" fill="none">
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
- <X className="size-5" />
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 className="flex items-center self-stretch" style={anim ? { animation: anim } : undefined}>
269
- <WithTooltip key={copied ? "copied" : "copy"} label={copied ? "Copied!" : "Copy"} shortcut={copied ? undefined : ["command", "C"]} pressed={pressed}>
270
- <Button
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
- variant="ghost"
273
- size="icon"
415
+ data-penseat-btn
274
416
  onClick={onClick}
275
417
  disabled={disabled && !copied}
276
418
  aria-label="Done"
277
- className={`size-8 shrink-0 animate-in fade-in duration-200 cursor-pointer active:scale-[0.9] active:translate-y-0 transition-all ${
278
- copied
279
- ? "bg-blue-500 text-white hover:bg-blue-500 hover:text-white"
280
- : pressed
281
- ? "bg-zinc-800 text-zinc-100"
282
- : "text-zinc-400 hover:text-zinc-100 hover:bg-zinc-800"
283
- } disabled:opacity-50`}
284
- style={{ borderRadius: copied ? 9999 : undefined }}
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 className="size-3 animate-spin rounded-full border-2 border-zinc-400/30 border-t-zinc-400" />
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 className="relative size-4">
290
- <Copy className="size-4 absolute inset-0 transition-all duration-200" style={{ opacity: copied ? 0 : 1, transform: copied ? "scale(0.5)" : "scale(1)" }} />
291
- <Check className="size-4 absolute inset-0 transition-all duration-200" strokeWidth={3} style={{ opacity: copied ? 1 : 0, transform: copied ? "scale(1)" : "scale(0.5)" }} />
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
- </Button>
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
- className="fixed inset-0 z-[9998] pointer-events-none"
620
- style={{ width: "100%", height: "100%" }}
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
- className="flex flex-row items-stretch self-stretch shrink-0 pr-2"
667
- style={{ pointerEvents: expanded ? "auto" : "none" }}
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>
@@ -53,7 +53,7 @@ export default function Penseat() {
53
53
  setCopied(true);
54
54
  setTimeout(() => {
55
55
  setCopied(false);
56
- }, 1200);
56
+ }, 2000);
57
57
  }).catch((err) => {
58
58
  console.error("Capture failed:", err);
59
59
  triggerShake();
@@ -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 }
@@ -1,6 +0,0 @@
1
- import { clsx, type ClassValue } from "clsx"
2
- import { twMerge } from "tailwind-merge"
3
-
4
- export function cn(...inputs: ClassValue[]) {
5
- return twMerge(clsx(inputs))
6
- }