@proteos/ui 0.61.0 → 0.76.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.
Files changed (43) hide show
  1. package/dist/{chunk-NWGG5WFL.cjs → chunk-4ACELYEP.cjs} +23 -4
  2. package/dist/chunk-4ACELYEP.cjs.map +1 -0
  3. package/dist/{chunk-NSE4VW3Z.js → chunk-XRN5MNDX.js} +24 -5
  4. package/dist/chunk-XRN5MNDX.js.map +1 -0
  5. package/dist/index.cjs +811 -304
  6. package/dist/index.cjs.map +1 -1
  7. package/dist/index.d.cts +365 -15
  8. package/dist/index.d.ts +365 -15
  9. package/dist/index.js +537 -39
  10. package/dist/index.js.map +1 -1
  11. package/dist/{pdf-view-RRMAUK3L.cjs → pdf-view-2DSARHNQ.cjs} +7 -7
  12. package/dist/{pdf-view-RRMAUK3L.cjs.map → pdf-view-2DSARHNQ.cjs.map} +1 -1
  13. package/dist/{pdf-view-RQ2OZYHJ.js → pdf-view-2QU4FI37.js} +3 -3
  14. package/dist/{pdf-view-RQ2OZYHJ.js.map → pdf-view-2QU4FI37.js.map} +1 -1
  15. package/package.json +2 -2
  16. package/src/components/availability-calendar/availability-calendar.stories.tsx +78 -0
  17. package/src/components/availability-calendar/availability-calendar.test.tsx +89 -0
  18. package/src/components/availability-calendar/availability-calendar.tsx +182 -0
  19. package/src/components/availability-calendar/index.ts +5 -0
  20. package/src/components/button/button.stories.tsx +22 -1
  21. package/src/components/button/button.test.tsx +45 -1
  22. package/src/components/button/button.tsx +32 -2
  23. package/src/components/conversation-brief-card/conversation-brief-preview.tsx +176 -47
  24. package/src/components/conversation-brief-card/conversation-brief-review.test.tsx +58 -1
  25. package/src/components/data-table/data-table.test.tsx +65 -0
  26. package/src/components/data-table/data-table.tsx +33 -2
  27. package/src/components/glow-frame/glow-frame.stories.tsx +65 -0
  28. package/src/components/glow-frame/glow-frame.test.tsx +67 -0
  29. package/src/components/glow-frame/glow-frame.tsx +182 -0
  30. package/src/components/glow-frame/index.ts +2 -0
  31. package/src/components/stepper/index.ts +8 -0
  32. package/src/components/stepper/stepper.stories.tsx +84 -0
  33. package/src/components/stepper/stepper.test.tsx +79 -0
  34. package/src/components/stepper/stepper.tsx +127 -0
  35. package/src/components/time-slot-list/index.ts +2 -0
  36. package/src/components/time-slot-list/time-slot-list.stories.tsx +100 -0
  37. package/src/components/time-slot-list/time-slot-list.test.tsx +120 -0
  38. package/src/components/time-slot-list/time-slot-list.tsx +154 -0
  39. package/src/components/tooltip/tooltip.tsx +4 -1
  40. package/src/index.ts +4 -0
  41. package/src/styles.css +216 -0
  42. package/dist/chunk-NSE4VW3Z.js.map +0 -1
  43. package/dist/chunk-NWGG5WFL.cjs.map +0 -1
