@proteos/ui 0.75.0 → 0.77.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.
@@ -0,0 +1,182 @@
1
+ import { type ReactNode, type RefObject, useEffect, useRef, useState } from 'react'
2
+ import { tv, type VariantProps } from 'tailwind-variants'
3
+
4
+ import { cn } from '../../lib/cn'
5
+
6
+ /* =========================================================================
7
+ GlowFrame — the light round a model's output. Wrap the box the output
8
+ appears in and drive `status`:
9
+
10
+ - `working` beams travel the box's edge while the model works.
11
+ - leaving `working`, the beams fade out (FADE_MS) and stop painting.
12
+ - `working` → `ready` plays one flash: a short burst of colour inside the
13
+ edge, the "here it is". Mounting straight into `ready` (a remount, a tab
14
+ switch) plays nothing, nor does `working` → `idle` (error, cancel).
15
+
16
+ Give it the box's radius (`rounded-*`) via `className`; both layers
17
+ inherit it. The frame spills 4px of fading light round the box (the
18
+ `.glow-room` in styles.css) — leave that much room before any overflow
19
+ clip, or accept that the outer spill is cut. Styles: styles.css, GlowFrame.
20
+ ========================================================================= */
21
+
22
+ /** `room` spans the 4px of fading light round the box; `frame` is the box's
23
+ * own outline, the one the beams ride and the flash fills. The light itself
24
+ * is the `.glow-*` CSS in styles.css. */
25
+ export const glowFrameStyles = tv({
26
+ slots: {
27
+ room: 'glow-room flex min-h-0 flex-col',
28
+ frame: 'glow-frame flex min-h-0 flex-1 flex-col',
29
+ },
30
+ })
31
+
32
+ export type GlowFrameVariants = VariantProps<typeof glowFrameStyles>
33
+
34
+ export type GlowFrameStatus = 'idle' | 'working' | 'ready'
35
+
36
+ export type GlowFrameProps = {
37
+ status: GlowFrameStatus
38
+ className?: string
39
+ children?: ReactNode
40
+ }
41
+
42
+ /** How long the beams take to fade in / out, in ms. */
43
+ const FADE_MS = 700
44
+ /** One flash, in ms (matches `glow-flash` in styles.css). */
45
+ const FLASH_MS = 900
46
+ /** How often the flash's colours get new places round the edge, in ms. */
47
+ const REROLL_MS = 250
48
+ const HUES = [1, 2, 3, 4, 5, 6] as const
49
+
50
+ export function GlowFrame({ status, className, children }: GlowFrameProps) {
51
+ const isWorking = status === 'working'
52
+ const { run, isResting } = useBeams(isWorking)
53
+ const flash = useFlash(status)
54
+ const strokesRef = useReroll(flash)
55
+ const styles = glowFrameStyles()
56
+
57
+ return (
58
+ <div className={cn(styles.room(), className)}>
59
+ <div
60
+ className={styles.frame()}
61
+ data-status={status}
62
+ data-working={isWorking ? 'true' : 'false'}
63
+ data-flashing={flash !== 0 ? 'true' : 'false'}
64
+ >
65
+ {children}
66
+ {isResting ? null : (
67
+ <div key={`beams-${run}`} className="glow-fx" aria-hidden="true" data-glow="beams">
68
+ <div className="glow-orb">
69
+ <div className="glow-dot glow-a" />
70
+ </div>
71
+ <div className="glow-orb">
72
+ <div className="glow-dot glow-b" />
73
+ </div>
74
+ <div className="glow-beams">
75
+ <div className="glow-blob glow-tail glow-a" />
76
+ <div className="glow-blob glow-lead glow-a" />
77
+ <div className="glow-blob glow-tail glow-b" />
78
+ <div className="glow-blob glow-lead glow-b" />
79
+ </div>
80
+ </div>
81
+ )}
82
+ {flash !== 0 ? (
83
+ <div
84
+ key={`flash-${flash}`}
85
+ ref={strokesRef}
86
+ className="glow-flash"
87
+ aria-hidden="true"
88
+ data-glow="flash"
89
+ >
90
+ <div className="glow-ring glow-ring-1" />
91
+ <div className="glow-stroke glow-stroke-2">
92
+ <div className="glow-ring glow-ring-2" />
93
+ </div>
94
+ <div className="glow-stroke glow-stroke-3">
95
+ <div className="glow-ring glow-ring-3" />
96
+ </div>
97
+ <div className="glow-stroke glow-stroke-4">
98
+ <div className="glow-ring glow-ring-4" />
99
+ </div>
100
+ </div>
101
+ ) : null}
102
+ </div>
103
+ </div>
104
+ )
105
+ }
106
+
107
+ /** The beams' lifecycle: mounted while working; once off they fade for
108
+ * FADE_MS, then `isResting` stops them painting. Each new start bumps `run`,
109
+ * which keys the layer so the entrance plays again. */
110
+ function useBeams(isWorking: boolean): { run: number; isResting: boolean } {
111
+ const [run, setRun] = useState(0)
112
+ const [isResting, setIsResting] = useState(!isWorking)
113
+ const wasWorking = useRef(isWorking)
114
+
115
+ useEffect(() => {
116
+ if (isWorking) {
117
+ if (!wasWorking.current) setRun((n) => n + 1)
118
+ wasWorking.current = true
119
+ setIsResting(false)
120
+ return
121
+ }
122
+ wasWorking.current = false
123
+ const timer = window.setTimeout(() => setIsResting(true), FADE_MS + 50)
124
+ return () => window.clearTimeout(timer)
125
+ }, [isWorking])
126
+
127
+ return { run, isResting }
128
+ }
129
+
130
+ /** The flash being played (a counter, so a second working → ready replays
131
+ * it), 0 while dark. Only a working → ready seen by this instance lights it. */
132
+ function useFlash(status: GlowFrameStatus): number {
133
+ const [flash, setFlash] = useState(0)
134
+ const previous = useRef(status)
135
+
136
+ useEffect(() => {
137
+ const was = previous.current
138
+ previous.current = status
139
+ if (!(was === 'working' && status === 'ready')) return
140
+ setFlash((n) => n + 1)
141
+ }, [status])
142
+
143
+ useEffect(() => {
144
+ if (flash === 0) return
145
+ const timer = window.setTimeout(() => setFlash(0), FLASH_MS)
146
+ return () => window.clearTimeout(timer)
147
+ }, [flash])
148
+
149
+ return flash
150
+ }
151
+
152
+ /** While a flash plays (and motion is allowed), gives the six hues new
153
+ * random places round the edge every REROLL_MS by writing the targets onto
154
+ * the rings; their CSS transitions carry each ring there at its own pace. */
155
+ function useReroll(flash: number): RefObject<HTMLDivElement | null> {
156
+ const ref = useRef<HTMLDivElement>(null)
157
+
158
+ useEffect(() => {
159
+ if (flash === 0) return
160
+ if (window.matchMedia?.('(prefers-reduced-motion: reduce)').matches) return
161
+ const reroll = () => {
162
+ const rings = ref.current?.querySelectorAll<HTMLElement>('.glow-ring')
163
+ if (!rings) return
164
+ // Random places ARE the animation: each hue somewhere round the edge,
165
+ // sorted by place they become the gradient's six stops.
166
+ const stops = HUES.map((hue) => ({ hue, at: Math.random() * 100 })).sort(
167
+ (a, b) => a.at - b.at,
168
+ )
169
+ for (const ring of rings) {
170
+ stops.forEach((stop, i) => {
171
+ ring.style.setProperty(`--glow-p${i + 1}`, `${stop.at.toFixed(2)}%`)
172
+ ring.style.setProperty(`--glow-k${i + 1}`, `var(--color-glow-${stop.hue})`)
173
+ })
174
+ }
175
+ }
176
+ reroll()
177
+ const timer = window.setInterval(reroll, REROLL_MS)
178
+ return () => window.clearInterval(timer)
179
+ }, [flash])
180
+
181
+ return ref
182
+ }
@@ -0,0 +1,2 @@
1
+ export type { GlowFrameProps, GlowFrameStatus, GlowFrameVariants } from './glow-frame'
2
+ export { GlowFrame, glowFrameStyles } from './glow-frame'
package/src/index.ts CHANGED
@@ -26,6 +26,7 @@ export * from './components/error-animation'
26
26
  export * from './components/field'
