@open-mercato/ui 0.6.1-develop.3246.1.dbef9d7392 → 0.6.1-develop.3260.1.dc719cac84
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/__integration__/TC-AI-UI-003-aichat-registry.spec.tsx +13 -0
- package/dist/ai/AiAssistantLauncher.js +8 -0
- package/dist/ai/AiAssistantLauncher.js.map +2 -2
- package/dist/ai/AiChat.js +226 -58
- package/dist/ai/AiChat.js.map +2 -2
- package/dist/ai/AiDock.js +3 -2
- package/dist/ai/AiDock.js.map +2 -2
- package/dist/ai/ModelPicker.js +186 -0
- package/dist/ai/ModelPicker.js.map +7 -0
- package/dist/ai/index.js +3 -1
- package/dist/ai/index.js.map +2 -2
- package/dist/ai/useAiChat.js +12 -16
- package/dist/ai/useAiChat.js.map +2 -2
- package/dist/backend/CrudForm.js +46 -129
- package/dist/backend/CrudForm.js.map +3 -3
- package/dist/backend/DataTable.js +10 -3
- package/dist/backend/DataTable.js.map +2 -2
- package/dist/backend/EmptyState.js +1 -47
- package/dist/backend/EmptyState.js.map +2 -2
- package/dist/backend/FilterOverlay.js +31 -26
- package/dist/backend/FilterOverlay.js.map +2 -2
- package/dist/backend/FlashMessages.js +23 -25
- package/dist/backend/FlashMessages.js.map +2 -2
- package/dist/backend/JsonBuilder.js +4 -2
- package/dist/backend/JsonBuilder.js.map +2 -2
- package/dist/backend/LogList.js +49 -0
- package/dist/backend/LogList.js.map +7 -0
- package/dist/backend/crud/CollapsibleGroup.js +13 -3
- package/dist/backend/crud/CollapsibleGroup.js.map +2 -2
- package/dist/backend/detail/InlineEditors.js +80 -32
- package/dist/backend/detail/InlineEditors.js.map +2 -2
- package/dist/backend/inputs/ComboboxInput.js +60 -6
- package/dist/backend/inputs/ComboboxInput.js.map +2 -2
- package/dist/backend/inputs/DatePicker.js +6 -142
- package/dist/backend/inputs/DatePicker.js.map +2 -2
- package/dist/backend/inputs/DateTimePicker.js +6 -182
- package/dist/backend/inputs/DateTimePicker.js.map +2 -2
- package/dist/backend/inputs/TagsInput.js +16 -19
- package/dist/backend/inputs/TagsInput.js.map +2 -2
- package/dist/backend/inputs/TimeInput.js +1 -1
- package/dist/backend/inputs/TimeInput.js.map +2 -2
- package/dist/backend/inputs/TimePicker.js +83 -72
- package/dist/backend/inputs/TimePicker.js.map +2 -2
- package/dist/backend/utils/customFieldForms.js +2 -2
- package/dist/backend/utils/customFieldForms.js.map +1 -1
- package/dist/portal/components/PortalEmptyState.js +13 -9
- package/dist/portal/components/PortalEmptyState.js.map +2 -2
- package/dist/primitives/accordion.js +137 -0
- package/dist/primitives/accordion.js.map +7 -0
- package/dist/primitives/alert.js +180 -12
- package/dist/primitives/alert.js.map +2 -2
- package/dist/primitives/calendar.js +122 -32
- package/dist/primitives/calendar.js.map +2 -2
- package/dist/primitives/compact-select.js +47 -0
- package/dist/primitives/compact-select.js.map +7 -0
- package/dist/primitives/counter-input.js +174 -0
- package/dist/primitives/counter-input.js.map +7 -0
- package/dist/primitives/date-picker-helpers.js +79 -0
- package/dist/primitives/date-picker-helpers.js.map +7 -0
- package/dist/primitives/date-picker.js +216 -0
- package/dist/primitives/date-picker.js.map +7 -0
- package/dist/primitives/date-range-picker.js +239 -0
- package/dist/primitives/date-range-picker.js.map +7 -0
- package/dist/primitives/digit-input.js +157 -0
- package/dist/primitives/digit-input.js.map +7 -0
- package/dist/primitives/empty-state.js +93 -0
- package/dist/primitives/empty-state.js.map +7 -0
- package/dist/primitives/inline-input.js +29 -0
- package/dist/primitives/inline-input.js.map +7 -0
- package/dist/primitives/inline-select.js +41 -0
- package/dist/primitives/inline-select.js.map +7 -0
- package/dist/primitives/notification-stack.js +116 -0
- package/dist/primitives/notification-stack.js.map +7 -0
- package/dist/primitives/notification.js +62 -0
- package/dist/primitives/notification.js.map +7 -0
- package/dist/primitives/rich-editor.js +1230 -0
- package/dist/primitives/rich-editor.js.map +7 -0
- package/dist/primitives/select.js +1 -0
- package/dist/primitives/select.js.map +2 -2
- package/dist/primitives/skeleton.js +55 -0
- package/dist/primitives/skeleton.js.map +7 -0
- package/dist/primitives/tag-input.js +186 -0
- package/dist/primitives/tag-input.js.map +7 -0
- package/dist/primitives/tag.js +66 -13
- package/dist/primitives/tag.js.map +2 -2
- package/dist/primitives/time-picker.js +793 -0
- package/dist/primitives/time-picker.js.map +7 -0
- package/package.json +4 -3
- package/src/ai/AiAssistantLauncher.tsx +8 -0
- package/src/ai/AiChat.tsx +229 -11
- package/src/ai/AiDock.tsx +3 -2
- package/src/ai/ModelPicker.tsx +248 -0
- package/src/ai/__tests__/AiAssistantLauncher.test.tsx +58 -0
- package/src/ai/__tests__/AiChat.conversation.test.tsx +16 -0
- package/src/ai/__tests__/AiChat.debug.test.tsx +16 -0
- package/src/ai/__tests__/AiChat.registry.test.tsx +16 -0
- package/src/ai/__tests__/AiChat.test.tsx +180 -0
- package/src/ai/__tests__/AiChat.thinking-unmount.test.tsx +16 -0
- package/src/ai/__tests__/AiDock.test.tsx +12 -1
- package/src/ai/__tests__/ModelPicker.test.tsx +252 -0
- package/src/ai/index.ts +1 -0
- package/src/ai/useAiChat.ts +29 -16
- package/src/backend/CrudForm.tsx +61 -113
- package/src/backend/DataTable.tsx +17 -5
- package/src/backend/EmptyState.tsx +13 -63
- package/src/backend/FilterOverlay.tsx +38 -22
- package/src/backend/FlashMessages.tsx +23 -24
- package/src/backend/JsonBuilder.tsx +4 -2
- package/src/backend/LogList.tsx +109 -0
- package/src/backend/__tests__/CrudForm.datetime.test.tsx +12 -7
- package/src/backend/__tests__/FilterOverlay.dateRange.test.tsx +89 -0
- package/src/backend/__tests__/LogList.test.tsx +163 -0
- package/src/backend/crud/CollapsibleGroup.tsx +17 -3
- package/src/backend/detail/InlineEditors.tsx +78 -25
- package/src/backend/detail/__tests__/InlineEditors.test.tsx +81 -1
- package/src/backend/inputs/ComboboxInput.tsx +81 -9
- package/src/backend/inputs/DatePicker.tsx +28 -155
- package/src/backend/inputs/DateTimePicker.tsx +25 -193
- package/src/backend/inputs/TagsInput.tsx +10 -13
- package/src/backend/inputs/TimeInput.tsx +2 -1
- package/src/backend/inputs/TimePicker.tsx +99 -70
- package/src/backend/inputs/__tests__/ComboboxInput.test.tsx +133 -0
- package/src/backend/inputs/__tests__/TimePicker.test.tsx +2 -2
- package/src/backend/utils/__tests__/richTextSanitizer.test.ts +5 -2
- package/src/backend/utils/customFieldForms.ts +2 -2
- package/src/portal/components/PortalEmptyState.tsx +27 -22
- package/src/primitives/__tests__/accordion.test.tsx +197 -0
- package/src/primitives/__tests__/alert.test.tsx +204 -0
- package/src/primitives/__tests__/compact-select.test.tsx +98 -0
- package/src/primitives/__tests__/counter-input.test.tsx +165 -0
- package/src/primitives/__tests__/date-picker.test.tsx +221 -0
- package/src/primitives/__tests__/date-range-picker.test.tsx +173 -0
- package/src/primitives/__tests__/digit-input.test.tsx +168 -0
- package/src/primitives/__tests__/empty-state.test.tsx +154 -0
- package/src/primitives/__tests__/inline-input.test.tsx +100 -0
- package/src/primitives/__tests__/inline-select.test.tsx +103 -0
- package/src/primitives/__tests__/notification-stack.test.tsx +174 -0
- package/src/primitives/__tests__/notification.test.tsx +119 -0
- package/src/primitives/__tests__/rich-editor.test.tsx +411 -0
- package/src/primitives/__tests__/skeleton.test.tsx +76 -0
- package/src/primitives/__tests__/tag-input.test.tsx +198 -0
- package/src/primitives/__tests__/tag.test.tsx +123 -0
- package/src/primitives/__tests__/time-picker.test.tsx +392 -0
- package/src/primitives/accordion.tsx +225 -0
- package/src/primitives/alert.tsx +234 -24
- package/src/primitives/calendar.tsx +146 -38
- package/src/primitives/compact-select.tsx +59 -0
- package/src/primitives/counter-input.tsx +211 -0
- package/src/primitives/date-picker-helpers.ts +96 -0
- package/src/primitives/date-picker.tsx +290 -0
- package/src/primitives/date-range-picker.tsx +281 -0
- package/src/primitives/digit-input.tsx +208 -0
- package/src/primitives/empty-state.tsx +140 -0
- package/src/primitives/inline-input.tsx +55 -0
- package/src/primitives/inline-select.tsx +74 -0
- package/src/primitives/notification-stack.tsx +185 -0
- package/src/primitives/notification.tsx +136 -0
- package/src/primitives/rich-editor.tsx +1578 -0
- package/src/primitives/select.tsx +1 -0
- package/src/primitives/skeleton.tsx +70 -0
- package/src/primitives/tag-input.tsx +223 -0
- package/src/primitives/tag.tsx +69 -10
- package/src/primitives/time-picker.tsx +1059 -0
|
@@ -1,32 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @deprecated Import `EmptyState` from `@open-mercato/ui/primitives/empty-state`
|
|
3
|
+
* directly with `variant="subtle"` and `size="lg"` (matches the previous
|
|
4
|
+
* portal layout). The standalone `PortalEmptyState` is replaced by the
|
|
5
|
+
* unified primitive in DS Foundation v3.
|
|
6
|
+
*
|
|
7
|
+
* This shim preserves the legacy import path
|
|
8
|
+
* `@open-mercato/ui/portal/components/PortalEmptyState` for existing
|
|
9
|
+
* portal consumers — it forwards to the primitive with the portal-style
|
|
10
|
+
* variant + size pre-set.
|
|
11
|
+
*/
|
|
12
|
+
|
|
1
13
|
"use client"
|
|
2
|
-
import type { ReactNode } from 'react'
|
|
3
14
|
|
|
4
|
-
|
|
5
|
-
|
|
15
|
+
import * as React from 'react'
|
|
16
|
+
import { EmptyState } from '../../primitives/empty-state'
|
|
17
|
+
|
|
18
|
+
export type PortalEmptyStateProps = {
|
|
19
|
+
icon?: React.ReactNode
|
|
6
20
|
title: string
|
|
7
21
|
description?: string
|
|
8
|
-
action?: ReactNode
|
|
22
|
+
action?: React.ReactNode
|
|
9
23
|
}
|
|
10
24
|
|
|
11
|
-
/**
|
|
12
|
-
* Empty state placeholder for portal sections with no content.
|
|
13
|
-
* Dashed border, centered layout, optional icon and CTA.
|
|
14
|
-
*/
|
|
25
|
+
/** @deprecated Use `EmptyState` from `@open-mercato/ui/primitives/empty-state` with `variant="subtle"` and `size="lg"`. */
|
|
15
26
|
export function PortalEmptyState({ icon, title, description, action }: PortalEmptyStateProps) {
|
|
16
27
|
return (
|
|
17
|
-
<
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
{description ? (
|
|
26
|
-
<p className="mt-0.5 max-w-sm text-sm text-muted-foreground">{description}</p>
|
|
27
|
-
) : null}
|
|
28
|
-
</div>
|
|
29
|
-
{action ?? null}
|
|
30
|
-
</div>
|
|
28
|
+
<EmptyState
|
|
29
|
+
variant="subtle"
|
|
30
|
+
size="lg"
|
|
31
|
+
icon={icon}
|
|
32
|
+
title={title}
|
|
33
|
+
description={description}
|
|
34
|
+
actions={action}
|
|
35
|
+
/>
|
|
31
36
|
)
|
|
32
37
|
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as React from 'react'
|
|
6
|
+
import { fireEvent, render, screen } from '@testing-library/react'
|
|
7
|
+
import {
|
|
8
|
+
Accordion,
|
|
9
|
+
AccordionContent,
|
|
10
|
+
AccordionItem,
|
|
11
|
+
AccordionTrigger,
|
|
12
|
+
} from '../accordion'
|
|
13
|
+
|
|
14
|
+
function renderBasic(
|
|
15
|
+
props: Partial<React.ComponentProps<typeof AccordionTrigger>> = {},
|
|
16
|
+
itemProps: Partial<React.ComponentProps<typeof AccordionItem>> = {},
|
|
17
|
+
) {
|
|
18
|
+
return render(
|
|
19
|
+
<Accordion type="single" collapsible>
|
|
20
|
+
<AccordionItem value="one" {...itemProps}>
|
|
21
|
+
<AccordionTrigger {...props}>How do I update my account?</AccordionTrigger>
|
|
22
|
+
<AccordionContent>Visit Settings → Profile to edit your details.</AccordionContent>
|
|
23
|
+
</AccordionItem>
|
|
24
|
+
</Accordion>,
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe('Accordion primitive', () => {
|
|
29
|
+
it('renders the trigger label', () => {
|
|
30
|
+
renderBasic()
|
|
31
|
+
expect(screen.getByText('How do I update my account?')).toBeInTheDocument()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('starts closed by default (single + collapsible)', () => {
|
|
35
|
+
const { container } = renderBasic()
|
|
36
|
+
const item = container.querySelector('[data-slot="accordion-item"]')
|
|
37
|
+
expect(item).toHaveAttribute('data-state', 'closed')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('opens the item when the trigger is clicked', () => {
|
|
41
|
+
const { container } = renderBasic()
|
|
42
|
+
fireEvent.click(screen.getByRole('button', { name: /How do I update/ }))
|
|
43
|
+
expect(container.querySelector('[data-slot="accordion-item"]')).toHaveAttribute('data-state', 'open')
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('closes the item on a second click when collapsible', () => {
|
|
47
|
+
const { container } = renderBasic()
|
|
48
|
+
const trigger = screen.getByRole('button', { name: /How do I update/ })
|
|
49
|
+
fireEvent.click(trigger)
|
|
50
|
+
fireEvent.click(trigger)
|
|
51
|
+
expect(container.querySelector('[data-slot="accordion-item"]')).toHaveAttribute('data-state', 'closed')
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('only opens one item at a time with type="single"', () => {
|
|
55
|
+
const { container } = render(
|
|
56
|
+
<Accordion type="single" collapsible>
|
|
57
|
+
<AccordionItem value="a">
|
|
58
|
+
<AccordionTrigger>A</AccordionTrigger>
|
|
59
|
+
<AccordionContent>A body</AccordionContent>
|
|
60
|
+
</AccordionItem>
|
|
61
|
+
<AccordionItem value="b">
|
|
62
|
+
<AccordionTrigger>B</AccordionTrigger>
|
|
63
|
+
<AccordionContent>B body</AccordionContent>
|
|
64
|
+
</AccordionItem>
|
|
65
|
+
</Accordion>,
|
|
66
|
+
)
|
|
67
|
+
fireEvent.click(screen.getByRole('button', { name: 'A' }))
|
|
68
|
+
fireEvent.click(screen.getByRole('button', { name: 'B' }))
|
|
69
|
+
const items = container.querySelectorAll('[data-slot="accordion-item"]')
|
|
70
|
+
expect(items[0]).toHaveAttribute('data-state', 'closed')
|
|
71
|
+
expect(items[1]).toHaveAttribute('data-state', 'open')
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('allows multiple items to stay open with type="multiple"', () => {
|
|
75
|
+
const { container } = render(
|
|
76
|
+
<Accordion type="multiple">
|
|
77
|
+
<AccordionItem value="a">
|
|
78
|
+
<AccordionTrigger>A</AccordionTrigger>
|
|
79
|
+
<AccordionContent>A body</AccordionContent>
|
|
80
|
+
</AccordionItem>
|
|
81
|
+
<AccordionItem value="b">
|
|
82
|
+
<AccordionTrigger>B</AccordionTrigger>
|
|
83
|
+
<AccordionContent>B body</AccordionContent>
|
|
84
|
+
</AccordionItem>
|
|
85
|
+
</Accordion>,
|
|
86
|
+
)
|
|
87
|
+
fireEvent.click(screen.getByRole('button', { name: 'A' }))
|
|
88
|
+
fireEvent.click(screen.getByRole('button', { name: 'B' }))
|
|
89
|
+
const items = container.querySelectorAll('[data-slot="accordion-item"]')
|
|
90
|
+
expect(items[0]).toHaveAttribute('data-state', 'open')
|
|
91
|
+
expect(items[1]).toHaveAttribute('data-state', 'open')
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('marks the root slots with data-slot attributes', () => {
|
|
95
|
+
const { container } = renderBasic()
|
|
96
|
+
expect(container.querySelector('[data-slot="accordion-item"]')).not.toBeNull()
|
|
97
|
+
expect(container.querySelector('[data-slot="accordion-trigger"]')).not.toBeNull()
|
|
98
|
+
expect(container.querySelector('[data-slot="accordion-content"]')).not.toBeNull()
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('renders the Plus/Minus indicator by default (plus-minus triggerIcon)', () => {
|
|
102
|
+
const { container } = renderBasic()
|
|
103
|
+
const indicator = container.querySelector('[data-slot="accordion-trigger-indicator"]')
|
|
104
|
+
expect(indicator).not.toBeNull()
|
|
105
|
+
// Plus and Minus are both present; CSS toggles visibility via data-state.
|
|
106
|
+
expect(indicator?.querySelectorAll('svg').length).toBe(2)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('renders a single ChevronDown when triggerIcon="chevron"', () => {
|
|
110
|
+
const { container } = renderBasic({ triggerIcon: 'chevron' })
|
|
111
|
+
const indicator = container.querySelector('[data-slot="accordion-trigger-indicator"]')
|
|
112
|
+
expect(indicator?.querySelectorAll('svg').length).toBe(1)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('omits the indicator entirely when triggerIcon="none"', () => {
|
|
116
|
+
const { container } = renderBasic({ triggerIcon: 'none' })
|
|
117
|
+
expect(container.querySelector('[data-slot="accordion-trigger-indicator"]')).toBeNull()
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('renders a custom indicator node when provided (overrides triggerIcon)', () => {
|
|
121
|
+
const { container } = renderBasic({
|
|
122
|
+
triggerIcon: 'chevron',
|
|
123
|
+
indicator: <span data-testid="custom-indicator">42</span>,
|
|
124
|
+
})
|
|
125
|
+
expect(screen.getByTestId('custom-indicator')).toBeInTheDocument()
|
|
126
|
+
// No Chevron rendered when indicator override is supplied.
|
|
127
|
+
const indicator = container.querySelector('[data-slot="accordion-trigger-indicator"]')
|
|
128
|
+
expect(indicator?.querySelector('svg')).toBeNull()
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('renders leftIcon in a dedicated slot before the label', () => {
|
|
132
|
+
const { container } = renderBasic({ leftIcon: <span data-testid="left-icon" /> })
|
|
133
|
+
expect(container.querySelector('[data-slot="accordion-trigger-left-icon"]')).not.toBeNull()
|
|
134
|
+
expect(screen.getByTestId('left-icon')).toBeInTheDocument()
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('suppresses leftIcon when iconPosition="start" (indicator owns the leading slot)', () => {
|
|
138
|
+
const { container } = renderBasic({
|
|
139
|
+
leftIcon: <span data-testid="left-icon" />,
|
|
140
|
+
iconPosition: 'start',
|
|
141
|
+
})
|
|
142
|
+
expect(container.querySelector('[data-slot="accordion-trigger-left-icon"]')).toBeNull()
|
|
143
|
+
expect(screen.queryByTestId('left-icon')).toBeNull()
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
it('places the indicator at the start of the trigger when iconPosition="start"', () => {
|
|
147
|
+
const { container } = renderBasic({ iconPosition: 'start' })
|
|
148
|
+
const trigger = container.querySelector('[data-slot="accordion-trigger"]')!
|
|
149
|
+
const indicator = trigger.querySelector('[data-slot="accordion-trigger-indicator"]')
|
|
150
|
+
const label = trigger.querySelector('[data-slot="accordion-trigger-label"]')
|
|
151
|
+
expect(indicator).not.toBeNull()
|
|
152
|
+
// Indicator precedes the label in DOM order.
|
|
153
|
+
expect(indicator?.compareDocumentPosition(label!) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy()
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it('applies card variant classes by default (border + shadow + bg-card)', () => {
|
|
157
|
+
const { container } = renderBasic()
|
|
158
|
+
const item = container.querySelector('[data-slot="accordion-item"]')!
|
|
159
|
+
expect(item).toHaveClass('border-border')
|
|
160
|
+
expect(item).toHaveClass('bg-card')
|
|
161
|
+
expect(item).toHaveClass('shadow-xs')
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
it('applies borderless variant classes when variant="borderless"', () => {
|
|
165
|
+
const { container } = renderBasic({}, { variant: 'borderless' })
|
|
166
|
+
const item = container.querySelector('[data-slot="accordion-item"]')!
|
|
167
|
+
expect(item).toHaveClass('border-transparent')
|
|
168
|
+
expect(item).toHaveClass('bg-transparent')
|
|
169
|
+
expect(item).toHaveClass('shadow-none')
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('exposes --accordion-indent on the item with a Tailwind has-[] promoter for the leftIcon case', () => {
|
|
173
|
+
const { container } = renderBasic({ leftIcon: <span /> })
|
|
174
|
+
const item = container.querySelector('[data-slot="accordion-item"]')!
|
|
175
|
+
// We can't assert the resolved CSS variable value in jsdom (no real CSS engine),
|
|
176
|
+
// so we assert the Tailwind utility that flips the variable is on the item.
|
|
177
|
+
expect(item.className).toContain('[--accordion-indent:14px]')
|
|
178
|
+
expect(item.className).toContain('has-[[data-slot=accordion-trigger-left-icon]]:[--accordion-indent:44px]')
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
it('forwards refs to the underlying Radix Item / Trigger / Content nodes', () => {
|
|
182
|
+
const itemRef = React.createRef<HTMLDivElement>()
|
|
183
|
+
const triggerRef = React.createRef<HTMLButtonElement>()
|
|
184
|
+
const contentRef = React.createRef<HTMLDivElement>()
|
|
185
|
+
render(
|
|
186
|
+
<Accordion type="single" collapsible defaultValue="one">
|
|
187
|
+
<AccordionItem ref={itemRef} value="one">
|
|
188
|
+
<AccordionTrigger ref={triggerRef}>Title</AccordionTrigger>
|
|
189
|
+
<AccordionContent ref={contentRef}>Body</AccordionContent>
|
|
190
|
+
</AccordionItem>
|
|
191
|
+
</Accordion>,
|
|
192
|
+
)
|
|
193
|
+
expect(itemRef.current).not.toBeNull()
|
|
194
|
+
expect(triggerRef.current?.tagName).toBe('BUTTON')
|
|
195
|
+
expect(contentRef.current).not.toBeNull()
|
|
196
|
+
})
|
|
197
|
+
})
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as React from 'react'
|
|
6
|
+
import { fireEvent, render, screen } from '@testing-library/react'
|
|
7
|
+
import { Alert, AlertDescription, AlertTitle } from '../alert'
|
|
8
|
+
|
|
9
|
+
describe('Alert primitive', () => {
|
|
10
|
+
it('renders with role="alert"', () => {
|
|
11
|
+
render(<Alert>Hello</Alert>)
|
|
12
|
+
expect(screen.getByRole('alert')).toBeInTheDocument()
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('defaults to status="information" + style="light" + size="sm"', () => {
|
|
16
|
+
const { container } = render(<Alert>Body</Alert>)
|
|
17
|
+
const alert = container.querySelector('[data-slot="alert"]')
|
|
18
|
+
expect(alert).toHaveAttribute('data-status', 'information')
|
|
19
|
+
expect(alert).toHaveAttribute('data-style', 'light')
|
|
20
|
+
expect(alert).toHaveClass('min-h-9')
|
|
21
|
+
expect(alert).toHaveClass('rounded-lg')
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
describe('Figma tokens — status × style matrix', () => {
|
|
25
|
+
it('error / filled: bg-status-error-icon + text-white', () => {
|
|
26
|
+
const { container } = render(<Alert status="error" style="filled">Oops</Alert>)
|
|
27
|
+
const alert = container.querySelector('[data-slot="alert"]')
|
|
28
|
+
expect(alert).toHaveClass('bg-status-error-icon')
|
|
29
|
+
expect(alert).toHaveClass('text-white')
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('error / light: bg-status-error-border + text-status-error-text (Figma #fecaca)', () => {
|
|
33
|
+
const { container } = render(<Alert status="error" style="light">Oops</Alert>)
|
|
34
|
+
const alert = container.querySelector('[data-slot="alert"]')
|
|
35
|
+
expect(alert).toHaveClass('bg-status-error-border')
|
|
36
|
+
expect(alert).toHaveClass('text-foreground')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('error / lighter: bg-status-error-bg + text-status-error-text (Figma #fef2f2)', () => {
|
|
40
|
+
const { container } = render(<Alert status="error" style="lighter">Oops</Alert>)
|
|
41
|
+
const alert = container.querySelector('[data-slot="alert"]')
|
|
42
|
+
expect(alert).toHaveClass('bg-status-error-bg')
|
|
43
|
+
expect(alert).toHaveClass('text-foreground')
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('stroke style: bg-background + text-foreground + border-border + shadow-lg', () => {
|
|
47
|
+
const { container } = render(<Alert status="warning" style="stroke">Heads up</Alert>)
|
|
48
|
+
const alert = container.querySelector('[data-slot="alert"]')
|
|
49
|
+
expect(alert).toHaveClass('bg-background')
|
|
50
|
+
expect(alert).toHaveClass('text-foreground')
|
|
51
|
+
expect(alert).toHaveClass('border-border')
|
|
52
|
+
expect(alert).toHaveClass('shadow-lg')
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('feature status maps to status-neutral tokens (NOT brand-violet)', () => {
|
|
56
|
+
const { container } = render(<Alert status="feature" style="light">New release</Alert>)
|
|
57
|
+
const alert = container.querySelector('[data-slot="alert"]')
|
|
58
|
+
expect(alert).toHaveClass('bg-status-neutral-border')
|
|
59
|
+
expect(alert).toHaveClass('text-foreground')
|
|
60
|
+
expect(alert).not.toHaveClass('bg-brand-violet')
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('feature / filled: bg-status-neutral-icon + text-white', () => {
|
|
64
|
+
const { container } = render(<Alert status="feature" style="filled">Hi</Alert>)
|
|
65
|
+
const alert = container.querySelector('[data-slot="alert"]')
|
|
66
|
+
expect(alert).toHaveClass('bg-status-neutral-icon')
|
|
67
|
+
expect(alert).toHaveClass('text-white')
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
describe('Sizes', () => {
|
|
72
|
+
it('size="xs" applies min-h-8 + rounded-lg + text-xs', () => {
|
|
73
|
+
const { container } = render(<Alert size="xs">xs</Alert>)
|
|
74
|
+
const alert = container.querySelector('[data-slot="alert"]')
|
|
75
|
+
expect(alert).toHaveClass('min-h-8')
|
|
76
|
+
expect(alert).toHaveClass('rounded-lg')
|
|
77
|
+
expect(alert).toHaveClass('text-xs')
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('size="sm" applies min-h-9 + rounded-lg + text-sm (Figma Paragraph/Small)', () => {
|
|
81
|
+
const { container } = render(<Alert size="sm">sm</Alert>)
|
|
82
|
+
const alert = container.querySelector('[data-slot="alert"]')
|
|
83
|
+
expect(alert).toHaveClass('min-h-9')
|
|
84
|
+
expect(alert).toHaveClass('rounded-lg')
|
|
85
|
+
expect(alert).toHaveClass('text-sm')
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('size="default" applies rounded-xl + larger padding + no min height', () => {
|
|
89
|
+
const { container } = render(<Alert size="default">default</Alert>)
|
|
90
|
+
const alert = container.querySelector('[data-slot="alert"]')
|
|
91
|
+
expect(alert).toHaveClass('rounded-xl')
|
|
92
|
+
expect(alert).toHaveClass('text-sm')
|
|
93
|
+
})
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
describe('Icon badge (non-filled styles)', () => {
|
|
97
|
+
it('renders a badge wrap with status-x-icon bg + white icon for light style', () => {
|
|
98
|
+
const { container } = render(<Alert status="success" style="light">Saved</Alert>)
|
|
99
|
+
const badge = container.querySelector('[data-slot="alert-icon-badge"]')
|
|
100
|
+
expect(badge).not.toBeNull()
|
|
101
|
+
expect(badge).toHaveAttribute('data-status', 'success')
|
|
102
|
+
expect(badge).toHaveClass('bg-status-success-icon')
|
|
103
|
+
expect(badge).toHaveClass('rounded-full')
|
|
104
|
+
expect(badge).toHaveClass('text-white')
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('renders a badge wrap for lighter style too', () => {
|
|
108
|
+
const { container } = render(<Alert status="information" style="lighter">FYI</Alert>)
|
|
109
|
+
expect(container.querySelector('[data-slot="alert-icon-badge"]')).not.toBeNull()
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('renders a badge wrap for stroke style too', () => {
|
|
113
|
+
const { container } = render(<Alert status="error" style="stroke">Oops</Alert>)
|
|
114
|
+
expect(container.querySelector('[data-slot="alert-icon-badge"]')).not.toBeNull()
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('does NOT render a badge wrap for filled style — plain icon instead', () => {
|
|
118
|
+
const { container } = render(<Alert status="error" style="filled">Oops</Alert>)
|
|
119
|
+
expect(container.querySelector('[data-slot="alert-icon-badge"]')).toBeNull()
|
|
120
|
+
expect(container.querySelector('[data-slot="alert-icon"]')).not.toBeNull()
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('hides the icon entirely when showIcon is false', () => {
|
|
124
|
+
const { container } = render(
|
|
125
|
+
<Alert status="success" showIcon={false}>Done</Alert>,
|
|
126
|
+
)
|
|
127
|
+
expect(container.querySelector('[data-slot="alert-icon-badge"]')).toBeNull()
|
|
128
|
+
expect(container.querySelector('[data-slot="alert-icon"]')).toBeNull()
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('respects a custom icon override inside the badge', () => {
|
|
132
|
+
const Custom = () => <svg data-testid="custom-icon" aria-hidden="true" />
|
|
133
|
+
render(<Alert status="information" style="light" icon={<Custom />}>Info</Alert>)
|
|
134
|
+
expect(screen.getByTestId('custom-icon')).toBeInTheDocument()
|
|
135
|
+
})
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
describe('Action slot + dismiss', () => {
|
|
139
|
+
it('renders an inline action slot to the right of the description', () => {
|
|
140
|
+
render(
|
|
141
|
+
<Alert action={<button data-testid="action-link">Undo</button>}>Saved</Alert>,
|
|
142
|
+
)
|
|
143
|
+
expect(screen.getByTestId('action-link')).toBeInTheDocument()
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
it('renders a dismiss button when dismissible is true', () => {
|
|
147
|
+
render(<Alert dismissible onDismiss={() => {}}>Closable</Alert>)
|
|
148
|
+
expect(screen.getByRole('button', { name: 'Dismiss' })).toBeInTheDocument()
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('fires onDismiss when the dismiss button is clicked', () => {
|
|
152
|
+
const onDismiss = jest.fn()
|
|
153
|
+
render(<Alert dismissible onDismiss={onDismiss}>Closable</Alert>)
|
|
154
|
+
fireEvent.click(screen.getByRole('button', { name: 'Dismiss' }))
|
|
155
|
+
expect(onDismiss).toHaveBeenCalledTimes(1)
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it('honors a custom dismissAriaLabel for i18n', () => {
|
|
159
|
+
render(
|
|
160
|
+
<Alert dismissible onDismiss={() => {}} dismissAriaLabel="Zamknij">
|
|
161
|
+
Closable
|
|
162
|
+
</Alert>,
|
|
163
|
+
)
|
|
164
|
+
expect(screen.getByRole('button', { name: 'Zamknij' })).toBeInTheDocument()
|
|
165
|
+
})
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
it('AlertTitle and AlertDescription compose inside the body slot', () => {
|
|
169
|
+
render(
|
|
170
|
+
<Alert status="success">
|
|
171
|
+
<AlertTitle>Saved</AlertTitle>
|
|
172
|
+
<AlertDescription>Your changes were saved.</AlertDescription>
|
|
173
|
+
</Alert>,
|
|
174
|
+
)
|
|
175
|
+
expect(screen.getByText('Saved').tagName).toBe('H5')
|
|
176
|
+
expect(screen.getByText('Your changes were saved.').tagName).toBe('P')
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
describe('legacy variant prop (BC)', () => {
|
|
180
|
+
it('maps variant="destructive" to status="error" with the new light default', () => {
|
|
181
|
+
const { container } = render(<Alert variant="destructive">Oops</Alert>)
|
|
182
|
+
const alert = container.querySelector('[data-slot="alert"]')
|
|
183
|
+
expect(alert).toHaveAttribute('data-status', 'error')
|
|
184
|
+
expect(alert).toHaveAttribute('data-style', 'light')
|
|
185
|
+
expect(alert).toHaveClass('bg-status-error-border')
|
|
186
|
+
expect(alert).toHaveClass('text-foreground')
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
it('maps variant="info" to status="information"', () => {
|
|
190
|
+
const { container } = render(<Alert variant="info">FYI</Alert>)
|
|
191
|
+
expect(container.querySelector('[data-slot="alert"]')).toHaveAttribute('data-status', 'information')
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
it('maps variant="default" to status="information"', () => {
|
|
195
|
+
const { container } = render(<Alert variant="default">Hello</Alert>)
|
|
196
|
+
expect(container.querySelector('[data-slot="alert"]')).toHaveAttribute('data-status', 'information')
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
it('explicit status overrides the legacy variant prop', () => {
|
|
200
|
+
const { container } = render(<Alert variant="destructive" status="success">Saved</Alert>)
|
|
201
|
+
expect(container.querySelector('[data-slot="alert"]')).toHaveAttribute('data-status', 'success')
|
|
202
|
+
})
|
|
203
|
+
})
|
|
204
|
+
})
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as React from 'react'
|
|
6
|
+
import { render, screen } from '@testing-library/react'
|
|
7
|
+
import {
|
|
8
|
+
CompactSelectTrigger,
|
|
9
|
+
Select,
|
|
10
|
+
SelectContent,
|
|
11
|
+
SelectItem,
|
|
12
|
+
SelectValue,
|
|
13
|
+
} from '../compact-select'
|
|
14
|
+
|
|
15
|
+
function renderCompactSelect(
|
|
16
|
+
props: Partial<React.ComponentProps<typeof CompactSelectTrigger>> = {},
|
|
17
|
+
defaultValue: string = 'all',
|
|
18
|
+
) {
|
|
19
|
+
return render(
|
|
20
|
+
<Select defaultValue={defaultValue}>
|
|
21
|
+
<CompactSelectTrigger aria-label="View" {...props}>
|
|
22
|
+
<SelectValue />
|
|
23
|
+
</CompactSelectTrigger>
|
|
24
|
+
<SelectContent>
|
|
25
|
+
<SelectItem value="all">All</SelectItem>
|
|
26
|
+
<SelectItem value="open">Open</SelectItem>
|
|
27
|
+
</SelectContent>
|
|
28
|
+
</Select>,
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe('CompactSelect primitive', () => {
|
|
33
|
+
it('renders the trigger at h-7 (xs) regardless of explicit size attempts', () => {
|
|
34
|
+
const { container } = renderCompactSelect()
|
|
35
|
+
const trigger = container.querySelector('[data-slot="compact-select-trigger"]')
|
|
36
|
+
expect(trigger).not.toBeNull()
|
|
37
|
+
expect(trigger).toHaveClass('h-7')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('uses the smaller px-2 text-xs styling of the xs size variant', () => {
|
|
41
|
+
const { container } = renderCompactSelect()
|
|
42
|
+
const trigger = container.querySelector('[data-slot="compact-select-trigger"]')!
|
|
43
|
+
expect(trigger).toHaveClass('px-2')
|
|
44
|
+
expect(trigger).toHaveClass('text-xs')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('renders the triggerLabel prefix when provided', () => {
|
|
48
|
+
renderCompactSelect({ triggerLabel: 'View:' })
|
|
49
|
+
expect(screen.getByText('View:')).toBeInTheDocument()
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('omits the triggerLabel slot when no label is provided', () => {
|
|
53
|
+
const { container } = renderCompactSelect()
|
|
54
|
+
expect(container.querySelector('[data-slot="compact-select-trigger-label"]')).toBeNull()
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('marks the trigger label slot with data-slot for styling hooks', () => {
|
|
58
|
+
const { container } = renderCompactSelect({ triggerLabel: 'Sort:' })
|
|
59
|
+
const slot = container.querySelector('[data-slot="compact-select-trigger-label"]')
|
|
60
|
+
expect(slot).not.toBeNull()
|
|
61
|
+
expect(slot?.textContent).toBe('Sort:')
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('forwards the trigger to a ref consumer', () => {
|
|
65
|
+
const ref = React.createRef<HTMLButtonElement>()
|
|
66
|
+
render(
|
|
67
|
+
<Select defaultValue="a">
|
|
68
|
+
<CompactSelectTrigger ref={ref} aria-label="Pick">
|
|
69
|
+
<SelectValue />
|
|
70
|
+
</CompactSelectTrigger>
|
|
71
|
+
<SelectContent>
|
|
72
|
+
<SelectItem value="a">A</SelectItem>
|
|
73
|
+
</SelectContent>
|
|
74
|
+
</Select>,
|
|
75
|
+
)
|
|
76
|
+
expect(ref.current).not.toBeNull()
|
|
77
|
+
expect(ref.current?.tagName).toBe('BUTTON')
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('applies className alongside the size variant', () => {
|
|
81
|
+
const { container } = renderCompactSelect({ className: 'rounded-full border-brand-violet' })
|
|
82
|
+
const trigger = container.querySelector('[data-slot="compact-select-trigger"]')
|
|
83
|
+
expect(trigger).toHaveClass('rounded-full')
|
|
84
|
+
expect(trigger).toHaveClass('border-brand-violet')
|
|
85
|
+
expect(trigger).toHaveClass('h-7')
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('forwards SelectTrigger ARIA props to the underlying Radix trigger', () => {
|
|
89
|
+
renderCompactSelect({ 'aria-label': 'Period selector' })
|
|
90
|
+
expect(screen.getByRole('combobox', { name: 'Period selector' })).toBeInTheDocument()
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('renders aria-invalid styling when aria-invalid is set', () => {
|
|
94
|
+
const { container } = renderCompactSelect({ 'aria-invalid': true })
|
|
95
|
+
const trigger = container.querySelector('[aria-invalid="true"]')
|
|
96
|
+
expect(trigger).not.toBeNull()
|
|
97
|
+
})
|
|
98
|
+
})
|