@slipstream-ui/core 0.3.0 → 0.4.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 (128) hide show
  1. package/dist/index.d.mts +672 -52
  2. package/dist/index.d.ts +672 -52
  3. package/dist/index.js +1860 -657
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.mjs +1836 -655
  6. package/dist/index.mjs.map +1 -1
  7. package/dist/manifests/app-shell.manifest.json +156 -0
  8. package/dist/manifests/auth-layout.manifest.json +99 -0
  9. package/dist/manifests/autocomplete.manifest.json +53 -5
  10. package/dist/manifests/badge.manifest.json +12 -0
  11. package/dist/manifests/button.manifest.json +6 -2
  12. package/dist/manifests/card.manifest.json +53 -6
  13. package/dist/manifests/date-picker.manifest.json +87 -0
  14. package/dist/manifests/dialog.manifest.json +49 -4
  15. package/dist/manifests/drawer.manifest.json +18 -4
  16. package/dist/manifests/empty-state.manifest.json +108 -0
  17. package/dist/manifests/file-upload.manifest.json +103 -0
  18. package/dist/manifests/filter-bar.manifest.json +100 -0
  19. package/dist/manifests/menu.manifest.json +13 -1
  20. package/dist/manifests/metric-card.manifest.json +105 -0
  21. package/dist/manifests/page-header.manifest.json +94 -0
  22. package/dist/manifests/pagination.manifest.json +62 -34
  23. package/dist/manifests/password-input.manifest.json +75 -0
  24. package/dist/manifests/select-menu.manifest.json +142 -0
  25. package/dist/manifests/select.manifest.json +49 -12
  26. package/dist/manifests/stepper.manifest.json +95 -0
  27. package/dist/manifests/table-card.manifest.json +123 -0
  28. package/dist/manifests/time-picker.manifest.json +86 -0
  29. package/dist/manifests/timeline.manifest.json +69 -0
  30. package/dist/manifests/toast.manifest.json +101 -25
  31. package/package.json +1 -1
  32. package/src/components/app-shell/AppShell.test.tsx +100 -0
  33. package/src/components/app-shell/AppShell.tsx +162 -0
  34. package/src/components/app-shell/index.ts +1 -0
  35. package/src/components/auth-layout/AuthLayout.test.tsx +34 -0
  36. package/src/components/auth-layout/AuthLayout.tsx +47 -0
  37. package/src/components/auth-layout/index.ts +1 -0
  38. package/src/components/autocomplete/Autocomplete.test.tsx +53 -0
  39. package/src/components/autocomplete/Autocomplete.tsx +160 -41
  40. package/src/components/badge/Badge.test.tsx +14 -0
  41. package/src/components/badge/Badge.tsx +10 -4
  42. package/src/components/button/Button.test.tsx +29 -0
  43. package/src/components/button/Button.tsx +10 -0
  44. package/src/components/button-group/ButtonGroup.test.tsx +6 -8
  45. package/src/components/button-group/ButtonGroup.tsx +17 -9
  46. package/src/components/card/Card.test.tsx +14 -0
  47. package/src/components/card/Card.tsx +15 -8
  48. package/src/components/date-picker/DatePicker.test.tsx +30 -0
  49. package/src/components/date-picker/DatePicker.tsx +57 -0
  50. package/src/components/date-picker/index.ts +1 -0
  51. package/src/components/dialog/Dialog.test.tsx +64 -1
  52. package/src/components/dialog/Dialog.tsx +113 -23
  53. package/src/components/dialog/index.ts +1 -2
  54. package/src/components/drawer/Drawer.test.tsx +50 -0
  55. package/src/components/drawer/Drawer.tsx +44 -6
  56. package/src/components/empty-state/EmptyState.test.tsx +30 -0
  57. package/src/components/empty-state/EmptyState.tsx +48 -0
  58. package/src/components/empty-state/index.ts +1 -0
  59. package/src/components/file-upload/FileUpload.test.tsx +43 -0
  60. package/src/components/file-upload/FileUpload.tsx +143 -0
  61. package/src/components/file-upload/index.ts +1 -0
  62. package/src/components/filter-bar/FilterBar.test.tsx +32 -0
  63. package/src/components/filter-bar/FilterBar.tsx +71 -0
  64. package/src/components/filter-bar/index.ts +1 -0
  65. package/src/components/input-group/InputGroup.test.tsx +6 -3
  66. package/src/components/input-group/InputGroup.tsx +9 -6
  67. package/src/components/menu/Menu.test.tsx +21 -0
  68. package/src/components/menu/Menu.tsx +8 -3
  69. package/src/components/metric-card/MetricCard.test.tsx +28 -0
  70. package/src/components/metric-card/MetricCard.tsx +54 -0
  71. package/src/components/metric-card/index.ts +1 -0
  72. package/src/components/page-header/PageHeader.test.tsx +31 -0
  73. package/src/components/page-header/PageHeader.tsx +37 -0
  74. package/src/components/page-header/index.ts +1 -0
  75. package/src/components/pagination/Pagination.test.tsx +25 -0
  76. package/src/components/pagination/Pagination.tsx +53 -12
  77. package/src/components/password-input/PasswordInput.test.tsx +29 -0
  78. package/src/components/password-input/PasswordInput.tsx +41 -0
  79. package/src/components/password-input/index.ts +1 -0
  80. package/src/components/select/Select.test.tsx +40 -1
  81. package/src/components/select/Select.tsx +52 -14
  82. package/src/components/select/index.ts +1 -1
  83. package/src/components/select-menu/SelectMenu.test.tsx +105 -0
  84. package/src/components/select-menu/SelectMenu.tsx +368 -0
  85. package/src/components/select-menu/index.ts +1 -0
  86. package/src/components/stepper/Stepper.test.tsx +35 -0
  87. package/src/components/stepper/Stepper.tsx +91 -0
  88. package/src/components/stepper/index.ts +1 -0
  89. package/src/components/table-card/TableCard.test.tsx +59 -0
  90. package/src/components/table-card/TableCard.tsx +48 -0
  91. package/src/components/table-card/index.ts +1 -0
  92. package/src/components/time-picker/TimePicker.test.tsx +30 -0
  93. package/src/components/time-picker/TimePicker.tsx +56 -0
  94. package/src/components/time-picker/index.ts +1 -0
  95. package/src/components/timeline/Timeline.test.tsx +26 -0
  96. package/src/components/timeline/Timeline.tsx +53 -0
  97. package/src/components/timeline/index.ts +1 -0
  98. package/src/components/toast/Toast.test.tsx +5 -0
  99. package/src/components/toast/Toast.tsx +2 -16
  100. package/src/components/toast/Toaster.test.tsx +84 -0
  101. package/src/components/toast/Toaster.tsx +116 -0
  102. package/src/components/toast/index.ts +4 -0
  103. package/src/components/toast/toast-store.ts +117 -0
  104. package/src/index.ts +14 -0
  105. package/src/manifests/app-shell.manifest.json +156 -0
  106. package/src/manifests/auth-layout.manifest.json +99 -0
  107. package/src/manifests/autocomplete.manifest.json +53 -5
  108. package/src/manifests/badge.manifest.json +12 -0
  109. package/src/manifests/button.manifest.json +6 -2
  110. package/src/manifests/card.manifest.json +53 -6
  111. package/src/manifests/date-picker.manifest.json +87 -0
  112. package/src/manifests/dialog.manifest.json +49 -4
  113. package/src/manifests/drawer.manifest.json +18 -4
  114. package/src/manifests/empty-state.manifest.json +108 -0
  115. package/src/manifests/file-upload.manifest.json +103 -0
  116. package/src/manifests/filter-bar.manifest.json +100 -0
  117. package/src/manifests/menu.manifest.json +13 -1
  118. package/src/manifests/metric-card.manifest.json +105 -0
  119. package/src/manifests/page-header.manifest.json +94 -0
  120. package/src/manifests/pagination.manifest.json +62 -34
  121. package/src/manifests/password-input.manifest.json +75 -0
  122. package/src/manifests/select-menu.manifest.json +142 -0
  123. package/src/manifests/select.manifest.json +49 -12
  124. package/src/manifests/stepper.manifest.json +95 -0
  125. package/src/manifests/table-card.manifest.json +123 -0
  126. package/src/manifests/time-picker.manifest.json +86 -0
  127. package/src/manifests/timeline.manifest.json +69 -0
  128. package/src/manifests/toast.manifest.json +101 -25