27
27
  export * from './components/file-field'
28
28
  export * from './components/file-viewer'
29
+ export * from './components/glow-frame'
29
30
  export * from './components/icon-picker'
30
31
  export * from './components/image-attachment'
31
32
  export * from './components/image-lightbox'
package/src/styles.css CHANGED
@@ -96,6 +96,20 @@
96
96
  --color-tile-slate: #1a1d2b;
97
97
  --color-tile-fg: #f0efe8;
98
98
 
99
+ /* Glow — the "model at work / output ready" light (see GlowFrame). A
100
+ deliberate decorative surface: its colours have no semantic token and
101
+ read the same in every theme. Beam: the two-colour light travelling the
102
+ edge while the model works. 1–6: the hues of the flash when the
103
+ output lands. */
104
+ --color-glow-beam-from: #3b82f6;
105
+ --color-glow-beam-to: #8b5cf6;
106
+ --color-glow-1: #bc82f3;
107
+ --color-glow-2: #f5b9ea;
108
+ --color-glow-3: #8d9fff;
109
+ --color-glow-4: #ff6778;
110
+ --color-glow-5: #ffba71;
111
+ --color-glow-6: #c686ff;
112
+
99
113
  /* Type */
100
114
  --font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
101
115
  --font-sans: "Inter", ui-sans-serif, system-ui, -apple-system, sans-serif;
