@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.
- package/dist/{chunk-NWGG5WFL.cjs → chunk-4ACELYEP.cjs} +23 -4
- package/dist/chunk-4ACELYEP.cjs.map +1 -0
- package/dist/{chunk-NSE4VW3Z.js → chunk-XRN5MNDX.js} +24 -5
- package/dist/chunk-XRN5MNDX.js.map +1 -0
- package/dist/index.cjs +409 -288
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +58 -3
- package/dist/index.d.ts +58 -3
- package/dist/index.js +124 -5
- package/dist/index.js.map +1 -1
- package/dist/{pdf-view-RRMAUK3L.cjs → pdf-view-2DSARHNQ.cjs} +7 -7
- package/dist/{pdf-view-RRMAUK3L.cjs.map → pdf-view-2DSARHNQ.cjs.map} +1 -1
- package/dist/{pdf-view-RQ2OZYHJ.js → pdf-view-2QU4FI37.js} +3 -3
- package/dist/{pdf-view-RQ2OZYHJ.js.map → pdf-view-2QU4FI37.js.map} +1 -1
- package/package.json +2 -2
- package/src/components/button/button.stories.tsx +22 -1
- package/src/components/button/button.test.tsx +45 -1
- package/src/components/button/button.tsx +32 -2
- package/src/components/data-table/data-table.test.tsx +41 -0
- package/src/components/data-table/data-table.tsx +24 -2
- package/src/components/glow-frame/glow-frame.stories.tsx +65 -0
- package/src/components/glow-frame/glow-frame.test.tsx +67 -0
- package/src/components/glow-frame/glow-frame.tsx +182 -0
- package/src/components/glow-frame/index.ts +2 -0
- package/src/index.ts +1 -0
- package/src/styles.css +216 -0
- package/dist/chunk-NSE4VW3Z.js.map +0 -1
- package/dist/chunk-NWGG5WFL.cjs.map +0 -1
|
@@ -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
|
+
}
|
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"]}
|