@open-mercato/ui 0.6.4-develop.4178.1.aad9ddaa95 → 0.6.4-develop.4210.1.d412061cfe

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 (92) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +14 -0
  3. package/dist/backend/DataTable.js +28 -62
  4. package/dist/backend/DataTable.js.map +2 -2
  5. package/dist/backend/detail/AttachmentMetadataDialog.js +1 -1
  6. package/dist/backend/detail/AttachmentMetadataDialog.js.map +2 -2
  7. package/dist/primitives/activity-feed.js +196 -0
  8. package/dist/primitives/activity-feed.js.map +7 -0
  9. package/dist/primitives/avatar.js +140 -14
  10. package/dist/primitives/avatar.js.map +3 -3
  11. package/dist/primitives/badge.js +94 -5
  12. package/dist/primitives/badge.js.map +2 -2
  13. package/dist/primitives/button-group.js +52 -0
  14. package/dist/primitives/button-group.js.map +7 -0
  15. package/dist/primitives/color-picker.js +520 -0
  16. package/dist/primitives/color-picker.js.map +7 -0
  17. package/dist/primitives/command-menu.js +355 -0
  18. package/dist/primitives/command-menu.js.map +7 -0
  19. package/dist/primitives/dialog.js +210 -35
  20. package/dist/primitives/dialog.js.map +2 -2
  21. package/dist/primitives/drawer.js +219 -0
  22. package/dist/primitives/drawer.js.map +7 -0
  23. package/dist/primitives/notification-feed.js +213 -0
  24. package/dist/primitives/notification-feed.js.map +7 -0
  25. package/dist/primitives/notification.js +23 -2
  26. package/dist/primitives/notification.js.map +2 -2
  27. package/dist/primitives/pagination.js +245 -0
  28. package/dist/primitives/pagination.js.map +7 -0
  29. package/dist/primitives/progress.js +178 -5
  30. package/dist/primitives/progress.js.map +2 -2
  31. package/dist/primitives/rating.js +174 -0
  32. package/dist/primitives/rating.js.map +7 -0
  33. package/dist/primitives/scroll-area.js +82 -0
  34. package/dist/primitives/scroll-area.js.map +7 -0
  35. package/dist/primitives/segmented-control.js +101 -0
  36. package/dist/primitives/segmented-control.js.map +7 -0
  37. package/dist/primitives/separator.js +103 -4
  38. package/dist/primitives/separator.js.map +2 -2
  39. package/dist/primitives/slider.js +115 -0
  40. package/dist/primitives/slider.js.map +7 -0
  41. package/dist/primitives/step-indicator.js +187 -0
  42. package/dist/primitives/step-indicator.js.map +7 -0
  43. package/dist/primitives/table.js +102 -10
  44. package/dist/primitives/table.js.map +2 -2
  45. package/dist/primitives/tabs.js +122 -14
  46. package/dist/primitives/tabs.js.map +2 -2
  47. package/dist/primitives/time-picker.js +9 -11
  48. package/dist/primitives/time-picker.js.map +2 -2
  49. package/package.json +8 -4
  50. package/src/backend/DataTable.tsx +43 -60
  51. package/src/backend/detail/AttachmentMetadataDialog.tsx +1 -1
  52. package/src/primitives/__tests__/activity-feed.test.tsx +216 -0
  53. package/src/primitives/__tests__/avatar.test.tsx +131 -0
  54. package/src/primitives/__tests__/badge.test.tsx +189 -0
  55. package/src/primitives/__tests__/button-group.test.tsx +117 -0
  56. package/src/primitives/__tests__/color-picker.test.tsx +589 -0
  57. package/src/primitives/__tests__/command-menu.test.tsx +231 -0
  58. package/src/primitives/__tests__/dialog.test.tsx +290 -0
  59. package/src/primitives/__tests__/drawer.test.tsx +264 -0
  60. package/src/primitives/__tests__/notification-feed.test.tsx +303 -0
  61. package/src/primitives/__tests__/notification.test.tsx +102 -2
  62. package/src/primitives/__tests__/pagination.test.tsx +268 -0
  63. package/src/primitives/__tests__/progress.test.tsx +222 -0
  64. package/src/primitives/__tests__/rating.test.tsx +164 -0
  65. package/src/primitives/__tests__/scroll-area.test.tsx +109 -0
  66. package/src/primitives/__tests__/segmented-control.test.tsx +129 -0
  67. package/src/primitives/__tests__/separator.test.tsx +143 -0
  68. package/src/primitives/__tests__/slider.test.tsx +86 -0
  69. package/src/primitives/__tests__/step-indicator.test.tsx +205 -0
  70. package/src/primitives/__tests__/table.test.tsx +199 -0
  71. package/src/primitives/__tests__/tabs.test.tsx +139 -0
  72. package/src/primitives/activity-feed.tsx +326 -0
  73. package/src/primitives/avatar.tsx +225 -22
  74. package/src/primitives/badge.tsx +141 -6
  75. package/src/primitives/button-group.tsx +100 -0
  76. package/src/primitives/color-picker.tsx +693 -0
  77. package/src/primitives/command-menu.tsx +503 -0
  78. package/src/primitives/dialog.tsx +315 -51
  79. package/src/primitives/drawer.tsx +338 -0
  80. package/src/primitives/notification-feed.tsx +358 -0
  81. package/src/primitives/notification.tsx +42 -2
  82. package/src/primitives/pagination.tsx +376 -0
  83. package/src/primitives/progress.tsx +267 -6
  84. package/src/primitives/rating.tsx +245 -0
  85. package/src/primitives/scroll-area.tsx +169 -0
  86. package/src/primitives/segmented-control.tsx +162 -0
  87. package/src/primitives/separator.tsx +163 -3
  88. package/src/primitives/slider.tsx +160 -0
  89. package/src/primitives/step-indicator.tsx +287 -0
  90. package/src/primitives/table.tsx +142 -11
  91. package/src/primitives/tabs.tsx +174 -12
  92. package/src/primitives/time-picker.tsx +9 -11
