@ramme-io/ui 4.1.0 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +4 -3
  2. package/dist/component-registry.js +55 -52
  3. package/dist/components/ai/AIChatThread/AIChatThread.js +3 -3
  4. package/dist/components/ai/NewsSummaryCard/NewsSummaryCard.js +17 -17
  5. package/dist/components/ai/StockChart/StockChart.js +35 -35
  6. package/dist/components/auth/LoginPage.d.ts +22 -0
  7. package/dist/components/auth/LoginPage.js +50 -42
  8. package/dist/components/data-display/StatusBadge.d.ts +4 -3
  9. package/dist/components/data-display/StatusBadge.js +18 -18
  10. package/dist/components/feedback/Alert.js +19 -19
  11. package/dist/components/forms/ComboBox.js +2 -2
  12. package/dist/components/forms/DatePicker.js +37 -32
  13. package/dist/components/forms/FileUpload.js +39 -37
  14. package/dist/components/forms/Input.js +19 -18
  15. package/dist/components/forms/MultiSelect.js +9 -8
  16. package/dist/components/forms/SegmentedControl.js +10 -10
  17. package/dist/components/forms/Select.js +9 -8
  18. package/dist/components/forms/Textarea.d.ts +13 -11
  19. package/dist/components/forms/Textarea.js +36 -16
  20. package/dist/components/layout/Accordion.js +25 -32
  21. package/dist/components/layout/Card.js +38 -21
  22. package/dist/components/layout/DetailDrawer.d.ts +9 -1
  23. package/dist/components/layout/DetailDrawer.js +60 -51
  24. package/dist/components/layout/Drawer.js +47 -39
  25. package/dist/components/layout/Modal.js +28 -23
  26. package/dist/components/layout/SettingsContainer.js +23 -13
  27. package/dist/components/layout/Sheet.d.ts +58 -0
  28. package/dist/components/layout/Sheet.js +146 -0
  29. package/dist/components/navigation/MobileNav.d.ts +44 -1
  30. package/dist/components/navigation/MobileNav.js +96 -70
  31. package/dist/components/navigation/Tabs.js +19 -12
  32. package/dist/components/ui/icon-loader.d.ts +1 -1
  33. package/dist/index.css +1 -1
  34. package/dist/index.d.ts +3 -0
  35. package/dist/index.js +218 -209
  36. package/dist/lib/hooks/useDialogFocus.d.ts +1 -0
  37. package/dist/lib/hooks/useDialogFocus.js +46 -0
  38. package/dist/lib/hooks/useSheetGesture.d.ts +81 -0
  39. package/dist/lib/hooks/useSheetGesture.js +70 -0
  40. package/dist/setupTests.d.ts +1 -0
  41. package/dist/wizardManifest.js +139 -14
  42. package/package.json +9 -2
  43. package/src/wizardManifest.ts +139 -14
  44. package/tailwind.preset.js +8 -0
