@startsimpli/ui 0.1.0 → 0.1.3

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 (80) hide show
  1. package/dist/chunk-27YUQBOE.mjs +3954 -0
  2. package/dist/chunk-27YUQBOE.mjs.map +1 -0
  3. package/dist/chunk-G2AM3DBU.mjs +1026 -0
  4. package/dist/chunk-G2AM3DBU.mjs.map +1 -0
  5. package/dist/chunk-G4XBXCFH.mjs +63 -0
  6. package/dist/chunk-G4XBXCFH.mjs.map +1 -0
  7. package/dist/chunk-LZOMFHX3.mjs +35 -0
  8. package/dist/chunk-LZOMFHX3.mjs.map +1 -0
  9. package/dist/chunk-QYXFLOO7.mjs +210 -0
  10. package/dist/chunk-QYXFLOO7.mjs.map +1 -0
  11. package/dist/components/index.d.mts +472 -0
  12. package/dist/components/index.d.ts +472 -0
  13. package/dist/components/index.js +5149 -0
  14. package/dist/components/index.js.map +1 -0
  15. package/dist/components/index.mjs +6 -0
  16. package/dist/components/index.mjs.map +1 -0
  17. package/dist/components/unified-table/index.d.mts +725 -0
  18. package/dist/components/unified-table/index.d.ts +725 -0
  19. package/dist/components/unified-table/index.js +4000 -0
  20. package/dist/components/unified-table/index.js.map +1 -0
  21. package/dist/components/unified-table/index.mjs +5 -0
  22. package/dist/components/unified-table/index.mjs.map +1 -0
  23. package/dist/index.d.mts +26 -0
  24. package/dist/index.d.ts +26 -0
  25. package/dist/index.js +5448 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/index.mjs +12 -0
  28. package/dist/index.mjs.map +1 -0
  29. package/dist/theme/index.d.mts +20 -0
  30. package/dist/theme/index.d.ts +20 -0
  31. package/dist/theme/index.js +245 -0
  32. package/dist/theme/index.js.map +1 -0
  33. package/dist/theme/index.mjs +9 -0
  34. package/dist/theme/index.mjs.map +1 -0
  35. package/dist/utils/index.d.mts +38 -0
  36. package/dist/utils/index.d.ts +38 -0
  37. package/dist/utils/index.js +72 -0
  38. package/dist/utils/index.js.map +1 -0
  39. package/dist/utils/index.mjs +4 -0
  40. package/dist/utils/index.mjs.map +1 -0
  41. package/package.json +62 -21
  42. package/src/__mocks__/next/navigation.js +18 -0
  43. package/src/components/__tests__/safe-html.test.tsx +45 -0
  44. package/src/components/__tests__/states.test.tsx +94 -0
  45. package/src/components/__tests__/status-badge.test.tsx +101 -0
  46. package/src/components/__tests__/toast.test.tsx +124 -0
  47. package/src/components/badge/StatusBadge.tsx +55 -0
  48. package/src/components/badge/index.ts +2 -0
  49. package/src/components/dialog/BaseDialog.tsx +184 -0
  50. package/src/components/dialog/index.ts +8 -0
  51. package/src/components/index.ts +25 -0
  52. package/src/components/loading/DashboardSkeleton.tsx +27 -0
  53. package/src/components/loading/TableSkeleton.tsx +63 -0
  54. package/src/components/loading/index.ts +4 -0
  55. package/src/components/safe-html.tsx +18 -0
  56. package/src/components/states/EmptyState.tsx +48 -0
  57. package/src/components/states/ErrorState.tsx +76 -0
  58. package/src/components/states/index.ts +4 -0
  59. package/src/components/toast/Toaster.tsx +72 -0
  60. package/src/components/toast/index.ts +5 -0
  61. package/src/components/toast/use-notify.ts +45 -0
  62. package/src/components/toast/use-toast.ts +150 -0
  63. package/src/components/ui/api-error-boundary.tsx +64 -0
  64. package/src/components/ui/feature-gate.tsx +87 -0
  65. package/src/components/ui/index.ts +4 -0
  66. package/src/components/ui/page-loader.tsx +31 -0
  67. package/src/components/ui/query-provider.tsx +30 -0
  68. package/src/components/unified-table/components/Toolbar/StandardTableToolbar.tsx +1 -1
  69. package/src/components/unified-table/hooks/useFilters.ts +1 -0
  70. package/src/components/unified-table/hooks/usePagination.ts +1 -0
  71. package/src/components/unified-table/hooks/useSelection.ts +2 -1
  72. package/src/components/unified-table/hooks/useTableKeyboard.ts +2 -1
  73. package/src/components/unified-table/hooks/useTablePreferences.ts +1 -0
  74. package/src/components/unified-table/hooks/useTableState.ts +1 -0
  75. package/src/components/unified-table/hooks/useTableURL.test.tsx +1 -1
  76. package/src/components/unified-table/index.ts +4 -0
  77. package/src/components/wizard/StepIndicator.tsx +60 -0
  78. package/src/components/wizard/index.ts +2 -0
  79. package/src/theme/tailwind.config.d.ts +3 -0
  80. package/tailwind.preset.js +87 -0