@@ -0,0 +1,120 @@
1
+ import { render, screen } from '@testing-library/react'
2
+ import userEvent from '@testing-library/user-event'
3
+ import { useState } from 'react'
4
+ import { describe, expect, it, vi } from 'vitest'
5
+
6
+ import { TimeSlotList } from './time-slot-list'
7
+
8
+ const TIMES = ['09:00', '09:30', '10:00'].map((label) => ({ id: label, label }))
9
+
10
+ function Harness({
11
+ initial = null,
12
+ onSelect,
13
+ onConfirm,
14
+ confirmLabel,
15
+ items = TIMES,
16
+ }: {
17
+ initial?: string | null
18
+ onSelect?: (id: string) => void
19
+ onConfirm?: (id: string) => void
20
+ confirmLabel?: string
21
+ items?: ReadonlyArray<{ id: string; label: string; isDisabled?: boolean }>
22
+ }) {
23
+ const [selected, setSelected] = useState<string | null>(initial)
24
+ return (
25
+ <TimeSlotList
26
+ aria-label="Times"
27
+ items={items}
28
+ selectedId={selected}
29
+ onSelect={(id) => {
30
+ setSelected(id)
31
+ onSelect?.(id)
32
+ }}
33
+ {...(confirmLabel ? { confirmLabel } : {})}
34
+ {...(onConfirm ? { onConfirm } : {})}
35
+ />
36
+ )
37
+ }
38
+
39
+ describe('TimeSlotList', () => {
40
+ it('renders one exclusive option per item', () => {
41
+ render(<Harness />)
42
+ expect(screen.getByRole('radiogroup', { name: 'Times' })).toBeInTheDocument()
43
+ expect(screen.getAllByRole('radio')).toHaveLength(3)
44
+ })
45
+
46
+ it('selects on click and reports the id', async () => {
47
+ const user = userEvent.setup()
48
+ const onSelect = vi.fn()
49
+ render(<Harness onSelect={onSelect} />)
50
+ await user.click(screen.getByRole('radio', { name: '09:30' }))
51
+ expect(onSelect).toHaveBeenLastCalledWith('09:30')
52
+ expect(screen.getByRole('radio', { name: '09:30' })).toBeChecked()
53
+ })
54
+
55
+ it('keeps the selection when the selected item is clicked again', async () => {
56
+ const user = userEvent.setup()
57
+ const onSelect = vi.fn()
58
+ render(<Harness initial="09:00" onSelect={onSelect} />)
59
+ await user.click(screen.getByRole('radio', { name: '09:00' }))
60
+ expect(screen.getByRole('radio', { name: '09:00' })).toBeChecked()
61
+ })
62
+
63
+ it('does not select a disabled item', async () => {
64
+ const user = userEvent.setup()
65
+ const onSelect = vi.fn()
66
+ render(
67
+ <Harness
68
+ onSelect={onSelect}
69
+ items={[{ id: '09:00', label: '09:00', isDisabled: true }, ...TIMES.slice(1)]}
70
+ />,
71
+ )
72
+ await user.click(screen.getByRole('radio', { name: '09:00' }))
73
+ expect(onSelect).not.toHaveBeenCalled()
74
+ })
75
+
76
+ it('shows the inline confirm beside the selected item only', async () => {
77
+ const user = userEvent.setup()
78
+ const onConfirm = vi.fn()
79
+ render(<Harness confirmLabel="Confirm" onConfirm={onConfirm} />)
80
+ expect(screen.queryByRole('button', { name: 'Confirm' })).not.toBeInTheDocument()
81
+ await user.click(screen.getByRole('radio', { name: '10:00' }))
82
+ await user.click(screen.getByRole('button', { name: 'Confirm' }))
83
+ expect(onConfirm).toHaveBeenCalledWith('10:00')
84
+ })
85
+
86
+ it('freezes every pick and the confirm while disabled', async () => {
87
+ const user = userEvent.setup()
88
+ const onSelect = vi.fn()
89
+ const onConfirm = vi.fn()
90
+ render(
91
+ <TimeSlotList
92
+ aria-label="Times"
93
+ items={TIMES}
94
+ selectedId="09:00"
95
+ onSelect={onSelect}
96
+ confirmLabel="Confirm"
97
+ onConfirm={onConfirm}
98
+ isDisabled
99
+ />,
100
+ )
101
+ await user.click(screen.getByRole('radio', { name: '09:30' }))
102
+ await user.click(screen.getByRole('button', { name: 'Confirm' }))
103
+ expect(onSelect).not.toHaveBeenCalled()
104
+ expect(onConfirm).not.toHaveBeenCalled()
105
+ })
106
+
107
+ it('renders the empty message when there are no items', () => {
108
+ render(
109
+ <TimeSlotList
110
+ aria-label="Times"
111
+ items={[]}
112
+ selectedId={null}
113
+ onSelect={() => {}}
114
+ emptyMessage="All booked today"
115
+ />,
116
+ )
117
+ expect(screen.getByRole('status')).toHaveTextContent('All booked today')
118
+ expect(screen.queryByRole('radiogroup')).not.toBeInTheDocument()
119
+ })
120
+ })
@@ -0,0 +1,154 @@
1
+ import type { ReactNode } from 'react'
2
+ import {
3
+ ToggleButton as AriaToggleButton,
4
+ ToggleButtonGroup as AriaToggleButtonGroup,
5
+ type ToggleButtonGroupProps as AriaToggleButtonGroupProps,
6
+ type Key,
7
+ } from 'react-aria-components'
8
+ import { tv, type VariantProps } from 'tailwind-variants'
9
+
10
+ import { cn } from '../../lib/cn'
11
+ import { Button } from '../button/button'
12
+
13
+ /* =========================================================================
14
+ TimeSlotList — a column of exclusive picks: free times on a day, shifts,
15
+ delivery windows. Each item is a full-width outlined button; the chosen
16
+ one carries an ink ring and may grow an inline confirm button beside it
17
+ (the "pick and go" register). Wraps RAC ToggleButtonGroup in single
18
+ selection so the group is one radiogroup with arrow-key movement and one
19
+ tab stop — and so a confirm button can sit beside an item without nesting
20
+ interactive content inside an option.
21
+
22
+ Labels are plain strings: formatting the time (12h / 24h, zone) is the
23
+ consumer's job; the list only lays the picks out.
24
+ ========================================================================= */
25
+
26
+ export const timeSlotListStyles = tv({
27
+ slots: {
28
+ group: 'flex flex-col gap-2',
29
+ row: 'flex gap-2 min-w-0',
30
+ item: [
31
+ 'flex-1 min-h-9 px-3 inline-flex items-center justify-center cursor-pointer outline-none',
32
+ 'font-sans text-[13.5px] font-medium text-ink rounded-md',
33
+ 'bg-surface border border-rule-strong',
34
+ 'transition-[background-color,color,border-color,box-shadow] duration-150',
35
+ 'hovered:bg-surface-2 hovered:border-ink-3',
36
+ 'pressed:bg-surface-3',
37
+ 'selected:border-ink selected:bg-surface selected:shadow-[inset_0_0_0_1px_var(--color-ink)]',
38
+ 'selected:hovered:bg-surface',
39
+ 'data-[focus-visible]:shadow-[var(--shadow-focus)]',
40
+ 'selected:data-[focus-visible]:shadow-[var(--shadow-focus),inset_0_0_0_1px_var(--color-ink)]',
41
+ // Taken while you looked: dashed and quiet.
42
+ 'data-[disabled]:border-dashed data-[disabled]:border-rule data-[disabled]:text-ink-4',
43
+ 'data-[disabled]:cursor-default data-[disabled]:bg-transparent',
44
+ ],
45
+ confirm: 'flex-1 min-h-9',
46
+ empty: [
47
+ 'rounded-md border border-rule-2 bg-surface-2 px-4 py-5 text-center',
48
+ 'font-sans text-[13px] text-ink-3',
49
+ ],
50
+ },
51
+ variants: {
52
+ columns: {
53
+ 1: { group: 'grid grid-cols-1' },
54
+ 2: { group: 'grid grid-cols-2' },
55
+ },
56
+ },
57
+ defaultVariants: { columns: 1 },
58
+ })
59
+
60
+ export type TimeSlotListVariants = VariantProps<typeof timeSlotListStyles>
61
+
62
+ export type TimeSlotListItem<T extends Key = Key> = {
63
+ id: T
64
+ label: ReactNode
65
+ /** No longer offered (taken, outside the window): rendered dashed, not pickable. */
66
+ isDisabled?: boolean
67
+ }
68
+
69
+ export type TimeSlotListProps<T extends Key = Key> = Omit<
70
+ AriaToggleButtonGroupProps,
71
+ 'children' | 'selectionMode' | 'selectedKeys' | 'onSelectionChange' | 'className' | 'style'
72
+ > & {
73
+ items: ReadonlyArray<TimeSlotListItem<T>>
74
+ /** `null` = nothing picked. */
75
+ selectedId: T | null
76
+ onSelect: (id: T) => void
77
+ /** Renders a cta beside the selected item; pressing it calls `onConfirm`. */
78
+ confirmLabel?: string
79
+ onConfirm?: (id: T) => void
80
+ /** Shown instead of the list when `items` is empty. */
81
+ emptyMessage?: ReactNode
82
+ columns?: TimeSlotListVariants['columns']
83
+ /** Focus the selected item (or the first) on mount. */
84
+ autoFocus?: boolean
85
+ /** Freeze every pick and the confirm while a request is in flight. */
86
+ isDisabled?: boolean
87
+ className?: string
88
+ }
89
+
90
+ export function TimeSlotList<T extends Key>({
91
+ items,
92
+ selectedId,
93
+ onSelect,
94
+ confirmLabel,
95
+ onConfirm,
96
+ emptyMessage = 'Nothing available.',
97
+ columns,
98
+ autoFocus = false,
99
+ isDisabled = false,
100
+ className,
101
+ orientation = 'vertical',
102
+ ...props
103
+ }: TimeSlotListProps<T>) {
104
+ const styles = timeSlotListStyles({ columns })
105
+ if (items.length === 0) {
106
+ return (
107
+ <div role="status" className={cn(styles.empty(), className)}>
108
+ {emptyMessage}
109
+ </div>
110
+ )
111
+ }
112
+ const focusTarget = selectedId ?? items.find((item) => !item.isDisabled)?.id ?? null
113
+ return (
114
+ <AriaToggleButtonGroup
115
+ {...props}
116
+ orientation={orientation}
117
+ selectionMode="single"
118
+ disallowEmptySelection
119
+ isDisabled={isDisabled}
120
+ selectedKeys={selectedId === null ? new Set() : new Set([selectedId])}
121
+ onSelectionChange={(keys) => {
122
+ const [next] = Array.from(keys)
123
+ if (next !== undefined) onSelect(next as T)
124
+ }}
125
+ className={cn(styles.group(), className)}
126
+ >
127
+ {items.map((item) => {
128
+ const isSelected = item.id === selectedId
129
+ return (
130
+ <div key={String(item.id)} className={styles.row()}>
131
+ <AriaToggleButton
132
+ id={item.id}
133
+ {...(item.isDisabled ? { isDisabled: true } : {})}
134
+ {...(autoFocus && item.id === focusTarget ? { autoFocus: true } : {})}
135
+ className={styles.item()}
136
+ >
137
+ {item.label}
138
+ </AriaToggleButton>
139
+ {isSelected && confirmLabel && (
140
+ <Button
141
+ variant="cta"
142
+ className={styles.confirm()}
143
+ isDisabled={isDisabled}
144
+ onPress={() => onConfirm?.(item.id)}
145
+ >
146
+ {confirmLabel}
147
+ </Button>
148
+ )}
149
+ </div>
150
+ )
151
+ })}
152
+ </AriaToggleButtonGroup>
153
+ )
154
+ }
@@ -42,7 +42,10 @@ export function Tooltip({ className, offset = 6, ...props }: TooltipProps) {
42
42
  offset={offset}
43
43
  className={(renderProps) =>
44
44
  cn(
45
- 'z-50 max-w-xs',
45
+ // Above the floating chat / softphone windows (1100 / 1200) and their
46
+ // full-screen workspace, like Select and Toast — a page inside the
47
+ // window keeps its tooltips.
48
+ 'z-[1300] max-w-xs',
46
49
  'rounded-md border border-rule bg-surface shadow-[var(--shadow-raised)]',
47
50
  'px-2.5 py-1.5',
48
51
  'font-mono text-[11.5px] text-ink-2',
package/src/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from './components/attachment-chip'
2
2
  export * from './components/attribute-type-icon'
3
+ export * from './components/availability-calendar'
3
4
  export * from './components/avatar'
4
5
  export * from './components/avatar-group'
5
6
  export * from './components/badge'
@@ -25,6 +26,7 @@ export * from './components/error-animation'
25
26
  export * from './components/field'
26
27
  export * from './components/file-field'
27
28
  export * from './components/file-viewer'
29
+ export * from './components/glow-frame'
28
30
  export * from './components/icon-picker'
29
31
  export * from './components/image-attachment'
30
32
  export * from './components/image-lightbox'
@@ -47,6 +49,7 @@ export * from './components/segmented-control'
47
49
  export * from './components/select'
48
50
  export * from './components/separator'
49
51
  export * from './components/skeleton'
52
+ export * from './components/stepper'
50
53
  export * from './components/success-animation'
51
54
  export * from './components/switch'
52
55
  export * from './components/table'
@@ -55,6 +58,7 @@ export * from './components/tag-input'
55
58
  export * from './components/text-shimmer'
56
59
  export * from './components/textarea'
57
60
  export * from './components/time-field'
61
+ export * from './components/time-slot-list'
58
62
  export * from './components/toast'
59
63
  export * from './components/tooltip'
60
64
 
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"]}