@duro-app/ui 0.25.0 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/dist/components/Combobox/Combobox.d.ts.map +1 -1
  2. package/dist/components/Combobox/Combobox.meta.d.ts.map +1 -1
  3. package/dist/components/Combobox/Combobox.stories.d.ts.map +1 -1
  4. package/dist/components/Field/Field.d.ts.map +1 -1
  5. package/dist/components/Field/Field.stories.d.ts.map +1 -1
  6. package/dist/components/Form/Form.d.ts.map +1 -1
  7. package/dist/components/Table/Table.stories.d.ts.map +1 -1
  8. package/dist/components/Tag/Tag.d.ts +18 -0
  9. package/dist/components/Tag/Tag.d.ts.map +1 -0
  10. package/dist/components/Tag/Tag.meta.d.ts +3 -0
  11. package/dist/components/Tag/Tag.meta.d.ts.map +1 -0
  12. package/dist/components/Tag/Tag.stories.d.ts +11 -0
  13. package/dist/components/Tag/Tag.stories.d.ts.map +1 -0
  14. package/dist/components/Tag/styles.css.d.ts +80 -0
  15. package/dist/components/Tag/styles.css.d.ts.map +1 -0
  16. package/dist/components/TagGroup/TagGroup.d.ts +30 -0
  17. package/dist/components/TagGroup/TagGroup.d.ts.map +1 -0
  18. package/dist/components/TagGroup/TagGroup.meta.d.ts +3 -0
  19. package/dist/components/TagGroup/TagGroup.meta.d.ts.map +1 -0
  20. package/dist/components/TagGroup/TagGroup.stories.d.ts +11 -0
  21. package/dist/components/TagGroup/TagGroup.stories.d.ts.map +1 -0
  22. package/dist/components/TagGroup/TagGroupContext.d.ts +17 -0
  23. package/dist/components/TagGroup/TagGroupContext.d.ts.map +1 -0
  24. package/dist/components/TagGroup/styles.css.d.ts +75 -0
  25. package/dist/components/TagGroup/styles.css.d.ts.map +1 -0
  26. package/dist/index.css +1 -1
  27. package/dist/index.d.ts +2 -0
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +2602 -2247
  30. package/dist/index.js.map +1 -1
  31. package/package.json +2 -2
  32. package/src/components/Combobox/Combobox.meta.ts +8 -2
  33. package/src/components/Combobox/Combobox.stories.tsx +52 -14
  34. package/src/components/Combobox/Combobox.tsx +3 -15
  35. package/src/components/Field/Field.stories.tsx +3 -1
  36. package/src/components/Field/Field.tsx +1 -4
  37. package/src/components/Form/Form.tsx +1 -5
  38. package/src/components/Table/Table.stories.tsx +66 -11
  39. package/src/components/Tag/Tag.meta.ts +22 -0
  40. package/src/components/Tag/Tag.stories.tsx +92 -0
  41. package/src/components/Tag/Tag.tsx +139 -0
  42. package/src/components/Tag/styles.css.ts +99 -0
  43. package/src/components/TagGroup/TagGroup.meta.ts +27 -0
  44. package/src/components/TagGroup/TagGroup.stories.tsx +193 -0
  45. package/src/components/TagGroup/TagGroup.tsx +302 -0
  46. package/src/components/TagGroup/TagGroupContext.ts +22 -0
  47. package/src/components/TagGroup/styles.css.ts +97 -0
  48. package/src/index.ts +2 -0
