@torch-ui/solid 0.1.3

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 (118) hide show
  1. package/README.md +166 -0
  2. package/package.json +67 -0
  3. package/src/components/actions/Button.tsx +612 -0
  4. package/src/components/actions/ButtonGroup.tsx +728 -0
  5. package/src/components/actions/Copy.tsx +98 -0
  6. package/src/components/actions/DarkModeToggle.tsx +80 -0
  7. package/src/components/actions/Link.tsx +37 -0
  8. package/src/components/actions/index.ts +19 -0
  9. package/src/components/actions/useCopyToClipboard.ts +90 -0
  10. package/src/components/charts/Chart.tsx +331 -0
  11. package/src/components/charts/Sparkline.tsx +156 -0
  12. package/src/components/charts/index.ts +13 -0
  13. package/src/components/data-display/Avatar.tsx +208 -0
  14. package/src/components/data-display/AvatarGroup.tsx +228 -0
  15. package/src/components/data-display/Badge.tsx +70 -0
  16. package/src/components/data-display/Carousel.tsx +214 -0
  17. package/src/components/data-display/ColorSwatch.tsx +56 -0
  18. package/src/components/data-display/DataTable.tsx +886 -0
  19. package/src/components/data-display/EmptyState.tsx +61 -0
  20. package/src/components/data-display/Image.tsx +277 -0
  21. package/src/components/data-display/Kbd.tsx +114 -0
  22. package/src/components/data-display/Persona.tsx +78 -0
  23. package/src/components/data-display/StatCard.tsx +338 -0
  24. package/src/components/data-display/Table.tsx +147 -0
  25. package/src/components/data-display/Tag.tsx +91 -0
  26. package/src/components/data-display/Timeline.tsx +200 -0
  27. package/src/components/data-display/TreeView.tsx +172 -0
  28. package/src/components/data-display/Video.tsx +95 -0
  29. package/src/components/data-display/avatar-utils.ts +32 -0
  30. package/src/components/data-display/index.ts +81 -0
  31. package/src/components/feedback/Loading.tsx +159 -0
  32. package/src/components/feedback/Progress.tsx +321 -0
  33. package/src/components/feedback/Skeleton.tsx +62 -0
  34. package/src/components/feedback/SkeletonBlocks.tsx +222 -0
  35. package/src/components/feedback/Toast.tsx +648 -0
  36. package/src/components/feedback/index.ts +44 -0
  37. package/src/components/feedback/password/PasswordStrengthIndicator.tsx +232 -0
  38. package/src/components/feedback/password/password-strength.ts +115 -0
  39. package/src/components/feedback/password/password-validation-data.ts +66 -0
  40. package/src/components/feedback/password/password-validation.ts +93 -0
  41. package/src/components/forms/Autocomplete.tsx +268 -0
  42. package/src/components/forms/Checkbox.tsx +155 -0
  43. package/src/components/forms/CodeInput.tsx +237 -0
  44. package/src/components/forms/ColorPicker/ColorPicker.tsx +469 -0
  45. package/src/components/forms/ColorPicker/color-utils.ts +75 -0
  46. package/src/components/forms/ColorPicker/index.ts +2 -0
  47. package/src/components/forms/DatePicker.tsx +516 -0
  48. package/src/components/forms/DateRangePicker.tsx +464 -0
  49. package/src/components/forms/FieldPicker.tsx +64 -0
  50. package/src/components/forms/FileUpload.tsx +614 -0
  51. package/src/components/forms/FilterBuilder/FilterGroupBlock.ts +6 -0
  52. package/src/components/forms/FilterBuilder.tsx +16 -0
  53. package/src/components/forms/FilterRuleRow.tsx +68 -0
  54. package/src/components/forms/Input.tsx +200 -0
  55. package/src/components/forms/MultiSelect.tsx +361 -0
  56. package/src/components/forms/NumberField.tsx +145 -0
  57. package/src/components/forms/RadioGroup.tsx +135 -0
  58. package/src/components/forms/RelativeDateDefaultInput.tsx +62 -0
  59. package/src/components/forms/ReorderableList.tsx +163 -0
  60. package/src/components/forms/Select.tsx +268 -0
  61. package/src/components/forms/Slider.tsx +260 -0
  62. package/src/components/forms/Switch.tsx +135 -0
  63. package/src/components/forms/TextArea.tsx +202 -0
  64. package/src/components/forms/ViewCustomizer.tsx +44 -0
  65. package/src/components/forms/index.ts +43 -0
  66. package/src/components/layout/Accordion.tsx +110 -0
  67. package/src/components/layout/Alert.tsx +156 -0
  68. package/src/components/layout/BlockQuote.tsx +70 -0
  69. package/src/components/layout/Card.tsx +166 -0
  70. package/src/components/layout/CodeBlock/CodeBlock.tsx +477 -0
  71. package/src/components/layout/CodeBlock/code-block-tokens.css +104 -0
  72. package/src/components/layout/CodeBlock/prism.ts +81 -0
  73. package/src/components/layout/Collapsible.tsx +84 -0
  74. package/src/components/layout/Container.tsx +55 -0
  75. package/src/components/layout/Divider.tsx +64 -0
  76. package/src/components/layout/Form.tsx +39 -0
  77. package/src/components/layout/FormActions.tsx +50 -0
  78. package/src/components/layout/Grid.tsx +53 -0
  79. package/src/components/layout/PageHeading.tsx +46 -0
  80. package/src/components/layout/PromptWithAction.tsx +49 -0
  81. package/src/components/layout/Section.tsx +60 -0
  82. package/src/components/layout/TablePanel.tsx +24 -0
  83. package/src/components/layout/TableView/TableView.tsx +1018 -0
  84. package/src/components/layout/TableView/index.ts +3 -0
  85. package/src/components/layout/TableView/types.ts +51 -0
  86. package/src/components/layout/WizardStep.tsx +40 -0
  87. package/src/components/layout/WizardStepper.tsx +173 -0
  88. package/src/components/layout/index.ts +96 -0
  89. package/src/components/navigation/Breadcrumbs.tsx +66 -0
  90. package/src/components/navigation/DropdownMenu.tsx +86 -0
  91. package/src/components/navigation/MegaMenu.tsx +480 -0
  92. package/src/components/navigation/NavigationMenu.tsx +305 -0
  93. package/src/components/navigation/Pagination.tsx +298 -0
  94. package/src/components/navigation/Sidebar.tsx +280 -0
  95. package/src/components/navigation/Tabs.tsx +122 -0
  96. package/src/components/navigation/ViewSwitcher.tsx +314 -0
  97. package/src/components/navigation/index.ts +66 -0
  98. package/src/components/overlays/AlertDialog.tsx +174 -0
  99. package/src/components/overlays/ContextMenu.tsx +65 -0
  100. package/src/components/overlays/Dialog.tsx +279 -0
  101. package/src/components/overlays/Drawer.tsx +370 -0
  102. package/src/components/overlays/HoverCard.tsx +107 -0
  103. package/src/components/overlays/Popover.tsx +73 -0
  104. package/src/components/overlays/Tooltip.tsx +31 -0
  105. package/src/components/overlays/index.ts +71 -0
  106. package/src/components/typography/Code.tsx +72 -0
  107. package/src/components/typography/Icon.tsx +36 -0
  108. package/src/components/typography/index.ts +10 -0
  109. package/src/env.d.ts +9 -0
  110. package/src/index.ts +13 -0
  111. package/src/styles/theme.css +226 -0
  112. package/src/types/avatar-types.ts +11 -0
  113. package/src/types/filter-types.ts +35 -0
  114. package/src/utilities/classNames.ts +6 -0
  115. package/src/utilities/componentSize.ts +46 -0
  116. package/src/utilities/i18n.tsx +60 -0
  117. package/src/utilities/mergeRefs.ts +12 -0
  118. package/src/utilities/relativeDateDefault.ts +14 -0