@@ -0,0 +1,81 @@
1
+ /**
2
+ * The gesture behind a bottom sheet.
3
+ *
4
+ * This is the library's first pointer-driven interaction. Nothing else in
5
+ * @ramme-io/ui uses Pointer Events at all — every interactive component is an
6
+ * HTML form element — so the rules it establishes are worth stating once:
7
+ *
8
+ * - **Pointer Events only.** One code path for touch, mouse, pen and trackpad.
9
+ * Separate `mousemove` and `touchmove` handlers are how you end up with a
10
+ * control that works on one and jitters on the other.
11
+ * - **`setPointerCapture`.** A drag must survive the finger leaving the
12
+ * element. Without capture, dragging a sheet up past its own top edge stops
13
+ * tracking, which reads as the sheet sticking.
14
+ * - **`touch-action: none`** on the drag surface, applied by the component.
15
+ * Without it the browser claims the gesture for scrolling and the sheet
16
+ * never moves.
17
+ *
18
+ * WHY THE SETTLE MATH IS A PURE FUNCTION
19
+ *
20
+ * jsdom has no layout and cannot drag, so the only honest way to test a gesture
21
+ * is to make its decisions independent of the DOM. `settleTo` below takes
22
+ * numbers and returns a number; every interesting rule — flick beats position,
23
+ * dragging below the smallest detent dismisses, a slow drag snaps to nearest —
24
+ * is checkable without a browser.
25
+ */
26
+ /** Pixels per millisecond past which a drag counts as a flick rather than a move. */
27
+ export declare const FLICK_VELOCITY = 0.5;
28
+ /**
29
+ * How far below the smallest detent the sheet must be dragged before release
30
+ * dismisses it. Expressed as a fraction of the viewport so it scales with the
31
+ * screen rather than being generous on a tablet and twitchy on a phone.
32
+ */
33
+ export declare const DISMISS_THRESHOLD = 0.12;
34
+ export interface SettleInput {
35
+ /** Current height of the sheet as a fraction of the viewport, 0–1. */
36
+ current: number;
37
+ /** Allowed resting heights, ascending fractions of the viewport. */
38
+ detents: number[];
39
+ /**
40
+ * Vertical velocity in px/ms. NEGATIVE is upward (the sheet growing), matching
41
+ * pointer coordinates where y decreases toward the top of the screen.
42
+ */
43
+ velocity: number;
44
+ }
45
+ /**
46
+ * Where the sheet should come to rest when the finger lifts.
47
+ *
48
+ * Returns the target detent, or `null` meaning "dismiss".
49
+ *
50
+ * Order matters: a flick is honoured before position, because a fast downward
51
+ * flick from near the top should close the sheet rather than snap it to the
52
+ * detent it happens to be nearest. That is what every native sheet does, and
53
+ * getting it backwards makes the sheet feel like it is fighting you.
54
+ */
55
+ export declare function settleTo({ current, detents, velocity }: SettleInput): number | null;
56
+ export interface SheetGestureOptions {
57
+ detents: number[];
58
+ initialDetent?: number;
59
+ onDismiss: () => void;
60
+ /** Skips the drag entirely; the sheet still opens and closes, just without motion. */
61
+ reducedMotion?: boolean;
62
+ }
63
+ export interface SheetGesture {
64
+ /** Current height as a fraction of the viewport. */
65
+ height: number;
66
+ /** True while a finger is down — the component uses this to drop its transition. */
67
+ dragging: boolean;
68
+ /** True when the sheet is at its tallest detent. */
69
+ expanded: boolean;
70
+ /** Attach to the grabber and header. */
71
+ onPointerDown: (e: React.PointerEvent) => void;
72
+ /**
73
+ * Attach to the scrollable body. Implements the behaviour the pattern is
74
+ * named for: at a partial detent, dragging up grows the sheet instead of
75
+ * scrolling; once expanded, the content scrolls normally; and at the top of
76
+ * the content, dragging down shrinks the sheet again.
77
+ */
78
+ onBodyPointerDown: (e: React.PointerEvent) => void;
79
+ setHeight: (h: number) => void;
80
+ }
81
+ export declare function useSheetGesture({ detents, initialDetent, onDismiss, reducedMotion, }: SheetGestureOptions): SheetGesture;
@@ -0,0 +1,70 @@
1
+ import { useState as M, useRef as u, useCallback as T, useEffect as C } from "react";
2
+ const D = 0.5, P = 0.12;
3
+ function y({ current: l, detents: f, velocity: a }) {
4
+ if (!f.length) return null;
5
+ const s = [...f].sort((r, e) => r - e), n = s[0], c = s[s.length - 1];
6
+ if (Math.abs(a) >= D) {
7
+ if (a > 0) {
8
+ const e = s.filter((g) => g < l - 0.01);
9
+ return e.length ? e[e.length - 1] : null;
10
+ }
11
+ const r = s.filter((e) => e > l + 0.01);
12
+ return r.length ? r[0] : c;
13
+ }
14
+ return l < n - P ? null : s.reduce(
15
+ (r, e) => Math.abs(e - l) < Math.abs(r - l) ? e : r
16
+ );
17
+ }
18
+ function R({
19
+ detents: l,
20
+ initialDetent: f = 0,
21
+ onDismiss: a,
22
+ reducedMotion: s = !1
23
+ }) {
24
+ const n = [...l].sort((t, i) => t - i), [c, r] = M(n[f] ?? n[0]), [e, g] = M(!1), L = u(0), S = u(0), w = u(0), v = u(0), E = u(0), d = u(null), h = T(
25
+ (t, i) => {
26
+ if (!s && t.isPrimary) {
27
+ if (d.current = i ? t.currentTarget : null, i) {
28
+ const o = t.currentTarget.scrollTop <= 0;
29
+ if (c >= n[n.length - 1] - 1e-3 && !o) return;
30
+ }
31
+ L.current = t.clientY, w.current = t.clientY, v.current = t.timeStamp, S.current = c, E.current = 0, g(!0), t.currentTarget.setPointerCapture(t.pointerId);
32
+ }
33
+ },
34
+ [c, s, n]
35
+ ), b = T((t) => h(t, !1), [h]), H = T((t) => h(t, !0), [h]);
36
+ return C(() => {
37
+ if (!e) return;
38
+ const t = (o) => {
39
+ const Y = window.innerHeight || 1, I = o.clientY - L.current, x = o.timeStamp - v.current;
40
+ x > 0 && (E.current = (o.clientY - w.current) / x), w.current = o.clientY, v.current = o.timeStamp;
41
+ let p = S.current - I / Y;
42
+ const m = n[n.length - 1];
43
+ p > m && (p = m + (p - m) * 0.25), r(Math.max(0, Math.min(p, m + 0.05))), d.current && (d.current.scrollTop = 0);
44
+ }, i = () => {
45
+ g(!1);
46
+ const o = y({
47
+ current: c,
48
+ detents: n,
49
+ velocity: E.current
50
+ });
51
+ o === null ? a() : r(o), d.current = null;
52
+ };
53
+ return window.addEventListener("pointermove", t), window.addEventListener("pointerup", i), window.addEventListener("pointercancel", i), () => {
54
+ window.removeEventListener("pointermove", t), window.removeEventListener("pointerup", i), window.removeEventListener("pointercancel", i);
55
+ };
56
+ }, [e, c, a, n]), {
57
+ height: c,
58
+ dragging: e,
59
+ expanded: c >= n[n.length - 1] - 1e-3,
60
+ onPointerDown: b,
61
+ onBodyPointerDown: H,
62
+ setHeight: r
63
+ };
64
+ }
65
+ export {
66
+ P as DISMISS_THRESHOLD,
67
+ D as FLICK_VELOCITY,
68
+ y as settleTo,
69
+ R as useSheetGesture
70
+ };
@@ -0,0 +1 @@
1
+ import '@testing-library/jest-dom/vitest';
@@ -596,7 +596,7 @@ const e = {
596
596
  {
597
597
  id: "login-page",
598
598
  name: "LoginPage",
599
- version: "1.0.0",
599
+ version: "1.0.1",
600
600
  description: "The standard sign-in screen — email and password, with inline error and loading states already wired. Strategic purpose: a drop-in credential form so products don't rebuild one; `quickProfiles` adds one-click demo personas, which is what makes a prototype instantly explorable without real accounts.",
601
601
  tags: [
602
602
  "auth",
@@ -626,6 +626,21 @@ const e = {
626
626
  name: "quickProfiles",
627
627
  type: "Array<QuickProfile>",
628
628
  description: "Optional one-click demo identities, each `{ name, email, role }`."
629
+ },
630
+ {
631
+ name: "signupHref",
632
+ type: "string",
633
+ description: 'Where "Create one" points. Pass `null` to hide that line entirely, which is what an invite-only product wants. Defaults to `/auth/signup`.'
634
+ },
635
+ {
636
+ name: "signupLabel",
637
+ type: "string",
638
+ description: 'Text for that link. Defaults to "Create one".'
639
+ },
640
+ {
641
+ name: "forgotHref",
642
+ type: "string",
643
+ description: 'Where "Forgot?" points. Pass `null` to hide it — a product with no reset email has nothing to link to. Defaults to `#`.'
629
644
  }
630
645
  ],
631
646
  importPath: "./components/auth/LoginPage",
@@ -1083,7 +1098,7 @@ const e = {
1083
1098
  {
1084
1099
  id: "status-badge",
1085
1100
  name: "StatusBadge",
1086
- version: "1.0.1",
1101
+ version: "1.0.2",
1087
1102
  description: "A consistent status indicator featuring a color-coded dot and an optional text label. Essential for IoT devices and system states.",
1088
1103
  tags: [
1089
1104
  "status",
@@ -1102,7 +1117,7 @@ const e = {
1102
1117
  {
1103
1118
  name: "color",
1104
1119
  type: "string",
1105
- description: "Tailwind background class for the dot (e.g. 'bg-emerald-500'). Overrides the `status` preset's color when both are given."
1120
+ description: "Background class for the dot (e.g. 'bg-success'). Use a semantic token — a numeric shade does not adapt to the active theme. Overrides the `status` preset's colour when both are given."
1106
1121
  },
1107
1122
  {
1108
1123
  name: "label",
@@ -1961,31 +1976,53 @@ const e = {
1961
1976
  {
1962
1977
  id: "textarea",
1963
1978
  name: "Textarea",
1964
- version: "1.0.0",
1965
- description: "A theme-aware textarea component for multi-line text input.",
1979
+ version: "1.0.1",
1980
+ description: "A theme-aware multi-line text input with label, helper text, and validation state support. Strategic purpose: the multi-line sibling of Input, styled identically so the two are indistinguishable when stacked in one form. Forwards all standard HTML textarea attributes via spread.",
1966
1981
  tags: [
1967
1982
  "form",
1968
1983
  "input",
1969
- "ui"
1984
+ "ui",
1985
+ "text",
1986
+ "multiline"
1970
1987
  ],
1971
1988
  category: "form",
1972
1989
  props: [
1973
1990
  {
1974
1991
  name: "label",
1975
1992
  type: "string",
1976
- description: "An optional label to display above the textarea."
1993
+ description: "Optional label displayed above the textarea."
1994
+ },
1995
+ {
1996
+ name: "helperText",
1997
+ type: "string",
1998
+ description: "Supporting text rendered below the textarea. Appears in destructive color when error is true."
1999
+ },
2000
+ {
2001
+ name: "error",
2002
+ type: "boolean",
2003
+ description: "If true, applies destructive border styling to indicate a validation error. Defaults to false."
2004
+ },
2005
+ {
2006
+ name: "required",
2007
+ type: "boolean",
2008
+ description: "If true, appends a required (*) indicator next to the label. Defaults to false."
2009
+ },
2010
+ {
2011
+ name: "showOptionalLabel",
2012
+ type: "boolean",
2013
+ description: "If true, appends an (optional) label when the field is not required. Defaults to false."
1977
2014
  },
1978
2015
  {
1979
2016
  name: "containerClassName",
1980
2017
  type: "string",
1981
- description: "Optional additional CSS classes for the container div."
2018
+ description: "Additional CSS classes applied to the outer container div."
1982
2019
  }
1983
2020
  ],
1984
2021
  importPath: "./components/forms/Textarea",
1985
2022
  filePath: "src/components/forms/Textarea.tsx",
1986
2023
  sku: "@atom/form/textarea",
1987
2024
  humanLabel: "Textarea",
1988
- humanDescription: "A theme-aware textarea component for multi-line text input."
2025
+ humanDescription: "A theme-aware multi-line text input with label, helper text, and validation state support. Strategic purpose: the multi-line sibling of Input, styled identically so the two are indistinguishable when stacked in one form. Forwards all standard HTML textarea attributes via spread."
1989
2026
  },
1990
2027
  {
1991
2028
  id: "toggle-switch",
@@ -2619,8 +2656,8 @@ const e = {
2619
2656
  {
2620
2657
  id: "mobile-nav",
2621
2658
  name: "MobileNav",
2622
- version: "1.0.0",
2623
- description: 'A fixed bottom tab bar for mobile screens, providing native-feeling navigation with primary icon+label tabs and a "More" drawer for secondary links. Hidden on md: and up via Tailwind. Designed as the mobile counterpart to the desktop Sidebar.',
2659
+ version: "1.0.3",
2660
+ description: `Mobile navigation, in two shapes. 'bar' is a fixed bottom tab bar with icon+label tabs and a "More" drawer for secondary links. 'sheet' drops the bar entirely and puts every destination in a bottom drawer the host opens from its own trigger — the right choice when a mobile browser's toolbar already owns the bottom edge. Hidden on md: and up via Tailwind. The mobile counterpart to the desktop Sidebar.`,
2624
2661
  tags: [
2625
2662
  "navigation",
2626
2663
  "mobile",
@@ -2669,13 +2706,33 @@ const e = {
2669
2706
  name: "className",
2670
2707
  type: "string",
2671
2708
  description: "Additional CSS classes on the root nav bar."
2709
+ },
2710
+ {
2711
+ name: "variant",
2712
+ type: "string",
2713
+ description: `'bar' (default) renders the fixed bottom tab bar with a "More" Drawer. 'sheet' renders no bar at all and puts every destination in a Sheet — opening at 60% and growing to full as you drag or scroll it up — which the host opens itself — use it when a top header can hold the trigger, since a fixed bar and a mobile browser's own toolbar stack into ~120px of lost screen.`
2714
+ },
2715
+ {
2716
+ name: "isOpen",
2717
+ type: "boolean",
2718
+ description: "Controlled drawer state. Required by 'sheet', which has no trigger of its own; ignored by 'bar'."
2719
+ },
2720
+ {
2721
+ name: "onOpenChange",
2722
+ type: "function",
2723
+ description: "Fired when the sheet wants to close (backdrop, Escape, or an item tapped)."
2724
+ },
2725
+ {
2726
+ name: "children",
2727
+ type: "ReactNode",
2728
+ description: "Extra content rendered in the panel body after the destinations, scrolling with them. The mirror of drawerHeader; use it instead of drawerFooter when the content belongs with the list rather than pinned under it."
2672
2729
  }
2673
2730
  ],
2674
2731
  importPath: "./components/navigation/MobileNav",
2675
2732
  filePath: "src/components/navigation/MobileNav.tsx",
2676
2733
  sku: "@molecule/nav/mobile-nav",
2677
2734
  humanLabel: "MobileNav",
2678
- humanDescription: 'A fixed bottom tab bar for mobile screens, providing native-feeling navigation with primary icon+label tabs and a "More" drawer for secondary links. Hidden on md: and up via Tailwind. Designed as the mobile counterpart to the desktop Sidebar.'
2735
+ humanDescription: `Mobile navigation, in two shapes. 'bar' is a fixed bottom tab bar with icon+label tabs and a "More" drawer for secondary links. 'sheet' drops the bar entirely and puts every destination in a bottom drawer the host opens from its own trigger — the right choice when a mobile browser's toolbar already owns the bottom edge. Hidden on md: and up via Tailwind. The mobile counterpart to the desktop Sidebar.`
2679
2736
  },
2680
2737
  {
2681
2738
  id: "pagination",
@@ -3614,7 +3671,7 @@ const e = {
3614
3671
  {
3615
3672
  id: "detail-drawer",
3616
3673
  name: "DetailDrawer",
3617
- version: "1.0.0",
3674
+ version: "1.0.2",
3618
3675
  description: 'A resizable side panel for inspecting one record without leaving the list. Strategic purpose: the master-detail companion — `push` compresses its siblings in a flex layout so the list stays visible and usable, while `overlay` floats above the page like a classic drawer. Use this for "click a row, see the detail" flows; use Modal when the task must block everything else.',
3619
3676
  tags: [
3620
3677
  "layout",
@@ -3639,7 +3696,7 @@ const e = {
3639
3696
  {
3640
3697
  name: "variant",
3641
3698
  type: "string",
3642
- description: "'push' compresses sibling flex content so the list stays visible; 'overlay' floats fixed above the page. Defaults to push."
3699
+ description: "'push' compresses sibling flex content so the list stays visible; 'overlay' floats fixed above the page. Defaults to overlay."
3643
3700
  },
3644
3701
  {
3645
3702
  name: "side",
@@ -3671,6 +3728,11 @@ const e = {
3671
3728
  type: "number",
3672
3729
  description: "Upper bound for resizing. Defaults to 60% of the window width."
3673
3730
  },
3731
+ {
3732
+ name: "title",
3733
+ type: "string",
3734
+ description: 'Names the panel for assistive technology. Not rendered: there is no header row to put it in, and adding one is a design decision. Without it an overlay announces only as "dialog".'
3735
+ },
3674
3736
  {
3675
3737
  name: "children",
3676
3738
  type: "ReactNode",
@@ -4086,6 +4148,69 @@ const e = {
4086
4148
  humanLabel: "SettingsContainer",
4087
4149
  humanDescription: "The canonical settings page arrangement, with a fixed slot per section so every app's settings read the same way. Strategic purpose: settings sprawl fast — this pins the section order and navigation so General, AI Gateway, Theme Lab, and About always live in the same place."
4088
4150
  },
4151
+ {
4152
+ id: "sheet",
4153
+ name: "Sheet",
4154
+ version: "1.0.0",
4155
+ description: "A bottom sheet that opens at a partial height and grows to full screen as you drag or scroll it up. Strategic purpose: the native-feeling surface for mobile navigation, settings and pickers — reachable by thumb, dismissed by flicking down. Use instead of Drawer when the surface is primarily touched.",
4156
+ tags: [
4157
+ "layout",
4158
+ "overlay",
4159
+ "sheet",
4160
+ "mobile",
4161
+ "touch",
4162
+ "drawer",
4163
+ "ui"
4164
+ ],
4165
+ category: "layout",
4166
+ props: [
4167
+ {
4168
+ name: "isOpen",
4169
+ type: "boolean",
4170
+ description: "Controls the open/closed state. Required."
4171
+ },
4172
+ {
4173
+ name: "onClose",
4174
+ type: "function",
4175
+ description: "Fired when the sheet requests dismissal (flick down, drag below the smallest detent, Escape, overlay tap, or the close button). Required."
4176
+ },
4177
+ {
4178
+ name: "detents",
4179
+ type: "Array<number>",
4180
+ description: "Resting heights as fractions of the viewport, ascending. Defaults to [0.6, 1] — opens at 60%, expands to full."
4181
+ },
4182
+ {
4183
+ name: "initialDetent",
4184
+ type: "number",
4185
+ description: "Index into detents to open at. Defaults to 0."
4186
+ },
4187
+ {
4188
+ name: "title",
4189
+ type: "string",
4190
+ description: "Optional title in the sticky header, beside the close button."
4191
+ },
4192
+ {
4193
+ name: "footer",
4194
+ type: "ReactNode",
4195
+ description: "Optional sticky footer, kept clear of the home indicator."
4196
+ },
4197
+ {
4198
+ name: "overlayDismiss",
4199
+ type: "boolean",
4200
+ description: "Tap the dimmed backdrop to close. Defaults to true."
4201
+ },
4202
+ {
4203
+ name: "className",
4204
+ type: "string",
4205
+ description: "Extra classes for the sheet panel."
4206
+ }
4207
+ ],
4208
+ importPath: "./components/layout/Sheet",
4209
+ filePath: "src/components/layout/Sheet.tsx",
4210
+ sku: "@chassis/layout/sheet",
4211
+ humanLabel: "Sheet",
4212
+ humanDescription: "A bottom sheet that opens at a partial height and grows to full screen as you drag or scroll it up. Strategic purpose: the native-feeling surface for mobile navigation, settings and pickers — reachable by thumb, dismissed by flicking down. Use instead of Drawer when the surface is primarily touched."
4213
+ },
4089
4214
  {
4090
4215
  id: "sidebar",
4091
4216
  name: "Sidebar",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ramme-io/ui",
3
- "version": "4.1.0",
3
+ "version": "4.2.0",
4
4
  "description": "The structural component library for the Ramme Framework.",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -61,6 +61,8 @@
61
61
  "@storybook/addon-themes": "^10.5.9",
62
62
  "@storybook/react": "^10.5.9",
63
63
  "@storybook/react-vite": "^10.5.9",
64
+ "@testing-library/jest-dom": "^6.9.1",
65
+ "@testing-library/react": "^16.3.2",
64
66
  "@types/doctrine": "^0.0.9",
65
67
  "@types/node": "^20.12.12",
66
68
  "@types/react": "^18.3.23",
@@ -70,16 +72,21 @@
70
72
  "ag-grid-community": "^31.3.2",
71
73
  "ag-grid-react": "^31.3.2",
72
74
  "autoprefixer": "^10.4.19",
75
+ "axe-core": "^4.13.0",
73
76
  "doctrine": "^3.0.0",
77
+ "jsdom": "^29.0.0",
74
78
  "postcss": "^8.4.38",
75
79
  "storybook": "^10.5.9",
76
80
  "tailwindcss": "^3.4.4",
77
81
  "typescript": "^5.4.5",
78
82
  "vite": "^5.3.1",
79
- "vite-plugin-dts": "^3.9.1"
83
+ "vite-plugin-dts": "^3.9.1",
84
+ "vitest": "^2.1.9",
85
+ "vitest-axe": "^0.1.0"
80
86
  },
81
87
  "scripts": {
82
88
  "dev": "vite",
89
+ "test": "vitest run",
83
90
  "generate:manifest": "node scripts/generate-manifest.js",
84
91
  "generate:registry": "node scripts/generate-registry.js",
85
92
  "generate": "npm run generate:manifest && npm run generate:registry",