@@ -0,0 +1,18 @@
1
+ const mockReplace = jest.fn()
2
+ const mockPush = jest.fn()
3
+ const mockSearchParams = new URLSearchParams()
4
+ const mockPathname = '/test-path'
5
+
6
+ module.exports = {
7
+ useSearchParams: jest.fn(() => mockSearchParams),
8
+ useRouter: jest.fn(() => ({
9
+ replace: mockReplace,
10
+ push: mockPush,
11
+ back: jest.fn(),
12
+ forward: jest.fn(),
13
+ prefetch: jest.fn(),
14
+ refresh: jest.fn(),
15
+ })),
16
+ usePathname: jest.fn(() => mockPathname),
17
+ useParams: jest.fn(() => ({})),
18
+ }
@@ -0,0 +1,45 @@
1
+ import { render, screen } from '@testing-library/react'
2
+ import { SafeHtml } from '../safe-html'
3
+
4
+ describe('SafeHtml', () => {
5
+ it('renders safe HTML content', () => {
6
+ render(<SafeHtml html="<p>Hello <strong>world</strong></p>" />)
7
+ expect(screen.getByText(/Hello/)).toBeInTheDocument()
8
+ expect(screen.getByText(/world/)).toBeInTheDocument()
9
+ })
10
+
11
+ it('strips script tags (XSS protection)', () => {
12
+ const { container } = render(
13
+ <SafeHtml html='<p>Safe</p><script>alert("xss")</script>' />
14
+ )
15
+ expect(container.querySelector('script')).toBeNull()
16
+ expect(container.textContent).toContain('Safe')
17
+ })
18
+
19
+ it('strips event handlers (XSS protection)', () => {
20
+ const { container } = render(
21
+ <SafeHtml html='<img src="x" onerror="alert(1)">' />
22
+ )
23
+ const img = container.querySelector('img')
24
+ expect(img?.getAttribute('onerror')).toBeNull()
25
+ })
26
+
27
+ it('preserves className prop', () => {
28
+ const { container } = render(
29
+ <SafeHtml html="<p>Test</p>" className="prose" />
30
+ )
31
+ expect(container.firstChild).toHaveAttribute('class', 'prose')
32
+ })
33
+
34
+ it('renders with custom tag via as prop', () => {
35
+ const { container } = render(
36
+ <SafeHtml html="<p>Test</p>" as="section" />
37
+ )
38
+ expect(container.querySelector('section')).not.toBeNull()
39
+ })
40
+
41
+ it('handles empty string', () => {
42
+ const { container } = render(<SafeHtml html="" />)
43
+ expect(container.firstChild).toBeInTheDocument()
44
+ })
45
+ })
@@ -0,0 +1,94 @@
1
+ import { render, screen, fireEvent } from '@testing-library/react'
2
+ import { ErrorState } from '../states/ErrorState'
3
+ import { EmptyState } from '../states/EmptyState'
4
+
5
+ describe('ErrorState', () => {
6
+ it('renders title and message', () => {
7
+ render(<ErrorState message="Something broke" />)
8
+ expect(screen.getByText('Something went wrong')).toBeInTheDocument()
9
+ expect(screen.getByText('Something broke')).toBeInTheDocument()
10
+ })
11
+
12
+ it('renders custom title', () => {
13
+ render(<ErrorState title="Custom Error" message="Details here" />)
14
+ expect(screen.getByText('Custom Error')).toBeInTheDocument()
15
+ })
16
+
17
+ it('renders with role="alert" and aria-live', () => {
18
+ const { container } = render(<ErrorState message="Error occurred" />)
19
+ const alertEl = container.querySelector('[role="alert"]')
20
+ expect(alertEl).toBeInTheDocument()
21
+ expect(alertEl).toHaveAttribute('aria-live', 'assertive')
22
+ })
23
+
24
+ it('renders retry button when onRetry is provided', () => {
25
+ const onRetry = jest.fn()
26
+ render(<ErrorState message="Failed" onRetry={onRetry} />)
27
+
28
+ const retryButton = screen.getByLabelText('Retry loading content')
29
+ expect(retryButton).toBeInTheDocument()
30
+
31
+ fireEvent.click(retryButton)
32
+ expect(onRetry).toHaveBeenCalledTimes(1)
33
+ })
34
+
35
+ it('does not render retry button without onRetry', () => {
36
+ render(<ErrorState message="Failed" />)
37
+ expect(screen.queryByText('Try Again')).not.toBeInTheDocument()
38
+ })
39
+
40
+ it('applies custom className', () => {
41
+ const { container } = render(
42
+ <ErrorState message="Error" className="custom-class" />
43
+ )
44
+ expect(container.firstChild).toHaveClass('custom-class')
45
+ })
46
+ })
47
+
48
+ describe('EmptyState', () => {
49
+ it('renders title', () => {
50
+ render(<EmptyState title="No items" />)
51
+ expect(screen.getByText('No items')).toBeInTheDocument()
52
+ })
53
+
54
+ it('renders description when provided', () => {
55
+ render(
56
+ <EmptyState title="No items" description="Add some items to get started" />
57
+ )
58
+ expect(screen.getByText('Add some items to get started')).toBeInTheDocument()
59
+ })
60
+
61
+ it('does not render description when not provided', () => {
62
+ const { container } = render(<EmptyState title="No items" />)
63
+ const paragraphs = container.querySelectorAll('p')
64
+ expect(paragraphs).toHaveLength(0)
65
+ })
66
+
67
+ it('renders action button when action is provided', () => {
68
+ const onClick = jest.fn()
69
+ render(
70
+ <EmptyState
71
+ title="No items"
72
+ action={{ label: 'Add Item', onClick }}
73
+ />
74
+ )
75
+
76
+ const button = screen.getByLabelText('Add Item')
77
+ expect(button).toBeInTheDocument()
78
+
79
+ fireEvent.click(button)
80
+ expect(onClick).toHaveBeenCalledTimes(1)
81
+ })
82
+
83
+ it('does not render action button without action', () => {
84
+ render(<EmptyState title="No items" />)
85
+ expect(screen.queryByRole('button')).not.toBeInTheDocument()
86
+ })
87
+
88
+ it('has role="status" and aria-live="polite"', () => {
89
+ const { container } = render(<EmptyState title="Empty" />)
90
+ const statusEl = container.querySelector('[role="status"]')
91
+ expect(statusEl).toBeInTheDocument()
92
+ expect(statusEl).toHaveAttribute('aria-live', 'polite')
93
+ })
94
+ })
@@ -0,0 +1,101 @@
1
+ import { render, screen } from '@testing-library/react'
2
+ import { StatusBadge } from '../badge/StatusBadge'
3
+
4
+ type TestStatus = 'active' | 'inactive' | 'pending'
5
+
6
+ const testConfig: Record<TestStatus, { label: string; className: string }> = {
7
+ active: {
8
+ label: 'Active',
9
+ className: 'bg-green-100 text-green-700',
10
+ },
11
+ inactive: {
12
+ label: 'Inactive',
13
+ className: 'bg-gray-100 text-gray-700',
14
+ },
15
+ pending: {
16
+ label: 'Pending',
17
+ className: 'bg-yellow-100 text-yellow-700',
18
+ },
19
+ }
20
+
21
+ describe('StatusBadge', () => {
22
+ it('renders the correct label for a given status', () => {
23
+ render(
24
+ <StatusBadge<TestStatus>
25
+ status="active"
26
+ config={testConfig}
27
+ />
28
+ )
29
+ expect(screen.getByText('Active')).toBeInTheDocument()
30
+ })
31
+
32
+ it('applies the correct className for a given status', () => {
33
+ render(
34
+ <StatusBadge<TestStatus>
35
+ status="inactive"
36
+ config={testConfig}
37
+ />
38
+ )
39
+ const badge = screen.getByText('Inactive')
40
+ expect(badge).toHaveClass('bg-gray-100')
41
+ expect(badge).toHaveClass('text-gray-700')
42
+ })
43
+
44
+ it('renders each status correctly', () => {
45
+ const { rerender } = render(
46
+ <StatusBadge<TestStatus> status="active" config={testConfig} />
47
+ )
48
+ expect(screen.getByText('Active')).toBeInTheDocument()
49
+
50
+ rerender(
51
+ <StatusBadge<TestStatus> status="pending" config={testConfig} />
52
+ )
53
+ expect(screen.getByText('Pending')).toBeInTheDocument()
54
+ expect(screen.getByText('Pending')).toHaveClass('bg-yellow-100')
55
+ })
56
+
57
+ it('renders raw status string for unknown status', () => {
58
+ render(
59
+ <StatusBadge
60
+ status="unknown"
61
+ config={testConfig as Record<string, { label: string; className: string }>}
62
+ />
63
+ )
64
+ expect(screen.getByText('unknown')).toBeInTheDocument()
65
+ expect(screen.getByText('unknown')).toHaveClass('bg-gray-100')
66
+ })
67
+
68
+ it('applies sm size class', () => {
69
+ render(
70
+ <StatusBadge<TestStatus>
71
+ status="active"
72
+ config={testConfig}
73
+ size="sm"
74
+ />
75
+ )
76
+ const badge = screen.getByText('Active')
77
+ expect(badge).toHaveClass('px-2')
78
+ })
79
+
80
+ it('applies md size class by default', () => {
81
+ render(
82
+ <StatusBadge<TestStatus>
83
+ status="active"
84
+ config={testConfig}
85
+ />
86
+ )
87
+ const badge = screen.getByText('Active')
88
+ expect(badge).toHaveClass('px-2.5')
89
+ })
90
+
91
+ it('applies custom className', () => {
92
+ render(
93
+ <StatusBadge<TestStatus>
94
+ status="active"
95
+ config={testConfig}
96
+ className="custom-badge"
97
+ />
98
+ )
99
+ expect(screen.getByText('Active')).toHaveClass('custom-badge')
100
+ })
101
+ })
@@ -0,0 +1,124 @@
1
+ import { renderHook, act } from '@testing-library/react'
2
+ import { useToast, clearAllToasts, reducer } from '../toast/use-toast'
3
+ import type { ToasterToast } from '../toast/use-toast'
4
+
5
+ beforeEach(() => {
6
+ clearAllToasts()
7
+ })
8
+
9
+ describe('useToast', () => {
10
+ it('adds a toast', () => {
11
+ const { result } = renderHook(() => useToast())
12
+
13
+ act(() => {
14
+ result.current.toast({ title: 'Hello', description: 'World' })
15
+ })
16
+
17
+ expect(result.current.toasts).toHaveLength(1)
18
+ expect(result.current.toasts[0].title).toBe('Hello')
19
+ expect(result.current.toasts[0].description).toBe('World')
20
+ })
21
+
22
+ it('adds toast with variant', () => {
23
+ const { result } = renderHook(() => useToast())
24
+
25
+ act(() => {
26
+ result.current.toast({ title: 'Error', variant: 'destructive' })
27
+ })
28
+
29
+ expect(result.current.toasts[0].variant).toBe('destructive')
30
+ })
31
+
32
+ it('dismisses a specific toast', () => {
33
+ jest.useFakeTimers()
34
+ const { result } = renderHook(() => useToast())
35
+
36
+ let toastId: string
37
+ act(() => {
38
+ // Use duration: 0 to prevent auto-dismiss interference
39
+ const t = result.current.toast({ title: 'Dismiss me', duration: 0 })
40
+ toastId = t.id
41
+ })
42
+
43
+ expect(result.current.toasts).toHaveLength(1)
44
+
45
+ act(() => {
46
+ result.current.dismiss(toastId)
47
+ })
48
+
49
+ // Advance past the dismiss animation delay
50
+ act(() => {
51
+ jest.advanceTimersByTime(500)
52
+ })
53
+
54
+ expect(result.current.toasts).toHaveLength(0)
55
+ jest.useRealTimers()
56
+ })
57
+
58
+ it('clears all toasts', () => {
59
+ const { result } = renderHook(() => useToast())
60
+
61
+ act(() => {
62
+ result.current.toast({ title: 'One', duration: 0 })
63
+ result.current.toast({ title: 'Two', duration: 0 })
64
+ result.current.toast({ title: 'Three', duration: 0 })
65
+ })
66
+
67
+ expect(result.current.toasts.length).toBeGreaterThanOrEqual(1)
68
+
69
+ act(() => {
70
+ result.current.clear()
71
+ })
72
+
73
+ expect(result.current.toasts).toHaveLength(0)
74
+ })
75
+ })
76
+
77
+ describe('reducer', () => {
78
+ const makeToast = (id: string, title: string): ToasterToast => ({
79
+ id,
80
+ title,
81
+ })
82
+
83
+ it('ADD_TOAST adds to front of list', () => {
84
+ const state = { toasts: [makeToast('1', 'First')] }
85
+ const result = reducer(state, {
86
+ type: 'ADD_TOAST',
87
+ toast: makeToast('2', 'Second'),
88
+ })
89
+ expect(result.toasts[0].title).toBe('Second')
90
+ expect(result.toasts).toHaveLength(2)
91
+ })
92
+
93
+ it('UPDATE_TOAST updates matching toast', () => {
94
+ const state = { toasts: [makeToast('1', 'Original')] }
95
+ const result = reducer(state, {
96
+ type: 'UPDATE_TOAST',
97
+ toast: { id: '1', title: 'Updated' },
98
+ })
99
+ expect(result.toasts[0].title).toBe('Updated')
100
+ })
101
+
102
+ it('REMOVE_TOAST with undefined removes all', () => {
103
+ const state = {
104
+ toasts: [makeToast('1', 'A'), makeToast('2', 'B')],
105
+ }
106
+ const result = reducer(state, {
107
+ type: 'REMOVE_TOAST',
108
+ toastId: undefined,
109
+ })
110
+ expect(result.toasts).toHaveLength(0)
111
+ })
112
+
113
+ it('REMOVE_TOAST with id removes specific toast', () => {
114
+ const state = {
115
+ toasts: [makeToast('1', 'A'), makeToast('2', 'B')],
116
+ }
117
+ const result = reducer(state, {
118
+ type: 'REMOVE_TOAST',
119
+ toastId: '1',
120
+ })
121
+ expect(result.toasts).toHaveLength(1)
122
+ expect(result.toasts[0].id).toBe('2')
123
+ })
124
+ })
@@ -0,0 +1,55 @@
1
+ import * as React from 'react'
2
+ import { cn } from '../../lib/utils'
3
+
4
+ export interface StatusBadgeConfig {
5
+ label: string
6
+ className: string
7
+ }
8
+
9
+ export interface StatusBadgeProps<T extends string> {
10
+ status: T
11
+ config: Record<T, StatusBadgeConfig>
12
+ size?: 'sm' | 'md'
13
+ className?: string
14
+ }
15
+
16
+ const sizeClasses = {
17
+ sm: 'px-2 py-0.5 text-xs',
18
+ md: 'px-2.5 py-0.5 text-xs',
19
+ }
20
+
21
+ export function StatusBadge<T extends string>({
22
+ status,
23
+ config,
24
+ size = 'md',
25
+ className,
26
+ }: StatusBadgeProps<T>) {
27
+ const entry = config[status]
28
+
29
+ if (!entry) {
30
+ return (
31
+ <span
32
+ className={cn(
33
+ 'inline-flex items-center rounded-full font-medium bg-gray-100 text-gray-700 border border-gray-200',
34
+ sizeClasses[size],
35
+ className
36
+ )}
37
+ >
38
+ {status}
39
+ </span>
40
+ )
41
+ }
42
+
43
+ return (
44
+ <span
45
+ className={cn(
46
+ 'inline-flex items-center rounded-full font-medium',
47
+ sizeClasses[size],
48
+ entry.className,
49
+ className
50
+ )}
51
+ >
52
+ {entry.label}
53
+ </span>
54
+ )
55
+ }
@@ -0,0 +1,2 @@
1
+ export { StatusBadge } from './StatusBadge'
2
+ export type { StatusBadgeProps, StatusBadgeConfig } from './StatusBadge'
@@ -0,0 +1,184 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import * as DialogPrimitive from '@radix-ui/react-dialog'
5
+ import { cn } from '../../lib/utils'
6
+
7
+ interface DialogContextValue {
8
+ isLoading: boolean
9
+ setIsLoading: (loading: boolean) => void
10
+ canClose: boolean
11
+ setCanClose: (canClose: boolean) => void
12
+ }
13
+
14
+ const DialogContext = React.createContext<DialogContextValue | null>(null)
15
+
16
+ function useDialogContext() {
17
+ const context = React.useContext(DialogContext)
18
+ if (!context) {
19
+ throw new Error('Dialog components must be used within BaseDialog')
20
+ }
21
+ return context
22
+ }
23
+
24
+ export interface BaseDialogProps {
25
+ open: boolean
26
+ onOpenChange: (open: boolean) => void
27
+ children: React.ReactNode
28
+ size?: 'sm' | 'md' | 'lg'
29
+ loading?: boolean
30
+ }
31
+
32
+ const sizeClasses = {
33
+ sm: 'max-w-sm',
34
+ md: 'max-w-md',
35
+ lg: 'max-w-lg',
36
+ }
37
+
38
+ export function BaseDialog({
39
+ open,
40
+ onOpenChange,
41
+ children,
42
+ size = 'md',
43
+ loading = false,
44
+ }: BaseDialogProps) {
45
+ const [isLoading, setIsLoading] = React.useState(loading)
46
+ const [canClose, setCanClose] = React.useState(!loading)
47
+
48
+ React.useEffect(() => {
49
+ setIsLoading(loading)
50
+ setCanClose(!loading)
51
+ }, [loading])
52
+
53
+ const handleOpenChange = React.useCallback(
54
+ (newOpen: boolean) => {
55
+ if (!newOpen && !canClose) {
56
+ return
57
+ }
58
+ onOpenChange(newOpen)
59
+ },
60
+ [canClose, onOpenChange]
61
+ )
62
+
63
+ const contextValue = React.useMemo(
64
+ () => ({ isLoading, setIsLoading, canClose, setCanClose }),
65
+ [isLoading, canClose]
66
+ )
67
+
68
+ return (
69
+ <DialogContext.Provider value={contextValue}>
70
+ <DialogPrimitive.Root open={open} onOpenChange={handleOpenChange}>
71
+ <DialogPrimitive.Portal>
72
+ <DialogPrimitive.Overlay className="fixed inset-0 z-50 bg-black/50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0" />
73
+ <DialogPrimitive.Content
74
+ className={cn(
75
+ 'fixed left-[50%] top-[50%] z-50 grid w-full translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
76
+ sizeClasses[size]
77
+ )}
78
+ onInteractOutside={(e) => {
79
+ if (!canClose) e.preventDefault()
80
+ }}
81
+ onEscapeKeyDown={(e) => {
82
+ if (!canClose) e.preventDefault()
83
+ }}
84
+ >
85
+ {children}
86
+ </DialogPrimitive.Content>
87
+ </DialogPrimitive.Portal>
88
+ </DialogPrimitive.Root>
89
+ </DialogContext.Provider>
90
+ )
91
+ }
92
+
93
+ export interface DialogHeaderProps {
94
+ children: React.ReactNode
95
+ className?: string
96
+ }
97
+
98
+ function DialogHeader({ children, className }: DialogHeaderProps) {
99
+ const { canClose } = useDialogContext()
100
+
101
+ return (
102
+ <div className={cn('flex items-start justify-between', className)}>
103
+ <div className="flex flex-col space-y-1.5">{children}</div>
104
+ <DialogPrimitive.Close
105
+ className={cn(
106
+ 'rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none',
107
+ !canClose && 'pointer-events-none opacity-30'
108
+ )}
109
+ disabled={!canClose}
110
+ >
111
+ <svg
112
+ xmlns="http://www.w3.org/2000/svg"
113
+ width="16"
114
+ height="16"
115
+ viewBox="0 0 24 24"
116
+ fill="none"
117
+ stroke="currentColor"
118
+ strokeWidth="2"
119
+ strokeLinecap="round"
120
+ strokeLinejoin="round"
121
+ >
122
+ <line x1="18" y1="6" x2="6" y2="18" />
123
+ <line x1="6" y1="6" x2="18" y2="18" />
124
+ </svg>
125
+ <span className="sr-only">Close</span>
126
+ </DialogPrimitive.Close>
127
+ </div>
128
+ )
129
+ }
130
+
131
+ export interface DialogTitleProps {
132
+ children: React.ReactNode
133
+ className?: string
134
+ }
135
+
136
+ function DialogTitle({ children, className }: DialogTitleProps) {
137
+ return (
138
+ <DialogPrimitive.Title
139
+ className={cn(
140
+ 'text-lg font-semibold leading-none tracking-tight',
141
+ className
142
+ )}
143
+ >
144
+ {children}
145
+ </DialogPrimitive.Title>
146
+ )
147
+ }
148
+
149
+ export interface DialogBodyProps {
150
+ children: React.ReactNode
151
+ className?: string
152
+ }
153
+
154
+ function DialogBody({ children, className }: DialogBodyProps) {
155
+ return (
156
+ <div className={cn('py-4 overflow-y-auto', className)}>{children}</div>
157
+ )
158
+ }
159
+
160
+ export interface DialogFooterProps {
161
+ children: React.ReactNode
162
+ className?: string
163
+ }
164
+
165
+ function DialogFooter({ children, className }: DialogFooterProps) {
166
+ return (
167
+ <div
168
+ className={cn(
169
+ 'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
170
+ className
171
+ )}
172
+ >
173
+ {children}
174
+ </div>
175
+ )
176
+ }
177
+
178
+ // Compound component pattern
179
+ BaseDialog.Header = DialogHeader
180
+ BaseDialog.Title = DialogTitle
181
+ BaseDialog.Body = DialogBody
182
+ BaseDialog.Footer = DialogFooter
183
+
184
+ export { useDialogContext }
@@ -0,0 +1,8 @@
1
+ export { BaseDialog, useDialogContext } from './BaseDialog'
2
+ export type {
3
+ BaseDialogProps,
4
+ DialogHeaderProps,
5
+ DialogTitleProps,
6
+ DialogBodyProps,
7
+ DialogFooterProps,
8
+ } from './BaseDialog'
@@ -6,6 +6,10 @@ export { UnifiedTable } from './unified-table'
6
6
  export type { UnifiedTableProps } from './unified-table'
7
7
  export * from './unified-table/types'
8
8
 
9
+ // Export StandardTableToolbar for app-level table toolbars
10
+ export { StandardTableToolbar } from './unified-table'
11
+ export type { StandardTableToolbarProps } from './unified-table'
12
+
9
13
  // MobileView - Card re-exported under non-conflicting names
10
14
  export { MobileView, Card as MobileCard, CardActions as MobileCardActions } from './unified-table'
11
15
  export type {
@@ -48,3 +52,24 @@ export * from './unified-table/utils'
48
52
 
49
53
  // Export Navigation
50
54
  export * from './navigation/sidebar'
55
+
56
+ // HTML rendering with XSS protection
57
+ export { SafeHtml } from './safe-html'
58
+
59
+ // Toast system
60
+ export * from './toast'
61
+
62
+ // State components (ErrorState, EmptyState)
63
+ export * from './states'
64
+
65
+ // BaseDialog compound component
66
+ export * from './dialog'
67
+
68
+ // StatusBadge
69
+ export * from './badge'
70
+
71
+ // Loading skeletons
72
+ export * from './loading'
73
+
74
+ // Wizard / StepIndicator
75
+ export * from './wizard'