@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.
- package/README.md +166 -0
- package/package.json +67 -0
- package/src/components/actions/Button.tsx +612 -0
- package/src/components/actions/ButtonGroup.tsx +728 -0
- package/src/components/actions/Copy.tsx +98 -0
- package/src/components/actions/DarkModeToggle.tsx +80 -0
- package/src/components/actions/Link.tsx +37 -0
- package/src/components/actions/index.ts +19 -0
- package/src/components/actions/useCopyToClipboard.ts +90 -0
- package/src/components/charts/Chart.tsx +331 -0
- package/src/components/charts/Sparkline.tsx +156 -0
- package/src/components/charts/index.ts +13 -0
- package/src/components/data-display/Avatar.tsx +208 -0
- package/src/components/data-display/AvatarGroup.tsx +228 -0
- package/src/components/data-display/Badge.tsx +70 -0
- package/src/components/data-display/Carousel.tsx +214 -0
- package/src/components/data-display/ColorSwatch.tsx +56 -0
- package/src/components/data-display/DataTable.tsx +886 -0
- package/src/components/data-display/EmptyState.tsx +61 -0
- package/src/components/data-display/Image.tsx +277 -0
- package/src/components/data-display/Kbd.tsx +114 -0
- package/src/components/data-display/Persona.tsx +78 -0
- package/src/components/data-display/StatCard.tsx +338 -0
- package/src/components/data-display/Table.tsx +147 -0
- package/src/components/data-display/Tag.tsx +91 -0
- package/src/components/data-display/Timeline.tsx +200 -0
- package/src/components/data-display/TreeView.tsx +172 -0
- package/src/components/data-display/Video.tsx +95 -0
- package/src/components/data-display/avatar-utils.ts +32 -0
- package/src/components/data-display/index.ts +81 -0
- package/src/components/feedback/Loading.tsx +159 -0
- package/src/components/feedback/Progress.tsx +321 -0
- package/src/components/feedback/Skeleton.tsx +62 -0
- package/src/components/feedback/SkeletonBlocks.tsx +222 -0
- package/src/components/feedback/Toast.tsx +648 -0
- package/src/components/feedback/index.ts +44 -0
- package/src/components/feedback/password/PasswordStrengthIndicator.tsx +232 -0
- package/src/components/feedback/password/password-strength.ts +115 -0
- package/src/components/feedback/password/password-validation-data.ts +66 -0
- package/src/components/feedback/password/password-validation.ts +93 -0
- package/src/components/forms/Autocomplete.tsx +268 -0
- package/src/components/forms/Checkbox.tsx +155 -0
- package/src/components/forms/CodeInput.tsx +237 -0
- package/src/components/forms/ColorPicker/ColorPicker.tsx +469 -0
- package/src/components/forms/ColorPicker/color-utils.ts +75 -0
- package/src/components/forms/ColorPicker/index.ts +2 -0
- package/src/components/forms/DatePicker.tsx +516 -0
- package/src/components/forms/DateRangePicker.tsx +464 -0
- package/src/components/forms/FieldPicker.tsx +64 -0
- package/src/components/forms/FileUpload.tsx +614 -0
- package/src/components/forms/FilterBuilder/FilterGroupBlock.ts +6 -0
- package/src/components/forms/FilterBuilder.tsx +16 -0
- package/src/components/forms/FilterRuleRow.tsx +68 -0
- package/src/components/forms/Input.tsx +200 -0
- package/src/components/forms/MultiSelect.tsx +361 -0
- package/src/components/forms/NumberField.tsx +145 -0
- package/src/components/forms/RadioGroup.tsx +135 -0
- package/src/components/forms/RelativeDateDefaultInput.tsx +62 -0
- package/src/components/forms/ReorderableList.tsx +163 -0
- package/src/components/forms/Select.tsx +268 -0
- package/src/components/forms/Slider.tsx +260 -0
- package/src/components/forms/Switch.tsx +135 -0
- package/src/components/forms/TextArea.tsx +202 -0
- package/src/components/forms/ViewCustomizer.tsx +44 -0
- package/src/components/forms/index.ts +43 -0
- package/src/components/layout/Accordion.tsx +110 -0
- package/src/components/layout/Alert.tsx +156 -0
- package/src/components/layout/BlockQuote.tsx +70 -0
- package/src/components/layout/Card.tsx +166 -0
- package/src/components/layout/CodeBlock/CodeBlock.tsx +477 -0
- package/src/components/layout/CodeBlock/code-block-tokens.css +104 -0
- package/src/components/layout/CodeBlock/prism.ts +81 -0
- package/src/components/layout/Collapsible.tsx +84 -0
- package/src/components/layout/Container.tsx +55 -0
- package/src/components/layout/Divider.tsx +64 -0
- package/src/components/layout/Form.tsx +39 -0
- package/src/components/layout/FormActions.tsx +50 -0
- package/src/components/layout/Grid.tsx +53 -0
- package/src/components/layout/PageHeading.tsx +46 -0
- package/src/components/layout/PromptWithAction.tsx +49 -0
- package/src/components/layout/Section.tsx +60 -0
- package/src/components/layout/TablePanel.tsx +24 -0
- package/src/components/layout/TableView/TableView.tsx +1018 -0
- package/src/components/layout/TableView/index.ts +3 -0
- package/src/components/layout/TableView/types.ts +51 -0
- package/src/components/layout/WizardStep.tsx +40 -0
- package/src/components/layout/WizardStepper.tsx +173 -0
- package/src/components/layout/index.ts +96 -0
- package/src/components/navigation/Breadcrumbs.tsx +66 -0
- package/src/components/navigation/DropdownMenu.tsx +86 -0
- package/src/components/navigation/MegaMenu.tsx +480 -0
- package/src/components/navigation/NavigationMenu.tsx +305 -0
- package/src/components/navigation/Pagination.tsx +298 -0
- package/src/components/navigation/Sidebar.tsx +280 -0
- package/src/components/navigation/Tabs.tsx +122 -0
- package/src/components/navigation/ViewSwitcher.tsx +314 -0
- package/src/components/navigation/index.ts +66 -0
- package/src/components/overlays/AlertDialog.tsx +174 -0
- package/src/components/overlays/ContextMenu.tsx +65 -0
- package/src/components/overlays/Dialog.tsx +279 -0
- package/src/components/overlays/Drawer.tsx +370 -0
- package/src/components/overlays/HoverCard.tsx +107 -0
- package/src/components/overlays/Popover.tsx +73 -0
- package/src/components/overlays/Tooltip.tsx +31 -0
- package/src/components/overlays/index.ts +71 -0
- package/src/components/typography/Code.tsx +72 -0
- package/src/components/typography/Icon.tsx +36 -0
- package/src/components/typography/index.ts +10 -0
- package/src/env.d.ts +9 -0
- package/src/index.ts +13 -0
- package/src/styles/theme.css +226 -0
- package/src/types/avatar-types.ts +11 -0
- package/src/types/filter-types.ts +35 -0
- package/src/utilities/classNames.ts +6 -0
- package/src/utilities/componentSize.ts +46 -0
- package/src/utilities/i18n.tsx +60 -0
- package/src/utilities/mergeRefs.ts +12 -0
- 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
|
+
}
|