@@ -219,6 +233,208 @@
219
233
  }
220
234
  }
221
235
 
236
+ /* ==========================================================
237
+ GlowFrame — the light round a model's output (glow-frame.tsx).
238
+
239
+ Two layers over the wrapped box, both clipped to its radius:
240
+
241
+ Beams (status "working"). Two light pairs travel the box's edge, half a
242
+ lap apart, one lap per 4s, each a blurred lead + tail blob riding the
243
+ border-box path (offset-path) inside a 1px ring mask, plus a blurred dot
244
+ whose glow spills past the box. `screen` blending: nothing on .glow-fx
245
+ may create a stacking context (no z-index / opacity on it), or the blend
246
+ stops reaching the box. The fade in / out goes on its children instead.
247
+
248
+ Flash (working → ready). Four strokes of ONE conic gradient laid inside
249
+ the edge, each wider and blurrier than the last. Every 250ms GlowFrame
250
+ gives the six hues new random places round the edge; the places and
251
+ hues are registered properties, so each stroke eases there at its own
252
+ pace and the four chase each other. One 900ms burst: quick rise to a
253
+ soft peak, long fade.
254
+
255
+ .glow-room spans 4px round the box and fades everything in it to nothing
256
+ at its outer edge, so spilled light never meets an overflow clip as a
257
+ hard line.
258
+ ========================================================== */
259
+
260
+ @property --glow-p1 { syntax: '<percentage>'; inherits: false; initial-value: 0%; }
261
+ @property --glow-p2 { syntax: '<percentage>'; inherits: false; initial-value: 0%; }
262
+ @property --glow-p3 { syntax: '<percentage>'; inherits: false; initial-value: 0%; }
263
+ @property --glow-p4 { syntax: '<percentage>'; inherits: false; initial-value: 0%; }
264
+ @property --glow-p5 { syntax: '<percentage>'; inherits: false; initial-value: 0%; }
265
+ @property --glow-p6 { syntax: '<percentage>'; inherits: false; initial-value: 0%; }
266
+ @property --glow-k1 { syntax: '<color>'; inherits: false; initial-value: transparent; }
267
+ @property --glow-k2 { syntax: '<color>'; inherits: false; initial-value: transparent; }
268
+ @property --glow-k3 { syntax: '<color>'; inherits: false; initial-value: transparent; }
269
+ @property --glow-k4 { syntax: '<color>'; inherits: false; initial-value: transparent; }
270
+ @property --glow-k5 { syntax: '<color>'; inherits: false; initial-value: transparent; }
271
+ @property --glow-k6 { syntax: '<color>'; inherits: false; initial-value: transparent; }
272
+
273
+ .glow-room {
274
+ --glow-room: 4px;
275
+ margin: calc(var(--glow-room) * -1);
276
+ padding: var(--glow-room);
277
+ -webkit-mask:
278
+ linear-gradient(to right, transparent, #000 var(--glow-room), #000 calc(100% - var(--glow-room)), transparent),
279
+ linear-gradient(to bottom, transparent, #000 var(--glow-room), #000 calc(100% - var(--glow-room)), transparent);
280
+ -webkit-mask-composite: source-in;
281
+ mask:
282
+ linear-gradient(to right, transparent, #000 var(--glow-room), #000 calc(100% - var(--glow-room)), transparent) intersect,
283
+ linear-gradient(to bottom, transparent, #000 var(--glow-room), #000 calc(100% - var(--glow-room)), transparent);
284
+ }
285
+ .glow-frame {
286
+ position: relative;
287
+ isolation: isolate;
288
+ border-radius: inherit;
289
+ }
290
+
291
+ /* --- Beams --------------------------------------------------- */
292
+
293
+ .glow-fx {
294
+ position: absolute;
295
+ inset: 0;
296
+ border-radius: inherit;
297
+ pointer-events: none;
298
+ }
299
+ .glow-fx > div {
300
+ transition: opacity 700ms var(--ease-out-soft);
301
+ animation: glow-in 700ms var(--ease-out-soft) backwards;
302
+ }
303
+ .glow-frame[data-working="false"] > .glow-fx > div {
304
+ opacity: 0;
305
+ }
306
+ .glow-orb {
307
+ position: absolute;
308
+ inset: -1px;
309
+ border-radius: inherit;
310
+ pointer-events: none;
311
+ mix-blend-mode: screen;
312
+ }
313
+ .glow-beams {
314
+ position: absolute;
315
+ inset: -1px;
316
+ border-radius: inherit;
317
+ pointer-events: none;
318
+ overflow: hidden;
319
+ padding: 1px;
320
+ -webkit-mask: linear-gradient(#fff 0, #fff 0) content-box exclude, linear-gradient(#fff 0, #fff 0);
321
+ mask: linear-gradient(#fff 0, #fff 0) content-box exclude, linear-gradient(#fff 0, #fff 0);
322
+ }
323
+ .glow-dot,
324
+ .glow-blob {
325
+ position: absolute;
326
+ border-radius: 50%;
327
+ offset-path: border-box;
328
+ offset-anchor: 50% 50%;
329
+ offset-rotate: 0deg;
330
+ mix-blend-mode: screen;
331
+ pointer-events: none;
332
+ animation-duration: 4s;
333
+ animation-timing-function: linear;
334
+ animation-iteration-count: infinite;
335
+ }
336
+ .glow-dot {
337
+ width: 30px;
338
+ height: 30px;
339
+ background: radial-gradient(circle, var(--color-glow-beam-from) 0%, var(--color-glow-beam-from) 30%, transparent 100%);
340
+ filter: blur(30px);
341
+ opacity: 0.7;
342
+ }
343
+ .glow-lead {
344
+ width: 83px;
345
+ height: 83px;
346
+ background: radial-gradient(circle, var(--color-glow-beam-from) 0%, var(--color-glow-beam-from) 20%, transparent 70%);
347
+ filter: blur(12px);
348
+ }
349
+ .glow-tail {
350
+ width: 83px;
351
+ height: 83px;
352
+ background: radial-gradient(circle, var(--color-glow-beam-to) 0%, var(--color-glow-beam-to) 20%, transparent 70%);
353
+ filter: blur(14.4px);
354
+ }
355
+ .glow-dot.glow-a { animation-name: glow-dot; }
356
+ .glow-dot.glow-b { animation-name: glow-dot-offset; }
357
+ .glow-lead.glow-a { animation-name: glow-trail; }
358
+ .glow-lead.glow-b { animation-name: glow-trail-offset; }
359
+ .glow-tail.glow-a { animation-name: glow-trail-tail; }
360
+ .glow-tail.glow-b { animation-name: glow-trail-offset-tail; }
361
+
362
+ @keyframes glow-in { from { opacity: 0; } }
363
+ @keyframes glow-trail { from { offset-distance: 0%; } to { offset-distance: 100%; } }
364
+ @keyframes glow-trail-offset { from { offset-distance: 50%; } to { offset-distance: 150%; } }
365
+ @keyframes glow-trail-tail { from { offset-distance: -4%; } to { offset-distance: 96%; } }
366
+ @keyframes glow-trail-offset-tail { from { offset-distance: 46%; } to { offset-distance: 146%; } }
367
+ @keyframes glow-dot { from { offset-distance: 4%; } to { offset-distance: 104%; } }
368
+ @keyframes glow-dot-offset { from { offset-distance: 54%; } to { offset-distance: 154%; } }
369
+
370
+ /* --- Flash --------------------------------------------------- */
371
+
372
+ .glow-flash {
373
+ position: absolute;
374
+ inset: 0;
375
+ z-index: 2;
376
+ overflow: clip;
377
+ border-radius: inherit;
378
+ pointer-events: none;
379
+ animation: glow-flash 900ms linear both;
380
+ }
381
+ .glow-stroke {
382
+ position: absolute;
383
+ inset: 0;
384
+ border-radius: inherit;
385
+ }
386
+ .glow-ring {
387
+ /* Evenly spaced until the first re-roll (and for good under reduced
388
+ motion). */
389
+ --glow-p1: 0%; --glow-k1: var(--color-glow-1);
390
+ --glow-p2: 16.667%; --glow-k2: var(--color-glow-2);
391
+ --glow-p3: 33.333%; --glow-k3: var(--color-glow-3);
392
+ --glow-p4: 50%; --glow-k4: var(--color-glow-4);
393
+ --glow-p5: 66.667%; --glow-k5: var(--color-glow-5);
394
+ --glow-p6: 83.333%; --glow-k6: var(--color-glow-6);
395
+ position: absolute;
396
+ inset: 0;
397
+ border-radius: inherit;
398
+ padding: var(--glow-w);
399
+ -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
400
+ -webkit-mask-composite: xor;
401
+ mask: linear-gradient(#000 0 0) content-box exclude, linear-gradient(#000 0 0);
402
+ /* Angular from 3 o'clock; first and last stops repeat one turn away so
403
+ the loop closes without a seam. */
404
+ background: conic-gradient(from 90deg,
405
+ var(--glow-k6) calc(var(--glow-p6) - 100%),
406
+ var(--glow-k1) var(--glow-p1), var(--glow-k2) var(--glow-p2), var(--glow-k3) var(--glow-p3),
407
+ var(--glow-k4) var(--glow-p4), var(--glow-k5) var(--glow-p5), var(--glow-k6) var(--glow-p6),
408
+ var(--glow-k1) calc(var(--glow-p1) + 100%));
409
+ transition-property: --glow-p1, --glow-k1, --glow-p2, --glow-k2, --glow-p3, --glow-k3,
410
+ --glow-p4, --glow-k4, --glow-p5, --glow-k5, --glow-p6, --glow-k6;
411
+ transition-duration: var(--glow-dur);
412
+ transition-timing-function: ease-in-out;
413
+ }
414
+ .glow-ring-1 { --glow-w: 3.5px; --glow-dur: 500ms; }
415
+ .glow-ring-2 { --glow-w: 5.5px; --glow-dur: 600ms; }
416
+ .glow-ring-3 { --glow-w: 6.5px; --glow-dur: 800ms; }
417
+ .glow-ring-4 { --glow-w: 9px; --glow-dur: 1000ms; }
418
+ .glow-stroke-2 { filter: blur(2.5px); }
419
+ .glow-stroke-3 { filter: blur(7px); }
420
+ .glow-stroke-4 { filter: blur(9px); }
421
+
422
+ /* Up fast to a soft peak — a hint, not a second working light — then a
423
+ long fall. */
424
+ @keyframes glow-flash {
425
+ 0% { opacity: 0; animation-timing-function: var(--ease-out-soft); }
426
+ 18% { opacity: 0.6; animation-timing-function: ease-in-out; }
427
+ 100% { opacity: 0; }
428
+ }
429
+
430
+ /* Reduced motion: the beams hold still where they start, the flash keeps
431
+ its fade but its colours stay put. */
432
+ @media (prefers-reduced-motion: reduce) {
433
+ .glow-fx > div { animation: none; }
434
+ .glow-fx * { animation-play-state: paused; }
435
+ .glow-ring { transition: none; }
436
+ }
437
+
222
438
  /* Raw variables (non-color) for direct component consumption.
223
439
  Tailwind v4 mirrors @theme into :root for colors/radii/shadows, but we
224
440
  also expose spacing tokens here for components that need raw var() access. */
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/lib/cn.ts","../src/components/button/button.tsx","../src/components/skeleton/skeleton.tsx"],"names":["AriaButton","tv","jsx"],"mappings":"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","file":"chunk-NSE4VW3Z.js","sourcesContent":["import { type ClassValue, clsx } from 'clsx'\nimport { twMerge } from 'tailwind-merge'\n\nexport function cn(...inputs: ClassValue[]): string {\n return twMerge(clsx(inputs))\n}\n","import { Button as AriaButton, type ButtonProps as AriaButtonProps } from 'react-aria-components'\nimport { tv, type VariantProps } from 'tailwind-variants'\n\nimport { cn } from '../../lib/cn'\n\n/* =========================================================================\n Button — emphasis ladder, top to bottom:\n\n cta ink fill. The one strong action per view (never two visible).\n primary raised white: `surface` fill, `rule-strong` hairline, rest\n shadow. The everyday button; the default variant.\n secondary muted: `surface-3` fill, no border. Cancel / supporting actions.\n ghost secondary's chrome, only on hover. Invisible at rest.\n subtle borderless text button (toolbars, icon buttons).\n link inline underline.\n danger solid terracotta for destructive confirms.\n\n `accent` is a frozen alias of `cta` — variant names are part of the runtime\n contract for module component bundles, so nothing is ever removed.\n\n Interaction state comes from React Aria's data attributes (`hovered:`,\n `pressed:` — see the @custom-variant block in styles.css); `focus-visible:`\n and `disabled:` stay on the native pseudo-classes because the RAC root IS\n the focusable <button>. Hover is always a colour step on the surface ramp,\n never opacity; press is translateY(1px) plus one more ramp step.\n ========================================================================= */\n\n// Ink fill with the indigo-tinted hover. Kept verbatim from the old primary.\nconst ctaClasses = [\n 'bg-ink text-surface border-ink',\n 'hovered:bg-accent-ink hovered:border-accent-ink',\n]\n\n// Raised white: a lighter inner on the darker page ground. On a white card the\n// hairline + rest shadow carry it. Press sinks one step and drops the shadow.\nconst raisedClasses = [\n 'bg-surface text-ink border-rule shadow-[var(--shadow-rest)]',\n 'hovered:bg-surface-2 hovered:border-rule-strong',\n 'pressed:bg-surface-3 pressed:shadow-none',\n 'focus-visible:shadow-[var(--shadow-focus),var(--shadow-rest)]',\n]\n\n// Muted gray fill, no border. One step quieter than primary.\nconst mutedClasses = [\n 'bg-surface-3 text-ink-2 border-transparent',\n 'hovered:bg-surface-4 hovered:text-ink',\n 'pressed:bg-[color-mix(in_oklab,var(--color-ink)_8%,var(--color-surface-4))]',\n]\n\nexport const buttonStyles = tv({\n base: [\n 'inline-flex items-center justify-center gap-2 whitespace-nowrap cursor-pointer',\n // Button labels are human copy, not machine fact — Inter 500 (DESIGN.md\n // typography.body), not the mono reserved for ids, timestamps and badges.\n 'font-sans font-medium tracking-[-0.005em] border border-transparent',\n 'transition-[background-color,color,border-color,box-shadow,transform] duration-150',\n 'outline-none pressed:translate-y-px',\n 'focus-visible:shadow-[var(--shadow-focus)]',\n 'disabled:opacity-45 disabled:cursor-not-allowed disabled:pointer-events-none',\n ],\n variants: {\n variant: {\n cta: ctaClasses,\n accent: ctaClasses,\n primary: raisedClasses,\n secondary: mutedClasses,\n ghost: [\n 'bg-transparent text-ink-2 border-transparent',\n 'hovered:bg-surface-3 hovered:text-ink',\n 'pressed:bg-surface-4',\n ],\n subtle: [\n 'bg-transparent text-ink-3 border-transparent',\n 'hovered:bg-surface-2 hovered:text-ink',\n 'pressed:bg-surface-3',\n ],\n link: [\n 'bg-transparent text-ink-2 border-transparent',\n 'px-0.5 border-b border-b-ink-4',\n 'hovered:text-ink hovered:border-b-ink',\n ],\n // `text-surface`, not white: dark's --color-danger is a light salmon, and\n // the surface token flips dark with it while white would fail contrast.\n danger: [\n 'bg-danger text-surface border-danger',\n 'hovered:bg-[color-mix(in_oklab,var(--color-danger)_88%,var(--color-ink))]',\n 'hovered:border-[color-mix(in_oklab,var(--color-danger)_88%,var(--color-ink))]',\n ],\n },\n size: {\n sm: 'text-xs px-3 py-1.5 [&_svg]:size-3',\n md: 'text-[13px] px-4 py-[9px] [&_svg]:size-3',\n lg: 'text-sm px-[22px] py-3 [&_svg]:size-3.5',\n // Square dimensions for icon-only buttons. Requires an `aria-label`.\n 'icon-sm': 'size-7 p-0 [&_svg]:size-3.5',\n 'icon-md': 'size-[34px] p-0 [&_svg]:size-4',\n 'icon-lg': 'size-[42px] p-0 [&_svg]:size-[18px]',\n },\n // Corner shape. Default is rounded (soft 10px radius — the standard\n // Proteos button), `pill` is the fully-rounded treatment for a small set\n // of prominent CTAs. The `link` variant overrides both to a flat underline.\n shape: {\n rounded: 'rounded-md',\n pill: 'rounded-pill',\n },\n },\n compoundVariants: [\n { variant: 'link', size: 'sm', class: 'px-0.5 py-0' },\n { variant: 'link', size: 'md', class: 'px-0.5 py-0' },\n { variant: 'link', size: 'lg', class: 'px-0.5 py-0' },\n // Link never takes a corner radius — it's an underline, not a box.\n { variant: 'link', shape: 'rounded', class: 'rounded-none' },\n { variant: 'link', shape: 'pill', class: 'rounded-none' },\n // Destructive confirms get the terracotta halo instead of the indigo one.\n { variant: 'danger', class: 'focus-visible:shadow-[var(--shadow-focus-danger)]' },\n ],\n defaultVariants: {\n variant: 'primary',\n size: 'md',\n shape: 'rounded',\n },\n})\n\nexport type ButtonVariants = VariantProps<typeof buttonStyles>\nexport type ButtonProps = AriaButtonProps & ButtonVariants\n\nexport function Button({ variant, size, shape, className, ...props }: ButtonProps) {\n return (\n <AriaButton\n {...props}\n className={(renderProps) =>\n cn(\n buttonStyles({ variant, size, shape }),\n typeof className === 'function' ? className(renderProps) : className,\n )\n }\n />\n )\n}\n","import type { HTMLAttributes } from 'react'\nimport { tv, type VariantProps } from 'tailwind-variants'\n\nimport { cn } from '../../lib/cn'\n\n/* =========================================================================\n Skeleton — content placeholder for loading states.\n Purely presentational, so it doesn't wrap a React Aria primitive (RAC has\n no Skeleton — interactive primitives only). Accessibility lives in props:\n - decorative skeletons get `aria-hidden` so screen readers ignore them\n - skeletons that *announce* the loading state should be wrapped in a\n `role=\"status\"` region by the consumer (e.g. AppLayoutSkeleton).\n ========================================================================= */\n\nexport const skeletonStyles = tv({\n base: ['animate-pulse', 'bg-surface-3'],\n variants: {\n shape: {\n rect: 'rounded-md',\n circle: 'rounded-full',\n // `text` matches a typical line of body text — width is up to the\n // consumer; the rounded-sm + height keep proportions sensible.\n text: 'h-4 w-full rounded-sm',\n },\n },\n defaultVariants: { shape: 'rect' },\n})\n\nexport type SkeletonVariants = VariantProps<typeof skeletonStyles>\nexport type SkeletonProps = HTMLAttributes<HTMLDivElement> & SkeletonVariants\n\nexport function Skeleton({ shape, className, ...props }: SkeletonProps) {\n return (\n <div\n // Decorative by default. Consumers that want screen readers to announce\n // the loading state should wrap a Skeleton group in `role=\"status\"`\n // and pass `aria-hidden={false}` here, or just rely on the wrapper.\n aria-hidden=\"true\"\n {...props}\n className={cn(skeletonStyles({ shape }), className)}\n />\n )\n}\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/lib/cn.ts","../src/components/button/button.tsx","../src/components/skeleton/skeleton.tsx"],"names":["twMerge","clsx","tv","jsx","AriaButton"],"mappings":"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","file":"chunk-NWGG5WFL.cjs","sourcesContent":["import { type ClassValue, clsx } from 'clsx'\nimport { twMerge } from 'tailwind-merge'\n\nexport function cn(...inputs: ClassValue[]): string {\n return twMerge(clsx(inputs))\n}\n","import { Button as AriaButton, type ButtonProps as AriaButtonProps } from 'react-aria-components'\nimport { tv, type VariantProps } from 'tailwind-variants'\n\nimport { cn } from '../../lib/cn'\n\n/* =========================================================================\n Button — emphasis ladder, top to bottom:\n\n cta ink fill. The one strong action per view (never two visible).\n primary raised white: `surface` fill, `rule-strong` hairline, rest\n shadow. The everyday button; the default variant.\n secondary muted: `surface-3` fill, no border. Cancel / supporting actions.\n ghost secondary's chrome, only on hover. Invisible at rest.\n subtle borderless text button (toolbars, icon buttons).\n link inline underline.\n danger solid terracotta for destructive confirms.\n\n `accent` is a frozen alias of `cta` — variant names are part of the runtime\n contract for module component bundles, so nothing is ever removed.\n\n Interaction state comes from React Aria's data attributes (`hovered:`,\n `pressed:` — see the @custom-variant block in styles.css); `focus-visible:`\n and `disabled:` stay on the native pseudo-classes because the RAC root IS\n the focusable <button>. Hover is always a colour step on the surface ramp,\n never opacity; press is translateY(1px) plus one more ramp step.\n ========================================================================= */\n\n// Ink fill with the indigo-tinted hover. Kept verbatim from the old primary.\nconst ctaClasses = [\n 'bg-ink text-surface border-ink',\n 'hovered:bg-accent-ink hovered:border-accent-ink',\n]\n\n// Raised white: a lighter inner on the darker page ground. On a white card the\n// hairline + rest shadow carry it. Press sinks one step and drops the shadow.\nconst raisedClasses = [\n 'bg-surface text-ink border-rule shadow-[var(--shadow-rest)]',\n 'hovered:bg-surface-2 hovered:border-rule-strong',\n 'pressed:bg-surface-3 pressed:shadow-none',\n 'focus-visible:shadow-[var(--shadow-focus),var(--shadow-rest)]',\n]\n\n// Muted gray fill, no border. One step quieter than primary.\nconst mutedClasses = [\n 'bg-surface-3 text-ink-2 border-transparent',\n 'hovered:bg-surface-4 hovered:text-ink',\n 'pressed:bg-[color-mix(in_oklab,var(--color-ink)_8%,var(--color-surface-4))]',\n]\n\nexport const buttonStyles = tv({\n base: [\n 'inline-flex items-center justify-center gap-2 whitespace-nowrap cursor-pointer',\n // Button labels are human copy, not machine fact — Inter 500 (DESIGN.md\n // typography.body), not the mono reserved for ids, timestamps and badges.\n 'font-sans font-medium tracking-[-0.005em] border border-transparent',\n 'transition-[background-color,color,border-color,box-shadow,transform] duration-150',\n 'outline-none pressed:translate-y-px',\n 'focus-visible:shadow-[var(--shadow-focus)]',\n 'disabled:opacity-45 disabled:cursor-not-allowed disabled:pointer-events-none',\n ],\n variants: {\n variant: {\n cta: ctaClasses,\n accent: ctaClasses,\n primary: raisedClasses,\n secondary: mutedClasses,\n ghost: [\n 'bg-transparent text-ink-2 border-transparent',\n 'hovered:bg-surface-3 hovered:text-ink',\n 'pressed:bg-surface-4',\n ],\n subtle: [\n 'bg-transparent text-ink-3 border-transparent',\n 'hovered:bg-surface-2 hovered:text-ink',\n 'pressed:bg-surface-3',\n ],\n link: [\n 'bg-transparent text-ink-2 border-transparent',\n 'px-0.5 border-b border-b-ink-4',\n 'hovered:text-ink hovered:border-b-ink',\n ],\n // `text-surface`, not white: dark's --color-danger is a light salmon, and\n // the surface token flips dark with it while white would fail contrast.\n danger: [\n 'bg-danger text-surface border-danger',\n 'hovered:bg-[color-mix(in_oklab,var(--color-danger)_88%,var(--color-ink))]',\n 'hovered:border-[color-mix(in_oklab,var(--color-danger)_88%,var(--color-ink))]',\n ],\n },\n size: {\n sm: 'text-xs px-3 py-1.5 [&_svg]:size-3',\n md: 'text-[13px] px-4 py-[9px] [&_svg]:size-3',\n lg: 'text-sm px-[22px] py-3 [&_svg]:size-3.5',\n // Square dimensions for icon-only buttons. Requires an `aria-label`.\n 'icon-sm': 'size-7 p-0 [&_svg]:size-3.5',\n 'icon-md': 'size-[34px] p-0 [&_svg]:size-4',\n 'icon-lg': 'size-[42px] p-0 [&_svg]:size-[18px]',\n },\n // Corner shape. Default is rounded (soft 10px radius — the standard\n // Proteos button), `pill` is the fully-rounded treatment for a small set\n // of prominent CTAs. The `link` variant overrides both to a flat underline.\n shape: {\n rounded: 'rounded-md',\n pill: 'rounded-pill',\n },\n },\n compoundVariants: [\n { variant: 'link', size: 'sm', class: 'px-0.5 py-0' },\n { variant: 'link', size: 'md', class: 'px-0.5 py-0' },\n { variant: 'link', size: 'lg', class: 'px-0.5 py-0' },\n // Link never takes a corner radius — it's an underline, not a box.\n { variant: 'link', shape: 'rounded', class: 'rounded-none' },\n { variant: 'link', shape: 'pill', class: 'rounded-none' },\n // Destructive confirms get the terracotta halo instead of the indigo one.\n { variant: 'danger', class: 'focus-visible:shadow-[var(--shadow-focus-danger)]' },\n ],\n defaultVariants: {\n variant: 'primary',\n size: 'md',\n shape: 'rounded',\n },\n})\n\nexport type ButtonVariants = VariantProps<typeof buttonStyles>\nexport type ButtonProps = AriaButtonProps & ButtonVariants\n\nexport function Button({ variant, size, shape, className, ...props }: ButtonProps) {\n return (\n <AriaButton\n {...props}\n className={(renderProps) =>\n cn(\n buttonStyles({ variant, size, shape }),\n typeof className === 'function' ? className(renderProps) : className,\n )\n }\n />\n )\n}\n","import type { HTMLAttributes } from 'react'\nimport { tv, type VariantProps } from 'tailwind-variants'\n\nimport { cn } from '../../lib/cn'\n\n/* =========================================================================\n Skeleton — content placeholder for loading states.\n Purely presentational, so it doesn't wrap a React Aria primitive (RAC has\n no Skeleton — interactive primitives only). Accessibility lives in props:\n - decorative skeletons get `aria-hidden` so screen readers ignore them\n - skeletons that *announce* the loading state should be wrapped in a\n `role=\"status\"` region by the consumer (e.g. AppLayoutSkeleton).\n ========================================================================= */\n\nexport const skeletonStyles = tv({\n base: ['animate-pulse', 'bg-surface-3'],\n variants: {\n shape: {\n rect: 'rounded-md',\n circle: 'rounded-full',\n // `text` matches a typical line of body text — width is up to the\n // consumer; the rounded-sm + height keep proportions sensible.\n text: 'h-4 w-full rounded-sm',\n },\n },\n defaultVariants: { shape: 'rect' },\n})\n\nexport type SkeletonVariants = VariantProps<typeof skeletonStyles>\nexport type SkeletonProps = HTMLAttributes<HTMLDivElement> & SkeletonVariants\n\nexport function Skeleton({ shape, className, ...props }: SkeletonProps) {\n return (\n <div\n // Decorative by default. Consumers that want screen readers to announce\n // the loading state should wrap a Skeleton group in `role=\"status\"`\n // and pass `aria-hidden={false}` here, or just rely on the wrapper.\n aria-hidden=\"true\"\n {...props}\n className={cn(skeletonStyles({ shape }), className)}\n />\n )\n}\n"]}