@proteos/ui 0.31.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/LICENSE +105 -0
- package/dist/chunk-IKAX76MB.js +113 -0
- package/dist/chunk-IKAX76MB.js.map +1 -0
- package/dist/chunk-MWAXK66H.cjs +119 -0
- package/dist/chunk-MWAXK66H.cjs.map +1 -0
- package/dist/index.cjs +6246 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +2126 -0
- package/dist/index.d.ts +2126 -0
- package/dist/index.js +6069 -0
- package/dist/index.js.map +1 -0
- package/dist/pdf-view-3ZMMS6ML.cjs +95 -0
- package/dist/pdf-view-3ZMMS6ML.cjs.map +1 -0
- package/dist/pdf-view-BV5ZD6OC.js +93 -0
- package/dist/pdf-view-BV5ZD6OC.js.map +1 -0
- package/package.json +96 -0
- package/src/components/attachment-chip/attachment-chip.tsx +129 -0
- package/src/components/attachment-chip/index.ts +2 -0
- package/src/components/attribute-type-icon/attribute-type-icon.stories.tsx +70 -0
- package/src/components/attribute-type-icon/attribute-type-icon.tsx +125 -0
- package/src/components/attribute-type-icon/index.ts +8 -0
- package/src/components/avatar/avatar.stories.tsx +42 -0
- package/src/components/avatar/avatar.tsx +48 -0
- package/src/components/avatar/index.ts +2 -0
- package/src/components/avatar-group/avatar-group.test.tsx +38 -0
- package/src/components/avatar-group/avatar-group.tsx +60 -0
- package/src/components/avatar-group/index.ts +2 -0
- package/src/components/badge/badge.stories.tsx +62 -0
- package/src/components/badge/badge.test.tsx +36 -0
- package/src/components/badge/badge.tsx +54 -0
- package/src/components/badge/index.ts +2 -0
- package/src/components/badge-multi-select/badge-multi-select.stories.tsx +177 -0
- package/src/components/badge-multi-select/badge-multi-select.test.tsx +155 -0
- package/src/components/badge-multi-select/badge-multi-select.tsx +272 -0
- package/src/components/badge-multi-select/index.ts +2 -0
- package/src/components/breadcrumbs/breadcrumbs.stories.tsx +43 -0
- package/src/components/breadcrumbs/breadcrumbs.tsx +86 -0
- package/src/components/breadcrumbs/index.ts +2 -0
- package/src/components/button/button.stories.tsx +290 -0
- package/src/components/button/button.test.tsx +59 -0
- package/src/components/button/button.tsx +86 -0
- package/src/components/button/index.ts +2 -0
- package/src/components/card/card.stories.tsx +351 -0
- package/src/components/card/card.test.tsx +141 -0
- package/src/components/card/card.tsx +173 -0
- package/src/components/card/index.ts +13 -0
- package/src/components/checkbox/checkbox-group.tsx +52 -0
- package/src/components/checkbox/checkbox.stories.tsx +48 -0
- package/src/components/checkbox/checkbox.test.tsx +50 -0
- package/src/components/checkbox/checkbox.tsx +71 -0
- package/src/components/checkbox/index.ts +4 -0
- package/src/components/combobox/combobox.stories.tsx +97 -0
- package/src/components/combobox/combobox.test.tsx +123 -0
- package/src/components/combobox/combobox.tsx +239 -0
- package/src/components/combobox/index.ts +2 -0
- package/src/components/command/command-hint.tsx +46 -0
- package/src/components/command/command.stories.tsx +88 -0
- package/src/components/command/command.test.tsx +102 -0
- package/src/components/command/command.tsx +76 -0
- package/src/components/command/index.ts +16 -0
- package/src/components/currency-input/currency-input.tsx +192 -0
- package/src/components/currency-input/index.ts +6 -0
- package/src/components/data-table/cells.stories.tsx +120 -0
- package/src/components/data-table/cells.tsx +165 -0
- package/src/components/data-table/data-table.stories.tsx +371 -0
- package/src/components/data-table/data-table.tsx +556 -0
- package/src/components/data-table/index.ts +22 -0
- package/src/components/data-table/make-actions-column.ts +30 -0
- package/src/components/data-table/states/data-table-empty.tsx +40 -0
- package/src/components/data-table/states/data-table-error.tsx +30 -0
- package/src/components/data-table/states/data-table-skeleton.tsx +42 -0
- package/src/components/data-table/use-infinite-scroll-sentinel.ts +51 -0
- package/src/components/date-picker/calendar-internals.tsx +138 -0
- package/src/components/date-picker/date-picker.stories.tsx +78 -0
- package/src/components/date-picker/date-picker.test.tsx +71 -0
- package/src/components/date-picker/date-picker.tsx +140 -0
- package/src/components/date-picker/index.ts +2 -0
- package/src/components/date-time-picker/date-time-picker.stories.tsx +96 -0
- package/src/components/date-time-picker/date-time-picker.test.tsx +87 -0
- package/src/components/date-time-picker/date-time-picker.tsx +203 -0
- package/src/components/date-time-picker/index.ts +2 -0
- package/src/components/empty-state/empty-state.tsx +63 -0
- package/src/components/empty-state/index.ts +2 -0
- package/src/components/error-animation/error-animation.stories.tsx +50 -0
- package/src/components/error-animation/error-animation.test.tsx +22 -0
- package/src/components/error-animation/error-animation.tsx +26 -0
- package/src/components/error-animation/index.ts +2 -0
- package/src/components/field/field.tsx +142 -0
- package/src/components/field/index.ts +8 -0
- package/src/components/file-field/file-field.tsx +285 -0
- package/src/components/file-field/index.ts +2 -0
- package/src/components/file-viewer/fallback-card.tsx +68 -0
- package/src/components/file-viewer/file-viewer.stories.tsx +67 -0
- package/src/components/file-viewer/file-viewer.test.tsx +81 -0
- package/src/components/file-viewer/file-viewer.tsx +116 -0
- package/src/components/file-viewer/image-view.tsx +38 -0
- package/src/components/file-viewer/index.ts +7 -0
- package/src/components/file-viewer/pdf-view.tsx +122 -0
- package/src/components/file-viewer/video-view.tsx +16 -0
- package/src/components/image-attachment/image-attachment.stories.tsx +39 -0
- package/src/components/image-attachment/image-attachment.test.tsx +54 -0
- package/src/components/image-attachment/image-attachment.tsx +77 -0
- package/src/components/image-attachment/index.ts +3 -0
- package/src/components/image-attachment/is-previewable-image-name.ts +14 -0
- package/src/components/image-lightbox/image-lightbox.stories.tsx +38 -0
- package/src/components/image-lightbox/image-lightbox.test.tsx +72 -0
- package/src/components/image-lightbox/image-lightbox.tsx +92 -0
- package/src/components/image-lightbox/index.ts +2 -0
- package/src/components/input/index.ts +2 -0
- package/src/components/input/input.stories.tsx +71 -0
- package/src/components/input/input.test.tsx +36 -0
- package/src/components/input/input.tsx +76 -0
- package/src/components/kbd/index.ts +2 -0
- package/src/components/kbd/kbd.stories.tsx +34 -0
- package/src/components/kbd/kbd.tsx +27 -0
- package/src/components/menu/index.ts +2 -0
- package/src/components/menu/menu.tsx +121 -0
- package/src/components/modal/index.ts +10 -0
- package/src/components/modal/modal.stories.tsx +153 -0
- package/src/components/modal/modal.test.tsx +93 -0
- package/src/components/modal/modal.tsx +149 -0
- package/src/components/multi-select/index.ts +2 -0
- package/src/components/multi-select/multi-select.stories.tsx +211 -0
- package/src/components/multi-select/multi-select.test.tsx +151 -0
- package/src/components/multi-select/multi-select.tsx +339 -0
- package/src/components/nav/index.ts +7 -0
- package/src/components/nav/nav.stories.tsx +139 -0
- package/src/components/nav/nav.tsx +246 -0
- package/src/components/popover/index.ts +8 -0
- package/src/components/popover/popover.stories.tsx +92 -0
- package/src/components/popover/popover.tsx +135 -0
- package/src/components/progress-bar/index.ts +2 -0
- package/src/components/progress-bar/progress-bar.stories.tsx +75 -0
- package/src/components/progress-bar/progress-bar.test.tsx +29 -0
- package/src/components/progress-bar/progress-bar.tsx +57 -0
- package/src/components/radio-group/index.ts +2 -0
- package/src/components/radio-group/radio-group.stories.tsx +113 -0
- package/src/components/radio-group/radio-group.test.tsx +87 -0
- package/src/components/radio-group/radio-group.tsx +104 -0
- package/src/components/record-filter/attribute-picker.tsx +74 -0
- package/src/components/record-filter/chip-value.tsx +171 -0
- package/src/components/record-filter/filter-row.tsx +262 -0
- package/src/components/record-filter/filter-value-input.tsx +351 -0
- package/src/components/record-filter/group-add-controls.tsx +77 -0
- package/src/components/record-filter/group-body.tsx +173 -0
- package/src/components/record-filter/group-mutations.test.ts +104 -0
- package/src/components/record-filter/group-mutations.ts +116 -0
- package/src/components/record-filter/index.ts +58 -0
- package/src/components/record-filter/nested-group.tsx +138 -0
- package/src/components/record-filter/operator-picker.tsx +62 -0
- package/src/components/record-filter/operators.test.ts +105 -0
- package/src/components/record-filter/operators.ts +97 -0
- package/src/components/record-filter/record-filter-builder-panel.tsx +236 -0
- package/src/components/record-filter/record-filter-button.tsx +126 -0
- package/src/components/record-filter/record-filter-chips.tsx +527 -0
- package/src/components/record-filter/record-filter-editor.tsx +91 -0
- package/src/components/record-filter/record-filter.stories.tsx +196 -0
- package/src/components/record-filter/record-filter.test.tsx +437 -0
- package/src/components/record-filter/show-details-store.ts +72 -0
- package/src/components/record-filter/types.ts +150 -0
- package/src/components/segmented-control/index.ts +6 -0
- package/src/components/segmented-control/segmented-control.stories.tsx +67 -0
- package/src/components/segmented-control/segmented-control.test.tsx +47 -0
- package/src/components/segmented-control/segmented-control.tsx +105 -0
- package/src/components/select/index.ts +2 -0
- package/src/components/select/select.stories.tsx +76 -0
- package/src/components/select/select.test.tsx +56 -0
- package/src/components/select/select.tsx +159 -0
- package/src/components/separator/index.ts +2 -0
- package/src/components/separator/separator.stories.tsx +73 -0
- package/src/components/separator/separator.test.tsx +29 -0
- package/src/components/separator/separator.tsx +51 -0
- package/src/components/skeleton/index.ts +2 -0
- package/src/components/skeleton/skeleton.stories.tsx +68 -0
- package/src/components/skeleton/skeleton.test.tsx +38 -0
- package/src/components/skeleton/skeleton.tsx +43 -0
- package/src/components/success-animation/index.ts +2 -0
- package/src/components/success-animation/success-animation.stories.tsx +50 -0
- package/src/components/success-animation/success-animation.test.tsx +22 -0
- package/src/components/success-animation/success-animation.tsx +26 -0
- package/src/components/switch/index.ts +2 -0
- package/src/components/switch/switch.stories.tsx +60 -0
- package/src/components/switch/switch.test.tsx +50 -0
- package/src/components/switch/switch.tsx +49 -0
- package/src/components/table/index.ts +25 -0
- package/src/components/table/table.stories.tsx +306 -0
- package/src/components/table/table.test.tsx +115 -0
- package/src/components/table/table.tsx +543 -0
- package/src/components/tabs/index.ts +8 -0
- package/src/components/tabs/tabs.tsx +141 -0
- package/src/components/tag-input/index.ts +2 -0
- package/src/components/tag-input/tag-input.stories.tsx +73 -0
- package/src/components/tag-input/tag-input.test.tsx +80 -0
- package/src/components/tag-input/tag-input.tsx +193 -0
- package/src/components/textarea/index.ts +2 -0
- package/src/components/textarea/textarea.stories.tsx +75 -0
- package/src/components/textarea/textarea.test.tsx +41 -0
- package/src/components/textarea/textarea.tsx +86 -0
- package/src/components/time-field/index.ts +2 -0
- package/src/components/time-field/time-field.stories.tsx +76 -0
- package/src/components/time-field/time-field.test.tsx +51 -0
- package/src/components/time-field/time-field.tsx +98 -0
- package/src/components/tooltip/index.ts +2 -0
- package/src/components/tooltip/tooltip.stories.tsx +176 -0
- package/src/components/tooltip/tooltip.test.tsx +52 -0
- package/src/components/tooltip/tooltip.tsx +56 -0
- package/src/index.ts +47 -0
- package/src/lib/cn.ts +6 -0
- package/src/lib/commands/discovery-store.test.tsx +76 -0
- package/src/lib/commands/discovery-store.ts +110 -0
- package/src/lib/commands/index.ts +9 -0
- package/src/lib/commands/match-event.test.ts +99 -0
- package/src/lib/commands/match-event.ts +31 -0
- package/src/lib/commands/parse-combo.test.ts +65 -0
- package/src/lib/commands/parse-combo.ts +48 -0
- package/src/lib/commands/platform.ts +27 -0
- package/src/lib/commands/protected-combos.test.ts +18 -0
- package/src/lib/commands/protected-combos.ts +38 -0
- package/src/lib/commands/registry.test.ts +97 -0
- package/src/lib/commands/registry.ts +59 -0
- package/src/lib/commands/use-command.test.tsx +91 -0
- package/src/lib/commands/use-command.ts +32 -0
- package/src/lib/commands/use-escape-key.test.tsx +97 -0
- package/src/lib/commands/use-escape-key.ts +62 -0
- package/src/styles.css +321 -0
- package/src/test/setup.ts +9 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
|
|
3
|
+
import { Separator } from './separator'
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof Separator> = {
|
|
6
|
+
title: 'Components/Separator',
|
|
7
|
+
component: Separator,
|
|
8
|
+
tags: ['autodocs'],
|
|
9
|
+
argTypes: {
|
|
10
|
+
orientation: {
|
|
11
|
+
control: { type: 'inline-radio' },
|
|
12
|
+
options: ['horizontal', 'vertical'],
|
|
13
|
+
},
|
|
14
|
+
variant: {
|
|
15
|
+
control: { type: 'inline-radio' },
|
|
16
|
+
options: ['dashed', 'solid'],
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default meta
|
|
22
|
+
type Story = StoryObj<typeof Separator>
|
|
23
|
+
|
|
24
|
+
export const Dashed: Story = {
|
|
25
|
+
args: { variant: 'dashed' },
|
|
26
|
+
render: (args) => (
|
|
27
|
+
<div className="w-[360px] flex flex-col gap-3 text-[14px] text-ink-2">
|
|
28
|
+
<div>Records, tables, dashboards.</div>
|
|
29
|
+
<Separator {...args} />
|
|
30
|
+
<div className="font-mono text-[11.5px] text-ink-4">1,284 open · updated just now</div>
|
|
31
|
+
</div>
|
|
32
|
+
),
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const Solid: Story = {
|
|
36
|
+
args: { variant: 'solid' },
|
|
37
|
+
render: (args) => (
|
|
38
|
+
<div className="w-[360px] flex flex-col gap-3 text-[14px] text-ink-2">
|
|
39
|
+
<div>Records, tables, dashboards.</div>
|
|
40
|
+
<Separator {...args} />
|
|
41
|
+
<div className="font-mono text-[11.5px] text-ink-4">1,284 open · updated just now</div>
|
|
42
|
+
</div>
|
|
43
|
+
),
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const Variants: Story = {
|
|
47
|
+
parameters: { controls: { hideNoControlsWarning: true } },
|
|
48
|
+
render: () => (
|
|
49
|
+
<div className="w-[360px] grid gap-6">
|
|
50
|
+
<div className="grid gap-3">
|
|
51
|
+
<div className="font-mono text-[11px] uppercase tracking-[0.08em] text-ink-4">Dashed</div>
|
|
52
|
+
<Separator variant="dashed" />
|
|
53
|
+
</div>
|
|
54
|
+
<div className="grid gap-3">
|
|
55
|
+
<div className="font-mono text-[11px] uppercase tracking-[0.08em] text-ink-4">Solid</div>
|
|
56
|
+
<Separator variant="solid" />
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
),
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const Vertical: Story = {
|
|
63
|
+
parameters: { controls: { hideNoControlsWarning: true } },
|
|
64
|
+
render: () => (
|
|
65
|
+
<div className="h-8 flex items-center gap-3 font-mono text-[12.5px] text-ink-3">
|
|
66
|
+
<span>Filter</span>
|
|
67
|
+
<Separator orientation="vertical" />
|
|
68
|
+
<span>Group</span>
|
|
69
|
+
<Separator orientation="vertical" variant="solid" />
|
|
70
|
+
<span>Export</span>
|
|
71
|
+
</div>
|
|
72
|
+
),
|
|
73
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import { Separator } from './separator'
|
|
5
|
+
|
|
6
|
+
describe('Separator', () => {
|
|
7
|
+
it('renders with role="separator" by default', () => {
|
|
8
|
+
render(<Separator />)
|
|
9
|
+
expect(screen.getByRole('separator')).toBeInTheDocument()
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('is horizontal by default', () => {
|
|
13
|
+
render(<Separator />)
|
|
14
|
+
// <hr> elements default to aria-orientation="horizontal" (implicit via role)
|
|
15
|
+
const sep = screen.getByRole('separator')
|
|
16
|
+
expect(sep.tagName.toLowerCase()).toBe('hr')
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('reports vertical orientation when requested', () => {
|
|
20
|
+
render(<Separator orientation="vertical" />)
|
|
21
|
+
const sep = screen.getByRole('separator')
|
|
22
|
+
expect(sep).toHaveAttribute('aria-orientation', 'vertical')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('merges custom classes', () => {
|
|
26
|
+
render(<Separator className="my-4" />)
|
|
27
|
+
expect(screen.getByRole('separator').className).toContain('my-4')
|
|
28
|
+
})
|
|
29
|
+
})
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Separator as AriaSeparator,
|
|
3
|
+
type SeparatorProps as AriaSeparatorProps,
|
|
4
|
+
} from 'react-aria-components'
|
|
5
|
+
import { tv, type VariantProps } from 'tailwind-variants'
|
|
6
|
+
|
|
7
|
+
import { cn } from '../../lib/cn'
|
|
8
|
+
|
|
9
|
+
/* =========================================================================
|
|
10
|
+
Separator — hairline divider between sections.
|
|
11
|
+
- Default: dashed horizontal rule (matches the Proteos card/modal convention).
|
|
12
|
+
- `variant="solid"` for a continuous hairline (section breaks inside dense
|
|
13
|
+
tables, sidebars, etc. — see the spec's sub-label rows).
|
|
14
|
+
- `orientation="vertical"` for inline toolbars / nav separators.
|
|
15
|
+
========================================================================= */
|
|
16
|
+
|
|
17
|
+
export const separatorStyles = tv({
|
|
18
|
+
base: 'm-0 border-0 border-rule',
|
|
19
|
+
variants: {
|
|
20
|
+
orientation: {
|
|
21
|
+
horizontal: 'w-full border-t',
|
|
22
|
+
vertical: 'h-full border-l',
|
|
23
|
+
},
|
|
24
|
+
variant: {
|
|
25
|
+
dashed: 'border-dashed',
|
|
26
|
+
solid: 'border-solid',
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
defaultVariants: {
|
|
30
|
+
orientation: 'horizontal',
|
|
31
|
+
variant: 'dashed',
|
|
32
|
+
},
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
export type SeparatorVariants = VariantProps<typeof separatorStyles>
|
|
36
|
+
export type SeparatorProps = AriaSeparatorProps & { variant?: SeparatorVariants['variant'] }
|
|
37
|
+
|
|
38
|
+
export function Separator({
|
|
39
|
+
className,
|
|
40
|
+
orientation = 'horizontal',
|
|
41
|
+
variant = 'dashed',
|
|
42
|
+
...props
|
|
43
|
+
}: SeparatorProps) {
|
|
44
|
+
return (
|
|
45
|
+
<AriaSeparator
|
|
46
|
+
{...props}
|
|
47
|
+
orientation={orientation}
|
|
48
|
+
className={cn(separatorStyles({ orientation, variant }), className)}
|
|
49
|
+
/>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
|
|
3
|
+
import { Skeleton } from './skeleton'
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof Skeleton> = {
|
|
6
|
+
title: 'Components/Skeleton',
|
|
7
|
+
component: Skeleton,
|
|
8
|
+
tags: ['autodocs'],
|
|
9
|
+
args: {
|
|
10
|
+
shape: 'rect',
|
|
11
|
+
className: 'size-12',
|
|
12
|
+
},
|
|
13
|
+
argTypes: {
|
|
14
|
+
shape: {
|
|
15
|
+
control: { type: 'inline-radio' },
|
|
16
|
+
options: ['rect', 'circle', 'text'],
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default meta
|
|
22
|
+
type Story = StoryObj<typeof Skeleton>
|
|
23
|
+
|
|
24
|
+
export const Playground: Story = {}
|
|
25
|
+
|
|
26
|
+
export const Shapes: Story = {
|
|
27
|
+
parameters: { controls: { hideNoControlsWarning: true } },
|
|
28
|
+
render: () => (
|
|
29
|
+
<div className="flex items-center gap-4">
|
|
30
|
+
<Skeleton className="size-12" />
|
|
31
|
+
<Skeleton shape="circle" className="size-12" />
|
|
32
|
+
<Skeleton shape="text" className="w-40" />
|
|
33
|
+
</div>
|
|
34
|
+
),
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Mirrors a typical "loading user row" — avatar + two stacked text lines.
|
|
38
|
+
export const RowExample: Story = {
|
|
39
|
+
parameters: { controls: { hideNoControlsWarning: true } },
|
|
40
|
+
render: () => (
|
|
41
|
+
<div role="status" aria-label="Loading" className="flex items-center gap-3 w-[280px]">
|
|
42
|
+
<Skeleton shape="circle" className="size-10 shrink-0" />
|
|
43
|
+
<div className="flex-1 flex flex-col gap-2">
|
|
44
|
+
<Skeleton shape="text" className="w-3/4" />
|
|
45
|
+
<Skeleton shape="text" className="w-1/2" />
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
),
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Full app-layout shaped placeholder, mirroring the AppShell composition.
|
|
52
|
+
export const AppLayout: Story = {
|
|
53
|
+
parameters: { controls: { hideNoControlsWarning: true } },
|
|
54
|
+
render: () => (
|
|
55
|
+
<div role="status" aria-label="Loading workspace" className="flex h-[400px] w-[640px] gap-2">
|
|
56
|
+
<div className="w-[180px] h-full p-2 bg-surface-2 border border-rule rounded-md flex flex-col gap-2">
|
|
57
|
+
<Skeleton className="h-8 w-full" />
|
|
58
|
+
<Skeleton className="h-6 w-full" />
|
|
59
|
+
<Skeleton className="h-6 w-full" />
|
|
60
|
+
<Skeleton className="h-6 w-full" />
|
|
61
|
+
</div>
|
|
62
|
+
<div className="flex-1 p-2 flex flex-col gap-2">
|
|
63
|
+
<Skeleton className="h-10 w-full" />
|
|
64
|
+
<Skeleton className="flex-1 w-full" />
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
),
|
|
68
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import { Skeleton } from './skeleton'
|
|
5
|
+
|
|
6
|
+
describe('Skeleton', () => {
|
|
7
|
+
it('is hidden from assistive tech by default', () => {
|
|
8
|
+
render(<Skeleton data-testid="sk" />)
|
|
9
|
+
expect(screen.getByTestId('sk')).toHaveAttribute('aria-hidden', 'true')
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('applies the rect (default) shape classes', () => {
|
|
13
|
+
render(<Skeleton data-testid="sk" />)
|
|
14
|
+
expect(screen.getByTestId('sk').className).toContain('rounded-md')
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('applies the circle shape classes', () => {
|
|
18
|
+
render(<Skeleton shape="circle" data-testid="sk" />)
|
|
19
|
+
expect(screen.getByTestId('sk').className).toContain('rounded-full')
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('applies the text shape classes', () => {
|
|
23
|
+
render(<Skeleton shape="text" data-testid="sk" />)
|
|
24
|
+
expect(screen.getByTestId('sk').className).toContain('rounded-sm')
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('merges custom classes', () => {
|
|
28
|
+
render(<Skeleton className="size-10" data-testid="sk" />)
|
|
29
|
+
const el = screen.getByTestId('sk')
|
|
30
|
+
expect(el.className).toContain('size-10')
|
|
31
|
+
expect(el.className).toContain('animate-pulse')
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('lets the consumer override aria-hidden when announcing the loading state', () => {
|
|
35
|
+
render(<Skeleton aria-hidden={false} data-testid="sk" />)
|
|
36
|
+
expect(screen.getByTestId('sk')).toHaveAttribute('aria-hidden', 'false')
|
|
37
|
+
})
|
|
38
|
+
})
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { HTMLAttributes } from 'react'
|
|
2
|
+
import { tv, type VariantProps } from 'tailwind-variants'
|
|
3
|
+
|
|
4
|
+
import { cn } from '../../lib/cn'
|
|
5
|
+
|
|
6
|
+
/* =========================================================================
|
|
7
|
+
Skeleton — content placeholder for loading states.
|
|
8
|
+
Purely presentational, so it doesn't wrap a React Aria primitive (RAC has
|
|
9
|
+
no Skeleton — interactive primitives only). Accessibility lives in props:
|
|
10
|
+
- decorative skeletons get `aria-hidden` so screen readers ignore them
|
|
11
|
+
- skeletons that *announce* the loading state should be wrapped in a
|
|
12
|
+
`role="status"` region by the consumer (e.g. AppLayoutSkeleton).
|
|
13
|
+
========================================================================= */
|
|
14
|
+
|
|
15
|
+
export const skeletonStyles = tv({
|
|
16
|
+
base: ['animate-pulse', 'bg-surface-3'],
|
|
17
|
+
variants: {
|
|
18
|
+
shape: {
|
|
19
|
+
rect: 'rounded-md',
|
|
20
|
+
circle: 'rounded-full',
|
|
21
|
+
// `text` matches a typical line of body text — width is up to the
|
|
22
|
+
// consumer; the rounded-sm + height keep proportions sensible.
|
|
23
|
+
text: 'h-4 w-full rounded-sm',
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
defaultVariants: { shape: 'rect' },
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
export type SkeletonVariants = VariantProps<typeof skeletonStyles>
|
|
30
|
+
export type SkeletonProps = HTMLAttributes<HTMLDivElement> & SkeletonVariants
|
|
31
|
+
|
|
32
|
+
export function Skeleton({ shape, className, ...props }: SkeletonProps) {
|
|
33
|
+
return (
|
|
34
|
+
<div
|
|
35
|
+
// Decorative by default. Consumers that want screen readers to announce
|
|
36
|
+
// the loading state should wrap a Skeleton group in `role="status"`
|
|
37
|
+
// and pass `aria-hidden={false}` here, or just rely on the wrapper.
|
|
38
|
+
aria-hidden="true"
|
|
39
|
+
{...props}
|
|
40
|
+
className={cn(skeletonStyles({ shape }), className)}
|
|
41
|
+
/>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
import { useState } from 'react'
|
|
3
|
+
|
|
4
|
+
import { Button } from '../button/button'
|
|
5
|
+
|
|
6
|
+
import { SuccessAnimation } from './success-animation'
|
|
7
|
+
|
|
8
|
+
const meta: Meta<typeof SuccessAnimation> = {
|
|
9
|
+
title: 'Components/SuccessAnimation',
|
|
10
|
+
component: SuccessAnimation,
|
|
11
|
+
tags: ['autodocs'],
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default meta
|
|
15
|
+
type Story = StoryObj<typeof SuccessAnimation>
|
|
16
|
+
|
|
17
|
+
export const Playground: Story = {
|
|
18
|
+
render: (args) => (
|
|
19
|
+
<div className="size-32">
|
|
20
|
+
<SuccessAnimation {...args} />
|
|
21
|
+
</div>
|
|
22
|
+
),
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const Sizes: Story = {
|
|
26
|
+
parameters: { controls: { hideNoControlsWarning: true } },
|
|
27
|
+
render: () => (
|
|
28
|
+
<div className="flex items-end gap-6">
|
|
29
|
+
<SuccessAnimation className="size-16" />
|
|
30
|
+
<SuccessAnimation className="size-24" />
|
|
31
|
+
<SuccessAnimation className="size-40" />
|
|
32
|
+
</div>
|
|
33
|
+
),
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Replay demo — remount the component to restart the one-shot entrance.
|
|
37
|
+
export const Replay: Story = {
|
|
38
|
+
parameters: { controls: { hideNoControlsWarning: true } },
|
|
39
|
+
render: () => {
|
|
40
|
+
const [key, setKey] = useState(0)
|
|
41
|
+
return (
|
|
42
|
+
<div className="flex flex-col items-center gap-4">
|
|
43
|
+
<SuccessAnimation key={key} className="size-32" />
|
|
44
|
+
<Button size="sm" variant="ghost" onPress={() => setKey((k) => k + 1)}>
|
|
45
|
+
Replay
|
|
46
|
+
</Button>
|
|
47
|
+
</div>
|
|
48
|
+
)
|
|
49
|
+
},
|
|
50
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { render } from '@testing-library/react'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import { SuccessAnimation } from './success-animation'
|
|
5
|
+
|
|
6
|
+
describe('SuccessAnimation', () => {
|
|
7
|
+
it('renders a decorative wrapper with default sizing and the success color', () => {
|
|
8
|
+
const { container } = render(<SuccessAnimation />)
|
|
9
|
+
const wrapper = container.firstElementChild as HTMLElement | null
|
|
10
|
+
expect(wrapper).not.toBeNull()
|
|
11
|
+
expect(wrapper).toHaveAttribute('aria-hidden')
|
|
12
|
+
expect(wrapper?.className).toContain('size-24')
|
|
13
|
+
expect(wrapper?.className).toContain('text-success')
|
|
14
|
+
expect(wrapper?.querySelector('svg')).not.toBeNull()
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('forwards className to override the default size', () => {
|
|
18
|
+
const { container } = render(<SuccessAnimation className="size-40" />)
|
|
19
|
+
const wrapper = container.firstElementChild as HTMLElement | null
|
|
20
|
+
expect(wrapper?.className).toContain('size-40')
|
|
21
|
+
})
|
|
22
|
+
})
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { CircleCheck } from 'lucide-react'
|
|
2
|
+
|
|
3
|
+
import { cn } from '../../lib/cn'
|
|
4
|
+
|
|
5
|
+
/* =========================================================================
|
|
6
|
+
SuccessAnimation — success confirmation: a large green checkmark in a
|
|
7
|
+
circle (token `--color-success`), entering with the one-shot status-pop
|
|
8
|
+
scale+fade (suppressed under prefers-reduced-motion). Decorative:
|
|
9
|
+
rendered with `aria-hidden`, since the surrounding text region carries
|
|
10
|
+
the announcement. Size via className (default 96×96).
|
|
11
|
+
========================================================================= */
|
|
12
|
+
|
|
13
|
+
export type SuccessAnimationProps = {
|
|
14
|
+
className?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function SuccessAnimation({ className }: SuccessAnimationProps) {
|
|
18
|
+
return (
|
|
19
|
+
<div
|
|
20
|
+
aria-hidden
|
|
21
|
+
className={cn('size-24 text-success motion-safe:animate-status-pop', className)}
|
|
22
|
+
>
|
|
23
|
+
<CircleCheck className="size-full" strokeWidth={1.5} />
|
|
24
|
+
</div>
|
|
25
|
+
)
|
|
26
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite'
|
|
2
|
+
|
|
3
|
+
import { Switch } from './switch'
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof Switch> = {
|
|
6
|
+
title: 'Components/Switch',
|
|
7
|
+
component: Switch,
|
|
8
|
+
tags: ['autodocs'],
|
|
9
|
+
args: {
|
|
10
|
+
children: 'Live architect',
|
|
11
|
+
},
|
|
12
|
+
argTypes: {
|
|
13
|
+
isDisabled: { control: { type: 'boolean' } },
|
|
14
|
+
isSelected: { control: { type: 'boolean' } },
|
|
15
|
+
labelPlacement: {
|
|
16
|
+
control: { type: 'inline-radio' },
|
|
17
|
+
options: ['start', 'end'],
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export default meta
|
|
23
|
+
type Story = StoryObj<typeof Switch>
|
|
24
|
+
|
|
25
|
+
export const Playground: Story = {}
|
|
26
|
+
|
|
27
|
+
export const Row: Story = {
|
|
28
|
+
parameters: { controls: { hideNoControlsWarning: true } },
|
|
29
|
+
render: () => (
|
|
30
|
+
<div className="grid gap-4 w-[360px]">
|
|
31
|
+
<div className="flex items-center justify-between">
|
|
32
|
+
<Switch labelPlacement="start" defaultSelected>
|
|
33
|
+
Live architect
|
|
34
|
+
</Switch>
|
|
35
|
+
</div>
|
|
36
|
+
<div className="flex items-center justify-between">
|
|
37
|
+
<Switch labelPlacement="start">Speaker notes</Switch>
|
|
38
|
+
</div>
|
|
39
|
+
<div className="flex items-center justify-between">
|
|
40
|
+
<Switch labelPlacement="start" defaultSelected>
|
|
41
|
+
Version history
|
|
42
|
+
</Switch>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
),
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const AllStates: Story = {
|
|
49
|
+
parameters: { controls: { hideNoControlsWarning: true } },
|
|
50
|
+
render: () => (
|
|
51
|
+
<div className="grid gap-3">
|
|
52
|
+
<Switch>Off</Switch>
|
|
53
|
+
<Switch defaultSelected>On</Switch>
|
|
54
|
+
<Switch isDisabled>Disabled · off</Switch>
|
|
55
|
+
<Switch isDisabled defaultSelected>
|
|
56
|
+
Disabled · on
|
|
57
|
+
</Switch>
|
|
58
|
+
</div>
|
|
59
|
+
),
|
|
60
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react'
|
|
2
|
+
import userEvent from '@testing-library/user-event'
|
|
3
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
4
|
+
|
|
5
|
+
import { Switch } from './switch'
|
|
6
|
+
|
|
7
|
+
describe('Switch', () => {
|
|
8
|
+
it('renders its label with role switch', () => {
|
|
9
|
+
render(<Switch>Live architect</Switch>)
|
|
10
|
+
expect(screen.getByRole('switch', { name: 'Live architect' })).toBeInTheDocument()
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('toggles on click', async () => {
|
|
14
|
+
const user = userEvent.setup()
|
|
15
|
+
const onChange = vi.fn()
|
|
16
|
+
render(<Switch onChange={onChange}>Live architect</Switch>)
|
|
17
|
+
|
|
18
|
+
await user.click(screen.getByRole('switch', { name: 'Live architect' }))
|
|
19
|
+
expect(onChange).toHaveBeenCalledWith(true)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('toggles on keyboard Space', async () => {
|
|
23
|
+
const user = userEvent.setup()
|
|
24
|
+
const onChange = vi.fn()
|
|
25
|
+
render(<Switch onChange={onChange}>Live architect</Switch>)
|
|
26
|
+
|
|
27
|
+
await user.tab()
|
|
28
|
+
expect(screen.getByRole('switch')).toHaveFocus()
|
|
29
|
+
await user.keyboard(' ')
|
|
30
|
+
expect(onChange).toHaveBeenCalledWith(true)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('does not toggle when disabled', async () => {
|
|
34
|
+
const user = userEvent.setup()
|
|
35
|
+
const onChange = vi.fn()
|
|
36
|
+
render(
|
|
37
|
+
<Switch onChange={onChange} isDisabled>
|
|
38
|
+
Live architect
|
|
39
|
+
</Switch>,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
await user.click(screen.getByRole('switch', { name: 'Live architect' }))
|
|
43
|
+
expect(onChange).not.toHaveBeenCalled()
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('reflects a controlled selected state', () => {
|
|
47
|
+
render(<Switch isSelected>Version history</Switch>)
|
|
48
|
+
expect(screen.getByRole('switch', { name: 'Version history' })).toBeChecked()
|
|
49
|
+
})
|
|
50
|
+
})
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
import { Switch as AriaSwitch, type SwitchProps as AriaSwitchProps } from 'react-aria-components'
|
|
3
|
+
|
|
4
|
+
import { cn } from '../../lib/cn'
|
|
5
|
+
|
|
6
|
+
export type SwitchProps = Omit<AriaSwitchProps, 'children'> & {
|
|
7
|
+
children?: ReactNode
|
|
8
|
+
/** Render the label before the track instead of after it. */
|
|
9
|
+
labelPlacement?: 'start' | 'end'
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function Switch({ children, labelPlacement = 'end', className, ...props }: SwitchProps) {
|
|
13
|
+
return (
|
|
14
|
+
<AriaSwitch
|
|
15
|
+
{...props}
|
|
16
|
+
className={(renderProps) =>
|
|
17
|
+
cn(
|
|
18
|
+
'group inline-flex items-center gap-3 cursor-pointer select-none',
|
|
19
|
+
'font-sans text-[14px] text-ink-2',
|
|
20
|
+
labelPlacement === 'start' && 'flex-row-reverse',
|
|
21
|
+
'data-[disabled]:opacity-45 data-[disabled]:cursor-not-allowed',
|
|
22
|
+
typeof className === 'function' ? className(renderProps) : className,
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
>
|
|
26
|
+
<span
|
|
27
|
+
aria-hidden
|
|
28
|
+
className={cn(
|
|
29
|
+
'relative inline-block shrink-0',
|
|
30
|
+
'w-[38px] h-[22px] rounded-pill border',
|
|
31
|
+
'bg-surface-3 border-rule',
|
|
32
|
+
'transition-[background-color,border-color] duration-150',
|
|
33
|
+
'group-data-[selected]:bg-accent group-data-[selected]:border-accent',
|
|
34
|
+
'group-data-[focus-visible]:shadow-[var(--shadow-focus)]',
|
|
35
|
+
)}
|
|
36
|
+
>
|
|
37
|
+
<span
|
|
38
|
+
className={cn(
|
|
39
|
+
'absolute top-[1px] left-[1px]',
|
|
40
|
+
'block size-[18px] rounded-full bg-surface',
|
|
41
|
+
'transition-transform duration-200',
|
|
42
|
+
'group-data-[selected]:translate-x-[16px]',
|
|
43
|
+
)}
|
|
44
|
+
/>
|
|
45
|
+
</span>
|
|
46
|
+
{children}
|
|
47
|
+
</AriaSwitch>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
ColumnDragData,
|
|
3
|
+
SortDirection,
|
|
4
|
+
TableBodyProps,
|
|
5
|
+
TableCellProps,
|
|
6
|
+
TableHeaderCellProps,
|
|
7
|
+
TableHeaderProps,
|
|
8
|
+
TableProps,
|
|
9
|
+
TableRowProps,
|
|
10
|
+
TableVariants,
|
|
11
|
+
} from './table'
|
|
12
|
+
export {
|
|
13
|
+
COLUMN_DRAG_KIND,
|
|
14
|
+
isColumnDragData,
|
|
15
|
+
Table,
|
|
16
|
+
TableBody,
|
|
17
|
+
TableCell,
|
|
18
|
+
TableHeader,
|
|
19
|
+
TableHeaderCell,
|
|
20
|
+
TableHeaderSpacer,
|
|
21
|
+
TableRow,
|
|
22
|
+
TableScroller,
|
|
23
|
+
tableCellStyles,
|
|
24
|
+
tableHeaderCellStyles,
|
|
25
|
+
} from './table'
|