@@ -0,0 +1,56 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import { cn } from '../../utils/cn';
5
+ import { Input, type InputProps } from '../input/Input';
6
+
7
+ export type TimePickerProps = Omit<InputProps, 'type' | 'onChange' | 'value' | 'defaultValue'> & {
8
+ /** "HH:MM" (24h) — controlled. */
9
+ value?: string;
10
+ /** "HH:MM" — initial value when uncontrolled. */
11
+ defaultValue?: string;
12
+ /** Fired with the time string ('' when cleared). */
13
+ onValueChange?: (value: string) => void;
14
+ /** Show a clear button once a time is selected. */
15
+ clearable?: boolean;
16
+ };
17
+
18
+ /**
19
+ * Native time field with an "HH:MM" contract. The platform supplies the
20
+ * picker, keyboard editing and locale behavior.
21
+ */
22
+ export const TimePicker = React.forwardRef<HTMLInputElement, TimePickerProps>(function TimePicker(
23
+ { value, defaultValue, onValueChange, clearable = false, className, ...props },
24
+ ref
25
+ ) {
26
+ const [internal, setInternal] = React.useState(defaultValue ?? '');
27
+ const current = value ?? internal;
28
+
29
+ const commit = (next: string) => {
30
+ if (value === undefined) setInternal(next);
31
+ onValueChange?.(next);
32
+ };
33
+
34
+ return (
35
+ <div className={cn('relative flex items-center', className)}>
36
+ <Input
37
+ ref={ref}
38
+ type="time"
39
+ value={current}
40
+ onChange={(event) => commit(event.target.value)}
41
+ className={clearable && current ? 'pr-9' : undefined}
42
+ {...props}
43
+ />
44
+ {clearable && current && (
45
+ <button
46
+ type="button"
47
+ onClick={() => commit('')}
48
+ aria-label="Clear time"
49
+ className="absolute right-2 rounded p-1 text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
50
+ >
51
+ ✕
52
+ </button>
53
+ )}
54
+ </div>
55
+ );
56
+ });
@@ -0,0 +1 @@
1
+ export * from './TimePicker';
@@ -0,0 +1,26 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import { axe } from 'jest-axe';
3
+ import { describe, expect, it } from 'vitest';
4
+ import { Timeline } from './Timeline';
5
+
6
+ describe('Timeline', () => {
7
+ it('renders an ordered chronology with periods and descriptions', () => {
8
+ const { container } = render(
9
+ <Timeline
10
+ items={[
11
+ { title: 'Senior Engineer', period: '2023 — Present', description: 'Platform team' },
12
+ { title: 'Engineer', period: '2020 — 2023' },
13
+ ]}
14
+ />
15
+ );
16
+ expect(container.querySelector('ol')).toBeInTheDocument();
17
+ expect(screen.getAllByRole('listitem')).toHaveLength(2);
18
+ expect(screen.getByText('2023 — Present')).toBeInTheDocument();
19
+ expect(screen.getByText('Platform team')).toBeInTheDocument();
20
+ });
21
+
22
+ it('has no accessibility violations', async () => {
23
+ const { container } = render(<Timeline items={[{ title: 'Started' }]} />);
24
+ expect(await axe(container)).toHaveNoViolations();
25
+ });
26
+ });
@@ -0,0 +1,53 @@
1
+ import * as React from 'react';
2
+ import { cn } from '../../utils/cn';
3
+
4
+ export type TimelineItem = {
5
+ title: string;
6
+ /** Period/date label (e.g. "2023 — Present"). */
7
+ period?: string;
8
+ description?: string;
9
+ icon?: React.ReactNode;
10
+ /** Muted items render dimmed (upcoming/archived). */
11
+ muted?: boolean;
12
+ };
13
+
14
+ export type TimelineProps = Omit<React.OlHTMLAttributes<HTMLOListElement>, 'children'> & {
15
+ items: TimelineItem[];
16
+ };
17
+
18
+ /**
19
+ * Vertical chronology (experience, activity, audit trail). Renders an
20
+ * ordered list with a connector rule, so the sequence is conveyed
21
+ * structurally instead of by decoration alone.
22
+ */
23
+ export const Timeline = React.forwardRef<HTMLOListElement, TimelineProps>(function Timeline(
24
+ { items, className, ...props },
25
+ ref
26
+ ) {
27
+ return (
28
+ <ol ref={ref} className={cn('relative flex flex-col gap-6', className)} {...props}>
29
+ {items.map((item, index) => (
30
+ <li key={`${item.title}-${index}`} className="relative flex gap-4">
31
+ <div className="flex flex-col items-center">
32
+ <span
33
+ aria-hidden="true"
34
+ className={cn(
35
+ 'mt-1 flex h-3 w-3 shrink-0 items-center justify-center rounded-full border-2',
36
+ item.muted ? 'border-border bg-background' : 'border-primary bg-primary'
37
+ )}
38
+ />
39
+ {index < items.length - 1 && <span aria-hidden="true" className="mt-1 w-px flex-1 bg-border" />}
40
+ </div>
41
+ <div className={cn('min-w-0 pb-1', item.muted && 'opacity-60')}>
42
+ {item.period && (
43
+ <p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">{item.period}</p>
44
+ )}
45
+ <p className="text-sm font-semibold">{item.title}</p>
46
+ {item.description && <p className="mt-1 text-sm text-muted-foreground">{item.description}</p>}
47
+ {item.icon && <div className="mt-1 text-muted-foreground [&_svg]:size-4">{item.icon}</div>}
48
+ </div>
49
+ </li>
50
+ ))}
51
+ </ol>
52
+ );
53
+ });
@@ -0,0 +1 @@
1
+ export * from './Timeline';
@@ -48,7 +48,12 @@ describe('Toast', () => {
48
48
 
49
49
  it('throws when useToast is used outside the provider', () => {
50
50
  const spy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
51
+ // React 18 re-reports render errors asynchronously via jsdom's error
52
+ // event; swallow them so they cannot leak into the next test file.
53
+ const swallow = (event: ErrorEvent) => event.preventDefault();
54
+ window.addEventListener('error', swallow);
51
55
  expect(() => render(<Trigger options={{ title: 'x' }} />)).toThrow(/ToastProvider/);
56
+ window.removeEventListener('error', swallow);
52
57
  spy.mockRestore();
53
58
  });
54
59
  });
