@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
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  import * as React from 'react'
6
- import { screen } from '@testing-library/react'
6
+ import { act, fireEvent, screen } from '@testing-library/react'
7
7
  import { renderWithProviders } from '@open-mercato/shared/lib/testing/renderWithProviders'
8
8
  import { InlineTextEditor, resolveSafeInlineUrlHref } from '../InlineEditors'
9
9
 
@@ -42,6 +42,86 @@ describe('InlineTextEditor URL display', () => {
42
42
  })
43
43
  })
44
44
 
45
+ describe('InlineTextEditor date types — DS unification (no native date input)', () => {
46
+ function activateEditor() {
47
+ // Pencil/edit toggle is the trailing icon-only button on the InlineTextEditor.
48
+ // It has no accessible text label, so we grab the last data-slot="button".
49
+ const buttons = document.querySelectorAll('[data-slot="button"]')
50
+ expect(buttons.length).toBeGreaterThan(0)
51
+ const editToggle = buttons[buttons.length - 1] as HTMLButtonElement
52
+ act(() => {
53
+ fireEvent.click(editToggle)
54
+ })
55
+ }
56
+
57
+ it('inputType="date" renders the DatePicker primitive trigger when editing', () => {
58
+ renderWithProviders(
59
+ <InlineTextEditor
60
+ label="Date"
61
+ value="2026-05-09"
62
+ emptyLabel="No date"
63
+ type="date"
64
+ inputType="date"
65
+ onSave={jest.fn()}
66
+ />,
67
+ { dict: {} },
68
+ )
69
+ activateEditor()
70
+ expect(document.querySelector('[data-slot="date-picker-trigger"]')).toBeInTheDocument()
71
+ expect(document.querySelector('input[type="date"]')).toBeNull()
72
+ })
73
+
74
+ it('inputType="datetime-local" renders the DatePicker primitive (with withTime) when editing', () => {
75
+ renderWithProviders(
76
+ <InlineTextEditor
77
+ label="When"
78
+ value="2026-05-09T10:30"
79
+ emptyLabel="No value"
80
+ type="text"
81
+ inputType="datetime-local"
82
+ onSave={jest.fn()}
83
+ />,
84
+ { dict: {} },
85
+ )
86
+ activateEditor()
87
+ expect(document.querySelector('[data-slot="date-picker-trigger"]')).toBeInTheDocument()
88
+ expect(document.querySelector('input[type="datetime-local"]')).toBeNull()
89
+ })
90
+
91
+ it('non-date inputType still renders a raw input (regression: text editing path unchanged)', () => {
92
+ renderWithProviders(
93
+ <InlineTextEditor
94
+ label="Title"
95
+ value="Hello"
96
+ emptyLabel="No title"
97
+ type="text"
98
+ onSave={jest.fn()}
99
+ />,
100
+ { dict: {} },
101
+ )
102
+ activateEditor()
103
+ expect(document.querySelector('input[type="text"]')).toBeInTheDocument()
104
+ expect(document.querySelector('[data-slot="date-picker-trigger"]')).toBeNull()
105
+ })
106
+
107
+ it('date type hides the form Save/Cancel buttons (DatePicker provides Apply/Cancel in popover)', () => {
108
+ renderWithProviders(
109
+ <InlineTextEditor
110
+ label="Date"
111
+ value="2026-05-09"
112
+ emptyLabel="No date"
113
+ type="date"
114
+ inputType="date"
115
+ onSave={jest.fn()}
116
+ />,
117
+ { dict: {} },
118
+ )
119
+ activateEditor()
120
+ // Form Save (submit) button should not be rendered for date types
121
+ expect(screen.queryByRole('button', { name: /Save|Zapisz|⌘/ })).not.toBeInTheDocument()
122
+ })
123
+ })
124
+
45
125
  describe('resolveSafeInlineUrlHref', () => {
46
126
  it.each(['http://example.com', 'https://example.com', 'mailto:user@example.com', 'tel:+48123456789'])(
47
127
  'allows %s',
@@ -14,8 +14,15 @@ export type ComboboxInputProps = {
14
14
  onChange: (next: string) => void
15
15
  placeholder?: string
16
16
  suggestions?: Array<string | ComboboxOption>
17
+ // Options to hydrate the option map up front (typically the linked entity's
18
+ // display fields, already present in a record-detail payload). Merged with
19
+ // `suggestions` so a pre-selected value renders its label without interaction.
20
+ seedOptions?: ComboboxOption[]
17
21
  loadSuggestions?: (query?: string) => Promise<Array<string | ComboboxOption>>
18
- resolveLabel?: (value: string) => string
22
+ // Eagerly resolve a pre-selected `value` to a human label when it is not
23
+ // covered by `suggestions`/`seedOptions`/`loadSuggestions` results. Runs once
24
+ // per value, before any user interaction. May be sync or async.
25
+ resolveLabel?: (value: string) => string | Promise<string>
19
26
  resolveDescription?: (value: string) => string | null | undefined
20
27
  autoFocus?: boolean
21
28
  disabled?: boolean
@@ -47,6 +54,7 @@ export function ComboboxInput({
47
54
  onChange,
48
55
  placeholder,
49
56
  suggestions,
57
+ seedOptions,
50
58
  loadSuggestions,
51
59
  resolveLabel,
52
60
  resolveDescription,
@@ -56,6 +64,7 @@ export function ComboboxInput({
56
64
  }: ComboboxInputProps) {
57
65
  const [input, setInput] = React.useState('')
58
66
  const [asyncOptions, setAsyncOptions] = React.useState<ComboboxOption[]>([])
67
+ const [resolvedOptions, setResolvedOptions] = React.useState<ComboboxOption[]>([])
59
68
  const [loading, setLoading] = React.useState(false)
60
69
  const [touched, setTouched] = React.useState(false)
61
70
  const [showSuggestions, setShowSuggestions] = React.useState(false)
@@ -63,29 +72,46 @@ export function ComboboxInput({
63
72
  const inputRef = React.useRef<HTMLInputElement>(null)
64
73
  const suppressOpenOnFocusRef = React.useRef(Boolean(autoFocus && !disabled))
65
74
 
66
- const staticOptions = React.useMemo(() => normalizeOptions(suggestions), [suggestions])
75
+ const staticOptions = React.useMemo(
76
+ () => normalizeOptions([...(seedOptions ?? []), ...(suggestions ?? [])]),
77
+ [seedOptions, suggestions]
78
+ )
79
+
80
+ // Options whose label is genuinely known (not a self-mapping `value === label`
81
+ // placeholder). Used to decide whether eager resolution still needs to run.
82
+ const knownLabelValues = React.useMemo(() => {
83
+ const set = new Set<string>()
84
+ for (const option of [...staticOptions, ...asyncOptions, ...resolvedOptions]) {
85
+ if (option.label && option.label !== option.value) set.add(option.value)
86
+ }
87
+ return set
88
+ }, [staticOptions, asyncOptions, resolvedOptions])
67
89
 
68
90
  const optionMap = React.useMemo(() => {
69
91
  const map = new Map<string, ComboboxOption>()
70
92
  const register = (option: ComboboxOption) => {
71
- if (!map.has(option.value)) {
93
+ const existing = map.get(option.value)
94
+ // Prefer an entry that carries a real label over a self-mapping placeholder.
95
+ if (!existing || (existing.label === existing.value && option.label !== option.value)) {
72
96
  map.set(option.value, option)
73
97
  }
74
98
  }
75
99
  staticOptions.forEach(register)
76
100
  asyncOptions.forEach(register)
101
+ resolvedOptions.forEach(register)
77
102
  if (value) {
78
103
  const existing = map.get(value)
79
104
  if (!existing) {
105
+ const sync = typeof resolveLabel === 'function' ? resolveLabel(value) : undefined
80
106
  map.set(value, {
81
107
  value,
82
- label: resolveLabel?.(value) ?? value,
108
+ label: typeof sync === 'string' && sync ? sync : value,
83
109
  description: resolveDescription?.(value) ?? null,
84
110
  })
85
111
  }
86
112
  }
87
113
  return map
88
- }, [asyncOptions, resolveDescription, resolveLabel, staticOptions, value])
114
+ }, [asyncOptions, resolvedOptions, resolveDescription, resolveLabel, staticOptions, value])
89
115
 
90
116
  const availableOptions = React.useMemo(() => {
91
117
  return Array.from(optionMap.values())
@@ -122,7 +148,47 @@ export function ComboboxInput({
122
148
  }
123
149
  }, [disabled, input, loadSuggestions, touched])
124
150
 
125
- // Sync input with value when value changes externally and input is not focused
151
+ // Eagerly resolve a pre-selected value to its label without requiring the user
152
+ // to focus the field. Runs once per value when it is not already covered.
153
+ const eagerResolveLabel = typeof resolveLabel === 'function' ? resolveLabel : undefined
154
+ React.useEffect(() => {
155
+ if (!value || disabled) return
156
+ if (knownLabelValues.has(value)) return
157
+ let cancelled = false
158
+ const apply = (label?: string | null, description?: string | null) => {
159
+ const clean = typeof label === 'string' ? label.trim() : ''
160
+ if (cancelled || !clean || clean === value) return
161
+ setResolvedOptions((prev) => {
162
+ if (prev.some((option) => option.value === value && option.label === clean)) return prev
163
+ return [...prev.filter((option) => option.value !== value), { value, label: clean, description: description ?? null }]
164
+ })
165
+ }
166
+ if (eagerResolveLabel) {
167
+ Promise.resolve()
168
+ .then(() => eagerResolveLabel(value))
169
+ .then((label) => apply(label, resolveDescription?.(value)))
170
+ .catch(() => {})
171
+ return () => { cancelled = true }
172
+ }
173
+ // Fallback: pull the first page of async suggestions so a remount that lost
174
+ // its option cache can still recover the label without user interaction.
175
+ if (loadSuggestions) {
176
+ setLoading(true)
177
+ Promise.resolve()
178
+ .then(() => loadSuggestions())
179
+ .then((items) => {
180
+ if (cancelled) return
181
+ const normalized = normalizeOptions(items)
182
+ setAsyncOptions(normalized)
183
+ })
184
+ .catch(() => {})
185
+ .finally(() => { if (!cancelled) setLoading(false) })
186
+ }
187
+ return () => { cancelled = true }
188
+ // eslint-disable-next-line react-hooks/exhaustive-deps
189
+ }, [value, disabled, knownLabelValues, eagerResolveLabel, loadSuggestions])
190
+
191
+ // Sync input with value when value changes externally and input is not focused.
126
192
  React.useEffect(() => {
127
193
  if (document.activeElement !== inputRef.current) {
128
194
  const option = optionMap.get(value)
@@ -165,10 +231,16 @@ export function ComboboxInput({
165
231
  return
166
232
  }
167
233
  if (!allowCustomValues) {
168
- // Revert to current value if custom values not allowed
169
- const currentOption = optionMap.get(value)
170
- setInput(currentOption?.label ?? value ?? '')
234
+ // Revert to the current value's label — but only if we actually know it.
235
+ // Baking the raw value back in while eager resolution is still pending
236
+ // would freeze a placeholder (e.g. a UUID) into the visible input.
171
237
  setShowSuggestions(false)
238
+ const currentOption = optionMap.get(value)
239
+ if (currentOption && currentOption.label !== currentOption.value) {
240
+ setInput(currentOption.label)
241
+ } else if (!value) {
242
+ setInput('')
243
+ }
172
244
  return
173
245
  }
174
246
  selectValue(raw)
@@ -1,162 +1,35 @@
1
+ /**
2
+ * @deprecated Import from `@open-mercato/ui/primitives/date-picker` instead.
3
+ *
4
+ * This shim preserves the legacy import path
5
+ * `@open-mercato/ui/backend/inputs/DatePicker` for existing consumers
6
+ * (CrudForm, example pages) while the DatePicker is promoted to a
7
+ * primitive in DS Foundation v3.
8
+ *
9
+ * Default footer is the Figma-aligned `'apply-cancel'` mode (per user
10
+ * directive 2026-05-09 — "zmień globalnie"). Consumers that explicitly
11
+ * need the legacy `Today` / `Clear` footer can pass `footer="today-clear"`.
12
+ *
13
+ * Migration: replace
14
+ * import { DatePicker } from '@open-mercato/ui/backend/inputs/DatePicker'
15
+ * with
16
+ * import { DatePicker } from '@open-mercato/ui/primitives/date-picker'
17
+ */
18
+
1
19
  "use client"
2
20
 
3
21
  import * as React from 'react'
4
- import { format } from 'date-fns'
5
- import type { Locale } from 'date-fns'
6
- import { CalendarIcon } from 'lucide-react'
7
- import { cn } from '@open-mercato/shared/lib/utils'
8
- import { useT } from '@open-mercato/shared/lib/i18n/context'
9
- import { Popover, PopoverContent, PopoverTrigger } from '../../primitives/popover'
10
- import { Calendar } from '../../primitives/calendar'
11
-
12
- export type DatePickerProps = {
13
- value?: Date | null
14
- onChange: (date: Date | null) => void
15
- placeholder?: string
16
- disabled?: boolean
17
- readOnly?: boolean
18
- className?: string
19
- locale?: Locale
20
- displayFormat?: string
21
- showTodayButton?: boolean
22
- showClearButton?: boolean
23
- closeOnSelect?: boolean
24
- minDate?: Date
25
- maxDate?: Date
26
- }
22
+ import {
23
+ DatePicker as DatePickerPrimitive,
24
+ type DatePickerProps as DatePickerPrimitivePropsBase,
25
+ type DatePickerFooter,
26
+ } from '../../primitives/date-picker'
27
27
 
28
- const DAY_FIRST_LOCALE_CODES = new Set([
29
- 'pl', 'de', 'fr', 'es', 'it', 'pt', 'nl', 'ru', 'cs', 'sk', 'hu', 'ro',
30
- ])
28
+ export type DatePickerProps = DatePickerPrimitivePropsBase
31
29
 
32
- function deriveDisplayFormat(locale?: Locale): string {
33
- if (!locale) return 'MMM d, yyyy'
34
- const code = locale.code?.split('-')[0]?.toLowerCase() ?? ''
35
- return DAY_FIRST_LOCALE_CODES.has(code) ? 'd MMM yyyy' : 'MMM d, yyyy'
30
+ /** @deprecated Use `DatePicker` from `@open-mercato/ui/primitives/date-picker`. */
31
+ export function DatePicker(props: DatePickerProps) {
32
+ return <DatePickerPrimitive {...props} />
36
33
  }
37
34
 
38
- export function DatePicker({
39
- value,
40
- onChange,
41
- placeholder,
42
- disabled = false,
43
- readOnly = false,
44
- className,
45
- locale,
46
- displayFormat,
47
- showTodayButton = true,
48
- showClearButton = true,
49
- closeOnSelect = true,
50
- minDate,
51
- maxDate,
52
- }: DatePickerProps) {
53
- const t = useT()
54
- const [open, setOpen] = React.useState(false)
55
-
56
- const resolvedFormat = displayFormat ?? deriveDisplayFormat(locale)
57
- const placeholderText = placeholder ?? t('ui.datePicker.placeholder', 'Pick a date')
58
- const todayText = t('ui.datePicker.todayButton', 'Today')
59
- const clearText = t('ui.datePicker.clearButton', 'Clear')
60
-
61
- const formattedValue = React.useMemo(() => {
62
- if (!value) return null
63
- try {
64
- return format(value, resolvedFormat, locale ? { locale } : undefined)
65
- } catch {
66
- return null
67
- }
68
- }, [value, resolvedFormat, locale])
69
-
70
- const handleDaySelect = React.useCallback(
71
- (day: Date | undefined) => {
72
- if (!day) return
73
- const next = new Date(day)
74
- next.setHours(0, 0, 0, 0)
75
- onChange(next)
76
- if (closeOnSelect) setOpen(false)
77
- },
78
- [onChange, closeOnSelect]
79
- )
80
-
81
- const handleToday = React.useCallback(() => {
82
- const today = new Date()
83
- today.setHours(0, 0, 0, 0)
84
- onChange(today)
85
- setOpen(false)
86
- }, [onChange])
87
-
88
- const handleClear = React.useCallback(() => {
89
- onChange(null)
90
- setOpen(false)
91
- }, [onChange])
92
-
93
- const isInteractive = !disabled && !readOnly
94
-
95
- const disabledMatcher = React.useMemo(() => {
96
- if (!minDate && !maxDate) return undefined
97
- const matchers: import('react-day-picker').Matcher[] = []
98
- if (minDate) matchers.push({ before: minDate })
99
- if (maxDate) matchers.push({ after: maxDate })
100
- return matchers
101
- }, [minDate, maxDate])
102
-
103
- return (
104
- <Popover open={open} onOpenChange={isInteractive ? setOpen : undefined}>
105
- <PopoverTrigger asChild>
106
- <button
107
- type="button"
108
- data-crud-focus-target=""
109
- disabled={disabled}
110
- aria-haspopup="dialog"
111
- className={cn(
112
- 'w-full h-9 flex items-center gap-2 rounded border px-3 text-sm text-left',
113
- 'bg-background transition-colors',
114
- 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1',
115
- 'disabled:bg-muted disabled:text-muted-foreground disabled:cursor-not-allowed',
116
- readOnly && 'cursor-default opacity-70',
117
- !formattedValue && 'text-muted-foreground',
118
- className
119
- )}
120
- onClick={isInteractive ? undefined : (e) => e.preventDefault()}
121
- >
122
- <CalendarIcon className="h-4 w-4 shrink-0 text-muted-foreground" />
123
- <span className="flex-1 truncate">
124
- {formattedValue ?? placeholderText}
125
- </span>
126
- </button>
127
- </PopoverTrigger>
128
- <PopoverContent className="p-0 w-auto">
129
- <Calendar
130
- mode="single"
131
- selected={value ?? undefined}
132
- onSelect={handleDaySelect}
133
- locale={locale}
134
- disabled={disabledMatcher}
135
- initialFocus
136
- />
137
- {(showTodayButton || showClearButton) && (
138
- <div className="flex items-center justify-between gap-2 border-t px-3 py-2">
139
- {showTodayButton && (
140
- <button
141
- type="button"
142
- onClick={handleToday}
143
- className="text-sm text-primary hover:underline focus-visible:outline-none"
144
- >
145
- {todayText}
146
- </button>
147
- )}
148
- {showClearButton && (
149
- <button
150
- type="button"
151
- onClick={handleClear}
152
- className="text-sm text-muted-foreground hover:text-foreground hover:underline focus-visible:outline-none ml-auto"
153
- >
154
- {clearText}
155
- </button>
156
- )}
157
- </div>
158
- )}
159
- </PopoverContent>
160
- </Popover>
161
- )
162
- }
35
+ export type { DatePickerFooter }
@@ -1,199 +1,31 @@
1
+ /**
2
+ * @deprecated Import `DatePicker` from `@open-mercato/ui/primitives/date-picker`
3
+ * and pass `withTime` instead. The standalone `DateTimePicker` is replaced by
4
+ * a `withTime` prop on the unified primitive in DS Foundation v3.
5
+ *
6
+ * Default footer is the Figma-aligned `'apply-cancel'` mode (per user
7
+ * directive 2026-05-09 — "zmień globalnie"). Consumers that explicitly
8
+ * need the legacy `Today` / `Clear` footer can pass `footer="today-clear"`.
9
+ *
10
+ * Migration: replace
11
+ * import { DateTimePicker } from '@open-mercato/ui/backend/inputs/DateTimePicker'
12
+ * <DateTimePicker value={...} onChange={...} />
13
+ * with
14
+ * import { DatePicker } from '@open-mercato/ui/primitives/date-picker'
15
+ * <DatePicker value={...} onChange={...} withTime />
16
+ */
17
+
1
18
  "use client"
2
19
 
3
20
  import * as React from 'react'
4
- import { format } from 'date-fns'
5
- import type { Locale } from 'date-fns'
6
- import { CalendarIcon } from 'lucide-react'
7
- import { cn } from '@open-mercato/shared/lib/utils'
8
- import { useT } from '@open-mercato/shared/lib/i18n/context'
9
- import { Popover, PopoverContent, PopoverTrigger } from '../../primitives/popover'
10
- import { Calendar } from '../../primitives/calendar'
11
- import { TimeInput } from './TimeInput'
12
-
13
- export type DateTimePickerProps = {
14
- value?: Date | null
15
- onChange: (date: Date | null) => void
16
- placeholder?: string
17
- disabled?: boolean
18
- readOnly?: boolean
19
- className?: string
20
- locale?: Locale
21
- displayFormat?: string
22
- minuteStep?: number
23
- showTodayButton?: boolean
24
- showClearButton?: boolean
25
- minDate?: Date
26
- maxDate?: Date
27
- }
28
-
29
- const DAY_FIRST_LOCALE_CODES = new Set([
30
- 'pl', 'de', 'fr', 'es', 'it', 'pt', 'nl', 'ru', 'cs', 'sk', 'hu', 'ro',
31
- ])
32
-
33
- function deriveDisplayFormat(locale?: Locale): string {
34
- if (!locale) return 'MMM d, yyyy HH:mm'
35
- const code = locale.code?.split('-')[0]?.toLowerCase() ?? ''
36
- return DAY_FIRST_LOCALE_CODES.has(code) ? 'd MMM yyyy HH:mm' : 'MMM d, yyyy HH:mm'
37
- }
38
-
39
- function extractTime(date: Date): string {
40
- const hour = String(date.getHours()).padStart(2, '0')
41
- const minute = String(date.getMinutes()).padStart(2, '0')
42
- return `${hour}:${minute}`
43
- }
44
-
45
- function applyTimeToDate(base: Date, time: string): Date {
46
- const parts = time.split(':')
47
- const hour = parseInt(parts[0] ?? '0', 10)
48
- const minute = parseInt(parts[1] ?? '0', 10)
49
- const next = new Date(base)
50
- next.setHours(isNaN(hour) ? 0 : hour)
51
- next.setMinutes(isNaN(minute) ? 0 : minute)
52
- next.setSeconds(0)
53
- next.setMilliseconds(0)
54
- return next
55
- }
56
-
57
- export function DateTimePicker({
58
- value,
59
- onChange,
60
- placeholder,
61
- disabled = false,
62
- readOnly = false,
63
- className,
64
- locale,
65
- displayFormat,
66
- minuteStep = 1,
67
- showTodayButton = true,
68
- showClearButton = true,
69
- minDate,
70
- maxDate,
71
- }: DateTimePickerProps) {
72
- const t = useT()
73
- const [open, setOpen] = React.useState(false)
74
-
75
- const resolvedFormat = displayFormat ?? deriveDisplayFormat(locale)
76
- const placeholderText = placeholder ?? t('ui.dateTimePicker.placeholder', 'Pick date and time')
77
- const timeLabelText = t('ui.dateTimePicker.timeLabel', 'Time')
78
- const todayText = t('ui.dateTimePicker.todayButton', 'Today')
79
- const clearText = t('ui.dateTimePicker.clearButton', 'Clear')
80
-
81
- const formattedValue = React.useMemo(() => {
82
- if (!value) return null
83
- try {
84
- return format(value, resolvedFormat, locale ? { locale } : undefined)
85
- } catch {
86
- return null
87
- }
88
- }, [value, resolvedFormat, locale])
89
-
90
- const handleDaySelect = React.useCallback(
91
- (day: Date | undefined) => {
92
- if (!day) return
93
- const currentTime = value ? extractTime(value) : '00:00'
94
- onChange(applyTimeToDate(day, currentTime))
95
- },
96
- [onChange, value]
97
- )
98
-
99
- const handleTimeChange = React.useCallback(
100
- (time: string) => {
101
- const base = value ?? null
102
- if (!base) return
103
- onChange(applyTimeToDate(base, time))
104
- },
105
- [onChange, value]
106
- )
107
-
108
- const handleToday = React.useCallback(() => {
109
- const now = new Date()
110
- const currentTime = value ? extractTime(value) : extractTime(now)
111
- onChange(applyTimeToDate(now, currentTime))
112
- setOpen(false)
113
- }, [onChange, value])
114
-
115
- const handleClear = React.useCallback(() => {
116
- onChange(null)
117
- setOpen(false)
118
- }, [onChange])
119
-
120
- const isInteractive = !disabled && !readOnly
21
+ import {
22
+ DatePicker as DatePickerPrimitive,
23
+ type DatePickerProps as DatePickerPrimitiveProps,
24
+ } from '../../primitives/date-picker'
121
25
 
122
- const disabledMatcher = React.useMemo(() => {
123
- if (!minDate && !maxDate) return undefined
124
- const matchers: import('react-day-picker').Matcher[] = []
125
- if (minDate) matchers.push({ before: minDate })
126
- if (maxDate) matchers.push({ after: maxDate })
127
- return matchers
128
- }, [minDate, maxDate])
26
+ export type DateTimePickerProps = Omit<DatePickerPrimitiveProps, 'withTime'>
129
27
 
130
- return (
131
- <Popover open={open} onOpenChange={isInteractive ? setOpen : undefined}>
132
- <PopoverTrigger asChild>
133
- <button
134
- type="button"
135
- data-crud-focus-target=""
136
- disabled={disabled}
137
- aria-haspopup="dialog"
138
- className={cn(
139
- 'w-full h-9 flex items-center gap-2 rounded border px-3 text-sm text-left',
140
- 'bg-background transition-colors',
141
- 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1',
142
- 'disabled:bg-muted disabled:text-muted-foreground disabled:cursor-not-allowed',
143
- readOnly && 'cursor-default opacity-70',
144
- !formattedValue && 'text-muted-foreground',
145
- className
146
- )}
147
- onClick={isInteractive ? undefined : (e) => e.preventDefault()}
148
- >
149
- <CalendarIcon className="h-4 w-4 shrink-0 text-muted-foreground" />
150
- <span className="flex-1 truncate">
151
- {formattedValue ?? placeholderText}
152
- </span>
153
- </button>
154
- </PopoverTrigger>
155
- <PopoverContent className="p-0 w-auto">
156
- <Calendar
157
- mode="single"
158
- selected={value ?? undefined}
159
- onSelect={handleDaySelect}
160
- locale={locale}
161
- disabled={disabledMatcher}
162
- initialFocus
163
- />
164
- <div className="border-t px-3 py-2 space-y-2">
165
- <div className="flex items-center gap-2">
166
- <span className="text-sm text-muted-foreground shrink-0">{timeLabelText}:</span>
167
- <TimeInput
168
- value={value ? extractTime(value) : undefined}
169
- onChange={handleTimeChange}
170
- minuteStep={minuteStep}
171
- />
172
- </div>
173
- {(showTodayButton || showClearButton) && (
174
- <div className="flex items-center justify-between gap-2">
175
- {showTodayButton && (
176
- <button
177
- type="button"
178
- onClick={handleToday}
179
- className="text-sm text-primary hover:underline focus-visible:outline-none"
180
- >
181
- {todayText}
182
- </button>
183
- )}
184
- {showClearButton && (
185
- <button
186
- type="button"
187
- onClick={handleClear}
188
- className="text-sm text-muted-foreground hover:text-foreground hover:underline focus-visible:outline-none ml-auto"
189
- >
190
- {clearText}
191
- </button>
192
- )}
193
- </div>
194
- )}
195
- </div>
196
- </PopoverContent>
197
- </Popover>
198
- )
28
+ /** @deprecated Use `DatePicker withTime` from `@open-mercato/ui/primitives/date-picker`. */
29
+ export function DateTimePicker(props: DateTimePickerProps) {
30
+ return <DatePickerPrimitive {...props} withTime />
199
31
  }