@sovereignfs/ui 0.41.0 → 0.54.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/Accordion.module.css +3 -0
- package/dist/Alert.module.css +62 -0
- package/dist/AspectRatio.module.css +15 -0
- package/dist/Badge.module.css +75 -9
- package/dist/Breadcrumb.module.css +49 -0
- package/dist/Button.module.css +12 -3
- package/dist/ButtonGroup.module.css +29 -0
- package/dist/Card.module.css +6 -0
- package/dist/Checkbox.module.css +3 -1
- package/dist/Collapsible.module.css +70 -0
- package/dist/Combobox.module.css +122 -0
- package/dist/Command.module.css +80 -0
- package/dist/ConfirmDialog.module.css +5 -1
- package/dist/DataTable.module.css +28 -0
- package/dist/DatePicker.module.css +6 -2
- package/dist/Dialog.module.css +3 -0
- package/dist/HoverCard.module.css +3 -0
- package/dist/Input.module.css +6 -1
- package/dist/Item.module.css +70 -0
- package/dist/Kbd.module.css +14 -0
- package/dist/Label.module.css +11 -0
- package/dist/LegalLinks.module.css +24 -0
- package/dist/Markdown.module.css +49 -0
- package/dist/Marker.module.css +33 -0
- package/dist/Menu.module.css +8 -1
- package/dist/Message.module.css +99 -0
- package/dist/MessageScroller.module.css +54 -0
- package/dist/MobileAppsDrawer.module.css +81 -0
- package/dist/MobileFooter.module.css +73 -0
- package/dist/MobileHeader.module.css +63 -0
- package/dist/NavigationMenu.module.css +53 -0
- package/dist/PageContainer.module.css +20 -0
- package/dist/Pagination.module.css +98 -0
- package/dist/Progress.module.css +20 -0
- package/dist/RadioGroup.module.css +88 -0
- package/dist/Resizable.module.css +70 -0
- package/dist/ScrollArea.module.css +20 -0
- package/dist/Select.module.css +7 -3
- package/dist/Slider.module.css +95 -0
- package/dist/SwipableMobileCarousel.module.css +55 -0
- package/dist/SwipableMobileCarouselDots.module.css +49 -0
- package/dist/SwipableMobileCarouselSlideParts.module.css +29 -0
- package/dist/Table.module.css +45 -0
- package/dist/Tabs.module.css +6 -1
- package/dist/Toggle.module.css +6 -1
- package/dist/Typography.module.css +47 -0
- package/dist/index.d.ts +1268 -8
- package/dist/index.js +3046 -975
- package/dist/tokens/primitives.css +47 -8
- package/package.json +13 -9
- package/src/components/Accordion/Accordion.module.css +3 -0
- package/src/components/Accordion/Accordion.stories.tsx +110 -0
- package/src/components/Accordion/Accordion.tsx +53 -0
- package/src/components/Accordion/__tests__/Accordion.test.tsx +61 -0
- package/src/components/Alert/Alert.module.css +62 -0
- package/src/components/Alert/Alert.stories.tsx +96 -0
- package/src/components/Alert/Alert.tsx +58 -0
- package/src/components/Alert/__tests__/Alert.test.tsx +75 -0
- package/src/components/AspectRatio/AspectRatio.module.css +15 -0
- package/src/components/AspectRatio/AspectRatio.stories.tsx +78 -0
- package/src/components/AspectRatio/AspectRatio.tsx +21 -0
- package/src/components/AspectRatio/__tests__/AspectRatio.test.tsx +27 -0
- package/src/components/Badge/Badge.module.css +75 -9
- package/src/components/Badge/Badge.stories.tsx +109 -1
- package/src/components/Badge/Badge.tsx +31 -9
- package/src/components/Badge/__tests__/Badge.test.tsx +26 -2
- package/src/components/Breadcrumb/Breadcrumb.module.css +49 -0
- package/src/components/Breadcrumb/Breadcrumb.stories.tsx +47 -0
- package/src/components/Breadcrumb/Breadcrumb.tsx +57 -0
- package/src/components/Breadcrumb/__tests__/Breadcrumb.test.tsx +52 -0
- package/src/components/Button/Button.module.css +12 -3
- package/src/components/Button/Button.stories.tsx +3 -2
- package/src/components/Button/Button.tsx +1 -1
- package/src/components/Button/__tests__/Button.test.tsx +10 -0
- package/src/components/ButtonGroup/ButtonGroup.module.css +29 -0
- package/src/components/ButtonGroup/ButtonGroup.stories.tsx +57 -0
- package/src/components/ButtonGroup/ButtonGroup.tsx +27 -0
- package/src/components/ButtonGroup/__tests__/ButtonGroup.test.tsx +29 -0
- package/src/components/Card/Card.module.css +6 -0
- package/src/components/Checkbox/Checkbox.module.css +3 -1
- package/src/components/Collapsible/Collapsible.module.css +70 -0
- package/src/components/Collapsible/Collapsible.stories.tsx +62 -0
- package/src/components/Collapsible/Collapsible.tsx +66 -0
- package/src/components/Collapsible/__tests__/Collapsible.test.tsx +71 -0
- package/src/components/Combobox/Combobox.module.css +122 -0
- package/src/components/Combobox/Combobox.stories.tsx +79 -0
- package/src/components/Combobox/Combobox.tsx +186 -0
- package/src/components/Combobox/__tests__/Combobox.test.tsx +158 -0
- package/src/components/Command/Command.module.css +80 -0
- package/src/components/Command/Command.stories.tsx +104 -0
- package/src/components/Command/Command.tsx +168 -0
- package/src/components/Command/__tests__/Command.test.tsx +162 -0
- package/src/components/ConfirmDialog/ConfirmDialog.module.css +5 -1
- package/src/components/ContextMenu/ContextMenu.stories.tsx +81 -0
- package/src/components/ContextMenu/ContextMenu.tsx +100 -0
- package/src/components/ContextMenu/__tests__/ContextMenu.test.tsx +87 -0
- package/src/components/DataTable/DataTable.module.css +28 -0
- package/src/components/DataTable/DataTable.stories.tsx +74 -0
- package/src/components/DataTable/DataTable.tsx +154 -0
- package/src/components/DataTable/__tests__/DataTable.test.tsx +123 -0
- package/src/components/DatePicker/DatePicker.module.css +6 -2
- package/src/components/DeviceOnlyGate/DeviceOnlyGate.tsx +58 -0
- package/src/components/DeviceOnlyGate/__tests__/DeviceOnlyGate.test.tsx +45 -0
- package/src/components/DeviceStorageKeyGate/DeviceStorageKeyGate.tsx +100 -0
- package/src/components/DeviceStorageKeyGate/__tests__/DeviceStorageKeyGate.test.tsx +119 -0
- package/src/components/Dialog/Dialog.module.css +3 -0
- package/src/components/Dialog/Dialog.tsx +8 -59
- package/src/components/Drawer/Drawer.stories.tsx +44 -0
- package/src/components/Drawer/Drawer.tsx +18 -56
- package/src/components/Drawer/__tests__/Drawer.test.tsx +22 -0
- package/src/components/HoverCard/HoverCard.module.css +3 -0
- package/src/components/HoverCard/HoverCard.stories.tsx +66 -0
- package/src/components/HoverCard/HoverCard.tsx +111 -0
- package/src/components/HoverCard/__tests__/HoverCard.test.tsx +99 -0
- package/src/components/Icon/icons/circle-check.tsx +21 -0
- package/src/components/Icon/icons/circle-x.tsx +22 -0
- package/src/components/Icon/icons/index.ts +16 -0
- package/src/components/Icon/icons/menu.tsx +22 -0
- package/src/components/Icon/icons/smartphone.tsx +21 -0
- package/src/components/Input/Input.module.css +6 -1
- package/src/components/Item/Item.module.css +70 -0
- package/src/components/Item/Item.stories.tsx +62 -0
- package/src/components/Item/Item.tsx +54 -0
- package/src/components/Item/__tests__/Item.test.tsx +39 -0
- package/src/components/Kbd/Kbd.module.css +14 -0
- package/src/components/Kbd/Kbd.stories.tsx +32 -0
- package/src/components/Kbd/Kbd.tsx +12 -0
- package/src/components/Kbd/__tests__/Kbd.test.tsx +14 -0
- package/src/components/Label/Label.module.css +11 -0
- package/src/components/Label/Label.stories.tsx +40 -0
- package/src/components/Label/Label.tsx +24 -0
- package/src/components/Label/__tests__/Label.test.tsx +32 -0
- package/src/components/LegalLinks/LegalLinks.module.css +24 -0
- package/src/components/LegalLinks/LegalLinks.stories.tsx +22 -0
- package/src/components/LegalLinks/LegalLinks.tsx +40 -0
- package/src/components/Markdown/Markdown.module.css +49 -0
- package/src/components/Markdown/Markdown.stories.tsx +30 -0
- package/src/components/Markdown/Markdown.tsx +168 -0
- package/src/components/Marker/Marker.module.css +33 -0
- package/src/components/Marker/Marker.stories.tsx +39 -0
- package/src/components/Marker/Marker.tsx +34 -0
- package/src/components/Marker/__tests__/Marker.test.tsx +23 -0
- package/src/components/Menu/Menu.module.css +8 -1
- package/src/components/Menu/Menu.tsx +30 -8
- package/src/components/Menubar/Menubar.stories.tsx +52 -0
- package/src/components/Menubar/Menubar.tsx +42 -0
- package/src/components/Menubar/__tests__/Menubar.test.tsx +45 -0
- package/src/components/Message/Message.module.css +99 -0
- package/src/components/Message/Message.stories.tsx +79 -0
- package/src/components/Message/Message.tsx +41 -0
- package/src/components/Message/__tests__/Message.test.tsx +46 -0
- package/src/components/MessageScroller/MessageScroller.module.css +54 -0
- package/src/components/MessageScroller/MessageScroller.stories.tsx +84 -0
- package/src/components/MessageScroller/MessageScroller.tsx +72 -0
- package/src/components/MessageScroller/__tests__/MessageScroller.test.tsx +105 -0
- package/src/components/MobileAppsDrawer/MobileAppsDrawer.module.css +81 -0
- package/src/components/MobileAppsDrawer/MobileAppsDrawer.tsx +78 -0
- package/src/components/MobileAppsDrawer/__tests__/MobileAppsDrawer.test.tsx +80 -0
- package/src/components/MobileFooter/MobileFooter.module.css +73 -0
- package/src/components/MobileFooter/MobileFooter.stories.tsx +47 -0
- package/src/components/MobileFooter/MobileFooter.tsx +109 -0
- package/src/components/MobileFooter/__tests__/MobileFooter.test.tsx +71 -0
- package/src/components/MobileHeader/MobileHeader.module.css +63 -0
- package/src/components/MobileHeader/MobileHeader.stories.tsx +57 -0
- package/src/components/MobileHeader/MobileHeader.tsx +77 -0
- package/src/components/MobileHeader/__tests__/MobileHeader.test.tsx +58 -0
- package/src/components/NavTabs/NavTabs.tsx +39 -11
- package/src/components/NavigationMenu/NavigationMenu.module.css +53 -0
- package/src/components/NavigationMenu/NavigationMenu.stories.tsx +58 -0
- package/src/components/NavigationMenu/NavigationMenu.tsx +125 -0
- package/src/components/NavigationMenu/__tests__/NavigationMenu.test.tsx +93 -0
- package/src/components/OfflineGate/OfflineGate.tsx +44 -0
- package/src/components/OfflineGate/__tests__/OfflineGate.test.tsx +66 -0
- package/src/components/PageContainer/PageContainer.module.css +20 -0
- package/src/components/PageContainer/PageContainer.tsx +31 -0
- package/src/components/PageContainer/__tests__/PageContainer.test.tsx +37 -0
- package/src/components/PageHeader/PageHeader.tsx +16 -2
- package/src/components/Pagination/Pagination.module.css +98 -0
- package/src/components/Pagination/Pagination.stories.tsx +49 -0
- package/src/components/Pagination/Pagination.tsx +87 -0
- package/src/components/Pagination/__tests__/Pagination.test.tsx +56 -0
- package/src/components/Progress/Progress.module.css +20 -0
- package/src/components/Progress/Progress.stories.tsx +40 -0
- package/src/components/Progress/Progress.tsx +35 -0
- package/src/components/Progress/__tests__/Progress.test.tsx +31 -0
- package/src/components/RadioGroup/RadioGroup.module.css +88 -0
- package/src/components/RadioGroup/RadioGroup.stories.tsx +69 -0
- package/src/components/RadioGroup/RadioGroup.tsx +99 -0
- package/src/components/RadioGroup/__tests__/RadioGroup.test.tsx +59 -0
- package/src/components/Resizable/Resizable.module.css +70 -0
- package/src/components/Resizable/Resizable.stories.tsx +82 -0
- package/src/components/Resizable/Resizable.tsx +313 -0
- package/src/components/Resizable/__tests__/Resizable.test.tsx +128 -0
- package/src/components/ResponsiveSurface/ResponsiveSurface.tsx +24 -0
- package/src/components/ResponsiveSurface/__tests__/ResponsiveSurface.test.tsx +73 -0
- package/src/components/ScrollArea/ScrollArea.module.css +20 -0
- package/src/components/ScrollArea/ScrollArea.stories.tsx +57 -0
- package/src/components/ScrollArea/ScrollArea.tsx +27 -0
- package/src/components/ScrollArea/__tests__/ScrollArea.test.tsx +39 -0
- package/src/components/Select/Select.module.css +7 -3
- package/src/components/Sheet/Sheet.tsx +9 -51
- package/src/components/Slider/Slider.module.css +95 -0
- package/src/components/Slider/Slider.stories.tsx +64 -0
- package/src/components/Slider/Slider.tsx +77 -0
- package/src/components/Slider/__tests__/Slider.test.tsx +45 -0
- package/src/components/SwipableMobileCarousel/SwipableMobileCarousel.module.css +55 -0
- package/src/components/SwipableMobileCarousel/SwipableMobileCarousel.tsx +227 -0
- package/src/components/SwipableMobileCarousel/SwipableMobileCarouselSlide.tsx +35 -0
- package/src/components/SwipableMobileCarousel/SwipableMobileCarouselSlideBody.tsx +42 -0
- package/src/components/SwipableMobileCarousel/SwipableMobileCarouselSlideFooter.tsx +18 -0
- package/src/components/SwipableMobileCarousel/SwipableMobileCarouselSlideHeader.tsx +21 -0
- package/src/components/SwipableMobileCarousel/SwipableMobileCarouselSlideParts.module.css +29 -0
- package/src/components/SwipableMobileCarousel/__tests__/SwipableMobileCarousel.test.tsx +133 -0
- package/src/components/SwipableMobileCarousel/__tests__/SwipableMobileCarouselDevGuard.test.tsx +61 -0
- package/src/components/SwipableMobileCarousel/context.ts +13 -0
- package/src/components/SwipableMobileCarouselDots/SwipableMobileCarouselDots.module.css +49 -0
- package/src/components/SwipableMobileCarouselDots/SwipableMobileCarouselDots.tsx +62 -0
- package/src/components/SwipableMobileCarouselDots/__tests__/SwipableMobileCarouselDots.test.tsx +63 -0
- package/src/components/Table/Table.module.css +45 -0
- package/src/components/Table/Table.stories.tsx +125 -0
- package/src/components/Table/Table.tsx +51 -0
- package/src/components/Table/__tests__/Table.test.tsx +50 -0
- package/src/components/Tabs/Tabs.module.css +6 -1
- package/src/components/Toggle/Toggle.module.css +6 -1
- package/src/components/Typography/Typography.module.css +47 -0
- package/src/components/Typography/Typography.stories.tsx +62 -0
- package/src/components/Typography/Typography.tsx +30 -0
- package/src/components/Typography/__tests__/Typography.test.tsx +34 -0
- package/src/hooks/__tests__/useCarouselRouteSync.test.tsx +126 -0
- package/src/hooks/__tests__/useIsOffline.test.tsx +53 -0
- package/src/hooks/__tests__/useOfflineTileState.test.tsx +69 -0
- package/src/hooks/__tests__/useResponsiveLayout.test.tsx +87 -0
- package/src/hooks/__tests__/useSnapCarousel.test.tsx +93 -0
- package/src/hooks/__tests__/useSwipeReveal.test.tsx +110 -0
- package/src/hooks/index.ts +18 -0
- package/src/hooks/useCarouselRouteSync.ts +92 -0
- package/src/hooks/useIsOffline.ts +41 -0
- package/src/hooks/useOfflineTileState.ts +45 -0
- package/src/hooks/useResponsiveLayout.ts +36 -0
- package/src/hooks/useSnapCarousel.ts +79 -0
- package/src/hooks/useSwipeReveal.ts +107 -0
- package/src/index.ts +141 -3
- package/src/overlay-shell.ts +94 -0
- package/src/stories/DesignSystemOverview.stories.tsx +936 -3
- package/src/stories/DeviceOnlyGate.stories.tsx +35 -0
- package/src/stories/DeviceStorageKeyGate.stories.tsx +57 -0
- package/src/stories/InteractionHooks.stories.tsx +483 -0
- package/src/stories/MobilePatterns.stories.tsx +255 -4
- package/src/stories/NavTabs.stories.tsx +18 -0
- package/src/stories/OfflineGate.stories.tsx +42 -0
- package/src/stories/PageContainer.stories.tsx +37 -0
- package/src/stories/PageHeader.stories.tsx +11 -0
- package/src/stories/ResponsiveSurface.stories.tsx +47 -0
- package/src/stories/SwipableMobileCarousel.stories.tsx +304 -0
- package/src/stories/TokenGallery.stories.tsx +177 -16
- package/src/tokens/primitives.css +47 -8
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
3
|
+
import { cleanup, render, screen } from '@testing-library/react';
|
|
4
|
+
import { Breadcrumb } from '../Breadcrumb';
|
|
5
|
+
|
|
6
|
+
afterEach(cleanup);
|
|
7
|
+
|
|
8
|
+
const items = [
|
|
9
|
+
{ label: 'Console', href: '/console' },
|
|
10
|
+
{ label: 'Plugins', href: '/console/plugins' },
|
|
11
|
+
{ label: 'sovereign-tasks' },
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
describe('Breadcrumb', () => {
|
|
15
|
+
it('renders a link for every item with an href', () => {
|
|
16
|
+
render(<Breadcrumb items={items} />);
|
|
17
|
+
expect(screen.getByRole('link', { name: 'Console' })).toHaveProperty(
|
|
18
|
+
'href',
|
|
19
|
+
expect.stringContaining('/console'),
|
|
20
|
+
);
|
|
21
|
+
expect(screen.getByRole('link', { name: 'Plugins' })).toBeDefined();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('renders the item without href as plain text with aria-current', () => {
|
|
25
|
+
render(<Breadcrumb items={items} />);
|
|
26
|
+
expect(screen.queryByRole('link', { name: 'sovereign-tasks' })).toBeNull();
|
|
27
|
+
expect(screen.getByText('sovereign-tasks').getAttribute('aria-current')).toBe('page');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('uses a caller-supplied renderLink instead of a plain <a> when given', () => {
|
|
31
|
+
render(
|
|
32
|
+
<Breadcrumb
|
|
33
|
+
items={items}
|
|
34
|
+
renderLink={(item, children) => (
|
|
35
|
+
<button type="button" data-href={item.href}>
|
|
36
|
+
{children}
|
|
37
|
+
</button>
|
|
38
|
+
)}
|
|
39
|
+
/>,
|
|
40
|
+
);
|
|
41
|
+
expect(screen.queryByRole('link', { name: 'Console' })).toBeNull();
|
|
42
|
+
expect(screen.getByRole('button', { name: 'Console' })).toHaveProperty(
|
|
43
|
+
'dataset.href',
|
|
44
|
+
'/console',
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('defaults to an accessible nav label', () => {
|
|
49
|
+
render(<Breadcrumb items={items} />);
|
|
50
|
+
expect(screen.getByRole('navigation', { name: 'Breadcrumb' })).toBeDefined();
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -37,14 +37,23 @@
|
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
/* Sizes
|
|
40
|
+
/* Sizes — 32px / 36px / 40px rendered height (font-size + padding*2 + border*2).
|
|
41
|
+
Vertical padding is hardcoded where the --sv-space-* scale can't hit the
|
|
42
|
+
exact target height; horizontal padding stays token-based, growing with
|
|
43
|
+
size for visual rhythm with the rest of this scale. */
|
|
41
44
|
.sm {
|
|
42
|
-
padding: var(--sv-space-
|
|
45
|
+
padding: var(--sv-space-2) var(--sv-space-3);
|
|
43
46
|
font-size: var(--sv-font-size-sm);
|
|
44
47
|
}
|
|
45
48
|
|
|
46
49
|
.md {
|
|
47
|
-
padding:
|
|
50
|
+
padding: 10px var(--sv-space-4);
|
|
51
|
+
font-size: var(--sv-font-size-sm);
|
|
52
|
+
font-weight: var(--sv-font-weight-semibold);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.lg {
|
|
56
|
+
padding: var(--sv-space-3) var(--sv-space-5);
|
|
48
57
|
font-size: var(--sv-font-size-sm);
|
|
49
58
|
font-weight: var(--sv-font-weight-semibold);
|
|
50
59
|
}
|
|
@@ -16,7 +16,7 @@ const meta = {
|
|
|
16
16
|
},
|
|
17
17
|
argTypes: {
|
|
18
18
|
variant: { control: 'select', options: ['primary', 'secondary', 'ghost', 'destructive'] },
|
|
19
|
-
size: { control: 'select', options: ['sm', 'md'] },
|
|
19
|
+
size: { control: 'select', options: ['sm', 'md', 'lg'] },
|
|
20
20
|
disabled: { control: 'boolean' },
|
|
21
21
|
loading: { control: 'boolean' },
|
|
22
22
|
children: { control: 'text' },
|
|
@@ -38,6 +38,7 @@ export const Destructive: Story = { args: { variant: 'destructive', children: 'D
|
|
|
38
38
|
|
|
39
39
|
export const Small: Story = { args: { size: 'sm' } };
|
|
40
40
|
export const Medium: Story = { args: { size: 'md' } };
|
|
41
|
+
export const Large: Story = { args: { size: 'lg' } };
|
|
41
42
|
|
|
42
43
|
export const Disabled: Story = { args: { disabled: true } };
|
|
43
44
|
export const DisabledSecondary: Story = { args: { variant: 'secondary', disabled: true } };
|
|
@@ -71,7 +72,7 @@ export const AllVariants: Story = {
|
|
|
71
72
|
render: (_args) => (
|
|
72
73
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 16, alignItems: 'flex-start' }}>
|
|
73
74
|
{(['primary', 'secondary', 'ghost', 'destructive'] as const).map((v) =>
|
|
74
|
-
(['md', 'sm'] as const).map((s) => (
|
|
75
|
+
(['lg', 'md', 'sm'] as const).map((s) => (
|
|
75
76
|
<Button key={`${v}-${s}`} variant={v} size={s}>
|
|
76
77
|
{v} / {s}
|
|
77
78
|
</Button>
|
|
@@ -2,7 +2,7 @@ import type { ButtonHTMLAttributes, ReactNode } from 'react';
|
|
|
2
2
|
import styles from './Button.module.css';
|
|
3
3
|
|
|
4
4
|
export type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'destructive';
|
|
5
|
-
export type ButtonSize = 'sm' | 'md';
|
|
5
|
+
export type ButtonSize = 'sm' | 'md' | 'lg';
|
|
6
6
|
|
|
7
7
|
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
8
8
|
variant?: ButtonVariant;
|
|
@@ -20,6 +20,16 @@ describe('Button', () => {
|
|
|
20
20
|
expect(button.className).toContain('sm');
|
|
21
21
|
});
|
|
22
22
|
|
|
23
|
+
it('applies the lg size class', () => {
|
|
24
|
+
render(<Button size="lg">Upgrade</Button>);
|
|
25
|
+
expect(screen.getByRole('button', { name: 'Upgrade' }).className).toContain('lg');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('defaults to the md size class', () => {
|
|
29
|
+
render(<Button>Confirm</Button>);
|
|
30
|
+
expect(screen.getByRole('button', { name: 'Confirm' }).className).toContain('md');
|
|
31
|
+
});
|
|
32
|
+
|
|
23
33
|
it('defaults to type="button"', () => {
|
|
24
34
|
render(<Button>Save</Button>);
|
|
25
35
|
expect(screen.getByRole('button', { name: 'Save' }).getAttribute('type')).toBe('button');
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
.root {
|
|
2
|
+
display: inline-flex;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
.root > :global(button),
|
|
6
|
+
.root > :global(a) {
|
|
7
|
+
border-radius: 0;
|
|
8
|
+
margin-left: -1px;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.root > :global(button):first-child,
|
|
12
|
+
.root > :global(a):first-child {
|
|
13
|
+
margin-left: 0;
|
|
14
|
+
border-top-left-radius: var(--sv-radius-md);
|
|
15
|
+
border-bottom-left-radius: var(--sv-radius-md);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.root > :global(button):last-child,
|
|
19
|
+
.root > :global(a):last-child {
|
|
20
|
+
border-top-right-radius: var(--sv-radius-md);
|
|
21
|
+
border-bottom-right-radius: var(--sv-radius-md);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/* Keep the focus ring above sibling borders instead of being clipped by them. */
|
|
25
|
+
.root > :global(button):focus-visible,
|
|
26
|
+
.root > :global(a):focus-visible {
|
|
27
|
+
position: relative;
|
|
28
|
+
z-index: 1;
|
|
29
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
3
|
+
import { ButtonGroup } from './ButtonGroup';
|
|
4
|
+
import { Button } from '../Button/Button';
|
|
5
|
+
|
|
6
|
+
const meta = {
|
|
7
|
+
title: 'Components/ButtonGroup',
|
|
8
|
+
component: ButtonGroup,
|
|
9
|
+
parameters: { layout: 'padded' },
|
|
10
|
+
args: { children: null },
|
|
11
|
+
} satisfies Meta<typeof ButtonGroup>;
|
|
12
|
+
|
|
13
|
+
export default meta;
|
|
14
|
+
type Story = StoryObj<typeof meta>;
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
export const Default: Story = {
|
|
19
|
+
render: () => (
|
|
20
|
+
<ButtonGroup aria-label="View">
|
|
21
|
+
<Button variant="secondary" size="sm">
|
|
22
|
+
Day
|
|
23
|
+
</Button>
|
|
24
|
+
<Button variant="secondary" size="sm">
|
|
25
|
+
Week
|
|
26
|
+
</Button>
|
|
27
|
+
<Button variant="secondary" size="sm">
|
|
28
|
+
Month
|
|
29
|
+
</Button>
|
|
30
|
+
</ButtonGroup>
|
|
31
|
+
),
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const WithActiveSelection: Story = {
|
|
35
|
+
render: () => {
|
|
36
|
+
const [view, setView] = useState<'day' | 'week' | 'month'>('week');
|
|
37
|
+
const options: { value: typeof view; label: string }[] = [
|
|
38
|
+
{ value: 'day', label: 'Day' },
|
|
39
|
+
{ value: 'week', label: 'Week' },
|
|
40
|
+
{ value: 'month', label: 'Month' },
|
|
41
|
+
];
|
|
42
|
+
return (
|
|
43
|
+
<ButtonGroup aria-label="View">
|
|
44
|
+
{options.map((option) => (
|
|
45
|
+
<Button
|
|
46
|
+
key={option.value}
|
|
47
|
+
variant={option.value === view ? 'primary' : 'secondary'}
|
|
48
|
+
size="sm"
|
|
49
|
+
onClick={() => setView(option.value)}
|
|
50
|
+
>
|
|
51
|
+
{option.label}
|
|
52
|
+
</Button>
|
|
53
|
+
))}
|
|
54
|
+
</ButtonGroup>
|
|
55
|
+
);
|
|
56
|
+
},
|
|
57
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import styles from './ButtonGroup.module.css';
|
|
3
|
+
|
|
4
|
+
export interface ButtonGroupProps {
|
|
5
|
+
/** Adjacent Button elements. */
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
className?: string;
|
|
8
|
+
'aria-label'?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* ButtonGroup — visually joins adjacent `Button`s into one control (shared
|
|
13
|
+
* border, connected corners). Targets its children's rendered `<button>`/
|
|
14
|
+
* `<a>` elements directly rather than requiring a special child component,
|
|
15
|
+
* so it works with plain `Button`.
|
|
16
|
+
*/
|
|
17
|
+
export function ButtonGroup({ children, className, 'aria-label': ariaLabel }: ButtonGroupProps) {
|
|
18
|
+
return (
|
|
19
|
+
<div
|
|
20
|
+
role="group"
|
|
21
|
+
aria-label={ariaLabel}
|
|
22
|
+
className={[styles.root, className].filter(Boolean).join(' ')}
|
|
23
|
+
>
|
|
24
|
+
{children}
|
|
25
|
+
</div>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
3
|
+
import { cleanup, render, screen } from '@testing-library/react';
|
|
4
|
+
import { ButtonGroup } from '../ButtonGroup';
|
|
5
|
+
|
|
6
|
+
afterEach(cleanup);
|
|
7
|
+
|
|
8
|
+
describe('ButtonGroup', () => {
|
|
9
|
+
it('exposes a group role with the given aria-label', () => {
|
|
10
|
+
render(
|
|
11
|
+
<ButtonGroup aria-label="View">
|
|
12
|
+
<button type="button">Day</button>
|
|
13
|
+
<button type="button">Week</button>
|
|
14
|
+
</ButtonGroup>,
|
|
15
|
+
);
|
|
16
|
+
expect(screen.getByRole('group', { name: 'View' })).toBeDefined();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('renders every child button', () => {
|
|
20
|
+
render(
|
|
21
|
+
<ButtonGroup aria-label="View">
|
|
22
|
+
<button type="button">Day</button>
|
|
23
|
+
<button type="button">Week</button>
|
|
24
|
+
<button type="button">Month</button>
|
|
25
|
+
</ButtonGroup>,
|
|
26
|
+
);
|
|
27
|
+
expect(screen.getAllByRole('button').length).toBe(3);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/* Sizing audit: reviewed padding/radius against this scale's other
|
|
2
|
+
surfaces. Left unchanged rather than edited: Card is consumed across
|
|
3
|
+
most first-party plugin screens, and changing its padding/radius
|
|
4
|
+
site-wide has a blast radius no visual regression tooling exists yet to
|
|
5
|
+
verify (see epic task 9.14, not landed). Revisit once that tooling
|
|
6
|
+
exists. */
|
|
1
7
|
.card {
|
|
2
8
|
background: var(--sv-color-surface-raised);
|
|
3
9
|
border: 1px solid var(--sv-color-border);
|
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
cursor: pointer;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
/* The visual box
|
|
8
|
+
/* The visual box. Sizing audit: reviewed against the rest of this scale's
|
|
9
|
+
compact controls. Kept the 18px box unchanged — deliberately on the
|
|
10
|
+
larger side for touch-friendliness, not a gap to close. */
|
|
9
11
|
.box {
|
|
10
12
|
--checkbox-box-size: 18px;
|
|
11
13
|
position: relative;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
.root {
|
|
2
|
+
border-bottom: 1px solid var(--sv-color-border);
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
.trigger {
|
|
6
|
+
display: flex;
|
|
7
|
+
align-items: center;
|
|
8
|
+
justify-content: space-between;
|
|
9
|
+
gap: var(--sv-space-2);
|
|
10
|
+
width: 100%;
|
|
11
|
+
padding: var(--sv-space-3) 0;
|
|
12
|
+
border: none;
|
|
13
|
+
background: transparent;
|
|
14
|
+
color: var(--sv-color-text-primary);
|
|
15
|
+
font-family: var(--sv-font-family);
|
|
16
|
+
font-size: var(--sv-font-size-sm);
|
|
17
|
+
font-weight: var(--sv-font-weight-medium);
|
|
18
|
+
text-align: left;
|
|
19
|
+
cursor: pointer;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.trigger:focus-visible {
|
|
23
|
+
outline: 2px solid var(--sv-color-focus-ring);
|
|
24
|
+
outline-offset: 2px;
|
|
25
|
+
border-radius: var(--sv-radius-sm);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.triggerLabel {
|
|
29
|
+
min-width: 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.chevron {
|
|
33
|
+
flex-shrink: 0;
|
|
34
|
+
color: var(--sv-color-text-muted);
|
|
35
|
+
transition: transform 0.2s ease;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.chevronOpen {
|
|
39
|
+
transform: rotate(180deg);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/* Expand/collapse via grid-template-rows (0fr closed, 1fr open) instead of
|
|
43
|
+
animating height, which can't transition to/from auto in CSS. The inner
|
|
44
|
+
wrapper clips content during the transition. */
|
|
45
|
+
.contentWrapper {
|
|
46
|
+
display: grid;
|
|
47
|
+
grid-template-rows: 0fr;
|
|
48
|
+
transition: grid-template-rows 0.2s ease;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.contentWrapper[data-open='true'] {
|
|
52
|
+
grid-template-rows: 1fr;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.contentInner {
|
|
56
|
+
overflow: hidden;
|
|
57
|
+
font-size: var(--sv-font-size-sm);
|
|
58
|
+
color: var(--sv-color-text-primary);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.contentWrapper[data-open='true'] .contentInner {
|
|
62
|
+
padding-bottom: var(--sv-space-3);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@media (prefers-reduced-motion: reduce) {
|
|
66
|
+
.contentWrapper,
|
|
67
|
+
.chevron {
|
|
68
|
+
transition: none;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
3
|
+
import { Collapsible } from './Collapsible';
|
|
4
|
+
|
|
5
|
+
const meta = {
|
|
6
|
+
title: 'Components/Collapsible',
|
|
7
|
+
component: Collapsible,
|
|
8
|
+
parameters: {
|
|
9
|
+
layout: 'padded',
|
|
10
|
+
docs: {
|
|
11
|
+
description: {
|
|
12
|
+
component:
|
|
13
|
+
'Single expand/collapse primitive. Independently useful (e.g. a "show more" toggle), and composed internally by Accordion.',
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
args: { open: false, onOpenChange: () => {}, trigger: 'Trigger', children: 'Content' },
|
|
18
|
+
} satisfies Meta<typeof Collapsible>;
|
|
19
|
+
|
|
20
|
+
export default meta;
|
|
21
|
+
type Story = StoryObj<typeof meta>;
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
export const Default: Story = {
|
|
26
|
+
render: () => {
|
|
27
|
+
const [open, setOpen] = useState(false);
|
|
28
|
+
return (
|
|
29
|
+
<Collapsible open={open} onOpenChange={setOpen} trigger="What plugins are installed?">
|
|
30
|
+
Sovereign ships with the platform plugins (Console, Launcher, Account) plus whatever you've
|
|
31
|
+
declared in your own sovereign.plugins.json — nothing else is bundled by default.
|
|
32
|
+
</Collapsible>
|
|
33
|
+
);
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const StartsOpen: Story = {
|
|
38
|
+
render: () => {
|
|
39
|
+
const [open, setOpen] = useState(true);
|
|
40
|
+
return (
|
|
41
|
+
<Collapsible open={open} onOpenChange={setOpen} trigger="Advanced settings">
|
|
42
|
+
These settings are rarely needed — change them only if you know what you're doing.
|
|
43
|
+
</Collapsible>
|
|
44
|
+
);
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const LongContent: Story = {
|
|
49
|
+
render: () => {
|
|
50
|
+
const [open, setOpen] = useState(true);
|
|
51
|
+
return (
|
|
52
|
+
<Collapsible open={open} onOpenChange={setOpen} trigger="Full changelog">
|
|
53
|
+
<ul style={{ margin: 0, paddingLeft: 20 }}>
|
|
54
|
+
<li>Added component sizing alignment across the design system</li>
|
|
55
|
+
<li>Added RadioGroup, Slider, Progress, Table, Alert primitives</li>
|
|
56
|
+
<li>Added Breadcrumb, Pagination, and Kbd primitives</li>
|
|
57
|
+
<li>Added Accordion and Collapsible primitives</li>
|
|
58
|
+
</ul>
|
|
59
|
+
</Collapsible>
|
|
60
|
+
);
|
|
61
|
+
},
|
|
62
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useId } from 'react';
|
|
4
|
+
import type { ReactNode } from 'react';
|
|
5
|
+
import { Icon } from '../Icon/Icon';
|
|
6
|
+
import styles from './Collapsible.module.css';
|
|
7
|
+
|
|
8
|
+
export interface CollapsibleProps {
|
|
9
|
+
open: boolean;
|
|
10
|
+
onOpenChange: (open: boolean) => void;
|
|
11
|
+
trigger: ReactNode;
|
|
12
|
+
children: ReactNode;
|
|
13
|
+
id?: string;
|
|
14
|
+
className?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Collapsible — single expand/collapse primitive. Independently useful
|
|
19
|
+
* (e.g. a "show more" toggle), and composed internally by `Accordion`.
|
|
20
|
+
*
|
|
21
|
+
* The expand/collapse animation uses `grid-template-rows: 0fr` → `1fr`
|
|
22
|
+
* rather than animating `height`, which can't transition to/from `auto`
|
|
23
|
+
* in CSS without JS-measuring pixel heights. Collapsed content stays in
|
|
24
|
+
* the DOM (so no layout jump on re-open) but is marked `inert` — removed
|
|
25
|
+
* from the tab order and the accessibility tree — so it isn't reachable
|
|
26
|
+
* while hidden.
|
|
27
|
+
*/
|
|
28
|
+
export function Collapsible({
|
|
29
|
+
open,
|
|
30
|
+
onOpenChange,
|
|
31
|
+
trigger,
|
|
32
|
+
children,
|
|
33
|
+
id,
|
|
34
|
+
className,
|
|
35
|
+
}: CollapsibleProps) {
|
|
36
|
+
const generatedId = useId();
|
|
37
|
+
const contentId = `${id ?? generatedId}-content`;
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div className={[styles.root, className].filter(Boolean).join(' ')}>
|
|
41
|
+
<button
|
|
42
|
+
type="button"
|
|
43
|
+
className={styles.trigger}
|
|
44
|
+
aria-expanded={open}
|
|
45
|
+
aria-controls={contentId}
|
|
46
|
+
onClick={() => onOpenChange(!open)}
|
|
47
|
+
>
|
|
48
|
+
<span className={styles.triggerLabel}>{trigger}</span>
|
|
49
|
+
<Icon
|
|
50
|
+
name="chevron-down"
|
|
51
|
+
size="sm"
|
|
52
|
+
aria-hidden
|
|
53
|
+
className={[styles.chevron, open ? styles.chevronOpen : ''].filter(Boolean).join(' ')}
|
|
54
|
+
/>
|
|
55
|
+
</button>
|
|
56
|
+
<div
|
|
57
|
+
id={contentId}
|
|
58
|
+
className={styles.contentWrapper}
|
|
59
|
+
data-open={open}
|
|
60
|
+
inert={!open || undefined}
|
|
61
|
+
>
|
|
62
|
+
<div className={styles.contentInner}>{children}</div>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { cleanup, render, screen, fireEvent } from '@testing-library/react';
|
|
4
|
+
import { Collapsible } from '../Collapsible';
|
|
5
|
+
|
|
6
|
+
afterEach(cleanup);
|
|
7
|
+
|
|
8
|
+
describe('Collapsible', () => {
|
|
9
|
+
it('exposes aria-expanded matching the open prop', () => {
|
|
10
|
+
const { rerender } = render(
|
|
11
|
+
<Collapsible open={false} onOpenChange={() => {}} trigger="More info">
|
|
12
|
+
Body content
|
|
13
|
+
</Collapsible>,
|
|
14
|
+
);
|
|
15
|
+
expect(screen.getByRole('button', { name: 'More info' }).getAttribute('aria-expanded')).toBe(
|
|
16
|
+
'false',
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
rerender(
|
|
20
|
+
<Collapsible open onOpenChange={() => {}} trigger="More info">
|
|
21
|
+
Body content
|
|
22
|
+
</Collapsible>,
|
|
23
|
+
);
|
|
24
|
+
expect(screen.getByRole('button', { name: 'More info' }).getAttribute('aria-expanded')).toBe(
|
|
25
|
+
'true',
|
|
26
|
+
);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('calls onOpenChange with the toggled value when the trigger is clicked', () => {
|
|
30
|
+
const onOpenChange = vi.fn();
|
|
31
|
+
render(
|
|
32
|
+
<Collapsible open={false} onOpenChange={onOpenChange} trigger="More info">
|
|
33
|
+
Body content
|
|
34
|
+
</Collapsible>,
|
|
35
|
+
);
|
|
36
|
+
fireEvent.click(screen.getByRole('button', { name: 'More info' }));
|
|
37
|
+
expect(onOpenChange).toHaveBeenCalledWith(true);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('marks collapsed content inert', () => {
|
|
41
|
+
render(
|
|
42
|
+
<Collapsible open={false} onOpenChange={() => {}} trigger="More info">
|
|
43
|
+
Body content
|
|
44
|
+
</Collapsible>,
|
|
45
|
+
);
|
|
46
|
+
const content = screen.getByText('Body content').closest('[inert]');
|
|
47
|
+
expect(content).not.toBeNull();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('does not mark expanded content inert', () => {
|
|
51
|
+
render(
|
|
52
|
+
<Collapsible open onOpenChange={() => {}} trigger="More info">
|
|
53
|
+
Body content
|
|
54
|
+
</Collapsible>,
|
|
55
|
+
);
|
|
56
|
+
const content = screen.getByText('Body content').closest('[inert]');
|
|
57
|
+
expect(content).toBeNull();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('associates the trigger with the content via aria-controls', () => {
|
|
61
|
+
render(
|
|
62
|
+
<Collapsible open onOpenChange={() => {}} trigger="More info" id="details">
|
|
63
|
+
Body content
|
|
64
|
+
</Collapsible>,
|
|
65
|
+
);
|
|
66
|
+
const button = screen.getByRole('button', { name: 'More info' });
|
|
67
|
+
const controlsId = button.getAttribute('aria-controls');
|
|
68
|
+
expect(controlsId).toBeTruthy();
|
|
69
|
+
expect(document.getElementById(controlsId ?? '')).not.toBeNull();
|
|
70
|
+
});
|
|
71
|
+
});
|