@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
|
@@ -77,20 +77,44 @@
|
|
|
77
77
|
--sv-font-weight-semibold: 600;
|
|
78
78
|
--sv-font-weight-bold: 700;
|
|
79
79
|
|
|
80
|
+
/* In-app text-size control (task 10.2), discharging the pinch-zoom-disabled
|
|
81
|
+
accessibility debt (runtime/app/layout.tsx sets userScalable: false).
|
|
82
|
+
Every --sv-font-size-* above is rem-based, so scaling the root font size
|
|
83
|
+
scales the whole type system with no per-component changes. Driven by
|
|
84
|
+
[data-text-size] on <html>, set pre-paint by runtime/src/theme-script.ts
|
|
85
|
+
from the `sv-text-size` cookie — same mechanism as [data-theme]. Like
|
|
86
|
+
--sv-radius-scale, this is a theme-stable scale token with no separate
|
|
87
|
+
semantic tier. */
|
|
88
|
+
--sv-text-size-scale: 1;
|
|
89
|
+
|
|
80
90
|
/* Icon sizes — scale tokens used directly by the Icon component */
|
|
81
91
|
--sv-icon-size-xs: 0.75rem; /* 12px — inline metadata (e.g. a due-date badge) */
|
|
82
92
|
--sv-icon-size-sm: 1rem; /* 16px — inline with body text */
|
|
83
93
|
--sv-icon-size-md: 1.25rem; /* 20px — standard affordance */
|
|
84
94
|
--sv-icon-size-lg: 1.5rem; /* 24px — prominent / standalone */
|
|
85
95
|
|
|
86
|
-
/* Border radii (v3.1 spec values)
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
96
|
+
/* Border radii (v3.1 spec values). --sv-radius-scale is the one
|
|
97
|
+
instance-controllable exception to "scale tokens are theme-stable"
|
|
98
|
+
(RFC 0077) — an operator's corner-radius preset (None/XS/S/M/L)
|
|
99
|
+
overrides it at :root via the InstanceProvider style injection
|
|
100
|
+
(runtime/src/instance-provider.tsx), same pipeline as --sv-color-accent.
|
|
101
|
+
Every corner token below is proportional to it, so a plugin author never
|
|
102
|
+
references --sv-radius-scale directly — they keep using --sv-radius-sm
|
|
103
|
+
etc. exactly as before. --sv-radius-full is deliberately NOT derived
|
|
104
|
+
from the scale: it's a shape token (circle/pill), not a corner-rounding
|
|
105
|
+
one, and stays fixed regardless of the instance preset. */
|
|
106
|
+
--sv-radius-scale: 1;
|
|
107
|
+
--sv-radius-sm: calc(var(--sv-radius-scale) * 0.375rem); /* 6px @ scale 1 — badge, tag */
|
|
108
|
+
--sv-radius-md: calc(var(--sv-radius-scale) * 0.5rem); /* 8px @ scale 1 — button, input */
|
|
109
|
+
--sv-radius-lg: calc(
|
|
110
|
+
var(--sv-radius-scale) * 0.6875rem
|
|
111
|
+
); /* 11px @ scale 1 — sidebar icon, card */
|
|
112
|
+
--sv-radius-xl: calc(var(--sv-radius-scale) * 0.75rem); /* 12px @ scale 1 — card, panel */
|
|
113
|
+
--sv-radius-2xl: calc(var(--sv-radius-scale) * 0.875rem); /* 14px @ scale 1 — popover */
|
|
114
|
+
--sv-radius-3xl: calc(
|
|
115
|
+
var(--sv-radius-scale) * 1.25rem
|
|
116
|
+
); /* 20px @ scale 1 — bottom sheet (Drawer top corners) */
|
|
117
|
+
--sv-radius-full: 9999px; /* pill / avatar — unaffected by --sv-radius-scale */
|
|
94
118
|
|
|
95
119
|
/* Touch targets — minimum hit-area dimension for interactive controls.
|
|
96
120
|
44px follows the WCAG 2.5.5 / Apple HIG / Material guideline for reliable
|
|
@@ -116,3 +140,18 @@
|
|
|
116
140
|
1
|
|
117
141
|
); /* slight overshoot — playful emphasis, use sparingly */
|
|
118
142
|
}
|
|
143
|
+
|
|
144
|
+
/* Root font size scales with --sv-text-size-scale — see the token comment
|
|
145
|
+
above. calc(... * 100%) keeps the browser/OS text-zoom default intact at
|
|
146
|
+
scale 1 rather than hardcoding a px value. */
|
|
147
|
+
html {
|
|
148
|
+
font-size: calc(var(--sv-text-size-scale) * 100%);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
:root[data-text-size='large'] {
|
|
152
|
+
--sv-text-size-scale: 1.125; /* +12.5% */
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
:root[data-text-size='larger'] {
|
|
156
|
+
--sv-text-size-scale: 1.25; /* +25% */
|
|
157
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sovereignfs/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.54.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Sovereign Design System — design tokens and React components.",
|
|
6
6
|
"keywords": [
|
|
@@ -36,16 +36,17 @@
|
|
|
36
36
|
"react-dom": "^19.0.0"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
|
-
"@
|
|
40
|
-
"@storybook/addon-
|
|
41
|
-
"@storybook/
|
|
39
|
+
"@playwright/test": "^1.62.1",
|
|
40
|
+
"@storybook/addon-a11y": "^10.5.5",
|
|
41
|
+
"@storybook/addon-themes": "^10.5.5",
|
|
42
|
+
"@storybook/react-vite": "^10.5.5",
|
|
42
43
|
"@testing-library/dom": "^10.4.1",
|
|
43
44
|
"@testing-library/react": "^16.3.2",
|
|
44
|
-
"@types/react": "^19.
|
|
45
|
-
"@types/react-dom": "^19.
|
|
46
|
-
"react": "^19.
|
|
47
|
-
"react-dom": "^19.
|
|
48
|
-
"storybook": "^10.
|
|
45
|
+
"@types/react": "^19.2.18",
|
|
46
|
+
"@types/react-dom": "^19.2.4",
|
|
47
|
+
"react": "^19.2.8",
|
|
48
|
+
"react-dom": "^19.2.8",
|
|
49
|
+
"storybook": "^10.5.5",
|
|
49
50
|
"tsup": "^8.5.1",
|
|
50
51
|
"typescript": "^5.9.3",
|
|
51
52
|
"@sovereignfs/tsconfig": "0.0.0"
|
|
@@ -53,7 +54,10 @@
|
|
|
53
54
|
"scripts": {
|
|
54
55
|
"build": "tsup",
|
|
55
56
|
"storybook": "storybook dev -p 6006",
|
|
57
|
+
"storybook:visual": "storybook dev -p 6100 --ci --quiet",
|
|
56
58
|
"build-storybook": "storybook build --output-dir storybook-static",
|
|
59
|
+
"test:visual": "playwright test --config=playwright.visual.config.ts",
|
|
60
|
+
"test:visual:update": "playwright test --config=playwright.visual.config.ts --update-snapshots",
|
|
57
61
|
"typecheck": "tsc --noEmit"
|
|
58
62
|
},
|
|
59
63
|
"types": "./dist/index.d.ts"
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
3
|
+
import { expect, userEvent, within } from 'storybook/test';
|
|
4
|
+
import { Accordion } from './Accordion';
|
|
5
|
+
|
|
6
|
+
const meta = {
|
|
7
|
+
title: 'Components/Accordion',
|
|
8
|
+
component: Accordion,
|
|
9
|
+
parameters: {
|
|
10
|
+
layout: 'padded',
|
|
11
|
+
docs: {
|
|
12
|
+
description: {
|
|
13
|
+
component:
|
|
14
|
+
'One or more Collapsible sections. type="single" closes other sections when one opens; type="multiple" allows any number open at once.',
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
args: { items: [], type: 'single', openIds: [], onOpenIdsChange: () => {} },
|
|
19
|
+
} satisfies Meta<typeof Accordion>;
|
|
20
|
+
|
|
21
|
+
export default meta;
|
|
22
|
+
type Story = StoryObj<typeof meta>;
|
|
23
|
+
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
const FAQ_ITEMS = [
|
|
27
|
+
{
|
|
28
|
+
id: 'plugins',
|
|
29
|
+
trigger: 'What plugins ship by default?',
|
|
30
|
+
content: 'Only the platform plugins (Console, Launcher, Account) plus Sovereign Tasks.',
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: 'hosting',
|
|
34
|
+
trigger: 'Can I self-host this?',
|
|
35
|
+
content: 'Yes — Sovereign is designed to be self-hosted via Docker Compose.',
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: 'pricing',
|
|
39
|
+
trigger: 'Is it free?',
|
|
40
|
+
content: 'The platform is open source. Individual plugins may have their own pricing.',
|
|
41
|
+
},
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
export const Default: Story = {
|
|
45
|
+
render: () => {
|
|
46
|
+
const [openIds, setOpenIds] = useState<string[]>(['plugins']);
|
|
47
|
+
return (
|
|
48
|
+
<Accordion items={FAQ_ITEMS} type="single" openIds={openIds} onOpenIdsChange={setOpenIds} />
|
|
49
|
+
);
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const MultipleOpen: Story = {
|
|
54
|
+
render: () => {
|
|
55
|
+
const [openIds, setOpenIds] = useState<string[]>(['plugins', 'hosting']);
|
|
56
|
+
return (
|
|
57
|
+
<Accordion items={FAQ_ITEMS} type="multiple" openIds={openIds} onOpenIdsChange={setOpenIds} />
|
|
58
|
+
);
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const LongContent: Story = {
|
|
63
|
+
render: () => {
|
|
64
|
+
const [openIds, setOpenIds] = useState<string[]>(['changelog']);
|
|
65
|
+
return (
|
|
66
|
+
<Accordion
|
|
67
|
+
items={[
|
|
68
|
+
{
|
|
69
|
+
id: 'changelog',
|
|
70
|
+
trigger: 'Full changelog',
|
|
71
|
+
content: (
|
|
72
|
+
<ul style={{ margin: 0, paddingLeft: 20 }}>
|
|
73
|
+
<li>Added component sizing alignment across the design system</li>
|
|
74
|
+
<li>Added RadioGroup, Slider, Progress, Table, Alert primitives</li>
|
|
75
|
+
<li>Added Breadcrumb, Pagination, and Kbd primitives</li>
|
|
76
|
+
<li>Added Accordion and Collapsible primitives</li>
|
|
77
|
+
</ul>
|
|
78
|
+
),
|
|
79
|
+
},
|
|
80
|
+
]}
|
|
81
|
+
type="single"
|
|
82
|
+
openIds={openIds}
|
|
83
|
+
onOpenIdsChange={setOpenIds}
|
|
84
|
+
/>
|
|
85
|
+
);
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export const KeyboardInteraction: Story = {
|
|
90
|
+
render: () => {
|
|
91
|
+
const [openIds, setOpenIds] = useState<string[]>([]);
|
|
92
|
+
return (
|
|
93
|
+
<Accordion items={FAQ_ITEMS} type="single" openIds={openIds} onOpenIdsChange={setOpenIds} />
|
|
94
|
+
);
|
|
95
|
+
},
|
|
96
|
+
play: async ({ canvasElement }) => {
|
|
97
|
+
const canvas = within(canvasElement);
|
|
98
|
+
const trigger = canvas.getByRole('button', { name: 'What plugins ship by default?' });
|
|
99
|
+
|
|
100
|
+
await expect(trigger).toHaveAttribute('aria-expanded', 'false');
|
|
101
|
+
|
|
102
|
+
trigger.focus();
|
|
103
|
+
await userEvent.keyboard('{Enter}');
|
|
104
|
+
await expect(trigger).toHaveAttribute('aria-expanded', 'true');
|
|
105
|
+
await expect(canvas.getByText(/Only the platform plugins/)).toBeVisible();
|
|
106
|
+
|
|
107
|
+
await userEvent.keyboard(' ');
|
|
108
|
+
await expect(trigger).toHaveAttribute('aria-expanded', 'false');
|
|
109
|
+
},
|
|
110
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { Collapsible } from '../Collapsible/Collapsible';
|
|
3
|
+
import styles from './Accordion.module.css';
|
|
4
|
+
|
|
5
|
+
export interface AccordionItem {
|
|
6
|
+
id: string;
|
|
7
|
+
trigger: ReactNode;
|
|
8
|
+
content: ReactNode;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface AccordionProps {
|
|
12
|
+
items: AccordionItem[];
|
|
13
|
+
/** `single` closes other sections when one opens. `multiple` allows any
|
|
14
|
+
* number of sections open at once. */
|
|
15
|
+
type: 'single' | 'multiple';
|
|
16
|
+
openIds: string[];
|
|
17
|
+
onOpenIdsChange: (openIds: string[]) => void;
|
|
18
|
+
className?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Accordion — one or more `Collapsible` sections.
|
|
23
|
+
*
|
|
24
|
+
* Keyboard support (Enter/Space toggles the focused trigger) comes from
|
|
25
|
+
* `Collapsible`'s real `<button>` — no arrow-key roving-tabindex handling
|
|
26
|
+
* here, since accordion sections aren't a roving-tabindex widget per the
|
|
27
|
+
* WAI-ARIA accordion pattern (each trigger is independently Tab-focusable).
|
|
28
|
+
*/
|
|
29
|
+
export function Accordion({ items, type, openIds, onOpenIdsChange, className }: AccordionProps) {
|
|
30
|
+
function handleOpenChange(id: string, open: boolean) {
|
|
31
|
+
if (type === 'single') {
|
|
32
|
+
onOpenIdsChange(open ? [id] : []);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
onOpenIdsChange(open ? [...openIds, id] : openIds.filter((openId) => openId !== id));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div className={[styles.root, className].filter(Boolean).join(' ')}>
|
|
40
|
+
{items.map((item) => (
|
|
41
|
+
<Collapsible
|
|
42
|
+
key={item.id}
|
|
43
|
+
id={item.id}
|
|
44
|
+
trigger={item.trigger}
|
|
45
|
+
open={openIds.includes(item.id)}
|
|
46
|
+
onOpenChange={(open) => handleOpenChange(item.id, open)}
|
|
47
|
+
>
|
|
48
|
+
{item.content}
|
|
49
|
+
</Collapsible>
|
|
50
|
+
))}
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
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 { Accordion } from '../Accordion';
|
|
5
|
+
|
|
6
|
+
afterEach(cleanup);
|
|
7
|
+
|
|
8
|
+
const items = [
|
|
9
|
+
{ id: 'a', trigger: 'Section A', content: 'Content A' },
|
|
10
|
+
{ id: 'b', trigger: 'Section B', content: 'Content B' },
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
describe('Accordion', () => {
|
|
14
|
+
it('renders a trigger button per item', () => {
|
|
15
|
+
render(<Accordion items={items} type="single" openIds={[]} onOpenIdsChange={() => {}} />);
|
|
16
|
+
expect(screen.getByRole('button', { name: 'Section A' })).toBeDefined();
|
|
17
|
+
expect(screen.getByRole('button', { name: 'Section B' })).toBeDefined();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('type="single": opening one section replaces the open set entirely', () => {
|
|
21
|
+
const onOpenIdsChange = vi.fn();
|
|
22
|
+
render(
|
|
23
|
+
<Accordion items={items} type="single" openIds={['a']} onOpenIdsChange={onOpenIdsChange} />,
|
|
24
|
+
);
|
|
25
|
+
fireEvent.click(screen.getByRole('button', { name: 'Section B' }));
|
|
26
|
+
expect(onOpenIdsChange).toHaveBeenCalledWith(['b']);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('type="multiple": opening a section adds to the open set', () => {
|
|
30
|
+
const onOpenIdsChange = vi.fn();
|
|
31
|
+
render(
|
|
32
|
+
<Accordion items={items} type="multiple" openIds={['a']} onOpenIdsChange={onOpenIdsChange} />,
|
|
33
|
+
);
|
|
34
|
+
fireEvent.click(screen.getByRole('button', { name: 'Section B' }));
|
|
35
|
+
expect(onOpenIdsChange).toHaveBeenCalledWith(['a', 'b']);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('type="multiple": closing an open section removes only that id', () => {
|
|
39
|
+
const onOpenIdsChange = vi.fn();
|
|
40
|
+
render(
|
|
41
|
+
<Accordion
|
|
42
|
+
items={items}
|
|
43
|
+
type="multiple"
|
|
44
|
+
openIds={['a', 'b']}
|
|
45
|
+
onOpenIdsChange={onOpenIdsChange}
|
|
46
|
+
/>,
|
|
47
|
+
);
|
|
48
|
+
fireEvent.click(screen.getByRole('button', { name: 'Section A' }));
|
|
49
|
+
expect(onOpenIdsChange).toHaveBeenCalledWith(['b']);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('reflects openIds via aria-expanded on each trigger', () => {
|
|
53
|
+
render(<Accordion items={items} type="single" openIds={['a']} onOpenIdsChange={() => {}} />);
|
|
54
|
+
expect(screen.getByRole('button', { name: 'Section A' }).getAttribute('aria-expanded')).toBe(
|
|
55
|
+
'true',
|
|
56
|
+
);
|
|
57
|
+
expect(screen.getByRole('button', { name: 'Section B' }).getAttribute('aria-expanded')).toBe(
|
|
58
|
+
'false',
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
.alert {
|
|
2
|
+
display: flex;
|
|
3
|
+
align-items: flex-start;
|
|
4
|
+
gap: var(--sv-space-2);
|
|
5
|
+
padding: var(--sv-space-3) var(--sv-space-4);
|
|
6
|
+
border: 1px solid transparent;
|
|
7
|
+
border-radius: var(--sv-radius-md);
|
|
8
|
+
font-size: var(--sv-font-size-sm);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.icon {
|
|
12
|
+
flex-shrink: 0;
|
|
13
|
+
/* Optically align to the first line of text rather than the box's
|
|
14
|
+
vertical center — matches how the heading/body cap-height sits. */
|
|
15
|
+
margin-top: 2px;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.content {
|
|
19
|
+
display: flex;
|
|
20
|
+
flex-direction: column;
|
|
21
|
+
gap: var(--sv-space-1);
|
|
22
|
+
min-width: 0;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.heading {
|
|
26
|
+
margin: 0;
|
|
27
|
+
font-weight: var(--sv-font-weight-semibold);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.body {
|
|
31
|
+
margin: 0;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.info {
|
|
35
|
+
background: var(--sv-color-info-surface);
|
|
36
|
+
border-color: var(--sv-color-info-border);
|
|
37
|
+
color: var(--sv-color-info-text);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.success {
|
|
41
|
+
background: var(--sv-color-success-surface);
|
|
42
|
+
border-color: var(--sv-color-success-border);
|
|
43
|
+
color: var(--sv-color-success-text);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.warning {
|
|
47
|
+
background: var(--sv-color-warning-surface);
|
|
48
|
+
border-color: var(--sv-color-warning-border);
|
|
49
|
+
color: var(--sv-color-warning-text);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.error {
|
|
53
|
+
background: var(--sv-color-error-surface);
|
|
54
|
+
border-color: var(--sv-color-error-border);
|
|
55
|
+
color: var(--sv-color-error-text);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.neutral {
|
|
59
|
+
background: var(--sv-color-surface-sunken);
|
|
60
|
+
border-color: var(--sv-color-border);
|
|
61
|
+
color: var(--sv-color-text-primary);
|
|
62
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
import { Alert } from './Alert';
|
|
3
|
+
|
|
4
|
+
const meta = {
|
|
5
|
+
title: 'Components/Alert',
|
|
6
|
+
component: Alert,
|
|
7
|
+
parameters: {
|
|
8
|
+
layout: 'padded',
|
|
9
|
+
docs: {
|
|
10
|
+
description: {
|
|
11
|
+
component:
|
|
12
|
+
'Inline, non-dismissible banner. Distinct from Toast (transient) and SystemBanner (instance-wide) — for form-level errors or explaining an empty/blocked state. A leading icon defaults per variant (none for neutral) — pass `icon` to override it or `icon={false}` to suppress it.',
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
argTypes: {
|
|
17
|
+
variant: { control: 'select', options: ['info', 'success', 'warning', 'error', 'neutral'] },
|
|
18
|
+
},
|
|
19
|
+
args: {
|
|
20
|
+
variant: 'info',
|
|
21
|
+
children: 'This is an informational message.',
|
|
22
|
+
},
|
|
23
|
+
} satisfies Meta<typeof Alert>;
|
|
24
|
+
|
|
25
|
+
export default meta;
|
|
26
|
+
type Story = StoryObj<typeof meta>;
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
export const Info: Story = {
|
|
31
|
+
args: { variant: 'info' },
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const Success: Story = {
|
|
35
|
+
args: { variant: 'success', children: 'Your changes have been saved.' },
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const Warning: Story = {
|
|
39
|
+
args: { variant: 'warning', children: 'This action cannot be undone.' },
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const ErrorVariant: Story = {
|
|
43
|
+
name: 'Error',
|
|
44
|
+
args: { variant: 'error', children: 'Select a size to continue.' },
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const Neutral: Story = {
|
|
48
|
+
args: { variant: 'neutral', children: 'This project has no members yet.' },
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const WithHeading: Story = {
|
|
52
|
+
args: {
|
|
53
|
+
variant: 'error',
|
|
54
|
+
heading: 'Something went wrong',
|
|
55
|
+
children: 'We couldn’t save your changes. Check your connection and try again.',
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const WithoutHeading: Story = {
|
|
60
|
+
args: { variant: 'success', children: 'Invite sent.' },
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/** Icon + heading + body — the pattern most alert examples use. */
|
|
64
|
+
export const IconHeadingAndBody: Story = {
|
|
65
|
+
args: {
|
|
66
|
+
variant: 'success',
|
|
67
|
+
heading: 'Payment successful',
|
|
68
|
+
children:
|
|
69
|
+
'Your payment of $29.99 has been processed. A receipt has been sent to your email address.',
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const CustomIcon: Story = {
|
|
74
|
+
args: { variant: 'neutral', icon: 'bell', children: 'You have 3 unread notifications.' },
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export const NoIcon: Story = {
|
|
78
|
+
args: { variant: 'success', icon: false, children: 'Your changes have been saved.' },
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/** Every variant with its default icon, at a glance. */
|
|
82
|
+
export const AllVariants: Story = {
|
|
83
|
+
render: () => (
|
|
84
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 12, maxWidth: 480 }}>
|
|
85
|
+
<Alert variant="info" heading="New feature available">
|
|
86
|
+
We’ve added dark mode support. You can enable it in your account settings.
|
|
87
|
+
</Alert>
|
|
88
|
+
<Alert variant="success" heading="Payment successful">
|
|
89
|
+
Your payment of $29.99 has been processed. A receipt has been sent to your email address.
|
|
90
|
+
</Alert>
|
|
91
|
+
<Alert variant="warning">This action cannot be undone.</Alert>
|
|
92
|
+
<Alert variant="error">Select a size to continue.</Alert>
|
|
93
|
+
<Alert variant="neutral">This project has no members yet.</Alert>
|
|
94
|
+
</div>
|
|
95
|
+
),
|
|
96
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { Icon, type IconName } from '../Icon/Icon';
|
|
3
|
+
import styles from './Alert.module.css';
|
|
4
|
+
|
|
5
|
+
export type AlertVariant = 'info' | 'success' | 'warning' | 'error' | 'neutral';
|
|
6
|
+
|
|
7
|
+
/** Sensible default icon per variant. `neutral` has none — there's no
|
|
8
|
+
* universal "neutral" glyph, so it stays icon-less unless `icon` is set
|
|
9
|
+
* explicitly. */
|
|
10
|
+
const DEFAULT_ICON: Record<AlertVariant, IconName | undefined> = {
|
|
11
|
+
info: 'info',
|
|
12
|
+
success: 'circle-check',
|
|
13
|
+
warning: 'alert-triangle',
|
|
14
|
+
error: 'circle-x',
|
|
15
|
+
neutral: undefined,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export interface AlertProps {
|
|
19
|
+
variant: AlertVariant;
|
|
20
|
+
heading?: string;
|
|
21
|
+
/** Leading icon next to the heading. Defaults to a per-variant icon (see
|
|
22
|
+
* `DEFAULT_ICON`) — pass an `IconName` to override it, or `false` to
|
|
23
|
+
* suppress the icon entirely. Purely decorative: the variant's colour and
|
|
24
|
+
* `heading`/`children` text already carry the meaning. */
|
|
25
|
+
icon?: IconName | false;
|
|
26
|
+
children: ReactNode;
|
|
27
|
+
id?: string;
|
|
28
|
+
className?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Alert — inline, non-dismissible banner.
|
|
33
|
+
*
|
|
34
|
+
* Distinct from `Toast` (transient, auto-dismissing) and `SystemBanner`
|
|
35
|
+
* (instance-wide): Alert is for in-context messaging — form-level errors,
|
|
36
|
+
* or explaining an empty/blocked state.
|
|
37
|
+
*
|
|
38
|
+
* `role="alert"` for `error` (interrupts screen readers immediately, since
|
|
39
|
+
* an error demands attention); `role="status"` for the other variants
|
|
40
|
+
* (announced politely, without interrupting).
|
|
41
|
+
*/
|
|
42
|
+
export function Alert({ variant, heading, icon, children, id, className }: AlertProps) {
|
|
43
|
+
const resolvedIcon = icon === undefined ? DEFAULT_ICON[variant] : icon || undefined;
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div
|
|
47
|
+
id={id}
|
|
48
|
+
role={variant === 'error' ? 'alert' : 'status'}
|
|
49
|
+
className={[styles.alert, styles[variant], className].filter(Boolean).join(' ')}
|
|
50
|
+
>
|
|
51
|
+
{resolvedIcon && <Icon name={resolvedIcon} size="sm" className={styles.icon} aria-hidden />}
|
|
52
|
+
<div className={styles.content}>
|
|
53
|
+
{heading && <p className={styles.heading}>{heading}</p>}
|
|
54
|
+
<div className={styles.body}>{children}</div>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
3
|
+
import { cleanup, render, screen } from '@testing-library/react';
|
|
4
|
+
import { Alert } from '../Alert';
|
|
5
|
+
|
|
6
|
+
afterEach(cleanup);
|
|
7
|
+
|
|
8
|
+
describe('Alert', () => {
|
|
9
|
+
it('uses role="alert" for the error variant', () => {
|
|
10
|
+
render(<Alert variant="error">Something went wrong.</Alert>);
|
|
11
|
+
expect(screen.getByRole('alert')).toBeDefined();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it.each(['info', 'success', 'warning', 'neutral'] as const)(
|
|
15
|
+
'uses role="status" for the %s variant',
|
|
16
|
+
(variant) => {
|
|
17
|
+
render(<Alert variant={variant}>Message</Alert>);
|
|
18
|
+
expect(screen.getByRole('status')).toBeDefined();
|
|
19
|
+
},
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
it('renders a heading when given one', () => {
|
|
23
|
+
render(
|
|
24
|
+
<Alert variant="warning" heading="Heads up">
|
|
25
|
+
Body text.
|
|
26
|
+
</Alert>,
|
|
27
|
+
);
|
|
28
|
+
expect(screen.getByText('Heads up')).toBeDefined();
|
|
29
|
+
expect(screen.getByText('Body text.')).toBeDefined();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('renders without a heading', () => {
|
|
33
|
+
render(<Alert variant="info">Just the body.</Alert>);
|
|
34
|
+
expect(screen.getByText('Just the body.')).toBeDefined();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('renders a default icon for info, success, warning, and error, but none for neutral', () => {
|
|
38
|
+
const info = render(<Alert variant="info">Message</Alert>);
|
|
39
|
+
expect(info.container.querySelector('svg')).not.toBeNull();
|
|
40
|
+
info.unmount();
|
|
41
|
+
|
|
42
|
+
const success = render(<Alert variant="success">Message</Alert>);
|
|
43
|
+
expect(success.container.querySelector('svg')).not.toBeNull();
|
|
44
|
+
success.unmount();
|
|
45
|
+
|
|
46
|
+
const warning = render(<Alert variant="warning">Message</Alert>);
|
|
47
|
+
expect(warning.container.querySelector('svg')).not.toBeNull();
|
|
48
|
+
warning.unmount();
|
|
49
|
+
|
|
50
|
+
const error = render(<Alert variant="error">Message</Alert>);
|
|
51
|
+
expect(error.container.querySelector('svg')).not.toBeNull();
|
|
52
|
+
error.unmount();
|
|
53
|
+
|
|
54
|
+
const neutral = render(<Alert variant="neutral">Message</Alert>);
|
|
55
|
+
expect(neutral.container.querySelector('svg')).toBeNull();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('suppresses the default icon when icon={false}', () => {
|
|
59
|
+
const { container } = render(
|
|
60
|
+
<Alert variant="success" icon={false}>
|
|
61
|
+
Message
|
|
62
|
+
</Alert>,
|
|
63
|
+
);
|
|
64
|
+
expect(container.querySelector('svg')).toBeNull();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('overrides the default icon when icon is set explicitly', () => {
|
|
68
|
+
const { container } = render(
|
|
69
|
+
<Alert variant="neutral" icon="info">
|
|
70
|
+
Message
|
|
71
|
+
</Alert>,
|
|
72
|
+
);
|
|
73
|
+
expect(container.querySelector('svg')).not.toBeNull();
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
.root {
|
|
2
|
+
position: relative;
|
|
3
|
+
width: 100%;
|
|
4
|
+
overflow: hidden;
|
|
5
|
+
border-radius: var(--sv-radius-md);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/* A single direct child (img/video/iframe) fills the ratio box. */
|
|
9
|
+
.root > :global(*) {
|
|
10
|
+
position: absolute;
|
|
11
|
+
inset: 0;
|
|
12
|
+
width: 100%;
|
|
13
|
+
height: 100%;
|
|
14
|
+
object-fit: cover;
|
|
15
|
+
}
|