@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.
- 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 +811 -304
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +365 -15
- package/dist/index.d.ts +365 -15
- package/dist/index.js +537 -39
- 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/availability-calendar/availability-calendar.stories.tsx +78 -0
- package/src/components/availability-calendar/availability-calendar.test.tsx +89 -0
- package/src/components/availability-calendar/availability-calendar.tsx +182 -0
- package/src/components/availability-calendar/index.ts +5 -0
- 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/conversation-brief-card/conversation-brief-preview.tsx +176 -47
- package/src/components/conversation-brief-card/conversation-brief-review.test.tsx +58 -1
- package/src/components/data-table/data-table.test.tsx +65 -0
- package/src/components/data-table/data-table.tsx +33 -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/components/stepper/index.ts +8 -0
- package/src/components/stepper/stepper.stories.tsx +84 -0
- package/src/components/stepper/stepper.test.tsx +79 -0
- package/src/components/stepper/stepper.tsx +127 -0
- package/src/components/time-slot-list/index.ts +2 -0
- package/src/components/time-slot-list/time-slot-list.stories.tsx +100 -0
- package/src/components/time-slot-list/time-slot-list.test.tsx +120 -0
- package/src/components/time-slot-list/time-slot-list.tsx +154 -0
- package/src/components/tooltip/tooltip.tsx +4 -1
- package/src/index.ts +4 -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,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
|
-
|
|
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"]}
|