@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,145 @@
|
|
|
1
|
+
import type { JSX } from 'solid-js'
|
|
2
|
+
import { splitProps, Show } from 'solid-js'
|
|
3
|
+
import { NumberField as KobalteNumberField } from '@kobalte/core/number-field'
|
|
4
|
+
import { ChevronDown, ChevronUp } from 'lucide-solid'
|
|
5
|
+
import { cn } from '../../utilities/classNames'
|
|
6
|
+
import { type ComponentSize, inputSizeConfig } from '../../utilities/componentSize'
|
|
7
|
+
|
|
8
|
+
export interface NumberFieldProps {
|
|
9
|
+
/** Label above the input. */
|
|
10
|
+
label?: string
|
|
11
|
+
/** Error message and invalid styling. */
|
|
12
|
+
error?: string
|
|
13
|
+
/** Hint text below the input. */
|
|
14
|
+
helperText?: string
|
|
15
|
+
/** When true, never render label row or error/helper text (control only). */
|
|
16
|
+
bare?: boolean
|
|
17
|
+
/** When true, show required indicator on label. */
|
|
18
|
+
required?: boolean
|
|
19
|
+
/** When true, show "optional" on the label row when not required. Default false. */
|
|
20
|
+
optional?: boolean
|
|
21
|
+
/** Controlled value (number). */
|
|
22
|
+
value?: number
|
|
23
|
+
/** Called when value changes. */
|
|
24
|
+
onChange?: (value: number | undefined) => void
|
|
25
|
+
/** Minimum value. */
|
|
26
|
+
minValue?: number
|
|
27
|
+
/** Maximum value. */
|
|
28
|
+
maxValue?: number
|
|
29
|
+
/** Step for increment/decrement. */
|
|
30
|
+
step?: number
|
|
31
|
+
/** Disables the input and steppers. */
|
|
32
|
+
disabled?: boolean
|
|
33
|
+
/** Placeholder when empty. */
|
|
34
|
+
placeholder?: string
|
|
35
|
+
/** Input size. Controls height, text size, and padding. Default: md (36px). */
|
|
36
|
+
size?: ComponentSize
|
|
37
|
+
/** When true, show increment/decrement buttons. */
|
|
38
|
+
showStepper?: boolean
|
|
39
|
+
/** Additional class for the root. */
|
|
40
|
+
class?: string
|
|
41
|
+
/** Ref forwarded to the number input element. */
|
|
42
|
+
ref?: (el: HTMLInputElement) => void
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function NumberField(props: NumberFieldProps) {
|
|
46
|
+
const [local, others] = splitProps(props, [
|
|
47
|
+
'label',
|
|
48
|
+
'error',
|
|
49
|
+
'helperText',
|
|
50
|
+
'bare',
|
|
51
|
+
'required',
|
|
52
|
+
'optional',
|
|
53
|
+
'value',
|
|
54
|
+
'onChange',
|
|
55
|
+
'minValue',
|
|
56
|
+
'maxValue',
|
|
57
|
+
'step',
|
|
58
|
+
'disabled',
|
|
59
|
+
'placeholder',
|
|
60
|
+
'size',
|
|
61
|
+
'showStepper',
|
|
62
|
+
'class',
|
|
63
|
+
'ref',
|
|
64
|
+
])
|
|
65
|
+
|
|
66
|
+
const hasError = () => !!local.error
|
|
67
|
+
const sc = () => inputSizeConfig[local.size ?? 'md']
|
|
68
|
+
|
|
69
|
+
const handleRawValueChange = (v: number | undefined) => {
|
|
70
|
+
local.onChange?.(v)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<div class={cn('w-full', local.class)}>
|
|
75
|
+
<KobalteNumberField
|
|
76
|
+
rawValue={local.value}
|
|
77
|
+
onRawValueChange={handleRawValueChange}
|
|
78
|
+
minValue={local.minValue}
|
|
79
|
+
maxValue={local.maxValue}
|
|
80
|
+
step={local.step}
|
|
81
|
+
disabled={local.disabled}
|
|
82
|
+
validationState={hasError() ? 'invalid' : undefined}
|
|
83
|
+
{...others}
|
|
84
|
+
>
|
|
85
|
+
<Show when={!local.bare && local.label}>
|
|
86
|
+
<div class="flex items-center justify-between mb-1.5">
|
|
87
|
+
<KobalteNumberField.Label
|
|
88
|
+
class={cn(
|
|
89
|
+
'block text-md font-medium',
|
|
90
|
+
hasError() ? 'text-danger-600' : 'text-ink-700'
|
|
91
|
+
)}
|
|
92
|
+
>
|
|
93
|
+
{local.label}
|
|
94
|
+
<Show when={local.required}>
|
|
95
|
+
<span class="text-danger-500 ml-0.5" aria-hidden="true">*</span>
|
|
96
|
+
</Show>
|
|
97
|
+
</KobalteNumberField.Label>
|
|
98
|
+
<Show when={local.label && !local.required && local.optional}>
|
|
99
|
+
<span class="text-xs text-ink-500">optional</span>
|
|
100
|
+
</Show>
|
|
101
|
+
</div>
|
|
102
|
+
</Show>
|
|
103
|
+
<div class="relative flex items-stretch">
|
|
104
|
+
<KobalteNumberField.Input
|
|
105
|
+
ref={local.ref}
|
|
106
|
+
placeholder={local.placeholder}
|
|
107
|
+
class={cn(
|
|
108
|
+
'w-full rounded-lg transition-all outline-none border text-ink-900 placeholder:text-ink-400 dark:placeholder:text-ink-500 bg-surface-raised',
|
|
109
|
+
sc().h, sc().py, sc().text,
|
|
110
|
+
local.showStepper ? sc().plAdorn : sc().pl, sc().pr,
|
|
111
|
+
hasError()
|
|
112
|
+
? 'border-danger-500 focus:ring-2 focus:ring-inset focus:ring-danger-500 focus:border-transparent'
|
|
113
|
+
: 'border-ink-300 focus:ring-2 focus:ring-inset focus:ring-primary-500 focus:border-transparent',
|
|
114
|
+
'disabled:bg-surface-base disabled:text-ink-500 dark:disabled:text-ink-500 disabled:cursor-not-allowed'
|
|
115
|
+
)}
|
|
116
|
+
/>
|
|
117
|
+
<Show when={local.showStepper}>
|
|
118
|
+
<div class="absolute right-0 top-0 bottom-0 flex flex-col border-l border-ink-300 rounded-r-lg overflow-hidden">
|
|
119
|
+
<KobalteNumberField.IncrementTrigger
|
|
120
|
+
class="flex-1 flex items-center justify-center min-h-0 px-2 text-ink-500 hover:bg-ink-100 dark:hover:bg-ink-800 disabled:opacity-50 disabled:cursor-not-allowed focus:outline-none focus:ring-2 focus:ring-inset focus:ring-primary-500"
|
|
121
|
+
>
|
|
122
|
+
<ChevronUp class="h-4 w-4" />
|
|
123
|
+
</KobalteNumberField.IncrementTrigger>
|
|
124
|
+
<KobalteNumberField.DecrementTrigger
|
|
125
|
+
class="flex-1 flex items-center justify-center min-h-0 px-2 text-ink-500 hover:bg-ink-100 dark:hover:bg-ink-800 disabled:opacity-50 disabled:cursor-not-allowed focus:outline-none focus:ring-2 focus:ring-inset focus:ring-primary-500"
|
|
126
|
+
>
|
|
127
|
+
<ChevronDown class="h-4 w-4" />
|
|
128
|
+
</KobalteNumberField.DecrementTrigger>
|
|
129
|
+
</div>
|
|
130
|
+
</Show>
|
|
131
|
+
</div>
|
|
132
|
+
<Show when={!local.bare && !hasError() && local.helperText}>
|
|
133
|
+
<KobalteNumberField.Description class="mt-2 text-sm text-ink-500">
|
|
134
|
+
{local.helperText}
|
|
135
|
+
</KobalteNumberField.Description>
|
|
136
|
+
</Show>
|
|
137
|
+
<Show when={!local.bare && hasError()}>
|
|
138
|
+
<KobalteNumberField.ErrorMessage class="mt-2 text-sm text-danger-600">
|
|
139
|
+
{local.error}
|
|
140
|
+
</KobalteNumberField.ErrorMessage>
|
|
141
|
+
</Show>
|
|
142
|
+
</KobalteNumberField>
|
|
143
|
+
</div>
|
|
144
|
+
)
|
|
145
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { For, splitProps, Show } from 'solid-js'
|
|
2
|
+
import { RadioGroup as KobalteRadioGroup } from '@kobalte/core/radio-group'
|
|
3
|
+
import { cn } from '../../utilities/classNames'
|
|
4
|
+
|
|
5
|
+
export interface RadioGroupOption {
|
|
6
|
+
value: string
|
|
7
|
+
label: string
|
|
8
|
+
/** Optional description for the option. */
|
|
9
|
+
description?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface RadioGroupProps {
|
|
13
|
+
/** Group label (e.g. "Choose one"). */
|
|
14
|
+
label?: string
|
|
15
|
+
/** Optional description below the label. */
|
|
16
|
+
description?: string
|
|
17
|
+
/** Error message and invalid styling. */
|
|
18
|
+
error?: string
|
|
19
|
+
/** When true, show required indicator on label. */
|
|
20
|
+
required?: boolean
|
|
21
|
+
/** Options to display (value + label, optional description). */
|
|
22
|
+
options: RadioGroupOption[]
|
|
23
|
+
/** Selected value (controlled). */
|
|
24
|
+
value?: string
|
|
25
|
+
/** Called when selection changes. */
|
|
26
|
+
onChange?: (value: string) => void
|
|
27
|
+
/** Disables all options. */
|
|
28
|
+
disabled?: boolean
|
|
29
|
+
/** Form name. */
|
|
30
|
+
name?: string
|
|
31
|
+
/** Layout: vertical list (default) or horizontal. */
|
|
32
|
+
orientation?: 'vertical' | 'horizontal'
|
|
33
|
+
/** Additional class for the root. */
|
|
34
|
+
class?: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function RadioGroup(props: RadioGroupProps) {
|
|
38
|
+
const [local, others] = splitProps(props, [
|
|
39
|
+
'label',
|
|
40
|
+
'description',
|
|
41
|
+
'error',
|
|
42
|
+
'required',
|
|
43
|
+
'options',
|
|
44
|
+
'value',
|
|
45
|
+
'onChange',
|
|
46
|
+
'disabled',
|
|
47
|
+
'name',
|
|
48
|
+
'orientation',
|
|
49
|
+
'class',
|
|
50
|
+
])
|
|
51
|
+
|
|
52
|
+
const hasError = () => !!local.error
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<div class={cn('w-full', local.class)}>
|
|
56
|
+
<KobalteRadioGroup
|
|
57
|
+
value={local.value}
|
|
58
|
+
onChange={local.onChange}
|
|
59
|
+
disabled={local.disabled}
|
|
60
|
+
validationState={hasError() ? 'invalid' : undefined}
|
|
61
|
+
name={local.name}
|
|
62
|
+
orientation={local.orientation ?? 'vertical'}
|
|
63
|
+
{...others}
|
|
64
|
+
>
|
|
65
|
+
<Show when={local.label}>
|
|
66
|
+
<KobalteRadioGroup.Label
|
|
67
|
+
class={cn(
|
|
68
|
+
'block text-md font-medium mb-2',
|
|
69
|
+
hasError() ? 'text-danger-600' : 'text-ink-700',
|
|
70
|
+
local.disabled && 'opacity-50'
|
|
71
|
+
)}
|
|
72
|
+
>
|
|
73
|
+
{local.label}
|
|
74
|
+
{local.required && <span class="text-danger-500 ml-0.5" aria-hidden="true">*</span>}
|
|
75
|
+
</KobalteRadioGroup.Label>
|
|
76
|
+
</Show>
|
|
77
|
+
<div
|
|
78
|
+
role="presentation"
|
|
79
|
+
class={cn(
|
|
80
|
+
'flex gap-3',
|
|
81
|
+
local.orientation === 'horizontal' ? 'flex-row flex-wrap' : 'flex-col'
|
|
82
|
+
)}
|
|
83
|
+
>
|
|
84
|
+
<For each={local.options}>
|
|
85
|
+
{(opt) => (
|
|
86
|
+
<KobalteRadioGroup.Item
|
|
87
|
+
value={opt.value}
|
|
88
|
+
class={cn(
|
|
89
|
+
'inline-flex items-start gap-3 cursor-pointer select-none rounded-lg border border-transparent p-3 transition-colors outline-none',
|
|
90
|
+
'data-[disabled]:opacity-50 data-[disabled]:cursor-not-allowed',
|
|
91
|
+
'data-[highlighted]:bg-ink-50 dark:data-[highlighted]:bg-ink-800/50',
|
|
92
|
+
'focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2'
|
|
93
|
+
)}
|
|
94
|
+
>
|
|
95
|
+
<KobalteRadioGroup.ItemInput class="sr-only" />
|
|
96
|
+
<KobalteRadioGroup.ItemControl
|
|
97
|
+
class={cn(
|
|
98
|
+
'mt-0.5 h-4 w-4 shrink-0 rounded-full flex items-center justify-center transition-colors box-border',
|
|
99
|
+
// Unselected: gray ring, transparent interior
|
|
100
|
+
'border-2 border-ink-400 bg-transparent',
|
|
101
|
+
// Selected: primary ring, transparent interior; dot is the ItemIndicator
|
|
102
|
+
'data-[checked]:border-primary-500 data-[checked]:bg-transparent',
|
|
103
|
+
'data-[disabled]:opacity-50'
|
|
104
|
+
)}
|
|
105
|
+
>
|
|
106
|
+
<KobalteRadioGroup.ItemIndicator class="h-2 w-2 rounded-full bg-primary-500 shrink-0 pointer-events-none" />
|
|
107
|
+
</KobalteRadioGroup.ItemControl>
|
|
108
|
+
<span class="flex flex-col gap-0.5">
|
|
109
|
+
<KobalteRadioGroup.ItemLabel class="text-sm font-medium text-ink-900">
|
|
110
|
+
{opt.label}
|
|
111
|
+
</KobalteRadioGroup.ItemLabel>
|
|
112
|
+
<Show when={opt.description}>
|
|
113
|
+
<KobalteRadioGroup.ItemDescription class="text-xs text-ink-500">
|
|
114
|
+
{opt.description}
|
|
115
|
+
</KobalteRadioGroup.ItemDescription>
|
|
116
|
+
</Show>
|
|
117
|
+
</span>
|
|
118
|
+
</KobalteRadioGroup.Item>
|
|
119
|
+
)}
|
|
120
|
+
</For>
|
|
121
|
+
</div>
|
|
122
|
+
<Show when={local.description && !hasError()}>
|
|
123
|
+
<KobalteRadioGroup.Description class="mt-2 text-sm text-ink-500">
|
|
124
|
+
{local.description}
|
|
125
|
+
</KobalteRadioGroup.Description>
|
|
126
|
+
</Show>
|
|
127
|
+
<Show when={hasError()}>
|
|
128
|
+
<KobalteRadioGroup.ErrorMessage class="mt-2 text-sm text-danger-600">
|
|
129
|
+
{local.error}
|
|
130
|
+
</KobalteRadioGroup.ErrorMessage>
|
|
131
|
+
</Show>
|
|
132
|
+
</KobalteRadioGroup>
|
|
133
|
+
</div>
|
|
134
|
+
)
|
|
135
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { createMemo } from 'solid-js'
|
|
2
|
+
import { Input, Select } from './'
|
|
3
|
+
import { parseRelativeDateDefault, formatRelativeDateDefault } from '../../utilities/relativeDateDefault'
|
|
4
|
+
import { cn } from '../../utilities/classNames'
|
|
5
|
+
|
|
6
|
+
const SIGN_OPTIONS = [
|
|
7
|
+
{ value: '+', label: '+' },
|
|
8
|
+
{ value: '-', label: '-' },
|
|
9
|
+
] as const
|
|
10
|
+
|
|
11
|
+
export interface RelativeDateDefaultInputProps {
|
|
12
|
+
value: string
|
|
13
|
+
onValueChange: (value: string) => void
|
|
14
|
+
/** Label before the controls (default "Today") */
|
|
15
|
+
prefixLabel?: string
|
|
16
|
+
/** Label after days (default "day(s)") */
|
|
17
|
+
suffixLabel?: string
|
|
18
|
+
class?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Today + sign (+/−) + integer days. Produces stored value like "today+0", "today-7". */
|
|
22
|
+
export function RelativeDateDefaultInput(props: RelativeDateDefaultInputProps) {
|
|
23
|
+
const parsed = createMemo(() => parseRelativeDateDefault(props.value))
|
|
24
|
+
const sign = () => parsed().sign
|
|
25
|
+
const days = () => parsed().days
|
|
26
|
+
|
|
27
|
+
const setSign = (s: string) => {
|
|
28
|
+
props.onValueChange(formatRelativeDateDefault((s as '+' | '-') || '+', days()))
|
|
29
|
+
}
|
|
30
|
+
const setDaysFromInput = (v: string) => {
|
|
31
|
+
const n = v === '' ? 0 : Math.max(0, parseInt(v, 10) || 0)
|
|
32
|
+
props.onValueChange(formatRelativeDateDefault(sign(), n))
|
|
33
|
+
}
|
|
34
|
+
const daysStr = () => String(days())
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<div class={cn('flex flex-nowrap items-center gap-2', props.class)}>
|
|
38
|
+
<span class="shrink-0 text-sm font-medium text-ink-700">
|
|
39
|
+
{props.prefixLabel ?? 'Today'}
|
|
40
|
+
</span>
|
|
41
|
+
<div class="flex shrink-0 items-center gap-2">
|
|
42
|
+
<Select
|
|
43
|
+
value={sign()}
|
|
44
|
+
onValueChange={setSign}
|
|
45
|
+
options={[...SIGN_OPTIONS]}
|
|
46
|
+
class="w-36 min-w-0 rounded-lg"
|
|
47
|
+
/>
|
|
48
|
+
<Input
|
|
49
|
+
bare
|
|
50
|
+
type="number"
|
|
51
|
+
min={0}
|
|
52
|
+
step={1}
|
|
53
|
+
value={daysStr()}
|
|
54
|
+
onValueChange={setDaysFromInput}
|
|
55
|
+
placeholder="0"
|
|
56
|
+
class="w-24 rounded-lg pr-2"
|
|
57
|
+
/>
|
|
58
|
+
</div>
|
|
59
|
+
<span class="shrink-0 text-sm text-ink-500">{props.suffixLabel ?? 'day(s)'}</span>
|
|
60
|
+
</div>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { For, Show, type JSX, splitProps } from 'solid-js'
|
|
2
|
+
import { ChevronDown, ChevronUp, GripVertical, X } from 'lucide-solid'
|
|
3
|
+
import {
|
|
4
|
+
DragDropProvider,
|
|
5
|
+
DragDropSensors,
|
|
6
|
+
SortableProvider,
|
|
7
|
+
createSortable,
|
|
8
|
+
type DragEvent as DnDDragEvent,
|
|
9
|
+
} from '@thisbeyond/solid-dnd'
|
|
10
|
+
import { cn } from '../../utilities/classNames'
|
|
11
|
+
|
|
12
|
+
export interface ReorderableListItem {
|
|
13
|
+
id: string
|
|
14
|
+
label: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ReorderableListProps extends JSX.HTMLAttributes<HTMLDivElement> {
|
|
18
|
+
items: ReorderableListItem[]
|
|
19
|
+
onReorder: (ids: string[]) => void
|
|
20
|
+
/** When true, show up/down arrow buttons to reorder. Default false (drag only). */
|
|
21
|
+
showMoveButtons?: boolean
|
|
22
|
+
onRemove?: (id: string) => void
|
|
23
|
+
class?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function SortableItem(props: {
|
|
27
|
+
item: ReorderableListItem
|
|
28
|
+
index: number
|
|
29
|
+
count: number
|
|
30
|
+
showMoveButtons: boolean
|
|
31
|
+
onRemove?: (id: string) => void
|
|
32
|
+
move: (id: string, direction: 'up' | 'down') => void
|
|
33
|
+
}) {
|
|
34
|
+
const sortable = createSortable(props.item.id)
|
|
35
|
+
|
|
36
|
+
const canMoveUp = () => props.index > 0
|
|
37
|
+
const canMoveDown = () => props.index < props.count - 1
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div
|
|
41
|
+
role="listitem"
|
|
42
|
+
ref={sortable.ref}
|
|
43
|
+
// @ts-expect-error solid-dnd directive not in JSX namespace
|
|
44
|
+
use:sortable
|
|
45
|
+
class={cn(
|
|
46
|
+
'flex items-center justify-between rounded-lg border border-surface-border bg-surface-raised px-4 py-3 text-sm transition-shadow',
|
|
47
|
+
sortable.isActiveDraggable && 'z-50 shadow-lg',
|
|
48
|
+
)}
|
|
49
|
+
>
|
|
50
|
+
<div class="flex items-center gap-2 text-ink-700">
|
|
51
|
+
<button
|
|
52
|
+
type="button"
|
|
53
|
+
class="inline-flex h-7 w-7 shrink-0 cursor-grab items-center justify-center rounded-md text-ink-400 hover:bg-ink-100 active:cursor-grabbing dark:hover:bg-ink-700"
|
|
54
|
+
aria-label={`Drag to reorder ${props.item.label}`}
|
|
55
|
+
{...sortable.dragActivators}
|
|
56
|
+
>
|
|
57
|
+
<GripVertical class="h-4 w-4" aria-hidden="true" />
|
|
58
|
+
</button>
|
|
59
|
+
<span>{props.item.label}</span>
|
|
60
|
+
</div>
|
|
61
|
+
<div class="flex shrink-0 items-center gap-1">
|
|
62
|
+
<Show when={props.showMoveButtons}>
|
|
63
|
+
<button
|
|
64
|
+
type="button"
|
|
65
|
+
disabled={!canMoveUp()}
|
|
66
|
+
class={cn(
|
|
67
|
+
'rounded-md p-1 text-ink-500',
|
|
68
|
+
canMoveUp() && 'hover:bg-ink-100 dark:hover:bg-ink-700',
|
|
69
|
+
!canMoveUp() && 'opacity-40 cursor-not-allowed',
|
|
70
|
+
)}
|
|
71
|
+
aria-label={`Move ${props.item.label} up`}
|
|
72
|
+
onClick={() => props.move(props.item.id, 'up')}
|
|
73
|
+
>
|
|
74
|
+
<ChevronUp class="h-4 w-4" aria-hidden="true" />
|
|
75
|
+
</button>
|
|
76
|
+
<button
|
|
77
|
+
type="button"
|
|
78
|
+
disabled={!canMoveDown()}
|
|
79
|
+
class={cn(
|
|
80
|
+
'rounded-md p-1 text-ink-500',
|
|
81
|
+
canMoveDown() && 'hover:bg-ink-100 dark:hover:bg-ink-700',
|
|
82
|
+
!canMoveDown() && 'opacity-40 cursor-not-allowed',
|
|
83
|
+
)}
|
|
84
|
+
aria-label={`Move ${props.item.label} down`}
|
|
85
|
+
onClick={() => props.move(props.item.id, 'down')}
|
|
86
|
+
>
|
|
87
|
+
<ChevronDown class="h-4 w-4" aria-hidden="true" />
|
|
88
|
+
</button>
|
|
89
|
+
</Show>
|
|
90
|
+
<Show when={props.onRemove}>
|
|
91
|
+
<button
|
|
92
|
+
type="button"
|
|
93
|
+
class="rounded-md p-1 text-ink-500 hover:bg-ink-100 dark:hover:bg-ink-700"
|
|
94
|
+
aria-label={`Remove ${props.item.label}`}
|
|
95
|
+
onClick={() => props.onRemove?.(props.item.id)}
|
|
96
|
+
>
|
|
97
|
+
<X class="h-4 w-4" aria-hidden="true" />
|
|
98
|
+
</button>
|
|
99
|
+
</Show>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function ReorderableListInner(props: ReorderableListProps) {
|
|
106
|
+
const [local, others] = splitProps(props, ['items', 'onReorder', 'onRemove', 'showMoveButtons', 'class'])
|
|
107
|
+
const showMoveButtons = () => local.showMoveButtons === true
|
|
108
|
+
|
|
109
|
+
const move = (id: string, direction: 'up' | 'down') => {
|
|
110
|
+
const ids = local.items.map((item) => item.id)
|
|
111
|
+
const index = ids.indexOf(id)
|
|
112
|
+
const nextIndex = direction === 'up' ? index - 1 : index + 1
|
|
113
|
+
if (index === -1 || nextIndex < 0 || nextIndex >= ids.length) return
|
|
114
|
+
const next = [...ids]
|
|
115
|
+
const [moved] = next.splice(index, 1)
|
|
116
|
+
next.splice(nextIndex, 0, moved)
|
|
117
|
+
local.onReorder(next)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<div role="list" class={cn('space-y-2', local.class)} {...others}>
|
|
122
|
+
<For each={local.items}>
|
|
123
|
+
{(item, index) => (
|
|
124
|
+
<SortableItem
|
|
125
|
+
item={item}
|
|
126
|
+
index={index()}
|
|
127
|
+
count={local.items.length}
|
|
128
|
+
showMoveButtons={showMoveButtons()}
|
|
129
|
+
onRemove={local.onRemove}
|
|
130
|
+
move={move}
|
|
131
|
+
/>
|
|
132
|
+
)}
|
|
133
|
+
</For>
|
|
134
|
+
</div>
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function ReorderableList(props: ReorderableListProps) {
|
|
139
|
+
const ids = () => props.items.map((i) => i.id)
|
|
140
|
+
|
|
141
|
+
const handleDragEnd = (event: DnDDragEvent) => {
|
|
142
|
+
const { draggable, droppable } = event
|
|
143
|
+
if (!droppable || draggable.id === droppable.id) return
|
|
144
|
+
const order = props.items.map((i) => i.id)
|
|
145
|
+
const fromIndex = order.indexOf(String(draggable.id))
|
|
146
|
+
const toIndex = order.indexOf(String(droppable.id))
|
|
147
|
+
if (fromIndex === -1 || toIndex === -1) return
|
|
148
|
+
const next = [...order]
|
|
149
|
+
const [moved] = next.splice(fromIndex, 1)
|
|
150
|
+
next.splice(toIndex, 0, moved)
|
|
151
|
+
// Defer so Solid DnD can clear transforms before we re-render
|
|
152
|
+
queueMicrotask(() => props.onReorder(next))
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return (
|
|
156
|
+
<DragDropProvider onDragEnd={handleDragEnd}>
|
|
157
|
+
<DragDropSensors />
|
|
158
|
+
<SortableProvider ids={ids()}>
|
|
159
|
+
<ReorderableListInner {...props} />
|
|
160
|
+
</SortableProvider>
|
|
161
|
+
</DragDropProvider>
|
|
162
|
+
)
|
|
163
|
+
}
|