@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
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { cn } from '@open-mercato/shared/lib/utils'
|
|
5
|
+
|
|
6
|
+
export type SkeletonShape = 'rect' | 'circle' | 'text'
|
|
7
|
+
|
|
8
|
+
export type SkeletonProps = Omit<React.HTMLAttributes<HTMLDivElement>, 'role' | 'aria-busy'> & {
|
|
9
|
+
/**
|
|
10
|
+
* Visual shape of the placeholder.
|
|
11
|
+
* - `'rect'` (default): rectangular block with `rounded-md`. Sized via `className`.
|
|
12
|
+
* - `'circle'`: round placeholder for avatars / icons. Sized via `className` (typically `size-8`, `size-10`, `size-12`).
|
|
13
|
+
* - `'text'`: multi-line text placeholder. Use `lines` to set how many lines.
|
|
14
|
+
*/
|
|
15
|
+
shape?: SkeletonShape
|
|
16
|
+
/**
|
|
17
|
+
* Number of lines to render when `shape='text'`. Default `1`.
|
|
18
|
+
* The last line is rendered narrower (`w-3/4`) to mimic natural text wrap.
|
|
19
|
+
*/
|
|
20
|
+
lines?: number
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const baseLine = 'animate-pulse rounded-md bg-muted'
|
|
24
|
+
|
|
25
|
+
export function Skeleton({
|
|
26
|
+
shape = 'rect',
|
|
27
|
+
lines = 1,
|
|
28
|
+
className,
|
|
29
|
+
...rest
|
|
30
|
+
}: SkeletonProps) {
|
|
31
|
+
if (shape === 'text') {
|
|
32
|
+
const count = Math.max(1, lines)
|
|
33
|
+
return (
|
|
34
|
+
<div
|
|
35
|
+
role="status"
|
|
36
|
+
aria-busy="true"
|
|
37
|
+
aria-live="polite"
|
|
38
|
+
className={cn('w-full space-y-2', className)}
|
|
39
|
+
data-slot="skeleton"
|
|
40
|
+
{...rest}
|
|
41
|
+
>
|
|
42
|
+
{Array.from({ length: count }).map((_, index) => (
|
|
43
|
+
<div
|
|
44
|
+
key={index}
|
|
45
|
+
className={cn(
|
|
46
|
+
baseLine,
|
|
47
|
+
'h-4',
|
|
48
|
+
index === count - 1 && count > 1 ? 'w-3/4' : 'w-full',
|
|
49
|
+
)}
|
|
50
|
+
/>
|
|
51
|
+
))}
|
|
52
|
+
</div>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<div
|
|
58
|
+
role="status"
|
|
59
|
+
aria-busy="true"
|
|
60
|
+
aria-live="polite"
|
|
61
|
+
data-slot="skeleton"
|
|
62
|
+
className={cn(
|
|
63
|
+
baseLine,
|
|
64
|
+
shape === 'circle' && 'rounded-full',
|
|
65
|
+
className,
|
|
66
|
+
)}
|
|
67
|
+
{...rest}
|
|
68
|
+
/>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { cn } from '@open-mercato/shared/lib/utils'
|
|
5
|
+
import { Input, type InputProps } from './input'
|
|
6
|
+
import { Tag } from './tag'
|
|
7
|
+
|
|
8
|
+
export type TagInputProps = {
|
|
9
|
+
value?: string[]
|
|
10
|
+
onChange?: (value: string[]) => void
|
|
11
|
+
placeholder?: string
|
|
12
|
+
size?: 'sm' | 'default' | 'lg'
|
|
13
|
+
disabled?: boolean
|
|
14
|
+
maxTags?: number
|
|
15
|
+
validate?: (tag: string) => boolean | string
|
|
16
|
+
separator?: string | RegExp
|
|
17
|
+
allowDuplicates?: boolean
|
|
18
|
+
className?: string
|
|
19
|
+
id?: string
|
|
20
|
+
name?: string
|
|
21
|
+
'aria-label'?: string
|
|
22
|
+
'aria-invalid'?: boolean
|
|
23
|
+
/**
|
|
24
|
+
* Optional right-side icon for the input row (e.g. info-circle, search). Mirrors
|
|
25
|
+
* the Figma `Tag Input` Filled state with `Right Icon=true`.
|
|
26
|
+
*/
|
|
27
|
+
rightIcon?: InputProps['rightIcon']
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function escapeRegExp(input: string): string {
|
|
31
|
+
return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function buildSplitter(separator: string | RegExp): RegExp {
|
|
35
|
+
if (separator instanceof RegExp) {
|
|
36
|
+
return separator.flags.includes('g')
|
|
37
|
+
? separator
|
|
38
|
+
: new RegExp(separator.source, `${separator.flags}g`)
|
|
39
|
+
}
|
|
40
|
+
return new RegExp(escapeRegExp(separator), 'g')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const TagInput = React.forwardRef<HTMLInputElement, TagInputProps>(
|
|
44
|
+
(
|
|
45
|
+
{
|
|
46
|
+
value: valueProp,
|
|
47
|
+
onChange,
|
|
48
|
+
placeholder,
|
|
49
|
+
size = 'default',
|
|
50
|
+
disabled = false,
|
|
51
|
+
maxTags,
|
|
52
|
+
validate,
|
|
53
|
+
separator = ',',
|
|
54
|
+
allowDuplicates = false,
|
|
55
|
+
className,
|
|
56
|
+
id,
|
|
57
|
+
name,
|
|
58
|
+
'aria-label': ariaLabel,
|
|
59
|
+
'aria-invalid': ariaInvalid,
|
|
60
|
+
rightIcon,
|
|
61
|
+
},
|
|
62
|
+
ref,
|
|
63
|
+
) => {
|
|
64
|
+
const [internalValue, setInternalValue] = React.useState<string[]>([])
|
|
65
|
+
const [inputText, setInputText] = React.useState('')
|
|
66
|
+
const [error, setError] = React.useState<string | null>(null)
|
|
67
|
+
|
|
68
|
+
const isControlled = valueProp !== undefined
|
|
69
|
+
const value = isControlled ? (valueProp as string[]) : internalValue
|
|
70
|
+
|
|
71
|
+
const commitValue = React.useCallback(
|
|
72
|
+
(next: string[]) => {
|
|
73
|
+
if (!isControlled) setInternalValue(next)
|
|
74
|
+
onChange?.(next)
|
|
75
|
+
},
|
|
76
|
+
[isControlled, onChange],
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
const appendToList = React.useCallback(
|
|
80
|
+
(current: string[], rawTag: string): { next: string[]; errorText: string | null } | null => {
|
|
81
|
+
const tag = rawTag.trim()
|
|
82
|
+
if (!tag) return null
|
|
83
|
+
if (!allowDuplicates && current.includes(tag)) return null
|
|
84
|
+
if (typeof maxTags === 'number' && current.length >= maxTags) return null
|
|
85
|
+
if (validate) {
|
|
86
|
+
const result = validate(tag)
|
|
87
|
+
if (result === false) return null
|
|
88
|
+
if (typeof result === 'string') return { next: current, errorText: result }
|
|
89
|
+
}
|
|
90
|
+
return { next: [...current, tag], errorText: null }
|
|
91
|
+
},
|
|
92
|
+
[allowDuplicates, maxTags, validate],
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
const tryAddTag = React.useCallback(
|
|
96
|
+
(rawTag: string): boolean => {
|
|
97
|
+
const outcome = appendToList(value, rawTag)
|
|
98
|
+
if (!outcome) return false
|
|
99
|
+
if (outcome.errorText !== null) {
|
|
100
|
+
setError(outcome.errorText)
|
|
101
|
+
return false
|
|
102
|
+
}
|
|
103
|
+
setError(null)
|
|
104
|
+
if (outcome.next !== value) commitValue(outcome.next)
|
|
105
|
+
return outcome.next !== value
|
|
106
|
+
},
|
|
107
|
+
[appendToList, commitValue, value],
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
const tryAddManyTags = React.useCallback(
|
|
111
|
+
(rawTags: string[]) => {
|
|
112
|
+
let acc = value
|
|
113
|
+
let pendingError: string | null = null
|
|
114
|
+
for (const raw of rawTags) {
|
|
115
|
+
const outcome = appendToList(acc, raw)
|
|
116
|
+
if (!outcome) continue
|
|
117
|
+
if (outcome.errorText !== null) {
|
|
118
|
+
pendingError = outcome.errorText
|
|
119
|
+
continue
|
|
120
|
+
}
|
|
121
|
+
acc = outcome.next
|
|
122
|
+
}
|
|
123
|
+
if (acc !== value) commitValue(acc)
|
|
124
|
+
if (pendingError !== null) setError(pendingError)
|
|
125
|
+
else if (acc !== value) setError(null)
|
|
126
|
+
},
|
|
127
|
+
[appendToList, commitValue, value],
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
const removeTagAt = React.useCallback(
|
|
131
|
+
(index: number) => {
|
|
132
|
+
if (index < 0 || index >= value.length) return
|
|
133
|
+
const next = value.slice(0, index).concat(value.slice(index + 1))
|
|
134
|
+
commitValue(next)
|
|
135
|
+
setError(null)
|
|
136
|
+
},
|
|
137
|
+
[commitValue, value],
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
const splitter = React.useMemo(() => buildSplitter(separator), [separator])
|
|
141
|
+
|
|
142
|
+
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
143
|
+
const text = event.target.value
|
|
144
|
+
splitter.lastIndex = 0
|
|
145
|
+
if (splitter.test(text)) {
|
|
146
|
+
splitter.lastIndex = 0
|
|
147
|
+
const parts = text.split(splitter)
|
|
148
|
+
const trailing = parts.pop() ?? ''
|
|
149
|
+
tryAddManyTags(parts)
|
|
150
|
+
setInputText(trailing)
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
setInputText(text)
|
|
154
|
+
if (error) setError(null)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
|
158
|
+
if (event.key === 'Enter') {
|
|
159
|
+
event.preventDefault()
|
|
160
|
+
tryAddTag(inputText)
|
|
161
|
+
setInputText('')
|
|
162
|
+
return
|
|
163
|
+
}
|
|
164
|
+
if (event.key === 'Backspace' && inputText.length === 0 && value.length > 0) {
|
|
165
|
+
event.preventDefault()
|
|
166
|
+
removeTagAt(value.length - 1)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const limitReached = typeof maxTags === 'number' && value.length >= maxTags
|
|
171
|
+
|
|
172
|
+
return (
|
|
173
|
+
<div className={cn('flex w-full flex-col gap-1', className)} data-slot="tag-input">
|
|
174
|
+
<Input
|
|
175
|
+
ref={ref}
|
|
176
|
+
type="text"
|
|
177
|
+
size={size}
|
|
178
|
+
id={id}
|
|
179
|
+
name={name}
|
|
180
|
+
value={inputText}
|
|
181
|
+
onChange={handleChange}
|
|
182
|
+
onKeyDown={handleKeyDown}
|
|
183
|
+
placeholder={placeholder}
|
|
184
|
+
disabled={disabled || limitReached}
|
|
185
|
+
aria-label={ariaLabel}
|
|
186
|
+
aria-invalid={ariaInvalid || Boolean(error) || undefined}
|
|
187
|
+
rightIcon={rightIcon}
|
|
188
|
+
data-slot="tag-input-field"
|
|
189
|
+
/>
|
|
190
|
+
{value.length > 0 ? (
|
|
191
|
+
<div
|
|
192
|
+
className="flex flex-wrap items-center gap-2 pt-1"
|
|
193
|
+
data-slot="tag-input-chips"
|
|
194
|
+
>
|
|
195
|
+
{value.map((tag, index) => (
|
|
196
|
+
<Tag
|
|
197
|
+
key={`${tag}-${index}`}
|
|
198
|
+
variant="default"
|
|
199
|
+
shape="square"
|
|
200
|
+
disabled={disabled}
|
|
201
|
+
onRemove={() => removeTagAt(index)}
|
|
202
|
+
removeAriaLabel={`Remove ${tag}`}
|
|
203
|
+
>
|
|
204
|
+
{tag}
|
|
205
|
+
</Tag>
|
|
206
|
+
))}
|
|
207
|
+
</div>
|
|
208
|
+
) : null}
|
|
209
|
+
{error ? (
|
|
210
|
+
<p
|
|
211
|
+
className="text-xs text-status-error-text"
|
|
212
|
+
role="alert"
|
|
213
|
+
data-slot="tag-input-error"
|
|
214
|
+
>
|
|
215
|
+
{error}
|
|
216
|
+
</p>
|
|
217
|
+
) : null}
|
|
218
|
+
</div>
|
|
219
|
+
)
|
|
220
|
+
},
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
TagInput.displayName = 'TagInput'
|
package/src/primitives/tag.tsx
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import * as React from 'react'
|
|
2
|
-
import {
|
|
2
|
+
import { X } from 'lucide-react'
|
|
3
|
+
import { cva, type VariantProps } from 'class-variance-authority'
|
|
3
4
|
import { cn } from '@open-mercato/shared/lib/utils'
|
|
4
5
|
|
|
5
6
|
const tagVariants = cva(
|
|
6
|
-
'inline-flex items-center gap-1.5
|
|
7
|
+
'inline-flex items-center gap-1.5 border text-xs font-medium',
|
|
7
8
|
{
|
|
8
9
|
variants: {
|
|
9
10
|
variant: {
|
|
@@ -15,8 +16,12 @@ const tagVariants = cva(
|
|
|
15
16
|
neutral: 'border-status-neutral-border bg-status-neutral-bg text-status-neutral-text',
|
|
16
17
|
brand: 'border-brand-violet/30 bg-brand-violet/10 text-brand-violet',
|
|
17
18
|
},
|
|
19
|
+
shape: {
|
|
20
|
+
pill: 'rounded-full px-2.5 py-0.5',
|
|
21
|
+
square: 'rounded-md px-2 py-1',
|
|
22
|
+
},
|
|
18
23
|
},
|
|
19
|
-
defaultVariants: { variant: 'default' },
|
|
24
|
+
defaultVariants: { variant: 'default', shape: 'pill' },
|
|
20
25
|
}
|
|
21
26
|
)
|
|
22
27
|
|
|
@@ -32,21 +37,75 @@ const dotColorMap: Record<TagVariant, string> = {
|
|
|
32
37
|
|
|
33
38
|
export type TagVariant = 'default' | 'success' | 'warning' | 'error' | 'info' | 'neutral' | 'brand'
|
|
34
39
|
|
|
35
|
-
export type
|
|
36
|
-
variant?: TagVariant
|
|
37
|
-
dot?: boolean
|
|
38
|
-
}
|
|
40
|
+
export type TagShape = 'pill' | 'square'
|
|
39
41
|
|
|
40
|
-
export
|
|
42
|
+
export type TagProps = React.HTMLAttributes<HTMLSpanElement> &
|
|
43
|
+
Pick<VariantProps<typeof tagVariants>, 'shape'> & {
|
|
44
|
+
variant?: TagVariant
|
|
45
|
+
dot?: boolean
|
|
46
|
+
/**
|
|
47
|
+
* Render an inline close ("×") button after the label. When provided, the button
|
|
48
|
+
* fires `onRemove` on click and stops propagation so a click on the chip body is
|
|
49
|
+
* not triggered. Matches Figma "Dismiss Icon=On" Tag variants.
|
|
50
|
+
*/
|
|
51
|
+
onRemove?: () => void
|
|
52
|
+
/** Accessible label for the close button. Defaults to `Remove`. */
|
|
53
|
+
removeAriaLabel?: string
|
|
54
|
+
/** Visually mute the chip and disable its close button. */
|
|
55
|
+
disabled?: boolean
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function Tag({
|
|
59
|
+
className,
|
|
60
|
+
variant = 'default',
|
|
61
|
+
shape = 'pill',
|
|
62
|
+
dot = false,
|
|
63
|
+
onRemove,
|
|
64
|
+
removeAriaLabel = 'Remove',
|
|
65
|
+
disabled = false,
|
|
66
|
+
children,
|
|
67
|
+
...props
|
|
68
|
+
}: TagProps) {
|
|
41
69
|
return (
|
|
42
|
-
<span
|
|
70
|
+
<span
|
|
71
|
+
className={cn(
|
|
72
|
+
tagVariants({ variant, shape }),
|
|
73
|
+
disabled && 'opacity-60',
|
|
74
|
+
className,
|
|
75
|
+
)}
|
|
76
|
+
aria-disabled={disabled || undefined}
|
|
77
|
+
data-slot="tag"
|
|
78
|
+
data-shape={shape}
|
|
79
|
+
{...props}
|
|
80
|
+
>
|
|
43
81
|
{dot && (
|
|
44
82
|
<span
|
|
45
83
|
className={cn('inline-block size-1.5 rounded-full shrink-0', dotColorMap[variant])}
|
|
46
84
|
aria-hidden="true"
|
|
47
85
|
/>
|
|
48
86
|
)}
|
|
49
|
-
{children}
|
|
87
|
+
<span className="min-w-0">{children}</span>
|
|
88
|
+
{onRemove ? (
|
|
89
|
+
<button
|
|
90
|
+
type="button"
|
|
91
|
+
disabled={disabled}
|
|
92
|
+
onClick={(event) => {
|
|
93
|
+
event.stopPropagation()
|
|
94
|
+
onRemove()
|
|
95
|
+
}}
|
|
96
|
+
className={cn(
|
|
97
|
+
'inline-flex shrink-0 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus:outline-none focus:ring-2 focus:ring-ring/40 disabled:pointer-events-none disabled:opacity-60',
|
|
98
|
+
shape === 'pill' ? 'size-3' : 'size-4',
|
|
99
|
+
)}
|
|
100
|
+
aria-label={removeAriaLabel}
|
|
101
|
+
data-slot="tag-remove"
|
|
102
|
+
>
|
|
103
|
+
<X
|
|
104
|
+
className={shape === 'pill' ? 'size-2.5' : 'size-3.5'}
|
|
105
|
+
aria-hidden="true"
|
|
106
|
+
/>
|
|
107
|
+
</button>
|
|
108
|
+
) : null}
|
|
50
109
|
</span>
|
|
51
110
|
)
|
|
52
111
|
}
|