@@ -0,0 +1,193 @@
1
+ import {useState} from 'react'
2
+ import type {Meta, StoryObj} from '@storybook/react'
3
+ import {expect, userEvent, within} from 'storybook/test'
4
+ import {css, html} from 'react-strict-dom'
5
+ import {TagGroup} from './TagGroup'
6
+ import {Tag} from '../Tag/Tag'
7
+ import {Field} from '../Field/Field'
8
+ import {Stack} from '../Stack/Stack'
9
+
10
+ const meta: Meta = {
11
+ title: 'Components/TagGroup',
12
+ }
13
+
14
+ export default meta
15
+ type Story = StoryObj
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // Static display (no input)
19
+ // ---------------------------------------------------------------------------
20
+
21
+ export const StaticDisplay: Story = {
22
+ render: () => (
23
+ <TagGroup.Root>
24
+ <TagGroup.List aria-label="Technologies">
25
+ <Tag value="react">React</Tag>
26
+ <Tag value="ts">TypeScript</Tag>
27
+ <Tag value="node">Node.js</Tag>
28
+ </TagGroup.List>
29
+ </TagGroup.Root>
30
+ ),
31
+ play: async ({canvas}) => {
32
+ await expect(canvas.getByText('React')).toBeInTheDocument()
33
+ await expect(canvas.getByText('TypeScript')).toBeInTheDocument()
34
+ await expect(canvas.getByText('Node.js')).toBeInTheDocument()
35
+ },
36
+ }
37
+
38
+ // ---------------------------------------------------------------------------
39
+ // Editable with input
40
+ // ---------------------------------------------------------------------------
41
+
42
+ function EditableExample() {
43
+ const [tags, setTags] = useState(['react', 'typescript'])
44
+
45
+ return (
46
+ <TagGroup.Root value={tags} onValueChange={setTags}>
47
+ <TagGroup.List aria-label="Tags">
48
+ {tags.map((t) => (
49
+ <Tag key={t} value={t}>
50
+ {t}
51
+ </Tag>
52
+ ))}
53
+ </TagGroup.List>
54
+ <TagGroup.Input placeholder="Add tag..." />
55
+ </TagGroup.Root>
56
+ )
57
+ }
58
+
59
+ export const Editable: Story = {
60
+ render: () => <EditableExample />,
61
+ play: async ({canvas}) => {
62
+ await expect(canvas.getByText('react')).toBeInTheDocument()
63
+ await expect(canvas.getByText('typescript')).toBeInTheDocument()
64
+ // Input should be present
65
+ await expect(canvas.getByRole('textbox')).toBeInTheDocument()
66
+ },
67
+ }
68
+
69
+ // ---------------------------------------------------------------------------
70
+ // With default value (uncontrolled)
71
+ // ---------------------------------------------------------------------------
72
+
73
+ export const WithDefaultValue: Story = {
74
+ render: () => {
75
+ function Uncontrolled() {
76
+ const [tags, setTags] = useState(['one', 'two'])
77
+ return (
78
+ <TagGroup.Root defaultValue={['one', 'two']} onValueChange={setTags}>
79
+ <TagGroup.List aria-label="Items">
80
+ {tags.map((t) => (
81
+ <Tag key={t} value={t}>
82
+ {t}
83
+ </Tag>
84
+ ))}
85
+ </TagGroup.List>
86
+ <TagGroup.Input placeholder="Add..." />
87
+ </TagGroup.Root>
88
+ )
89
+ }
90
+ return <Uncontrolled />
91
+ },
92
+ }
93
+
94
+ // ---------------------------------------------------------------------------
95
+ // Email input with validation
96
+ // ---------------------------------------------------------------------------
97
+
98
+ function EmailExample() {
99
+ const [emails, setEmails] = useState<string[]>([])
100
+
101
+ return (
102
+ <Stack gap="md">
103
+ <Field.Root required>
104
+ <Field.Label>Invite emails</Field.Label>
105
+ <TagGroup.Root
106
+ name="emails"
107
+ value={emails}
108
+ onValueChange={setEmails}
109
+ onValidate={(v) => (v.includes('@') ? true : 'Invalid email')}
110
+ >
111
+ <TagGroup.List aria-label="Emails">
112
+ {emails.map((e) => (
113
+ <Tag key={e} value={e}>
114
+ {e}
115
+ </Tag>
116
+ ))}
117
+ </TagGroup.List>
118
+ <TagGroup.Input placeholder="Add email..." />
119
+ </TagGroup.Root>
120
+ <Field.Description>Press Enter or comma to add each email</Field.Description>
121
+ </Field.Root>
122
+ </Stack>
123
+ )
124
+ }
125
+
126
+ export const EmailInput: Story = {
127
+ render: () => <EmailExample />,
128
+ play: async ({canvas}) => {
129
+ const input = canvas.getByRole('textbox')
130
+ await expect(input).toBeInTheDocument()
131
+ // Type an email and press Enter
132
+ await userEvent.type(input, 'alice@example.com{Enter}')
133
+ await expect(canvas.getByText('alice@example.com')).toBeInTheDocument()
134
+ // Type another
135
+ await userEvent.type(input, 'bob@test.com,')
136
+ await expect(canvas.getByText('bob@test.com')).toBeInTheDocument()
137
+ // Invalid email should not create a tag
138
+ await userEvent.type(input, 'notanemail{Enter}')
139
+ expect(canvas.queryByText('notanemail')).not.toBeInTheDocument()
140
+ },
141
+ }
142
+
143
+ // ---------------------------------------------------------------------------
144
+ // Disabled
145
+ // ---------------------------------------------------------------------------
146
+
147
+ function DisabledExample() {
148
+ const tags = ['react', 'vue', 'svelte']
149
+ return (
150
+ <TagGroup.Root value={tags} disabled>
151
+ <TagGroup.List aria-label="Frameworks">
152
+ {tags.map((t) => (
153
+ <Tag key={t} value={t}>
154
+ {t}
155
+ </Tag>
156
+ ))}
157
+ </TagGroup.List>
158
+ <TagGroup.Input placeholder="Disabled..." />
159
+ </TagGroup.Root>
160
+ )
161
+ }
162
+
163
+ export const Disabled: Story = {
164
+ render: () => <DisabledExample />,
165
+ play: async ({canvas}) => {
166
+ await expect(canvas.getByRole('textbox')).toBeDisabled()
167
+ },
168
+ }
169
+
170
+ // ---------------------------------------------------------------------------
171
+ // Tag variants inside TagGroup
172
+ // ---------------------------------------------------------------------------
173
+
174
+ export const WithVariants: Story = {
175
+ render: () => (
176
+ <TagGroup.Root>
177
+ <TagGroup.List aria-label="Status">
178
+ <Tag value="active" variant="success">
179
+ Active
180
+ </Tag>
181
+ <Tag value="pending" variant="warning">
182
+ Pending
183
+ </Tag>
184
+ <Tag value="expired" variant="error">
185
+ Expired
186
+ </Tag>
187
+ <Tag value="info" variant="info">
188
+ Updated
189
+ </Tag>
190
+ </TagGroup.List>
191
+ </TagGroup.Root>
192
+ ),
193
+ }
@@ -0,0 +1,302 @@
1
+ import {
2
+ type ReactNode,
3
+ useCallback,
4
+ useContext,
5
+ useEffect,
6
+ useId,
7
+ useMemo,
8
+ useRef,
9
+ useState,
10
+ } from 'react'
11
+ import {html} from 'react-strict-dom'
12
+ import {useControllableValue} from '../../hooks/useControllableValue'
13
+ import {useFieldContext} from '../Field/FieldContext'
14
+ import {TagGroupContext, type TagGroupContextValue} from './TagGroupContext'
15
+ import {styles} from './styles.css'
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // Local context accessor (non-throwing, returns null)
19
+ // ---------------------------------------------------------------------------
20
+
21
+ function useTagGroupCtx() {
22
+ return useContext(TagGroupContext)
23
+ }
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // TagGroup.Root
27
+ // ---------------------------------------------------------------------------
28
+
29
+ interface RootProps {
30
+ value?: string[]
31
+ defaultValue?: string[]
32
+ onValueChange?: (values: string[]) => void
33
+ /** Validate a value before adding. Return `true` if valid, or an error string. */
34
+ onValidate?: (value: string) => true | string
35
+ disabled?: boolean
36
+ /** Form field name — renders hidden inputs for each tag value */
37
+ name?: string
38
+ 'aria-label'?: string
39
+ children: ReactNode
40
+ }
41
+
42
+ function Root({
43
+ value: controlledValue,
44
+ defaultValue = [],
45
+ onValueChange,
46
+ onValidate,
47
+ disabled: disabledProp = false,
48
+ name,
49
+ 'aria-label': ariaLabel,
50
+ children,
51
+ }: RootProps) {
52
+ const gridId = useId()
53
+ const fieldCtx = useFieldContext()
54
+ const [values, setValues] = useControllableValue(controlledValue, defaultValue, onValueChange)
55
+ const [focusedIndex, setFocusedIndex] = useState(-1)
56
+ const [editable, setEditable] = useState(false)
57
+ const [announcement, setAnnouncement] = useState('')
58
+ const inputRef = useRef<HTMLInputElement>(null)
59
+
60
+ const disabled = disabledProp || (fieldCtx?.disabled ?? false)
61
+ const isInvalid = fieldCtx?.invalid ?? false
62
+
63
+ const addTag = useCallback(
64
+ (value: string) => {
65
+ const trimmed = value.trim()
66
+ if (!trimmed) return
67
+ if (values.includes(trimmed)) return
68
+ if (onValidate) {
69
+ const result = onValidate(trimmed)
70
+ if (result !== true) return
71
+ }
72
+ setValues([...values, trimmed])
73
+ setAnnouncement(`Added ${trimmed}`)
74
+ },
75
+ [values, setValues, onValidate],
76
+ )
77
+
78
+ const removeTag = useCallback(
79
+ (value: string) => {
80
+ const idx = values.indexOf(value)
81
+ if (idx === -1) return
82
+ const next = values.filter((v) => v !== value)
83
+ setValues(next)
84
+ setAnnouncement(`Removed ${value}`)
85
+
86
+ // Focus management: move to next tag, or previous, or input
87
+ if (next.length === 0) {
88
+ inputRef.current?.focus()
89
+ setFocusedIndex(-1)
90
+ } else if (idx >= next.length) {
91
+ setFocusedIndex(next.length - 1)
92
+ } else {
93
+ setFocusedIndex(idx)
94
+ }
95
+ },
96
+ [values, setValues],
97
+ )
98
+
99
+ // Focus the tag at focusedIndex when it changes
100
+ useEffect(() => {
101
+ if (focusedIndex >= 0 && focusedIndex < values.length) {
102
+ const container = document.getElementById(gridId)
103
+ if (!container) return
104
+ const rows = container.querySelectorAll('[role="row"]')
105
+ const target = rows[focusedIndex] as HTMLElement | undefined
106
+ target?.focus()
107
+ }
108
+ }, [focusedIndex, gridId, values.length])
109
+
110
+ const ctx = useMemo<TagGroupContextValue>(
111
+ () => ({
112
+ values,
113
+ addTag,
114
+ removeTag,
115
+ editable,
116
+ setEditable,
117
+ disabled,
118
+ validate: onValidate,
119
+ focusedIndex,
120
+ setFocusedIndex,
121
+ inputRef,
122
+ gridId,
123
+ name,
124
+ }),
125
+ [values, addTag, removeTag, editable, disabled, onValidate, focusedIndex, gridId, name],
126
+ )
127
+
128
+ return (
129
+ <TagGroupContext.Provider value={ctx}>
130
+ <html.div style={styles.root} aria-label={ariaLabel}>
131
+ <html.div
132
+ style={[
133
+ styles.container,
134
+ !editable && styles.containerStatic,
135
+ isInvalid && styles.containerError,
136
+ disabled && styles.containerDisabled,
137
+ ]}
138
+ aria-describedby={
139
+ fieldCtx
140
+ ? `${fieldCtx.descriptionId} ${fieldCtx.invalid ? fieldCtx.errorId : ''}`.trim()
141
+ : undefined
142
+ }
143
+ aria-invalid={isInvalid || undefined}
144
+ >
145
+ {children}
146
+ </html.div>
147
+
148
+ {/* Hidden inputs for form submission */}
149
+ {name && values.map((v) => <html.input key={v} type="hidden" name={name} value={v} />)}
150
+
151
+ {/* Live region for screen reader announcements */}
152
+ <html.div role="status" aria-live="polite" aria-atomic style={styles.liveRegion}>
153
+ {announcement}
154
+ </html.div>
155
+ </html.div>
156
+ </TagGroupContext.Provider>
157
+ )
158
+ }
159
+
160
+ // ---------------------------------------------------------------------------
161
+ // TagGroup.List
162
+ // ---------------------------------------------------------------------------
163
+
164
+ interface ListProps {
165
+ children: ReactNode
166
+ 'aria-label'?: string
167
+ }
168
+
169
+ function List({children, 'aria-label': ariaLabel}: ListProps) {
170
+ const ctx = useTagGroupCtx()
171
+
172
+ return (
173
+ <html.div role={'grid' as any} aria-label={ariaLabel} id={ctx?.gridId} style={styles.list}>
174
+ {children}
175
+ </html.div>
176
+ )
177
+ }
178
+
179
+ // ---------------------------------------------------------------------------
180
+ // TagGroup.Input
181
+ // ---------------------------------------------------------------------------
182
+
183
+ interface InputProps {
184
+ placeholder?: string
185
+ }
186
+
187
+ function Input({placeholder}: InputProps) {
188
+ const ctx = useTagGroupCtx()
189
+ const [inputValue, setInputValue] = useState('')
190
+ const localRef = useRef<HTMLInputElement>(null)
191
+
192
+ // Sync local ref with context inputRef
193
+ useEffect(() => {
194
+ if (ctx?.inputRef) {
195
+ ;(ctx.inputRef as React.MutableRefObject<HTMLInputElement | null>).current = localRef.current
196
+ }
197
+ })
198
+
199
+ // Register as editable on mount
200
+ useEffect(() => {
201
+ ctx?.setEditable(true)
202
+ return () => ctx?.setEditable(false)
203
+ }, []) // eslint-disable-line react-hooks/exhaustive-deps
204
+
205
+ const commitValueRef = useRef<(raw: string) => boolean>(() => false)
206
+ commitValueRef.current = (raw: string) => {
207
+ const trimmed = raw.trim()
208
+ if (!trimmed || !ctx) return false
209
+ ctx.addTag(trimmed)
210
+ setInputValue('')
211
+ return true
212
+ }
213
+
214
+ // Native keydown for full KeyboardEvent access (preventDefault)
215
+ useEffect(() => {
216
+ const el = localRef.current
217
+ if (!el) return
218
+
219
+ function handleKeyDown(e: KeyboardEvent) {
220
+ if (!ctx) return
221
+ const target = e.target as HTMLInputElement
222
+
223
+ if (e.key === 'Enter' || e.key === ',' || e.key === ';') {
224
+ e.preventDefault()
225
+ commitValueRef.current(target.value)
226
+ return
227
+ }
228
+
229
+ if (e.key === 'Tab' && target.value.trim()) {
230
+ e.preventDefault()
231
+ commitValueRef.current(target.value)
232
+ return
233
+ }
234
+
235
+ if (e.key === 'Backspace' && !target.value && ctx.values.length > 0) {
236
+ e.preventDefault()
237
+ ctx.removeTag(ctx.values[ctx.values.length - 1])
238
+ return
239
+ }
240
+
241
+ if (e.key === 'ArrowLeft' && (target.selectionStart ?? 0) === 0 && ctx.values.length > 0) {
242
+ e.preventDefault()
243
+ ctx.setFocusedIndex(ctx.values.length - 1)
244
+ return
245
+ }
246
+
247
+ if (e.key === 'Escape') {
248
+ setInputValue('')
249
+ }
250
+ }
251
+
252
+ function handlePaste(e: ClipboardEvent) {
253
+ if (!ctx) return
254
+ const text = e.clipboardData?.getData('text') ?? ''
255
+ const items = text
256
+ .split(/[\n,;]+/)
257
+ .map((s) => s.trim())
258
+ .filter(Boolean)
259
+ if (items.length > 1) {
260
+ e.preventDefault()
261
+ for (const item of items) {
262
+ ctx.addTag(item)
263
+ }
264
+ }
265
+ }
266
+
267
+ function handleFocus() {
268
+ ctx?.setFocusedIndex(-1)
269
+ }
270
+
271
+ el.addEventListener('keydown', handleKeyDown)
272
+ el.addEventListener('paste', handlePaste)
273
+ el.addEventListener('focus', handleFocus)
274
+ return () => {
275
+ el.removeEventListener('keydown', handleKeyDown)
276
+ el.removeEventListener('paste', handlePaste)
277
+ el.removeEventListener('focus', handleFocus)
278
+ }
279
+ }, [ctx])
280
+
281
+ return (
282
+ <html.input
283
+ ref={localRef as React.Ref<HTMLInputElement>}
284
+ type="text"
285
+ value={inputValue}
286
+ onChange={(e: any) => setInputValue(e.target.value)}
287
+ placeholder={!ctx?.values.length ? placeholder : undefined}
288
+ disabled={ctx?.disabled}
289
+ style={styles.input}
290
+ />
291
+ )
292
+ }
293
+
294
+ // ---------------------------------------------------------------------------
295
+ // Namespace export
296
+ // ---------------------------------------------------------------------------
297
+
298
+ export const TagGroup = {
299
+ Root,
300
+ List,
301
+ Input,
302
+ }
@@ -0,0 +1,22 @@
1
+ import {createContext, useContext} from 'react'
2
+
3
+ export interface TagGroupContextValue {
4
+ values: string[]
5
+ addTag: (value: string) => void
6
+ removeTag: (value: string) => void
7
+ editable: boolean
8
+ setEditable: (v: boolean) => void
9
+ disabled: boolean
10
+ validate?: (value: string) => true | string
11
+ focusedIndex: number
12
+ setFocusedIndex: (index: number) => void
13
+ inputRef: React.RefObject<HTMLInputElement | null>
14
+ gridId: string
15
+ name?: string
16
+ }
17
+
18
+ export const TagGroupContext = createContext<TagGroupContextValue | null>(null)
19
+
20
+ export function useTagGroup(): TagGroupContextValue | null {
21
+ return useContext(TagGroupContext)
22
+ }
@@ -0,0 +1,97 @@
1
+ import {css} from 'react-strict-dom'
2
+ import {colors} from '@duro-app/tokens/tokens/colors.css'
3
+ import {spacing, radii} from '@duro-app/tokens/tokens/spacing.css'
4
+ import {typography} from '@duro-app/tokens/tokens/typography.css'
5
+
6
+ export const styles = css.create({
7
+ root: {
8
+ display: 'flex',
9
+ flexDirection: 'column',
10
+ },
11
+ container: {
12
+ display: 'flex',
13
+ flexWrap: 'wrap',
14
+ alignItems: 'center',
15
+ gap: spacing.xs,
16
+ paddingTop: spacing.xs,
17
+ paddingBottom: spacing.xs,
18
+ paddingLeft: spacing.sm,
19
+ paddingRight: spacing.sm,
20
+ borderWidth: 1,
21
+ borderStyle: 'solid',
22
+ borderColor: {
23
+ default: colors.border,
24
+ ':focus-within': colors.accent,
25
+ },
26
+ borderRadius: radii.sm,
27
+ backgroundColor: colors.bg,
28
+ minHeight: 40,
29
+ transitionProperty: 'border-color',
30
+ transitionDuration: '150ms',
31
+ transitionTimingFunction: 'ease',
32
+ outlineWidth: {
33
+ default: 0,
34
+ ':focus-within': 2,
35
+ },
36
+ outlineStyle: {
37
+ default: 'none',
38
+ ':focus-within': 'solid',
39
+ },
40
+ outlineColor: {
41
+ default: 'transparent',
42
+ ':focus-within': colors.accent,
43
+ },
44
+ outlineOffset: {
45
+ default: 0,
46
+ ':focus-within': 1,
47
+ },
48
+ },
49
+ containerError: {
50
+ borderColor: {
51
+ default: colors.error,
52
+ ':focus-within': colors.error,
53
+ },
54
+ },
55
+ containerDisabled: {
56
+ opacity: 0.5,
57
+ cursor: 'not-allowed',
58
+ },
59
+ // Static display (no Input) — no input-field border
60
+ containerStatic: {
61
+ borderWidth: 0,
62
+ paddingTop: 0,
63
+ paddingBottom: 0,
64
+ paddingLeft: 0,
65
+ paddingRight: 0,
66
+ minHeight: 0,
67
+ backgroundColor: 'transparent',
68
+ outlineWidth: 0,
69
+ },
70
+ list: {
71
+ display: 'flex',
72
+ flexWrap: 'wrap',
73
+ gap: spacing.xs,
74
+ alignItems: 'center',
75
+ },
76
+ input: {
77
+ flex: 1,
78
+ minWidth: 120,
79
+ borderWidth: 0,
80
+ backgroundColor: 'transparent',
81
+ fontFamily: typography.fontFamily,
82
+ fontSize: typography.fontSizeSm,
83
+ lineHeight: typography.lineHeight,
84
+ color: colors.text,
85
+ outlineWidth: 0,
86
+ paddingTop: spacing.xs,
87
+ paddingBottom: spacing.xs,
88
+ paddingLeft: spacing.xs,
89
+ paddingRight: spacing.xs,
90
+ },
91
+ liveRegion: {
92
+ position: 'absolute',
93
+ width: 1,
94
+ height: 1,
95
+ overflow: 'hidden',
96
+ },
97
+ })
package/src/index.ts CHANGED
@@ -43,6 +43,8 @@ export {
43
43
  type StatusIconName,
44
44
  type StatusIconVariant,
45
45
  } from './components/StatusIcon/StatusIcon'
46
+ export {Tag, type TagVariant, type TagSize} from './components/Tag/Tag'
47
+ export {TagGroup} from './components/TagGroup/TagGroup'
46
48
  export {Table, type TableVariant, type TableSize} from './components/Table/Table'
47
49
  // TanStack Table: consumers install @tanstack/react-table directly and use
48
50
  // Table.Pagination, Table.SortIndicator, Table.ColumnFilter for styled integration