@@ -0,0 +1,231 @@
1
+ /** @jest-environment jsdom */
2
+
3
+ import * as React from 'react'
4
+ import { render as rtlRender, fireEvent, screen } from '@testing-library/react'
5
+ import { I18nProvider } from '@open-mercato/shared/lib/i18n/context'
6
+
7
+ // CommandMenu uses useT() for the sr-only title + footer hints (Navigate / Select).
8
+ // Wrap every render in an empty-dict I18nProvider so the primitive
9
+ // falls back to its English fallbacks without real translations.
10
+ const render: typeof rtlRender = (ui: React.ReactElement, options?: Parameters<typeof rtlRender>[1]) =>
11
+ rtlRender(
12
+ <I18nProvider locale="en" dict={{}}>
13
+ {ui}
14
+ </I18nProvider>,
15
+ options,
16
+ )
17
+
18
+ // jsdom doesn't implement Element.scrollIntoView; cmdk uses it when
19
+ // the selected item changes. Provide a no-op so the component can mount.
20
+ if (typeof Element !== 'undefined' && !Element.prototype.scrollIntoView) {
21
+ Element.prototype.scrollIntoView = () => {}
22
+ }
23
+ import {
24
+ CommandMenu,
25
+ CommandMenuTrigger,
26
+ CommandMenuContent,
27
+ CommandMenuInput,
28
+ CommandMenuList,
29
+ CommandMenuEmpty,
30
+ CommandMenuGroup,
31
+ CommandMenuItem,
32
+ CommandMenuSeparator,
33
+ CommandMenuFooter,
34
+ } from '../command-menu'
35
+
36
+ function ExampleMenu({
37
+ onSelectMonday,
38
+ defaultOpen = true,
39
+ withSeparator = true,
40
+ }: {
41
+ onSelectMonday?: () => void
42
+ defaultOpen?: boolean
43
+ withSeparator?: boolean
44
+ }) {
45
+ return (
46
+ <CommandMenu defaultOpen={defaultOpen}>
47
+ <CommandMenuTrigger>Open</CommandMenuTrigger>
48
+ <CommandMenuContent>
49
+ <CommandMenuInput placeholder="Search..." />
50
+ <CommandMenuList>
51
+ <CommandMenuEmpty>No results.</CommandMenuEmpty>
52
+ <CommandMenuGroup heading="Tools & Apps">
53
+ <CommandMenuItem value="monday.com" onSelect={onSelectMonday}>
54
+ Monday.com
55
+ </CommandMenuItem>
56
+ <CommandMenuItem value="loom">Loom</CommandMenuItem>
57
+ <CommandMenuItem value="asana">Asana</CommandMenuItem>
58
+ </CommandMenuGroup>
59
+ {withSeparator ? <CommandMenuSeparator /> : null}
60
+ <CommandMenuGroup heading="Employees">
61
+ <CommandMenuItem value="james brown" description="Engineer">
62
+ James Brown
63
+ </CommandMenuItem>
64
+ <CommandMenuItem value="sophia williams" description="Designer">
65
+ Sophia Williams
66
+ </CommandMenuItem>
67
+ </CommandMenuGroup>
68
+ </CommandMenuList>
69
+ <CommandMenuFooter />
70
+ </CommandMenuContent>
71
+ </CommandMenu>
72
+ )
73
+ }
74
+
75
+ describe('CommandMenu', () => {
76
+ it('renders content/input/list/group/item/footer slots inside a Radix Dialog', () => {
77
+ render(<ExampleMenu />)
78
+ expect(document.querySelector('[data-slot="command-menu-overlay"]')).not.toBeNull()
79
+ expect(document.querySelector('[data-slot="command-menu-content"]')).not.toBeNull()
80
+ expect(document.querySelector('[data-slot="command-menu-root"]')).not.toBeNull()
81
+ expect(document.querySelector('[data-slot="command-menu-input"]')).not.toBeNull()
82
+ expect(document.querySelector('[data-slot="command-menu-list"]')).not.toBeNull()
83
+ expect(document.querySelectorAll('[data-slot="command-menu-group"]').length).toBe(2)
84
+ expect(document.querySelectorAll('[data-slot="command-menu-item"]').length).toBeGreaterThanOrEqual(5)
85
+ expect(document.querySelector('[data-slot="command-menu-separator"]')).not.toBeNull()
86
+ expect(document.querySelector('[data-slot="command-menu-footer"]')).not.toBeNull()
87
+ })
88
+
89
+ it('opens on trigger click when controlled-default is closed', () => {
90
+ function Controlled() {
91
+ const [open, setOpen] = React.useState(false)
92
+ return (
93
+ <CommandMenu open={open} onOpenChange={setOpen}>
94
+ <CommandMenuTrigger>Open palette</CommandMenuTrigger>
95
+ <CommandMenuContent>
96
+ <CommandMenuInput placeholder="Search..." />
97
+ <CommandMenuList>
98
+ <CommandMenuGroup heading="Tools">
99
+ <CommandMenuItem value="loom">Loom</CommandMenuItem>
100
+ </CommandMenuGroup>
101
+ </CommandMenuList>
102
+ </CommandMenuContent>
103
+ </CommandMenu>
104
+ )
105
+ }
106
+ const { getByText } = render(<Controlled />)
107
+ expect(document.querySelector('[data-slot="command-menu-content"]')).toBeNull()
108
+ fireEvent.click(getByText('Open palette'))
109
+ expect(document.querySelector('[data-slot="command-menu-content"]')).not.toBeNull()
110
+ })
111
+
112
+ it('renders role="dialog" with the auto-hidden title for screen readers', () => {
113
+ render(<ExampleMenu />)
114
+ expect(screen.getByRole('dialog')).toBeInTheDocument()
115
+ expect(screen.getByText('Command menu')).toBeInTheDocument()
116
+ })
117
+
118
+ it('renders leading magnifier icon and the ⌘K kbd hint by default', () => {
119
+ render(<ExampleMenu />)
120
+ expect(document.querySelector('[data-slot="command-menu-input-icon"]')).not.toBeNull()
121
+ expect(document.querySelector('[data-slot="command-menu-input-shortcut"]')).not.toBeNull()
122
+ expect(document.querySelector('[data-slot="command-menu-input-clear"]')).toBeNull()
123
+ })
124
+
125
+ it('swaps the ⌘K kbd for a clear × button once the input has a value', () => {
126
+ render(<ExampleMenu />)
127
+ const input = document.querySelector('[data-slot="command-menu-input"]') as HTMLInputElement
128
+ fireEvent.change(input, { target: { value: 'monday' } })
129
+ expect(document.querySelector('[data-slot="command-menu-input-shortcut"]')).toBeNull()
130
+ expect(document.querySelector('[data-slot="command-menu-input-clear"]')).not.toBeNull()
131
+ })
132
+
133
+ it('clears the value when the × button is clicked', () => {
134
+ render(<ExampleMenu />)
135
+ const input = document.querySelector('[data-slot="command-menu-input"]') as HTMLInputElement
136
+ fireEvent.change(input, { target: { value: 'monday' } })
137
+ const clear = document.querySelector('[data-slot="command-menu-input-clear"]') as HTMLButtonElement
138
+ fireEvent.click(clear)
139
+ expect(input.value).toBe('')
140
+ })
141
+
142
+ it('renders item description and chevron slot when applicable', () => {
143
+ render(<ExampleMenu />)
144
+ // Description slot exists for items configured with `description`.
145
+ expect(
146
+ document.querySelectorAll('[data-slot="command-menu-item-description"]').length,
147
+ ).toBeGreaterThan(0)
148
+ // Auto chevron renders by default (no `shortcut`, no `hideChevron`).
149
+ expect(document.querySelectorAll('[data-slot="command-menu-item-chevron"]').length).toBeGreaterThan(0)
150
+ })
151
+
152
+ it('fires onSelect when an item is clicked', () => {
153
+ const onSelectMonday = jest.fn()
154
+ render(<ExampleMenu onSelectMonday={onSelectMonday} />)
155
+ const items = document.querySelectorAll('[data-slot="command-menu-item"]') as NodeListOf<HTMLElement>
156
+ const monday = Array.from(items).find((el) => el.textContent?.includes('Monday.com')) as HTMLElement
157
+ expect(monday).toBeTruthy()
158
+ fireEvent.click(monday)
159
+ expect(onSelectMonday).toHaveBeenCalledTimes(1)
160
+ })
161
+
162
+ it('renders the group action button when actionLabel + onAction are provided', () => {
163
+ const onAction = jest.fn()
164
+ render(
165
+ <CommandMenu defaultOpen>
166
+ <CommandMenuTrigger>Open</CommandMenuTrigger>
167
+ <CommandMenuContent>
168
+ <CommandMenuInput placeholder="Search..." />
169
+ <CommandMenuList>
170
+ <CommandMenuGroup heading="Tools" actionLabel="See all" onAction={onAction}>
171
+ <CommandMenuItem value="loom">Loom</CommandMenuItem>
172
+ </CommandMenuGroup>
173
+ </CommandMenuList>
174
+ </CommandMenuContent>
175
+ </CommandMenu>,
176
+ )
177
+ const action = document.querySelector('[data-slot="command-menu-group-action"] button') as HTMLButtonElement
178
+ expect(action).not.toBeNull()
179
+ expect(action.textContent).toContain('See all')
180
+ fireEvent.click(action)
181
+ expect(onAction).toHaveBeenCalledTimes(1)
182
+ })
183
+
184
+ it('renders default footer hints (Navigate / Select)', () => {
185
+ render(<ExampleMenu />)
186
+ const hints = document.querySelector('[data-slot="command-menu-footer-hints"]') as HTMLElement
187
+ expect(hints).not.toBeNull()
188
+ expect(hints.textContent).toContain('Navigate')
189
+ expect(hints.textContent).toContain('Select')
190
+ })
191
+
192
+ it('renders the help slot when provided', () => {
193
+ render(
194
+ <CommandMenu defaultOpen>
195
+ <CommandMenuTrigger>Open</CommandMenuTrigger>
196
+ <CommandMenuContent>
197
+ <CommandMenuInput placeholder="Search..." />
198
+ <CommandMenuList>
199
+ <CommandMenuGroup heading="Tools">
200
+ <CommandMenuItem value="loom">Loom</CommandMenuItem>
201
+ </CommandMenuGroup>
202
+ </CommandMenuList>
203
+ <CommandMenuFooter helpSlot={<a href="#contact">Contact</a>} />
204
+ </CommandMenuContent>
205
+ </CommandMenu>,
206
+ )
207
+ const help = document.querySelector('[data-slot="command-menu-footer-help"]') as HTMLElement
208
+ expect(help).not.toBeNull()
209
+ expect(help.textContent).toContain('Contact')
210
+ })
211
+
212
+ it('forwards className to command-menu-content without dropping default positioning', () => {
213
+ render(
214
+ <CommandMenu defaultOpen>
215
+ <CommandMenuTrigger>Open</CommandMenuTrigger>
216
+ <CommandMenuContent className="custom-class">
217
+ <CommandMenuInput placeholder="Search..." />
218
+ <CommandMenuList>
219
+ <CommandMenuGroup heading="Tools">
220
+ <CommandMenuItem value="loom">Loom</CommandMenuItem>
221
+ </CommandMenuGroup>
222
+ </CommandMenuList>
223
+ </CommandMenuContent>
224
+ </CommandMenu>,
225
+ )
226
+ const content = document.querySelector('[data-slot="command-menu-content"]') as HTMLElement
227
+ expect(content.className).toContain('custom-class')
228
+ expect(content.className).toContain('fixed')
229
+ expect(content.className).toContain('z-popover')
230
+ })
231
+ })
@@ -0,0 +1,290 @@
1
+ /** @jest-environment jsdom */
2
+
3
+ import * as React from 'react'
4
+ import { render, fireEvent, screen } from '@testing-library/react'
5
+
6
+ import {
7
+ Dialog,
8
+ DialogTrigger,
9
+ DialogContent,
10
+ DialogHeader,
11
+ DialogTitle,
12
+ DialogDescription,
13
+ DialogFooter,
14
+ DialogClose,
15
+ } from '../dialog'
16
+ import { I18nProvider } from '@open-mercato/shared/lib/i18n/context'
17
+
18
+ function renderDialog(ui: React.ReactElement) {
19
+ return render(
20
+ <I18nProvider locale="en" dict={{ ui: { dialog: { close: { ariaLabel: 'Close' } } } }}>
21
+ {ui}
22
+ </I18nProvider>,
23
+ )
24
+ }
25
+
26
+ function ExampleDialog({
27
+ size,
28
+ dismissible,
29
+ defaultOpen = true,
30
+ leading,
31
+ footerLayout,
32
+ }: {
33
+ size?: 'sm' | 'default' | 'lg' | 'xl'
34
+ dismissible?: boolean
35
+ defaultOpen?: boolean
36
+ leading?: React.ReactNode
37
+ footerLayout?: 'default' | 'equal'
38
+ }) {
39
+ return (
40
+ <Dialog defaultOpen={defaultOpen}>
41
+ <DialogTrigger>Open</DialogTrigger>
42
+ <DialogContent size={size} dismissible={dismissible}>
43
+ <DialogHeader leading={leading}>
44
+ <DialogTitle>Confirm action</DialogTitle>
45
+ <DialogDescription>This dialog confirms a critical action.</DialogDescription>
46
+ </DialogHeader>
47
+ <DialogFooter layout={footerLayout}>
48
+ <DialogClose>Cancel</DialogClose>
49
+ <button>Continue</button>
50
+ </DialogFooter>
51
+ </DialogContent>
52
+ </Dialog>
53
+ )
54
+ }
55
+
56
+ describe('Dialog (Phase B.7)', () => {
57
+ it('renders content / overlay / header / title / description / footer slots inside a Radix Dialog', () => {
58
+ renderDialog(<ExampleDialog />)
59
+ expect(document.querySelector('[data-slot="dialog-content"]')).not.toBeNull()
60
+ expect(document.querySelector('[data-slot="dialog-overlay"]')).not.toBeNull()
61
+ expect(document.querySelector('[data-slot="dialog-header"]')).not.toBeNull()
62
+ expect(document.querySelector('[data-slot="dialog-title"]')).not.toBeNull()
63
+ expect(document.querySelector('[data-slot="dialog-description"]')).not.toBeNull()
64
+ expect(document.querySelector('[data-slot="dialog-footer"]')).not.toBeNull()
65
+ })
66
+
67
+ it('opens on trigger click when defaultOpen=false', () => {
68
+ function Controlled() {
69
+ const [open, setOpen] = React.useState(false)
70
+ return (
71
+ <Dialog open={open} onOpenChange={setOpen}>
72
+ <DialogTrigger>Open</DialogTrigger>
73
+ <DialogContent>
74
+ <DialogHeader>
75
+ <DialogTitle>Test</DialogTitle>
76
+ </DialogHeader>
77
+ </DialogContent>
78
+ </Dialog>
79
+ )
80
+ }
81
+ renderDialog(<Controlled />)
82
+ expect(document.querySelector('[data-slot="dialog-content"]')).toBeNull()
83
+ fireEvent.click(screen.getByText('Open'))
84
+ expect(document.querySelector('[data-slot="dialog-content"]')).not.toBeNull()
85
+ })
86
+
87
+ it('renders the auto X close button by default', () => {
88
+ renderDialog(<ExampleDialog />)
89
+ expect(document.querySelector('[data-slot="dialog-close-button"]')).not.toBeNull()
90
+ })
91
+
92
+ it('omits the auto X when dismissible=false', () => {
93
+ renderDialog(<ExampleDialog dismissible={false} />)
94
+ expect(document.querySelector('[data-slot="dialog-close-button"]')).toBeNull()
95
+ })
96
+
97
+ it('default size="default" applies sm:max-w-lg', () => {
98
+ renderDialog(<ExampleDialog />)
99
+ const content = document.querySelector('[data-slot="dialog-content"]') as HTMLElement
100
+ expect(content.getAttribute('data-size')).toBe('default')
101
+ expect(content.className).toContain('sm:max-w-lg')
102
+ })
103
+
104
+ it('size variants apply matching max-width', () => {
105
+ const cases: Array<{ size: 'sm' | 'default' | 'lg' | 'xl'; cls: string }> = [
106
+ { size: 'sm', cls: 'sm:max-w-sm' },
107
+ { size: 'default', cls: 'sm:max-w-lg' },
108
+ { size: 'lg', cls: 'sm:max-w-2xl' },
109
+ { size: 'xl', cls: 'sm:max-w-4xl' },
110
+ ]
111
+ for (const { size, cls } of cases) {
112
+ const { unmount } = renderDialog(<ExampleDialog size={size} />)
113
+ const content = document.querySelector('[data-slot="dialog-content"]') as HTMLElement
114
+ expect(content.getAttribute('data-size')).toBe(size)
115
+ expect(content.className).toContain(cls)
116
+ unmount()
117
+ }
118
+ })
119
+
120
+ it('renders the header leading badge when leading is provided', () => {
121
+ renderDialog(<ExampleDialog leading={<span data-testid="lead-icon">⚙</span>} />)
122
+ const badge = document.querySelector('[data-slot="dialog-header-leading"]') as HTMLElement
123
+ expect(badge).not.toBeNull()
124
+ expect(badge.querySelector('[data-testid="lead-icon"]')).not.toBeNull()
125
+ expect(badge.className).toContain('rounded-full')
126
+ expect(badge.className).toContain('size-10')
127
+ expect(badge.className).toContain('border')
128
+ // Title + description live inside the text wrapper alongside the badge.
129
+ const text = document.querySelector('[data-slot="dialog-header-text"]') as HTMLElement
130
+ expect(text).not.toBeNull()
131
+ expect(text.querySelector('[data-slot="dialog-title"]')).not.toBeNull()
132
+ expect(text.querySelector('[data-slot="dialog-description"]')).not.toBeNull()
133
+ })
134
+
135
+ it('omits the leading badge by default', () => {
136
+ renderDialog(<ExampleDialog />)
137
+ expect(document.querySelector('[data-slot="dialog-header-leading"]')).toBeNull()
138
+ expect(document.querySelector('[data-slot="dialog-header-text"]')).toBeNull()
139
+ })
140
+
141
+ it('default footer layout reads "default" + flex-col-reverse classes', () => {
142
+ renderDialog(<ExampleDialog />)
143
+ const footer = document.querySelector('[data-slot="dialog-footer"]') as HTMLElement
144
+ expect(footer.getAttribute('data-layout')).toBe('default')
145
+ expect(footer.className).toContain('flex-col-reverse')
146
+ expect(footer.className).toContain('sm:justify-end')
147
+ })
148
+
149
+ it('footer ships border-t separator by default per Figma `Modal Footer [1.1]`', () => {
150
+ renderDialog(<ExampleDialog />)
151
+ const footer = document.querySelector('[data-slot="dialog-footer"]') as HTMLElement
152
+ expect(footer.getAttribute('data-bordered')).toBe('true')
153
+ expect(footer.className).toContain('border-t')
154
+ expect(footer.className).toContain('pt-4')
155
+ })
156
+
157
+ it('footer bordered=false drops the separator', () => {
158
+ renderDialog(
159
+ <Dialog defaultOpen>
160
+ <DialogTrigger>Open</DialogTrigger>
161
+ <DialogContent>
162
+ <DialogHeader>
163
+ <DialogTitle>x</DialogTitle>
164
+ </DialogHeader>
165
+ <DialogFooter bordered={false}>
166
+ <DialogClose>Cancel</DialogClose>
167
+ </DialogFooter>
168
+ </DialogContent>
169
+ </Dialog>,
170
+ )
171
+ const footer = document.querySelector('[data-slot="dialog-footer"]') as HTMLElement
172
+ expect(footer.getAttribute('data-bordered')).toBeNull()
173
+ expect(footer.className).not.toContain('border-t')
174
+ })
175
+
176
+ it('footer layout="equal" stretches children flex-1', () => {
177
+ renderDialog(<ExampleDialog footerLayout="equal" />)
178
+ const footer = document.querySelector('[data-slot="dialog-footer"]') as HTMLElement
179
+ expect(footer.getAttribute('data-layout')).toBe('equal')
180
+ expect(footer.className).toContain('[&>*]:flex-1')
181
+ expect(footer.className).not.toContain('flex-col-reverse')
182
+ // border-t default still applies on equal layout.
183
+ expect(footer.className).toContain('border-t')
184
+ })
185
+
186
+ it('footer leading slot renders left content + right-aligned trailing buttons per Figma `Modal Footer [1.1]` variants 2-6', () => {
187
+ renderDialog(
188
+ <Dialog defaultOpen>
189
+ <DialogTrigger>Open</DialogTrigger>
190
+ <DialogContent>
191
+ <DialogHeader>
192
+ <DialogTitle>x</DialogTitle>
193
+ </DialogHeader>
194
+ <DialogFooter leading={<label data-testid="dont-show">Don&apos;t show again</label>}>
195
+ <DialogClose>Cancel</DialogClose>
196
+ <button>Continue</button>
197
+ </DialogFooter>
198
+ </DialogContent>
199
+ </Dialog>,
200
+ )
201
+ const leading = document.querySelector('[data-slot="dialog-footer-leading"]') as HTMLElement
202
+ expect(leading).not.toBeNull()
203
+ expect(leading.className).toContain('sm:mr-auto')
204
+ expect(leading.querySelector('[data-testid="dont-show"]')).not.toBeNull()
205
+ const trailing = document.querySelector('[data-slot="dialog-footer-trailing"]') as HTMLElement
206
+ expect(trailing).not.toBeNull()
207
+ // Footer wraps as flex with leading mr-auto pushing trailing to the right.
208
+ const footer = document.querySelector('[data-slot="dialog-footer"]') as HTMLElement
209
+ expect(footer.className).toContain('border-t')
210
+ expect(footer.className).not.toContain('justify-end')
211
+ })
212
+
213
+ it('header leadingTone defaults to "default" with bordered white badge', () => {
214
+ renderDialog(<ExampleDialog leading={<span>x</span>} />)
215
+ const badge = document.querySelector('[data-slot="dialog-header-leading"]') as HTMLElement
216
+ expect(badge.getAttribute('data-tone')).toBe('default')
217
+ expect(badge.className).toContain('border')
218
+ expect(badge.className).toContain('border-input')
219
+ expect(badge.className).toContain('bg-background')
220
+ })
221
+
222
+ it('header leadingTone applies soft status tint + colored icon per Figma `Status Modals [1.1]`', () => {
223
+ const cases: Array<{
224
+ tone: 'accent' | 'success' | 'warning' | 'error' | 'info'
225
+ bg: string
226
+ tx: string
227
+ }> = [
228
+ // Canonical Figma Status Modals — soft tint background paired
229
+ // with a saturated colored icon (red `!` on light pink, etc.).
230
+ { tone: 'accent', bg: 'bg-accent-indigo/10', tx: 'text-accent-indigo' },
231
+ { tone: 'success', bg: 'bg-status-success-bg', tx: 'text-status-success-icon' },
232
+ { tone: 'warning', bg: 'bg-status-warning-bg', tx: 'text-status-warning-icon' },
233
+ { tone: 'error', bg: 'bg-status-error-bg', tx: 'text-status-error-icon' },
234
+ { tone: 'info', bg: 'bg-status-info-bg', tx: 'text-status-info-icon' },
235
+ ]
236
+ for (const { tone, bg, tx } of cases) {
237
+ const { unmount } = renderDialog(
238
+ <Dialog defaultOpen>
239
+ <DialogTrigger>Open</DialogTrigger>
240
+ <DialogContent>
241
+ <DialogHeader leading={<span>x</span>} leadingTone={tone}>
242
+ <DialogTitle>t</DialogTitle>
243
+ </DialogHeader>
244
+ </DialogContent>
245
+ </Dialog>,
246
+ )
247
+ const badge = document.querySelector('[data-slot="dialog-header-leading"]') as HTMLElement
248
+ expect(badge.getAttribute('data-tone')).toBe(tone)
249
+ expect(badge.className).toContain(bg)
250
+ expect(badge.className).toContain(tx)
251
+ // Status tones drop the bordered white shell.
252
+ expect(badge.className).not.toContain('border-input')
253
+ unmount()
254
+ }
255
+ })
256
+
257
+ it('Radix Dialog ARIA contract: role="dialog", labelledby/describedby from Title + Description', () => {
258
+ renderDialog(<ExampleDialog />)
259
+ const dialog = screen.getByRole('dialog')
260
+ expect(dialog).toBeInTheDocument()
261
+ const labelledBy = dialog.getAttribute('aria-labelledby')
262
+ const describedBy = dialog.getAttribute('aria-describedby')
263
+ expect(labelledBy).toBeTruthy()
264
+ expect(describedBy).toBeTruthy()
265
+ expect(document.getElementById(labelledBy as string)?.textContent).toBe('Confirm action')
266
+ })
267
+
268
+ it('DialogClose dismisses the dialog when clicked', () => {
269
+ renderDialog(<ExampleDialog />)
270
+ expect(document.querySelector('[data-slot="dialog-content"]')).not.toBeNull()
271
+ fireEvent.click(screen.getByText('Cancel'))
272
+ expect(document.querySelector('[data-slot="dialog-content"]')).toBeNull()
273
+ })
274
+
275
+ it('forwards className to DialogContent without dropping size classes', () => {
276
+ renderDialog(
277
+ <Dialog defaultOpen>
278
+ <DialogTrigger>Open</DialogTrigger>
279
+ <DialogContent className="custom-class" size="lg">
280
+ <DialogHeader>
281
+ <DialogTitle>Title</DialogTitle>
282
+ </DialogHeader>
283
+ </DialogContent>
284
+ </Dialog>,
285
+ )
286
+ const content = document.querySelector('[data-slot="dialog-content"]') as HTMLElement
287
+ expect(content.className).toContain('custom-class')
288
+ expect(content.className).toContain('sm:max-w-2xl')
289
+ })
290
+ })