@yuno-payments/dashboard-design-system 0.0.1
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/.storybook/main.ts +20 -0
- package/.storybook/preview.ts +18 -0
- package/.storybook/vitest.setup.ts +7 -0
- package/README.md +69 -0
- package/components.json +21 -0
- package/eslint.config.js +26 -0
- package/index.html +13 -0
- package/package.json +57 -0
- package/public/vite.svg +1 -0
- package/src/App.css +42 -0
- package/src/App.tsx +11 -0
- package/src/assets/react.svg +1 -0
- package/src/components/atoms/button/button.stories.tsx +222 -0
- package/src/components/atoms/button/button.test.tsx +78 -0
- package/src/components/atoms/button/index.tsx +80 -0
- package/src/components/atoms/checkbox/checkbox.stories.tsx +314 -0
- package/src/components/atoms/checkbox/checkbox.test.tsx +278 -0
- package/src/components/atoms/checkbox/index.tsx +103 -0
- package/src/components/atoms/chip/chip.stories.tsx +317 -0
- package/src/components/atoms/chip/chip.test.tsx +300 -0
- package/src/components/atoms/chip/index.tsx +114 -0
- package/src/components/atoms/input/index.tsx +27 -0
- package/src/components/atoms/link/index.tsx +79 -0
- package/src/components/atoms/link/link.stories.tsx +159 -0
- package/src/components/atoms/link/link.test.tsx +176 -0
- package/src/components/atoms/radiobutton/index.tsx +103 -0
- package/src/components/atoms/radiobutton/radiobutton.stories.tsx +314 -0
- package/src/components/atoms/radiobutton/radiobutton.test.tsx +245 -0
- package/src/components/atoms/tag/index.tsx +196 -0
- package/src/components/atoms/tag/tag.stories.tsx +281 -0
- package/src/components/atoms/tag/tag.test.tsx +282 -0
- package/src/components/atoms/typography/index.tsx +62 -0
- package/src/components/atoms/typography/typography.stories.tsx +214 -0
- package/src/components/atoms/typography/typography.test.tsx +187 -0
- package/src/components/index.tsx +17 -0
- package/src/components/molecules/announcement/announcement.stories.tsx +277 -0
- package/src/components/molecules/announcement/announcement.test.tsx +354 -0
- package/src/components/molecules/announcement/index.tsx +200 -0
- package/src/components/molecules/notification-alert/index.tsx +293 -0
- package/src/components/molecules/notification-alert/notification-alert.stories.tsx +418 -0
- package/src/components/molecules/notification-alert/notification-alert.test.tsx +454 -0
- package/src/components/molecules/popover/index.tsx +175 -0
- package/src/components/molecules/popover/popover.stories.tsx +241 -0
- package/src/components/molecules/popover/popover.test.tsx +191 -0
- package/src/components/molecules/textfield/index.tsx +154 -0
- package/src/components/molecules/textfield/textfield.stories.tsx +168 -0
- package/src/components/molecules/textfield/textfield.test.tsx +157 -0
- package/src/components/molecules/tooltip/index.tsx +263 -0
- package/src/components/molecules/tooltip/tooltip.stories.tsx +363 -0
- package/src/components/molecules/tooltip/tooltip.test.tsx +468 -0
- package/src/components/organisms/dialog/dialog.stories.tsx +522 -0
- package/src/components/organisms/dialog/dialog.test.tsx +525 -0
- package/src/components/organisms/dialog/index.tsx +233 -0
- package/src/components/organisms/dropdown/dropdown.stories.tsx +529 -0
- package/src/components/organisms/dropdown/dropdown.test.tsx +390 -0
- package/src/components/organisms/dropdown/index.tsx +624 -0
- package/src/index.css +184 -0
- package/src/lib/color-utils.ts +94 -0
- package/src/lib/utils.ts +6 -0
- package/src/main.tsx +10 -0
- package/src/stories/Colors.stories.tsx +107 -0
- package/src/stories/Shadows.stories.tsx +110 -0
- package/src/stories/Spacing.stories.tsx +121 -0
- package/src/stories/Typography.stories.tsx +197 -0
- package/src/vite-env.d.ts +1 -0
- package/tsconfig.app.json +33 -0
- package/tsconfig.json +13 -0
- package/tsconfig.node.json +25 -0
- package/vite.config.ts +43 -0
- package/vitest.config.ts +15 -0
- package/vitest.shims.d.ts +1 -0
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
+
import * as React from 'react'
|
|
3
|
+
import { createRoot } from 'react-dom/client'
|
|
4
|
+
import { act } from 'react-dom/test-utils'
|
|
5
|
+
import { Announcement } from './index'
|
|
6
|
+
|
|
7
|
+
function render(ui: React.ReactElement) {
|
|
8
|
+
const container = document.createElement('div')
|
|
9
|
+
document.body.appendChild(container)
|
|
10
|
+
const root = createRoot(container)
|
|
11
|
+
act(() => {
|
|
12
|
+
root.render(ui)
|
|
13
|
+
})
|
|
14
|
+
return { container, root }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function getByText(container: HTMLElement, text: string) {
|
|
18
|
+
const el = Array.from(container.querySelectorAll('*')).find((n) => n.textContent?.includes(text))
|
|
19
|
+
if (!el) throw new Error(`Element with text "${text}" not found`)
|
|
20
|
+
return el as HTMLElement
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getByTestId(container: HTMLElement, testId: string) {
|
|
24
|
+
const el = container.querySelector(`[data-testid="${testId}"]`)
|
|
25
|
+
if (!el) throw new Error(`Element with data-testid="${testId}" not found`)
|
|
26
|
+
return el as HTMLElement
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('Announcement', () => {
|
|
30
|
+
it('renders with default props', () => {
|
|
31
|
+
const { container } = render(
|
|
32
|
+
<Announcement title="Test title" description="Test description" />
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
expect(getByText(container, 'Test title')).toBeTruthy()
|
|
36
|
+
expect(getByText(container, 'Test description')).toBeTruthy()
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('renders title when provided', () => {
|
|
40
|
+
const { container } = render(
|
|
41
|
+
<Announcement title="Test Title" />
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
const title = getByTestId(container, 'title')
|
|
45
|
+
expect(title.textContent).toBe('Test Title')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('renders description when provided', () => {
|
|
49
|
+
const { container } = render(
|
|
50
|
+
<Announcement description="Test Description" />
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
const description = getByTestId(container, 'description')
|
|
54
|
+
expect(description.textContent).toBe('Test Description')
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('does not render when visible is false', () => {
|
|
58
|
+
const { container } = render(
|
|
59
|
+
<Announcement title="Test" visible={false} />
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
expect(container.firstChild).toBeNull()
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('renders with neutral variant by default', () => {
|
|
66
|
+
const { container } = render(
|
|
67
|
+
<Announcement title="Test" />
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
const announcement = container.firstChild as HTMLElement
|
|
71
|
+
expect(announcement.className).toMatch(/bg-background/)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('renders with attentive variant', () => {
|
|
75
|
+
const { container } = render(
|
|
76
|
+
<Announcement title="Test" variant="attentive" />
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
const announcement = container.firstChild as HTMLElement
|
|
80
|
+
expect(announcement.className).toMatch(/bg-amber-50/)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('renders with informative variant', () => {
|
|
84
|
+
const { container } = render(
|
|
85
|
+
<Announcement title="Test" variant="informative" />
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
const announcement = container.firstChild as HTMLElement
|
|
89
|
+
expect(announcement.className).toMatch(/bg-blue-50/)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('renders with negative variant', () => {
|
|
93
|
+
const { container } = render(
|
|
94
|
+
<Announcement title="Test" variant="negative" />
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
const announcement = container.firstChild as HTMLElement
|
|
98
|
+
expect(announcement.className).toMatch(/bg-destructive/)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('renders with positive variant', () => {
|
|
102
|
+
const { container } = render(
|
|
103
|
+
<Announcement title="Test" variant="positive" />
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
const announcement = container.firstChild as HTMLElement
|
|
107
|
+
expect(announcement.className).toMatch(/bg-green-50/)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('renders with large size by default', () => {
|
|
111
|
+
const { container } = render(
|
|
112
|
+
<Announcement title="Test" />
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
const announcement = container.firstChild as HTMLElement
|
|
116
|
+
expect(announcement.className).toMatch(/w-full max-w-full/)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('renders with small size', () => {
|
|
120
|
+
const { container } = render(
|
|
121
|
+
<Announcement title="Test" size="small" />
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
const announcement = container.firstChild as HTMLElement
|
|
125
|
+
expect(announcement.className).toMatch(/max-w-\[343px\]/)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('renders icon by default', () => {
|
|
129
|
+
const { container } = render(
|
|
130
|
+
<Announcement title="Test" />
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
const icon = container.querySelector('svg')
|
|
134
|
+
expect(icon).toBeTruthy()
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('does not render icon when withIcon is false', () => {
|
|
138
|
+
const { container } = render(
|
|
139
|
+
<Announcement title="Test" withIcon={false} />
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
const icon = container.querySelector('svg')
|
|
143
|
+
expect(icon).toBeFalsy()
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
it('renders custom icon when provided', () => {
|
|
147
|
+
const customIcon = <span data-testid="custom-icon">Custom</span>
|
|
148
|
+
const { container } = render(
|
|
149
|
+
<Announcement title="Test" iconName={customIcon} />
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
expect(getByTestId(container, 'custom-icon')).toBeTruthy()
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it('renders link when linkText and link are provided', () => {
|
|
156
|
+
const { container } = render(
|
|
157
|
+
<Announcement
|
|
158
|
+
title="Test"
|
|
159
|
+
linkText="Click here"
|
|
160
|
+
link="https://example.com"
|
|
161
|
+
/>
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
const link = getByTestId(container, 'linkButton')
|
|
165
|
+
expect(link.textContent).toBe('Click here')
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
it('renders link when linkText and onClickLink are provided', () => {
|
|
169
|
+
const handleClick = vi.fn()
|
|
170
|
+
const { container } = render(
|
|
171
|
+
<Announcement
|
|
172
|
+
title="Test"
|
|
173
|
+
linkText="Click here"
|
|
174
|
+
onClickLink={handleClick}
|
|
175
|
+
/>
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
const link = getByTestId(container, 'linkButton')
|
|
179
|
+
expect(link.textContent).toBe('Click here')
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it('does not render link when only linkText is provided', () => {
|
|
183
|
+
const { container } = render(
|
|
184
|
+
<Announcement title="Test" linkText="Click here" />
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
expect(() => getByTestId(container, 'linkButton')).toThrow()
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
it('renders remove button when isRemovable is true', () => {
|
|
191
|
+
const { container } = render(
|
|
192
|
+
<Announcement title="Test" isRemovable />
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
const removeButton = getByTestId(container, 'removeAnnouncement')
|
|
196
|
+
expect(removeButton).toBeTruthy()
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
it('does not render remove button when isRemovable is false', () => {
|
|
200
|
+
const { container } = render(
|
|
201
|
+
<Announcement title="Test" isRemovable={false} />
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
expect(() => getByTestId(container, 'removeAnnouncement')).toThrow()
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
it('calls onClose when remove button is clicked', () => {
|
|
208
|
+
const handleClose = vi.fn()
|
|
209
|
+
const { container } = render(
|
|
210
|
+
<Announcement title="Test" isRemovable onClose={handleClose} />
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
const removeButton = getByTestId(container, 'removeAnnouncement')
|
|
214
|
+
act(() => {
|
|
215
|
+
removeButton.click()
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
expect(handleClose).toHaveBeenCalledTimes(1)
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
it('hides announcement when remove button is clicked', () => {
|
|
222
|
+
const { container } = render(
|
|
223
|
+
<Announcement title="Test" isRemovable />
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
const removeButton = getByTestId(container, 'removeAnnouncement')
|
|
227
|
+
act(() => {
|
|
228
|
+
removeButton.click()
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
expect(container.firstChild).toBeNull()
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
it('forwards ref correctly', () => {
|
|
235
|
+
const ref = { current: null }
|
|
236
|
+
render(
|
|
237
|
+
<Announcement ref={ref} title="Test" />
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
expect(ref.current).toBeTruthy()
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
it('applies custom className', () => {
|
|
244
|
+
const { container } = render(
|
|
245
|
+
<Announcement title="Test" className="custom-class" />
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
const announcement = container.firstChild as HTMLElement
|
|
249
|
+
expect(announcement.className).toMatch(/custom-class/)
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
it('passes through HTML attributes', () => {
|
|
253
|
+
const { container } = render(
|
|
254
|
+
<Announcement title="Test" data-custom="value" />
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
const announcement = container.firstChild as HTMLElement
|
|
258
|
+
expect(announcement.getAttribute('data-custom')).toBe('value')
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
it('updates visibility when visible prop changes', () => {
|
|
262
|
+
const { container, root } = render(
|
|
263
|
+
<Announcement title="Test" visible={true} />
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
expect(container.firstChild).toBeTruthy()
|
|
267
|
+
|
|
268
|
+
act(() => {
|
|
269
|
+
root.render(<Announcement title="Test" visible={false} />)
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
expect(container.firstChild).toBeNull()
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
it('renders correct icon for attentive variant', () => {
|
|
276
|
+
const { container } = render(
|
|
277
|
+
<Announcement title="Test" variant="attentive" />
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
const icon = container.querySelector('svg')
|
|
281
|
+
expect(icon).toBeTruthy()
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
it('renders correct icon for informative variant', () => {
|
|
285
|
+
const { container } = render(
|
|
286
|
+
<Announcement title="Test" variant="informative" />
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
const icon = container.querySelector('svg')
|
|
290
|
+
expect(icon).toBeTruthy()
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
it('renders correct icon for negative variant', () => {
|
|
294
|
+
const { container } = render(
|
|
295
|
+
<Announcement title="Test" variant="negative" />
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
const icon = container.querySelector('svg')
|
|
299
|
+
expect(icon).toBeTruthy()
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
it('renders correct icon for positive variant', () => {
|
|
303
|
+
const { container } = render(
|
|
304
|
+
<Announcement title="Test" variant="positive" />
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
const icon = container.querySelector('svg')
|
|
308
|
+
expect(icon).toBeTruthy()
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
it('handles all variant and size combinations', () => {
|
|
312
|
+
const variants = ['neutral', 'attentive', 'informative', 'negative', 'positive'] as const
|
|
313
|
+
const sizes = ['large', 'small'] as const
|
|
314
|
+
|
|
315
|
+
variants.forEach(variant => {
|
|
316
|
+
sizes.forEach(size => {
|
|
317
|
+
const { container } = render(
|
|
318
|
+
<Announcement title="Test" variant={variant} size={size} />
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
expect(container.firstChild).toBeTruthy()
|
|
322
|
+
})
|
|
323
|
+
})
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
it('renders without title or description', () => {
|
|
327
|
+
const { container } = render(
|
|
328
|
+
<Announcement />
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
expect(container.firstChild).toBeTruthy()
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
it('handles complex content structure', () => {
|
|
335
|
+
const { container } = render(
|
|
336
|
+
<Announcement
|
|
337
|
+
title="Complex Title"
|
|
338
|
+
description="Complex Description"
|
|
339
|
+
linkText="Complex Link"
|
|
340
|
+
link="https://example.com"
|
|
341
|
+
withIcon
|
|
342
|
+
isRemovable
|
|
343
|
+
variant="informative"
|
|
344
|
+
size="large"
|
|
345
|
+
/>
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
expect(getByText(container, 'Complex Title')).toBeTruthy()
|
|
349
|
+
expect(getByText(container, 'Complex Description')).toBeTruthy()
|
|
350
|
+
expect(getByText(container, 'Complex Link')).toBeTruthy()
|
|
351
|
+
expect(container.querySelector('svg')).toBeTruthy()
|
|
352
|
+
expect(getByTestId(container, 'removeAnnouncement')).toBeTruthy()
|
|
353
|
+
})
|
|
354
|
+
})
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
3
|
+
import { AlertTriangle, Info, X } from "lucide-react"
|
|
4
|
+
import { cn } from "../../../lib/utils"
|
|
5
|
+
import { Link } from "../../atoms/link"
|
|
6
|
+
|
|
7
|
+
const announcementVariants = cva(
|
|
8
|
+
"flex gap-4 rounded-lg border p-4 transition-all",
|
|
9
|
+
{
|
|
10
|
+
variants: {
|
|
11
|
+
variant: {
|
|
12
|
+
neutral: "bg-background border-border text-foreground",
|
|
13
|
+
attentive: "bg-amber-50 border-amber-200 text-amber-900 dark:bg-amber-950 dark:border-amber-800 dark:text-amber-100",
|
|
14
|
+
informative: "bg-blue-50 border-blue-200 text-blue-900 dark:bg-blue-950 dark:border-blue-800 dark:text-blue-100",
|
|
15
|
+
negative: "bg-destructive/10 border-destructive/20 text-destructive-foreground",
|
|
16
|
+
positive: "bg-green-50 border-green-200 text-green-900 dark:bg-green-950 dark:border-green-800 dark:text-green-100",
|
|
17
|
+
},
|
|
18
|
+
size: {
|
|
19
|
+
large: "w-full max-w-full",
|
|
20
|
+
small: "w-full max-w-[343px]",
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
defaultVariants: {
|
|
24
|
+
variant: "neutral",
|
|
25
|
+
size: "large",
|
|
26
|
+
},
|
|
27
|
+
}
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
const announcementContentVariants = cva(
|
|
31
|
+
"flex flex-1 gap-4",
|
|
32
|
+
{
|
|
33
|
+
variants: {
|
|
34
|
+
size: {
|
|
35
|
+
large: "md:flex-row md:items-center flex-col items-start",
|
|
36
|
+
small: "flex-col items-start",
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
defaultVariants: {
|
|
40
|
+
size: "large",
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
const announcementIconVariants = cva(
|
|
46
|
+
"shrink-0",
|
|
47
|
+
{
|
|
48
|
+
variants: {
|
|
49
|
+
variant: {
|
|
50
|
+
neutral: "text-muted-foreground",
|
|
51
|
+
attentive: "text-amber-600 dark:text-amber-400",
|
|
52
|
+
informative: "text-blue-600 dark:text-blue-400",
|
|
53
|
+
negative: "text-destructive",
|
|
54
|
+
positive: "text-green-600 dark:text-green-400",
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
defaultVariants: {
|
|
58
|
+
variant: "neutral",
|
|
59
|
+
},
|
|
60
|
+
}
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
export interface AnnouncementProps
|
|
64
|
+
extends React.HTMLAttributes<HTMLDivElement>,
|
|
65
|
+
VariantProps<typeof announcementVariants> {
|
|
66
|
+
title?: string
|
|
67
|
+
description?: string
|
|
68
|
+
linkText?: string
|
|
69
|
+
link?: string
|
|
70
|
+
withIcon?: boolean
|
|
71
|
+
iconName?: React.ReactNode
|
|
72
|
+
isRemovable?: boolean
|
|
73
|
+
visible?: boolean
|
|
74
|
+
onClose?: () => void
|
|
75
|
+
onClickLink?: React.MouseEventHandler<HTMLAnchorElement>
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const Announcement = React.forwardRef<HTMLDivElement, AnnouncementProps>(
|
|
79
|
+
(
|
|
80
|
+
{
|
|
81
|
+
className,
|
|
82
|
+
variant,
|
|
83
|
+
size,
|
|
84
|
+
title,
|
|
85
|
+
description,
|
|
86
|
+
linkText,
|
|
87
|
+
link,
|
|
88
|
+
withIcon = true,
|
|
89
|
+
iconName,
|
|
90
|
+
isRemovable = false,
|
|
91
|
+
visible = true,
|
|
92
|
+
onClose,
|
|
93
|
+
onClickLink,
|
|
94
|
+
...props
|
|
95
|
+
},
|
|
96
|
+
ref
|
|
97
|
+
) => {
|
|
98
|
+
const [isVisible, setIsVisible] = React.useState(visible)
|
|
99
|
+
|
|
100
|
+
const handleClose = React.useCallback(() => {
|
|
101
|
+
onClose?.()
|
|
102
|
+
setIsVisible(false)
|
|
103
|
+
}, [onClose])
|
|
104
|
+
|
|
105
|
+
const getDefaultIcon = () => {
|
|
106
|
+
switch (variant) {
|
|
107
|
+
case "attentive":
|
|
108
|
+
case "negative":
|
|
109
|
+
return <AlertTriangle className="size-6" />
|
|
110
|
+
case "informative":
|
|
111
|
+
case "positive":
|
|
112
|
+
return <Info className="size-6" />
|
|
113
|
+
default:
|
|
114
|
+
return <AlertTriangle className="size-6" />
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
React.useEffect(() => {
|
|
119
|
+
setIsVisible(visible)
|
|
120
|
+
}, [visible])
|
|
121
|
+
|
|
122
|
+
if (!isVisible) return null
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<div
|
|
126
|
+
ref={ref}
|
|
127
|
+
className={cn(announcementVariants({ variant, size, className }))}
|
|
128
|
+
{...props}
|
|
129
|
+
>
|
|
130
|
+
{withIcon && (
|
|
131
|
+
<div className={cn(announcementIconVariants({ variant }))}>
|
|
132
|
+
{iconName || getDefaultIcon()}
|
|
133
|
+
</div>
|
|
134
|
+
)}
|
|
135
|
+
|
|
136
|
+
<div className={cn(announcementContentVariants({ size }))}>
|
|
137
|
+
<div className="flex-1 space-y-1">
|
|
138
|
+
{title && (
|
|
139
|
+
<div
|
|
140
|
+
className="text-[var(--font-size-small)] font-medium leading-[var(--line-height-small)]"
|
|
141
|
+
data-testid="title"
|
|
142
|
+
>
|
|
143
|
+
{title}
|
|
144
|
+
</div>
|
|
145
|
+
)}
|
|
146
|
+
{description && (
|
|
147
|
+
<div
|
|
148
|
+
className="text-[var(--font-size-tiny)] leading-[var(--line-height-tiny)]"
|
|
149
|
+
data-testid="description"
|
|
150
|
+
>
|
|
151
|
+
{description}
|
|
152
|
+
</div>
|
|
153
|
+
)}
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
<div className="flex items-center gap-4">
|
|
157
|
+
{linkText && (link || onClickLink) && (
|
|
158
|
+
<Link
|
|
159
|
+
href={link}
|
|
160
|
+
onClick={onClickLink}
|
|
161
|
+
target={link ? "_blank" : undefined}
|
|
162
|
+
inline
|
|
163
|
+
size="small"
|
|
164
|
+
data-testid="linkButton"
|
|
165
|
+
>
|
|
166
|
+
{linkText}
|
|
167
|
+
</Link>
|
|
168
|
+
)}
|
|
169
|
+
|
|
170
|
+
{isRemovable && (size === "large" || size === undefined) && (
|
|
171
|
+
<button
|
|
172
|
+
onClick={handleClose}
|
|
173
|
+
className="inline-flex items-center justify-center size-6 rounded-sm opacity-70 hover:opacity-100 transition-opacity"
|
|
174
|
+
aria-label="Close announcement"
|
|
175
|
+
data-testid="removeAnnouncement"
|
|
176
|
+
>
|
|
177
|
+
<X className="size-4" />
|
|
178
|
+
</button>
|
|
179
|
+
)}
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
|
|
183
|
+
{isRemovable && size === "small" && (
|
|
184
|
+
<button
|
|
185
|
+
onClick={handleClose}
|
|
186
|
+
className="inline-flex items-center justify-center size-6 rounded-sm opacity-70 hover:opacity-100 transition-opacity"
|
|
187
|
+
aria-label="Close announcement"
|
|
188
|
+
data-testid="removeAnnouncement"
|
|
189
|
+
>
|
|
190
|
+
<X className="size-4" />
|
|
191
|
+
</button>
|
|
192
|
+
)}
|
|
193
|
+
</div>
|
|
194
|
+
)
|
|
195
|
+
}
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
Announcement.displayName = "Announcement"
|
|
199
|
+
|
|
200
|
+
export { Announcement }
|