@open-mercato/ui 0.6.1-develop.3256.1.fe3dec2464 → 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.
Files changed (137) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/backend/CrudForm.js +46 -129
  3. package/dist/backend/CrudForm.js.map +3 -3
  4. package/dist/backend/DataTable.js +10 -3
  5. package/dist/backend/DataTable.js.map +2 -2
  6. package/dist/backend/EmptyState.js +1 -47
  7. package/dist/backend/EmptyState.js.map +2 -2
  8. package/dist/backend/FilterOverlay.js +31 -26
  9. package/dist/backend/FilterOverlay.js.map +2 -2
  10. package/dist/backend/FlashMessages.js +23 -25
  11. package/dist/backend/FlashMessages.js.map +2 -2
  12. package/dist/backend/JsonBuilder.js +4 -2
  13. package/dist/backend/JsonBuilder.js.map +2 -2
  14. package/dist/backend/LogList.js +49 -0
  15. package/dist/backend/LogList.js.map +7 -0
  16. package/dist/backend/crud/CollapsibleGroup.js +13 -3
  17. package/dist/backend/crud/CollapsibleGroup.js.map +2 -2
  18. package/dist/backend/detail/InlineEditors.js +80 -32
  19. package/dist/backend/detail/InlineEditors.js.map +2 -2
  20. package/dist/backend/inputs/ComboboxInput.js +60 -6
  21. package/dist/backend/inputs/ComboboxInput.js.map +2 -2
  22. package/dist/backend/inputs/DatePicker.js +6 -142
  23. package/dist/backend/inputs/DatePicker.js.map +2 -2
  24. package/dist/backend/inputs/DateTimePicker.js +6 -182
  25. package/dist/backend/inputs/DateTimePicker.js.map +2 -2
  26. package/dist/backend/inputs/TagsInput.js +16 -19
  27. package/dist/backend/inputs/TagsInput.js.map +2 -2
  28. package/dist/backend/inputs/TimeInput.js +1 -1
  29. package/dist/backend/inputs/TimeInput.js.map +2 -2
  30. package/dist/backend/inputs/TimePicker.js +83 -72
  31. package/dist/backend/inputs/TimePicker.js.map +2 -2
  32. package/dist/backend/utils/customFieldForms.js +2 -2
  33. package/dist/backend/utils/customFieldForms.js.map +1 -1
  34. package/dist/portal/components/PortalEmptyState.js +13 -9
  35. package/dist/portal/components/PortalEmptyState.js.map +2 -2
  36. package/dist/primitives/accordion.js +137 -0
  37. package/dist/primitives/accordion.js.map +7 -0
  38. package/dist/primitives/alert.js +180 -12
  39. package/dist/primitives/alert.js.map +2 -2
  40. package/dist/primitives/calendar.js +122 -32
  41. package/dist/primitives/calendar.js.map +2 -2
  42. package/dist/primitives/compact-select.js +47 -0
  43. package/dist/primitives/compact-select.js.map +7 -0
  44. package/dist/primitives/counter-input.js +174 -0
  45. package/dist/primitives/counter-input.js.map +7 -0
  46. package/dist/primitives/date-picker-helpers.js +79 -0
  47. package/dist/primitives/date-picker-helpers.js.map +7 -0
  48. package/dist/primitives/date-picker.js +216 -0
  49. package/dist/primitives/date-picker.js.map +7 -0
  50. package/dist/primitives/date-range-picker.js +239 -0
  51. package/dist/primitives/date-range-picker.js.map +7 -0
  52. package/dist/primitives/digit-input.js +157 -0
  53. package/dist/primitives/digit-input.js.map +7 -0
  54. package/dist/primitives/empty-state.js +93 -0
  55. package/dist/primitives/empty-state.js.map +7 -0
  56. package/dist/primitives/inline-input.js +29 -0
  57. package/dist/primitives/inline-input.js.map +7 -0
  58. package/dist/primitives/inline-select.js +41 -0
  59. package/dist/primitives/inline-select.js.map +7 -0
  60. package/dist/primitives/notification-stack.js +116 -0
  61. package/dist/primitives/notification-stack.js.map +7 -0
  62. package/dist/primitives/notification.js +62 -0
  63. package/dist/primitives/notification.js.map +7 -0
  64. package/dist/primitives/rich-editor.js +1230 -0
  65. package/dist/primitives/rich-editor.js.map +7 -0
  66. package/dist/primitives/select.js +1 -0
  67. package/dist/primitives/select.js.map +2 -2
  68. package/dist/primitives/skeleton.js +55 -0
  69. package/dist/primitives/skeleton.js.map +7 -0
  70. package/dist/primitives/tag-input.js +186 -0
  71. package/dist/primitives/tag-input.js.map +7 -0
  72. package/dist/primitives/tag.js +66 -13
  73. package/dist/primitives/tag.js.map +2 -2
  74. package/dist/primitives/time-picker.js +793 -0
  75. package/dist/primitives/time-picker.js.map +7 -0
  76. package/package.json +4 -3
  77. package/src/backend/CrudForm.tsx +61 -113
  78. package/src/backend/DataTable.tsx +17 -5
  79. package/src/backend/EmptyState.tsx +13 -63
  80. package/src/backend/FilterOverlay.tsx +38 -22
  81. package/src/backend/FlashMessages.tsx +23 -24
  82. package/src/backend/JsonBuilder.tsx +4 -2
  83. package/src/backend/LogList.tsx +109 -0
  84. package/src/backend/__tests__/CrudForm.datetime.test.tsx +12 -7
  85. package/src/backend/__tests__/FilterOverlay.dateRange.test.tsx +89 -0
  86. package/src/backend/__tests__/LogList.test.tsx +163 -0
  87. package/src/backend/crud/CollapsibleGroup.tsx +17 -3
  88. package/src/backend/detail/InlineEditors.tsx +78 -25
  89. package/src/backend/detail/__tests__/InlineEditors.test.tsx +81 -1
  90. package/src/backend/inputs/ComboboxInput.tsx +81 -9
  91. package/src/backend/inputs/DatePicker.tsx +28 -155
  92. package/src/backend/inputs/DateTimePicker.tsx +25 -193
  93. package/src/backend/inputs/TagsInput.tsx +10 -13
  94. package/src/backend/inputs/TimeInput.tsx +2 -1
  95. package/src/backend/inputs/TimePicker.tsx +99 -70
  96. package/src/backend/inputs/__tests__/ComboboxInput.test.tsx +133 -0
  97. package/src/backend/inputs/__tests__/TimePicker.test.tsx +2 -2
  98. package/src/backend/utils/__tests__/richTextSanitizer.test.ts +5 -2
  99. package/src/backend/utils/customFieldForms.ts +2 -2
  100. package/src/portal/components/PortalEmptyState.tsx +27 -22
  101. package/src/primitives/__tests__/accordion.test.tsx +197 -0
  102. package/src/primitives/__tests__/alert.test.tsx +204 -0
  103. package/src/primitives/__tests__/compact-select.test.tsx +98 -0
  104. package/src/primitives/__tests__/counter-input.test.tsx +165 -0
  105. package/src/primitives/__tests__/date-picker.test.tsx +221 -0
  106. package/src/primitives/__tests__/date-range-picker.test.tsx +173 -0
  107. package/src/primitives/__tests__/digit-input.test.tsx +168 -0
  108. package/src/primitives/__tests__/empty-state.test.tsx +154 -0
  109. package/src/primitives/__tests__/inline-input.test.tsx +100 -0
  110. package/src/primitives/__tests__/inline-select.test.tsx +103 -0
  111. package/src/primitives/__tests__/notification-stack.test.tsx +174 -0
  112. package/src/primitives/__tests__/notification.test.tsx +119 -0
  113. package/src/primitives/__tests__/rich-editor.test.tsx +411 -0
  114. package/src/primitives/__tests__/skeleton.test.tsx +76 -0
  115. package/src/primitives/__tests__/tag-input.test.tsx +198 -0
  116. package/src/primitives/__tests__/tag.test.tsx +123 -0
  117. package/src/primitives/__tests__/time-picker.test.tsx +392 -0
  118. package/src/primitives/accordion.tsx +225 -0
  119. package/src/primitives/alert.tsx +234 -24
  120. package/src/primitives/calendar.tsx +146 -38
  121. package/src/primitives/compact-select.tsx +59 -0
  122. package/src/primitives/counter-input.tsx +211 -0
  123. package/src/primitives/date-picker-helpers.ts +96 -0
  124. package/src/primitives/date-picker.tsx +290 -0
  125. package/src/primitives/date-range-picker.tsx +281 -0
  126. package/src/primitives/digit-input.tsx +208 -0
  127. package/src/primitives/empty-state.tsx +140 -0
  128. package/src/primitives/inline-input.tsx +55 -0
  129. package/src/primitives/inline-select.tsx +74 -0
  130. package/src/primitives/notification-stack.tsx +185 -0
  131. package/src/primitives/notification.tsx +136 -0
  132. package/src/primitives/rich-editor.tsx +1578 -0
  133. package/src/primitives/select.tsx +1 -0
  134. package/src/primitives/skeleton.tsx +70 -0
  135. package/src/primitives/tag-input.tsx +223 -0
  136. package/src/primitives/tag.tsx +69 -10
  137. package/src/primitives/time-picker.tsx +1059 -0
@@ -11,6 +11,7 @@ const selectTriggerVariants = cva(
11
11
  {
12
12
  variants: {
13
13
  size: {
14
+ xs: 'h-7 px-2 text-xs',
14
15
  sm: 'h-8 px-2.5 text-xs',
15
16
  default: 'h-9 px-3 text-sm',
16
17
  lg: 'h-10 px-3 text-sm',
@@ -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'
@@ -1,9 +1,10 @@
1
1
  import * as React from 'react'
2
- import { cva } from 'class-variance-authority'
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 rounded-full border px-2.5 py-0.5 text-xs font-medium',
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 TagProps = React.HTMLAttributes<HTMLSpanElement> & {
36
- variant?: TagVariant
37
- dot?: boolean
38
- }
40
+ export type TagShape = 'pill' | 'square'
39
41
 
40
- export function Tag({ className, variant = 'default', dot = false, children, ...props }: TagProps) {
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 className={cn(tagVariants({ variant }), className)} {...props}>
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
  }