@@ -0,0 +1,464 @@
1
+ import { createSignal, createMemo, Show, For, splitProps, createUniqueId, createEffect } from 'solid-js'
2
+ import { Calendar as CalendarIcon, ChevronLeft, ChevronRight, X } from 'lucide-solid'
3
+ import { Popover as KobaltePopover } from '@kobalte/core/popover'
4
+ import { cn } from '../../utilities/classNames'
5
+
6
+ export interface DateRangePickerProps {
7
+ /** ISO date string YYYY-MM-DD for range start */
8
+ start?: string
9
+ /** ISO date string YYYY-MM-DD for range end */
10
+ end?: string
11
+ /** Called when range changes. end may be empty string if only start is selected. */
12
+ onRangeChange?: (start: string, end: string) => void
13
+ placeholder?: string
14
+ disabled?: boolean
15
+ /** Min date YYYY-MM-DD */
16
+ min?: string
17
+ /** Max date YYYY-MM-DD */
18
+ max?: string
19
+ label?: string
20
+ error?: string
21
+ helperText?: string
22
+ bare?: boolean
23
+ required?: boolean
24
+ optional?: boolean
25
+ /** Show two months side by side. Default: true */
26
+ dualMonth?: boolean
27
+ /** Allow clearing the range. Default: true */
28
+ clearable?: boolean
29
+ class?: string
30
+ id?: string
31
+ }
32
+
33
+ const DAY_NAMES = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa']
34
+ const MONTH_NAMES = [
35
+ 'January', 'February', 'March', 'April', 'May', 'June',
36
+ 'July', 'August', 'September', 'October', 'November', 'December',
37
+ ]
38
+
39
+ function parseDate(s: string): Date | null {
40
+ if (!s || !/^\d{4}-\d{2}-\d{2}$/.test(s)) return null
41
+ const d = new Date(s + 'T12:00:00')
42
+ return isNaN(d.getTime()) ? null : d
43
+ }
44
+
45
+ function toISODate(d: Date): string {
46
+ const y = d.getFullYear()
47
+ const m = String(d.getMonth() + 1).padStart(2, '0')
48
+ const day = String(d.getDate()).padStart(2, '0')
49
+ return `${y}-${m}-${day}`
50
+ }
51
+
52
+ function formatDisplay(s: string): string {
53
+ const d = parseDate(s)
54
+ if (!d) return ''
55
+ return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })
56
+ }
57
+
58
+ function sameDay(a: Date, b: Date) {
59
+ return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate()
60
+ }
61
+
62
+ function getCalendarDays(year: number, month: number): Date[] {
63
+ const first = new Date(year, month, 1)
64
+ const last = new Date(year, month + 1, 0)
65
+ const startPad = first.getDay()
66
+ const flat: Date[] = []
67
+ for (let i = 0; i < startPad; i++) {
68
+ flat.push(new Date(year, month, 1 - (startPad - i)))
69
+ }
70
+ for (let d = 1; d <= last.getDate(); d++) {
71
+ flat.push(new Date(year, month, d))
72
+ }
73
+ while (flat.length < 42) {
74
+ flat.push(new Date(year, month + 1, flat.length - last.getDate() - startPad + 1))
75
+ }
76
+ return flat
77
+ }
78
+
79
+ interface MonthGridProps {
80
+ year: number
81
+ month: number
82
+ start: Date | null
83
+ end: Date | null
84
+ hover: Date | null
85
+ min: Date | null
86
+ max: Date | null
87
+ onDayClick: (d: Date) => void
88
+ onDayHover: (d: Date | null) => void
89
+ }
90
+
91
+ function MonthGrid(props: MonthGridProps) {
92
+ const days = createMemo(() => getCalendarDays(props.year, props.month))
93
+
94
+ const isCurrentMonth = (d: Date) => d.getMonth() === props.month
95
+ const isDisabled = (d: Date) => {
96
+ if (props.min && d < props.min) return true
97
+ if (props.max && d > props.max) return true
98
+ return false
99
+ }
100
+ const isStart = (d: Date) => !!props.start && sameDay(d, props.start)
101
+
102
+ // Effective end: hover preview (when only start is set) or committed end
103
+ const effectiveEndDate = () => (!props.end && props.hover) ? props.hover : props.end
104
+
105
+ // Ordered [lo, hi] regardless of click order
106
+ const orderedRange = (): [Date, Date] | null => {
107
+ const s = props.start
108
+ const e = effectiveEndDate()
109
+ if (!s || !e) return null
110
+ return e < s ? [e, s] : [s, e]
111
+ }
112
+
113
+ const isInRange = (d: Date) => {
114
+ const range = orderedRange()
115
+ if (!range) return false
116
+ return d > range[0] && d < range[1]
117
+ }
118
+
119
+ const isRangeStart = (d: Date) => {
120
+ const range = orderedRange()
121
+ if (!range) return isStart(d)
122
+ return sameDay(d, range[0])
123
+ }
124
+
125
+ const isRangeEnd = (d: Date) => {
126
+ const range = orderedRange()
127
+ if (!range) return false
128
+ return sameDay(d, range[1])
129
+ }
130
+
131
+ const isToday = (d: Date) => sameDay(d, new Date())
132
+
133
+ return (
134
+ <div>
135
+ <div class="grid grid-cols-7 mb-2">
136
+ <For each={DAY_NAMES}>
137
+ {(name) => (
138
+ <div class="py-1 text-center text-xs font-medium text-ink-400">{name}</div>
139
+ )}
140
+ </For>
141
+ </div>
142
+ <div class="grid grid-cols-7">
143
+ <For each={days()}>
144
+ {(day) => {
145
+ const rangeStart = () => isRangeStart(day)
146
+ const rangeEnd = () => isRangeEnd(day)
147
+ const inRange = () => isInRange(day)
148
+ const disabled = () => isDisabled(day)
149
+ const otherMonth = () => !isCurrentMonth(day)
150
+ const today = () => isToday(day)
151
+ const selected = () => rangeStart() || rangeEnd()
152
+
153
+ return (
154
+ <div
155
+ class={cn(
156
+ 'relative h-8 flex items-center justify-center',
157
+ inRange() && 'bg-primary-50 dark:bg-primary-500/10',
158
+ rangeStart() && 'rounded-l-full',
159
+ rangeEnd() && 'rounded-r-full',
160
+ )}
161
+ >
162
+ <button
163
+ type="button"
164
+ disabled={disabled()}
165
+ onClick={() => !disabled() && props.onDayClick(day)}
166
+ onMouseEnter={() => !disabled() && props.onDayHover(day)}
167
+ onMouseLeave={() => props.onDayHover(null)}
168
+ class={cn(
169
+ 'relative z-10 h-7 w-7 rounded-full text-xs transition-colors',
170
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500/50',
171
+ selected()
172
+ ? 'bg-primary-500 text-white font-semibold hover:bg-primary-600'
173
+ : inRange()
174
+ ? 'text-primary-700 dark:text-primary-300 hover:bg-primary-100 dark:hover:bg-primary-500/20'
175
+ : otherMonth()
176
+ ? 'text-ink-300 dark:text-ink-600 hover:bg-surface-overlay'
177
+ : today()
178
+ ? 'text-primary-600 font-semibold hover:bg-surface-overlay dark:text-primary-400'
179
+ : 'text-ink-800 dark:text-ink-200 hover:bg-surface-overlay',
180
+ disabled() && 'cursor-not-allowed opacity-30',
181
+ )}
182
+ >
183
+ {day.getDate()}
184
+ {today() && !selected() && (
185
+ <span class="absolute bottom-0.5 left-1/2 h-1 w-1 -translate-x-1/2 rounded-full bg-primary-500" />
186
+ )}
187
+ </button>
188
+ </div>
189
+ )
190
+ }}
191
+ </For>
192
+ </div>
193
+ </div>
194
+ )
195
+ }
196
+
197
+ export function DateRangePicker(props: DateRangePickerProps) {
198
+ const [local] = splitProps(props, [
199
+ 'start', 'end', 'onRangeChange', 'placeholder', 'disabled',
200
+ 'min', 'max', 'label', 'error', 'helperText', 'bare',
201
+ 'required', 'optional', 'dualMonth', 'clearable', 'class', 'id',
202
+ ])
203
+
204
+ const generatedId = createUniqueId()
205
+ const inputId = () => local.id || `drp-${generatedId}`
206
+ const [open, setOpen] = createSignal(false)
207
+ const [hover, setHover] = createSignal<Date | null>(null)
208
+
209
+ // Picking state: null = picking start, 'start' = start picked, waiting for end
210
+ const [pickingEnd, setPickingEnd] = createSignal(false)
211
+
212
+ const startDate = () => parseDate(local.start ?? '')
213
+ const endDate = () => parseDate(local.end ?? '')
214
+
215
+ // View month: left calendar
216
+ const initView = () => {
217
+ const s = startDate()
218
+ return s ? { year: s.getFullYear(), month: s.getMonth() } : { year: new Date().getFullYear(), month: new Date().getMonth() }
219
+ }
220
+ const [viewLeft, setViewLeft] = createSignal(initView())
221
+
222
+ // Right calendar is always next month from left
223
+ const viewRight = () => {
224
+ const { year, month } = viewLeft()
225
+ const next = new Date(year, month + 1, 1)
226
+ return { year: next.getFullYear(), month: next.getMonth() }
227
+ }
228
+
229
+ createEffect(() => {
230
+ if (!open()) {
231
+ setPickingEnd(false)
232
+ setHover(null)
233
+ }
234
+ })
235
+
236
+ const dual = () => local.dualMonth !== false
237
+ const clearable = () => local.clearable !== false
238
+ const minDate = () => parseDate(local.min ?? '')
239
+ const maxDate = () => parseDate(local.max ?? '')
240
+
241
+ function handleDayClick(d: Date) {
242
+ if (!pickingEnd()) {
243
+ // First click: set start, clear end
244
+ local.onRangeChange?.(toISODate(d), '')
245
+ setPickingEnd(true)
246
+ } else {
247
+ // Second click: set end (or swap if before start)
248
+ const s = startDate()
249
+ if (s && d < s) {
250
+ local.onRangeChange?.(toISODate(d), toISODate(s))
251
+ } else {
252
+ local.onRangeChange?.(local.start ?? toISODate(d), toISODate(d))
253
+ }
254
+ setPickingEnd(false)
255
+ setOpen(false)
256
+ }
257
+ }
258
+
259
+ function clearRange() {
260
+ local.onRangeChange?.('', '')
261
+ setPickingEnd(false)
262
+ }
263
+
264
+ function prevMonth() {
265
+ const { year, month } = viewLeft()
266
+ const d = new Date(year, month - 1, 1)
267
+ setViewLeft({ year: d.getFullYear(), month: d.getMonth() })
268
+ }
269
+ function nextMonth() {
270
+ const { year, month } = dual() ? viewRight() : viewLeft()
271
+ const d = new Date(year, month + 1, 1)
272
+ const newLeft = dual()
273
+ ? new Date(viewLeft().year, viewLeft().month + 1, 1)
274
+ : d
275
+ setViewLeft({ year: newLeft.getFullYear(), month: newLeft.getMonth() })
276
+ }
277
+
278
+ const displayValue = () => {
279
+ const s = local.start ? formatDisplay(local.start) : ''
280
+ const e = local.end ? formatDisplay(local.end) : ''
281
+ if (s && e) return `${s} – ${e}`
282
+ if (s) return `${s} – …`
283
+ return ''
284
+ }
285
+
286
+ const hasError = () => !!local.error
287
+ const msgId = () => (local.error || local.helperText) ? `${inputId()}-msg` : undefined
288
+
289
+ return (
290
+ <div class={cn('w-full', local.class)}>
291
+ <Show when={!local.bare && local.label}>
292
+ <div class="mb-2 flex items-center justify-between">
293
+ <label
294
+ for={inputId()}
295
+ class={cn(
296
+ 'block text-sm font-medium',
297
+ hasError() ? 'text-danger-600 dark:text-danger-400' : 'text-ink-700',
298
+ )}
299
+ >
300
+ {local.label}
301
+ </label>
302
+ <Show when={!local.required && local.optional}>
303
+ <span class="text-xs text-ink-400">optional</span>
304
+ </Show>
305
+ </div>
306
+ </Show>
307
+
308
+ <KobaltePopover
309
+ open={open()}
310
+ onOpenChange={(next) => {
311
+ setOpen(next)
312
+ if (next) setViewLeft(initView())
313
+ }}
314
+ gutter={8}
315
+ >
316
+ <div class="relative">
317
+ <KobaltePopover.Trigger
318
+ as="button"
319
+ type="button"
320
+ id={inputId()}
321
+ disabled={local.disabled}
322
+ aria-describedby={msgId()}
323
+ aria-invalid={hasError() ? true : undefined}
324
+ class={cn(
325
+ 'inline-flex w-full items-center gap-2 rounded-lg border px-3 py-2 text-sm transition-colors',
326
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500/50',
327
+ hasError()
328
+ ? 'border-danger-500 bg-surface-raised text-ink-900 hover:border-danger-600'
329
+ : 'border-surface-border bg-surface-raised text-ink-900 hover:border-ink-400 dark:hover:border-ink-500',
330
+ local.disabled && 'cursor-not-allowed opacity-50',
331
+ clearable() && (local.start || local.end) && !local.disabled && 'pr-8',
332
+ )}
333
+ >
334
+ <CalendarIcon class="h-4 w-4 shrink-0 text-ink-400" aria-hidden="true" />
335
+ <span class={cn('truncate', displayValue() ? 'text-ink-900' : 'text-ink-400')}>
336
+ {displayValue() || (local.placeholder ?? 'Pick a date range')}
337
+ </span>
338
+ </KobaltePopover.Trigger>
339
+ <Show when={clearable() && (local.start || local.end) && !local.disabled}>
340
+ <button
341
+ type="button"
342
+ onClick={(e) => { e.stopPropagation(); clearRange() }}
343
+ class="absolute right-2 top-1/2 -translate-y-1/2 rounded p-0.5 text-ink-400 hover:text-ink-700 transition-colors"
344
+ aria-label="Clear date range"
345
+ >
346
+ <X class="h-3.5 w-3.5" />
347
+ </button>
348
+ </Show>
349
+ </div>
350
+
351
+ <KobaltePopover.Portal>
352
+ <KobaltePopover.Content
353
+ class={cn(
354
+ 'z-50 rounded-xl border border-surface-border bg-surface-raised shadow-xl',
355
+ 'origin-top data-[expanded]:animate-in data-[expanded]:fade-in-0 data-[expanded]:zoom-in-95',
356
+ 'data-[closed]:animate-out data-[closed]:fade-out-0 data-[closed]:zoom-out-95',
357
+ )}
358
+ >
359
+ <div class={cn('p-3', dual() ? 'w-[580px]' : 'w-[268px]')}>
360
+ {/* Header */}
361
+ <div class={cn('flex items-center justify-between', dual() ? 'mb-3' : 'mb-2')}>
362
+ <button
363
+ type="button"
364
+ onClick={prevMonth}
365
+ class="flex h-7 w-7 items-center justify-center rounded-md text-ink-500 hover:bg-surface-overlay transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500/50"
366
+ aria-label="Previous month"
367
+ >
368
+ <ChevronLeft class="h-4 w-4" />
369
+ </button>
370
+ <div class={cn('flex', dual() ? 'gap-8' : '')}>
371
+ <div class="text-sm font-semibold text-ink-900 min-w-[140px] text-center">
372
+ {MONTH_NAMES[viewLeft().month]} {viewLeft().year}
373
+ </div>
374
+ <Show when={dual()}>
375
+ <div class="text-sm font-semibold text-ink-900 min-w-[140px] text-center">
376
+ {MONTH_NAMES[viewRight().month]} {viewRight().year}
377
+ </div>
378
+ </Show>
379
+ </div>
380
+ <button
381
+ type="button"
382
+ onClick={nextMonth}
383
+ class="flex h-7 w-7 items-center justify-center rounded-md text-ink-500 hover:bg-surface-overlay transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500/50"
384
+ aria-label="Next month"
385
+ >
386
+ <ChevronRight class="h-4 w-4" />
387
+ </button>
388
+ </div>
389
+
390
+ {/* Calendars */}
391
+ <div class={cn('flex gap-6', !dual() && 'flex-col')}>
392
+ <MonthGrid
393
+ year={viewLeft().year}
394
+ month={viewLeft().month}
395
+ start={startDate()}
396
+ end={endDate()}
397
+ hover={hover()}
398
+ min={minDate()}
399
+ max={maxDate()}
400
+ onDayClick={handleDayClick}
401
+ onDayHover={setHover}
402
+ />
403
+ <Show when={dual()}>
404
+ <MonthGrid
405
+ year={viewRight().year}
406
+ month={viewRight().month}
407
+ start={startDate()}
408
+ end={endDate()}
409
+ hover={hover()}
410
+ min={minDate()}
411
+ max={maxDate()}
412
+ onDayClick={handleDayClick}
413
+ onDayHover={setHover}
414
+ />
415
+ </Show>
416
+ </div>
417
+
418
+ {/* Footer */}
419
+ <div class="mt-3 flex items-center justify-between border-t border-surface-border pt-3">
420
+ <div class="text-xs text-ink-400">
421
+ {pickingEnd()
422
+ ? 'Now select an end date'
423
+ : (local.start && local.end)
424
+ ? displayValue()
425
+ : 'Select a start date'}
426
+ </div>
427
+ <div class="flex gap-2">
428
+ <Show when={clearable() && (local.start || local.end)}>
429
+ <button
430
+ type="button"
431
+ onClick={clearRange}
432
+ class="rounded-md px-2 py-1 text-xs text-ink-500 hover:bg-surface-overlay hover:text-ink-700 transition-colors"
433
+ >
434
+ Clear
435
+ </button>
436
+ </Show>
437
+ <button
438
+ type="button"
439
+ onClick={() => setOpen(false)}
440
+ class="rounded-md px-2 py-1 text-xs font-medium text-primary-600 hover:bg-primary-50 dark:text-primary-400 dark:hover:bg-primary-500/10 transition-colors"
441
+ >
442
+ Done
443
+ </button>
444
+ </div>
445
+ </div>
446
+ </div>
447
+ </KobaltePopover.Content>
448
+ </KobaltePopover.Portal>
449
+ </KobaltePopover>
450
+
451
+ <Show when={local.error || local.helperText}>
452
+ <p
453
+ id={msgId()}
454
+ class={cn(
455
+ 'mt-1.5 text-xs',
456
+ hasError() ? 'text-danger-600 dark:text-danger-400' : 'text-ink-500',
457
+ )}
458
+ >
459
+ {local.error ?? local.helperText}
460
+ </p>
461
+ </Show>
462
+ </div>
463
+ )
464
+ }
@@ -0,0 +1,64 @@
1
+ import { splitProps } from 'solid-js'
2
+ import type { JSX } from 'solid-js'
3
+ import { Button } from '../actions'
4
+ import { Autocomplete } from './Autocomplete'
5
+ import { cn } from '../../utilities/classNames'
6
+
7
+ export interface FieldPickerOption {
8
+ value: string
9
+ label: string
10
+ }
11
+
12
+ export interface FieldPickerProps {
13
+ label?: string
14
+ options: FieldPickerOption[]
15
+ value: string
16
+ onValueChange: (value: string) => void
17
+ onAdd: () => void
18
+ addLabel?: string
19
+ addIcon?: JSX.Element
20
+ addDisabled?: boolean
21
+ placeholder?: string
22
+ class?: string
23
+ }
24
+
25
+ export const FieldPicker = (props: FieldPickerProps) => {
26
+ const [local] = splitProps(props, [
27
+ 'label',
28
+ 'options',
29
+ 'value',
30
+ 'onValueChange',
31
+ 'onAdd',
32
+ 'addLabel',
33
+ 'addIcon',
34
+ 'addDisabled',
35
+ 'placeholder',
36
+ 'class',
37
+ ])
38
+
39
+ return (
40
+ <div class={cn('space-y-2', local.class)}>
41
+ <div class="flex items-center gap-2">
42
+ <Autocomplete
43
+ label={local.label}
44
+ value={local.value}
45
+ onValueChange={local.onValueChange}
46
+ options={local.options}
47
+ placeholder={local.placeholder || 'Search fields...'}
48
+ class="flex-1 min-w-0"
49
+ />
50
+ <Button
51
+ type="button"
52
+ variant="outlined"
53
+ size="md"
54
+ startIcon={local.addIcon}
55
+ class="shrink-0 h-10"
56
+ disabled={local.addDisabled}
57
+ onClick={local.onAdd}
58
+ >
59
+ {local.addLabel || 'Add'}
60
+ </Button>
61
+ </div>
62
+ </div>
63
+ )
64
+ }