@@ -1,23 +1,9 @@
1
1
  'use client';
2
2
 
3
3
  import * as React from 'react';
4
+ import type { ToastAction, ToastOptions, ToastRecord } from './toast-store';
4
5
 
5
- export type ToastAction = {
6
- label: string;
7
- onClick: () => void;
8
- };
9
-
10
- export type ToastOptions = {
11
- title: string;
12
- description?: string;
13
- action?: ToastAction;
14
- /** Auto-dismiss delay in ms; 0 keeps the toast until dismissed. Default 5000. */
15
- duration?: number;
16
- };
17
-
18
- export type ToastRecord = ToastOptions & {
19
- id: string;
20
- };
6
+ export type { ToastAction, ToastOptions, ToastRecord };
21
7
 
22
8
  export type ToastContextValue = {
23
9
  toasts: ToastRecord[];
@@ -0,0 +1,84 @@
1
+ import { act, render, screen } from '@testing-library/react';
2
+ import userEvent from '@testing-library/user-event';
3
+ import { axe } from 'jest-axe';
4
+ import { afterEach, describe, expect, it, vi } from 'vitest';
5
+ import { Toaster } from './Toaster';
6
+ import { toast } from './toast-store';
7
+
8
+ afterEach(() => {
9
+ toast.clear();
10
+ vi.useRealTimers();
11
+ });
12
+
13
+ describe('imperative toast', () => {
14
+ it('renders a toast fired outside React with its description', () => {
15
+ render(<Toaster />);
16
+ act(() => {
17
+ toast.success('Saved', 'Your changes are live');
18
+ });
19
+ expect(screen.getByText('Saved')).toBeInTheDocument();
20
+ expect(screen.getByText('Your changes are live')).toBeInTheDocument();
21
+ expect(screen.getByRole('status')).toBeInTheDocument();
22
+ });
23
+
24
+ it('supports every variant helper and dismisses by id', async () => {
25
+ const user = userEvent.setup();
26
+ render(<Toaster richColors />);
27
+ act(() => {
28
+ toast.error('Upload failed');
29
+ toast.warning('Slow network');
30
+ toast.info('Heads up');
31
+ });
32
+ expect(screen.getByText('Upload failed')).toBeInTheDocument();
33
+ expect(screen.getByText('Slow network')).toBeInTheDocument();
34
+ expect(screen.getByText('Heads up')).toBeInTheDocument();
35
+
36
+ const ids = toast.dismiss;
37
+ expect(typeof ids).toBe('function');
38
+ await user.click(screen.getByRole('button', { name: 'Dismiss Upload failed' }));
39
+ expect(screen.queryByText('Upload failed')).not.toBeInTheDocument();
40
+ });
41
+
42
+ it('auto-dismisses after the duration', () => {
43
+ vi.useFakeTimers();
44
+ render(<Toaster />);
45
+ act(() => {
46
+ toast({ title: 'Ephemeral', duration: 1000 });
47
+ });
48
+ expect(screen.getByText('Ephemeral')).toBeInTheDocument();
49
+ act(() => {
50
+ vi.advanceTimersByTime(1200);
51
+ });
52
+ expect(screen.queryByText('Ephemeral')).not.toBeInTheDocument();
53
+ });
54
+
55
+ it('keeps duration=0 toasts until dismissed manually', async () => {
56
+ const user = userEvent.setup();
57
+ vi.useFakeTimers({ shouldAdvanceTime: true });
58
+ render(<Toaster />);
59
+ act(() => {
60
+ toast({ title: 'Sticky', duration: 0 });
61
+ });
62
+ act(() => {
63
+ vi.advanceTimersByTime(10000);
64
+ });
65
+ expect(screen.getByText('Sticky')).toBeInTheDocument();
66
+ await user.click(screen.getByRole('button', { name: 'Dismiss Sticky' }));
67
+ expect(screen.queryByText('Sticky')).not.toBeInTheDocument();
68
+ });
69
+
70
+ it('honours the position prop and exposes a labelled live region', () => {
71
+ const { container } = render(<Toaster position="top-left" label="Alerts" />);
72
+ const region = screen.getByRole('list', { name: 'Alerts' });
73
+ expect(region).toHaveClass('left-4', 'top-4', 'z-[60]');
74
+ expect(container).toBeTruthy();
75
+ });
76
+
77
+ it('has no accessibility violations', async () => {
78
+ const { container } = render(<Toaster />);
79
+ act(() => {
80
+ toast.info('Info', 'Details');
81
+ });
82
+ expect(await axe(container)).toHaveNoViolations();
83
+ });
84
+ });
@@ -0,0 +1,116 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import { cn } from '../../utils/cn';
5
+ import {
6
+ getToastServerSnapshot,
7
+ getToastSnapshot,
8
+ subscribeToToasts,
9
+ toast as imperativeToast,
10
+ type ToastRecord,
11
+ type ToastVariant,
12
+ } from './toast-store';
13
+
14
+ export type ToasterPosition =
15
+ | 'top-left'
16
+ | 'top-center'
17
+ | 'top-right'
18
+ | 'bottom-left'
19
+ | 'bottom-center'
20
+ | 'bottom-right';
21
+
22
+ export type ToasterProps = {
23
+ /** Corner/edge the stack renders at. Default 'bottom-right'. */
24
+ position?: ToasterPosition;
25
+ /** Accessible name of the live region. Default 'Notifications'. */
26
+ label?: string;
27
+ /** Tint the toast surface with its status color (neutral by default). */
28
+ richColors?: boolean;
29
+ };
30
+
31
+ const positionClasses: Record<ToasterPosition, string> = {
32
+ 'top-left': 'left-4 top-4 items-start',
33
+ 'top-center': 'left-1/2 top-4 -translate-x-1/2 items-center',
34
+ 'top-right': 'right-4 top-4 items-end',
35
+ 'bottom-left': 'bottom-4 left-4 items-start',
36
+ 'bottom-center': 'bottom-4 left-1/2 -translate-x-1/2 items-center',
37
+ 'bottom-right': 'bottom-4 right-4 items-end',
38
+ };
39
+
40
+ const richClasses: Record<ToastVariant, string> = {
41
+ default: 'border-border bg-card',
42
+ success: 'border-success/50 bg-success/10',
43
+ error: 'border-destructive/50 bg-destructive/10',
44
+ warning: 'border-warning/50 bg-warning/10',
45
+ info: 'border-info/50 bg-info/10',
46
+ };
47
+
48
+ const dotClasses: Record<ToastVariant, string> = {
49
+ default: 'bg-muted-foreground',
50
+ success: 'bg-success',
51
+ error: 'bg-destructive',
52
+ warning: 'bg-warning',
53
+ info: 'bg-info',
54
+ };
55
+
56
+ /**
57
+ * Renders the imperative toast queue. Mount once near the app root:
58
+ *
59
+ * <Toaster />
60
+ * toast.success('Saved')
61
+ *
62
+ * The region is a polite live region, so announcements don't steal focus.
63
+ */
64
+ export function Toaster({ position = 'bottom-right', label = 'Notifications', richColors = false }: ToasterProps) {
65
+ const entries = React.useSyncExternalStore(subscribeToToasts, getToastSnapshot, getToastServerSnapshot);
66
+
67
+ return (
68
+ <ol
69
+ aria-label={label}
70
+ className={cn('fixed z-[60] flex w-full max-w-sm flex-col gap-2', positionClasses[position])}
71
+ >
72
+ {entries.map((entry) => {
73
+ const variant = entry.variant ?? 'default';
74
+ return (
75
+ <li
76
+ key={entry.id}
77
+ className={cn(
78
+ 'rounded-lg border p-4 shadow-lg',
79
+ richColors ? richClasses[variant] : 'border-border bg-card',
80
+ )}
81
+ >
82
+ <div role="status" className="flex items-start gap-3">
83
+ <span aria-hidden="true" className={cn('mt-1 h-2 w-2 shrink-0 rounded-full', dotClasses[variant])} />
84
+ <div className="min-w-0 flex-1">
85
+ <p className="text-sm font-semibold">{entry.title}</p>
86
+ {entry.description && <p className="mt-1 text-sm text-muted-foreground">{entry.description}</p>}
87
+ {entry.action && (
88
+ <button
89
+ type="button"
90
+ onClick={() => {
91
+ entry.action?.onClick();
92
+ imperativeToast.dismiss(entry.id);
93
+ }}
94
+ className="mt-2 text-sm font-medium text-primary hover:underline"
95
+ >
96
+ {entry.action.label}
97
+ </button>
98
+ )}
99
+ </div>
100
+ <button
101
+ type="button"
102
+ onClick={() => imperativeToast.dismiss(entry.id)}
103
+ aria-label={`Dismiss ${entry.title}`}
104
+ className="rounded p-1 text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
105
+ >
106
+ ✕
107
+ </button>
108
+ </div>
109
+ </li>
110
+ );
111
+ })}
112
+ </ol>
113
+ );
114
+ }
115
+
116
+ export type { ToastRecord };
@@ -1,2 +1,6 @@
1
1
  export { ToastProvider, useToast } from './Toast';
2
2
  export type { ToastOptions, ToastRecord, ToastAction, ToastContextValue } from './Toast';
3
+ export { Toaster } from './Toaster';
4
+ export type { ToasterProps, ToasterPosition } from './Toaster';
5
+ export { toast } from './toast-store';
6
+ export type { ToastVariant } from './toast-store';
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Imperative toast store.
3
+ *
4
+ * Components can call `toast(...)` from anywhere — event handlers, data
5
+ * layers, async catch blocks — without threading a hook through the tree.
6
+ * `<Toaster />` subscribes to this store and renders the queue. The
7
+ * provider/hook API in Toast.tsx remains for tree-scoped usage.
8
+ */
9
+
10
+ export type ToastVariant = 'default' | 'success' | 'error' | 'warning' | 'info';
11
+
12
+ export type ToastAction = {
13
+ label: string;
14
+ onClick: () => void;
15
+ };
16
+
17
+ export type ToastOptions = {
18
+ title: string;
19
+ description?: string;
20
+ action?: ToastAction;
21
+ variant?: ToastVariant;
22
+ /** Auto-dismiss delay in ms; 0 keeps the toast until dismissed. Default 5000. */
23
+ duration?: number;
24
+ };
25
+
26
+ export type ToastRecord = ToastOptions & { id: string };
27
+
28
+ type Listener = (toasts: readonly ToastRecord[]) => void;
29
+
30
+ const DEFAULT_DURATION = 5000;
31
+
32
+ let toasts: readonly ToastRecord[] = [];
33
+ const EMPTY: readonly ToastRecord[] = [];
34
+ const listeners = new Set<Listener>();
35
+ const timers = new Map<string, ReturnType<typeof setTimeout>>();
36
+
37
+ function emit() {
38
+ for (const listener of listeners) listener(toasts);
39
+ }
40
+
41
+ function dismiss(id: string) {
42
+ const timer = timers.get(id);
43
+ if (timer) {
44
+ clearTimeout(timer);
45
+ timers.delete(id);
46
+ }
47
+ toasts = toasts.filter((entry) => entry.id !== id);
48
+ emit();
49
+ }
50
+
51
+ function show(options: ToastOptions): string {
52
+ const id = `toast-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
53
+ toasts = [...toasts, { ...options, id }];
54
+ emit();
55
+
56
+ const duration = options.duration ?? DEFAULT_DURATION;
57
+ if (duration > 0) {
58
+ timers.set(
59
+ id,
60
+ setTimeout(() => dismiss(id), duration),
61
+ );
62
+ }
63
+ return id;
64
+ }
65
+
66
+ type ToastFunction = ((options: ToastOptions) => string) & {
67
+ success: (title: string, description?: string) => string;
68
+ error: (title: string, description?: string) => string;
69
+ warning: (title: string, description?: string) => string;
70
+ info: (title: string, description?: string) => string;
71
+ dismiss: (id?: string) => void;
72
+ clear: () => void;
73
+ };
74
+
75
+ /**
76
+ * Fire a toast from anywhere. Variant helpers mirror the sonner/Radix
77
+ * shape most codebases already call:
78
+ *
79
+ * toast.success('Saved')
80
+ * toast.error('Upload failed', 'Check your connection')
81
+ * toast({ title: 'Heads up', duration: 0 })
82
+ */
83
+ export const toast: ToastFunction = Object.assign(
84
+ (options: ToastOptions) => show({ ...options, variant: options.variant ?? 'default' }),
85
+ {
86
+ success: (title: string, description?: string) => show({ title, description, variant: 'success' }),
87
+ error: (title: string, description?: string) => show({ title, description, variant: 'error' }),
88
+ warning: (title: string, description?: string) => show({ title, description, variant: 'warning' }),
89
+ info: (title: string, description?: string) => show({ title, description, variant: 'info' }),
90
+ dismiss: (id?: string) => {
91
+ if (id) {
92
+ dismiss(id);
93
+ return;
94
+ }
95
+ for (const entry of [...toasts]) dismiss(entry.id);
96
+ },
97
+ clear: () => {
98
+ for (const entry of [...toasts]) dismiss(entry.id);
99
+ },
100
+ },
101
+ );
102
+
103
+ /** Subscribe to the queue (used by `<Toaster />`). */
104
+ export function subscribeToToasts(listener: Listener): () => void {
105
+ listeners.add(listener);
106
+ return () => listeners.delete(listener);
107
+ }
108
+
109
+ /** Current queue; the reference is stable between mutations. */
110
+ export function getToastSnapshot(): readonly ToastRecord[] {
111
+ return toasts;
112
+ }
113
+
114
+ /** SSR-safe snapshot — always empty (toasts only exist client-side). */
115
+ export function getToastServerSnapshot(): readonly ToastRecord[] {
116
+ return EMPTY;
117
+ }
package/src/index.ts CHANGED
@@ -2,6 +2,7 @@ export * from './types/manifest';
2
2
  export * from './utils/cn';
3
3
  export * from './utils/gap';
4
4
  export * from './utils/presence';
5
+ export * from './components/app-shell';
5
6
  export * from './components/button';
6
7
  export * from './components/button-group';
7
8
  export * from './components/input';
@@ -13,6 +14,8 @@ export * from './components/toast';
13
14
  export * from './components/dialog';
14
15
  export * from './components/drawer';
15
16
  export * from './components/select';
17
+ export * from './components/table-card';
18
+ export * from './components/select-menu';
16
19
  export * from './components/search';
17
20
  export * from './components/autocomplete';
18
21
  export * from './components/checkbox';
@@ -36,6 +39,17 @@ export * from './components/data-table';
36
39
  export * from './components/list';
37
40
  export * from './components/text';
38
41
  export * from './components/heading';
42
+ export * from './components/empty-state';
43
+ export * from './components/page-header';
44
+ export * from './components/metric-card';
45
+ export * from './components/password-input';
46
+ export * from './components/auth-layout';
47
+ export * from './components/filter-bar';
48
+ export * from './components/date-picker';
49
+ export * from './components/time-picker';
50
+ export * from './components/stepper';
51
+ export * from './components/file-upload';
52
+ export * from './components/timeline';
39
53
  export * from './components/block';
40
54
  export * from './components/flex';
41
55
  export * from './components/grid';