@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,202 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type JSX,
|
|
3
|
+
splitProps,
|
|
4
|
+
Show,
|
|
5
|
+
createEffect,
|
|
6
|
+
onMount,
|
|
7
|
+
onCleanup,
|
|
8
|
+
createUniqueId,
|
|
9
|
+
} from 'solid-js'
|
|
10
|
+
import { TextField as KobalteTextField } from '@kobalte/core/text-field'
|
|
11
|
+
import { cn } from '../../utilities/classNames'
|
|
12
|
+
import { mergeRefs } from '../../utilities/mergeRefs'
|
|
13
|
+
|
|
14
|
+
export type TextAreaResize = 'none' | 'vertical' | 'horizontal' | 'both'
|
|
15
|
+
|
|
16
|
+
export interface TextAreaProps extends JSX.TextareaHTMLAttributes<HTMLTextAreaElement> {
|
|
17
|
+
label?: string
|
|
18
|
+
error?: string
|
|
19
|
+
helperText?: string
|
|
20
|
+
/** When true, never render label row or error/helper text (textarea only) */
|
|
21
|
+
bare?: boolean
|
|
22
|
+
required?: boolean
|
|
23
|
+
optional?: boolean
|
|
24
|
+
resize?: TextAreaResize
|
|
25
|
+
maxLength?: number
|
|
26
|
+
autoresize?: boolean
|
|
27
|
+
inputClass?: string
|
|
28
|
+
onValueChange?: (value: string) => void
|
|
29
|
+
onErrorClear?: () => void
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const resizeClasses: Record<TextAreaResize, string> = {
|
|
33
|
+
none: 'resize-none',
|
|
34
|
+
vertical: 'resize-y',
|
|
35
|
+
horizontal: 'resize-x',
|
|
36
|
+
both: 'resize',
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function TextArea(props: TextAreaProps) {
|
|
40
|
+
const [local, others] = splitProps(props, [
|
|
41
|
+
'label',
|
|
42
|
+
'error',
|
|
43
|
+
'helperText',
|
|
44
|
+
'bare',
|
|
45
|
+
'required',
|
|
46
|
+
'optional',
|
|
47
|
+
'resize',
|
|
48
|
+
'maxLength',
|
|
49
|
+
'autoresize',
|
|
50
|
+
'onValueChange',
|
|
51
|
+
'onErrorClear',
|
|
52
|
+
'class',
|
|
53
|
+
'inputClass',
|
|
54
|
+
'id',
|
|
55
|
+
'value',
|
|
56
|
+
'onInput',
|
|
57
|
+
'rows',
|
|
58
|
+
'ref',
|
|
59
|
+
'disabled',
|
|
60
|
+
])
|
|
61
|
+
|
|
62
|
+
// ✅ bare mode should still be named
|
|
63
|
+
if (import.meta.env.DEV && local.bare) {
|
|
64
|
+
if (
|
|
65
|
+
(others['aria-label'] as string | undefined) == null &&
|
|
66
|
+
(others['aria-labelledby'] as string | undefined) == null &&
|
|
67
|
+
(others['title'] as string | undefined) == null
|
|
68
|
+
) {
|
|
69
|
+
console.warn('TextArea: bare mode requires aria-label, aria-labelledby, or title for accessibility.')
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const hasError = () => !!local.error
|
|
74
|
+
|
|
75
|
+
const valueString = () =>
|
|
76
|
+
typeof local.value === 'string' ? local.value : local.value == null ? '' : String(local.value)
|
|
77
|
+
|
|
78
|
+
const currentLength = () => valueString().length
|
|
79
|
+
const maxLen = () => local.maxLength ?? 0
|
|
80
|
+
const atOrOverLimit = () => maxLen() > 0 && currentLength() >= maxLen()
|
|
81
|
+
const nearLimit = () => maxLen() > 0 && currentLength() >= maxLen() * 0.9 && currentLength() < maxLen()
|
|
82
|
+
const countColorClass = () =>
|
|
83
|
+
atOrOverLimit()
|
|
84
|
+
? 'text-danger-600 dark:text-danger-400'
|
|
85
|
+
: nearLimit()
|
|
86
|
+
? 'text-amber-600 dark:text-amber-400'
|
|
87
|
+
: 'text-ink-500'
|
|
88
|
+
|
|
89
|
+
let textareaRef: HTMLTextAreaElement | undefined
|
|
90
|
+
const resizeIfAutoresize = () => {
|
|
91
|
+
const el = textareaRef
|
|
92
|
+
if (!el || !local.autoresize) return
|
|
93
|
+
el.style.height = 'auto'
|
|
94
|
+
el.style.height = `${el.scrollHeight}px`
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
createEffect(() => {
|
|
98
|
+
if (!local.autoresize) return
|
|
99
|
+
valueString()
|
|
100
|
+
queueMicrotask(resizeIfAutoresize)
|
|
101
|
+
})
|
|
102
|
+
onMount(() => {
|
|
103
|
+
if (local.autoresize) queueMicrotask(resizeIfAutoresize)
|
|
104
|
+
})
|
|
105
|
+
onCleanup(() => {
|
|
106
|
+
if (textareaRef && local.autoresize) textareaRef.style.height = ''
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
const handleChange = (val: string) => {
|
|
110
|
+
if (local.error && local.onErrorClear) local.onErrorClear()
|
|
111
|
+
local.onValueChange?.(val)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const handleInput = (e: InputEvent) => {
|
|
115
|
+
if (local.onInput) (local.onInput as (e: InputEvent) => void)(e)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const resizeClass = () =>
|
|
119
|
+
local.autoresize ? 'resize-none' : resizeClasses[local.resize ?? 'vertical']
|
|
120
|
+
|
|
121
|
+
// ✅ link helper/error text to textarea (merges with any user aria-describedby)
|
|
122
|
+
const uid = createUniqueId()
|
|
123
|
+
const helperId = () => (!local.bare && local.helperText && !hasError() ? `ta-${uid}-help` : undefined)
|
|
124
|
+
const errorId = () => (!local.bare && local.error ? `ta-${uid}-error` : undefined)
|
|
125
|
+
const describedBy = () => {
|
|
126
|
+
const user = (others['aria-describedby'] as string | undefined)
|
|
127
|
+
const own = [helperId(), errorId()].filter(Boolean).join(' ')
|
|
128
|
+
return user && own ? `${user} ${own}` : (user ?? (own || undefined))
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
<KobalteTextField
|
|
133
|
+
value={valueString()}
|
|
134
|
+
onChange={handleChange}
|
|
135
|
+
validationState={hasError() ? 'invalid' : undefined}
|
|
136
|
+
required={local.required}
|
|
137
|
+
disabled={local.disabled}
|
|
138
|
+
class={cn('w-full', local.class)}
|
|
139
|
+
>
|
|
140
|
+
<Show when={!local.bare && local.label}>
|
|
141
|
+
<div class="flex items-center justify-between gap-2 mb-1.5">
|
|
142
|
+
<KobalteTextField.Label
|
|
143
|
+
class={cn(
|
|
144
|
+
'block text-md font-medium',
|
|
145
|
+
hasError() ? 'text-danger-600' : 'text-ink-700',
|
|
146
|
+
)}
|
|
147
|
+
>
|
|
148
|
+
{local.label}
|
|
149
|
+
<Show when={local.required}>
|
|
150
|
+
<span class="text-danger-500 dark:text-danger-400 ml-0.5" aria-hidden="true">*</span>
|
|
151
|
+
</Show>
|
|
152
|
+
</KobalteTextField.Label>
|
|
153
|
+
<Show when={local.label && !local.required && local.optional}>
|
|
154
|
+
<span class="text-xs text-ink-500">optional</span>
|
|
155
|
+
</Show>
|
|
156
|
+
</div>
|
|
157
|
+
</Show>
|
|
158
|
+
|
|
159
|
+
<KobalteTextField.TextArea
|
|
160
|
+
ref={mergeRefs((el: HTMLTextAreaElement) => (textareaRef = el), local.ref)}
|
|
161
|
+
id={local.id}
|
|
162
|
+
onInput={handleInput}
|
|
163
|
+
rows={local.rows ?? 3}
|
|
164
|
+
maxLength={local.maxLength}
|
|
165
|
+
aria-invalid={hasError() ? 'true' : undefined}
|
|
166
|
+
aria-describedby={describedBy()}
|
|
167
|
+
aria-errormessage={hasError() ? errorId() : undefined}
|
|
168
|
+
class={cn(
|
|
169
|
+
'w-full py-3 px-4 rounded-lg transition-all outline-none border text-base text-ink-900 placeholder:text-ink-400 dark:placeholder:text-ink-500 min-h-[80px] bg-surface-raised',
|
|
170
|
+
resizeClass(),
|
|
171
|
+
hasError()
|
|
172
|
+
? 'border-danger-500 focus:ring-2 focus:ring-inset focus:ring-danger-500 focus:border-transparent'
|
|
173
|
+
: 'border-ink-300 focus:ring-2 focus:ring-inset focus:ring-primary-500 focus:border-transparent',
|
|
174
|
+
'disabled:bg-surface-base disabled:text-ink-500 dark:disabled:text-ink-500 disabled:cursor-not-allowed',
|
|
175
|
+
local.inputClass,
|
|
176
|
+
)}
|
|
177
|
+
{...others}
|
|
178
|
+
/>
|
|
179
|
+
|
|
180
|
+
<Show when={!local.bare && local.maxLength != null && local.maxLength > 0}>
|
|
181
|
+
<div class="mt-1.5 flex items-center justify-between gap-2">
|
|
182
|
+
<span />
|
|
183
|
+
<span class={cn('text-xs tabular-nums', countColorClass())}>
|
|
184
|
+
{currentLength()}/{local.maxLength}
|
|
185
|
+
</span>
|
|
186
|
+
</div>
|
|
187
|
+
</Show>
|
|
188
|
+
|
|
189
|
+
<Show when={!local.bare && local.helperText && !hasError()}>
|
|
190
|
+
<KobalteTextField.Description id={helperId()} class="mt-2 text-sm text-ink-500">
|
|
191
|
+
{local.helperText}
|
|
192
|
+
</KobalteTextField.Description>
|
|
193
|
+
</Show>
|
|
194
|
+
|
|
195
|
+
<Show when={!local.bare && local.error}>
|
|
196
|
+
<KobalteTextField.ErrorMessage id={errorId()} class="mt-2 text-sm text-danger-600">
|
|
197
|
+
{local.error}
|
|
198
|
+
</KobalteTextField.ErrorMessage>
|
|
199
|
+
</Show>
|
|
200
|
+
</KobalteTextField>
|
|
201
|
+
)
|
|
202
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { JSX } from 'solid-js'
|
|
2
|
+
|
|
3
|
+
export interface ViewCustomizerField {
|
|
4
|
+
id: string
|
|
5
|
+
label: string
|
|
6
|
+
type: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface ViewCustomizerProps {
|
|
10
|
+
allFields: ViewCustomizerField[]
|
|
11
|
+
fields: ViewCustomizerField[]
|
|
12
|
+
value: any[]
|
|
13
|
+
onChange: (value: any[]) => void
|
|
14
|
+
viewName: string
|
|
15
|
+
onViewNameChange: (name: string) => void
|
|
16
|
+
usedFields: string[]
|
|
17
|
+
onUsedFieldsChange: (fields: string[]) => void
|
|
18
|
+
groupBy: string
|
|
19
|
+
onGroupByChange: (value: string) => void
|
|
20
|
+
mode: string
|
|
21
|
+
editingViewLabel?: string
|
|
22
|
+
onSaveChanges: () => void
|
|
23
|
+
onSaveAsNew: () => void
|
|
24
|
+
saveError?: string | null
|
|
25
|
+
onCancel: () => void
|
|
26
|
+
onClose: () => void
|
|
27
|
+
canDelete: boolean
|
|
28
|
+
onDelete: () => void
|
|
29
|
+
scope: string
|
|
30
|
+
onScopeChange: (scope: string) => void
|
|
31
|
+
canSetGlobalScope: boolean
|
|
32
|
+
pinned: boolean
|
|
33
|
+
onPinnedChange: (pinned: boolean) => void
|
|
34
|
+
title: string
|
|
35
|
+
description: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function ViewCustomizer(props: ViewCustomizerProps): JSX.Element {
|
|
39
|
+
return (
|
|
40
|
+
<div class="p-4 border rounded-md">
|
|
41
|
+
<div class="text-sm text-gray-600">ViewCustomizer placeholder</div>
|
|
42
|
+
</div>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** Form controls: Input, TextArea, Select, Autocomplete, MultiSelect, Checkbox, Switch, RadioGroup, NumberField, CodeInput, Slider, FileUpload, DatePicker, ColorPicker */
|
|
2
|
+
export { Input, type InputProps } from './Input'
|
|
3
|
+
export { TextArea, type TextAreaProps, type TextAreaResize } from './TextArea'
|
|
4
|
+
export { Select, type SelectProps, type SelectOption } from './Select'
|
|
5
|
+
export { Autocomplete, type AutocompleteProps, type AutocompleteOption } from './Autocomplete'
|
|
6
|
+
export { MultiSelect, type MultiSelectProps, type MultiSelectOption } from './MultiSelect'
|
|
7
|
+
export { Checkbox, type CheckboxProps, type CheckboxSize } from './Checkbox'
|
|
8
|
+
export { Switch, type SwitchProps } from './Switch'
|
|
9
|
+
export { RadioGroup, type RadioGroupProps, type RadioGroupOption } from './RadioGroup'
|
|
10
|
+
export { NumberField, type NumberFieldProps } from './NumberField'
|
|
11
|
+
export { CodeInput, type CodeInputProps } from './CodeInput'
|
|
12
|
+
export { Slider, type SliderProps } from './Slider'
|
|
13
|
+
export {
|
|
14
|
+
FileUpload,
|
|
15
|
+
type FileUploadProps,
|
|
16
|
+
type FileUploadItem,
|
|
17
|
+
type FileUploadVariant,
|
|
18
|
+
} from './FileUpload'
|
|
19
|
+
export { DatePicker, type DatePickerProps } from './DatePicker'
|
|
20
|
+
export { DateRangePicker, type DateRangePickerProps } from './DateRangePicker'
|
|
21
|
+
export {
|
|
22
|
+
ColorPicker,
|
|
23
|
+
type ColorPickerProps,
|
|
24
|
+
type ColorFormat,
|
|
25
|
+
} from './ColorPicker'
|
|
26
|
+
|
|
27
|
+
/** Form utilities: FieldPicker, ReorderableList, RelativeDateDefaultInput */
|
|
28
|
+
export { FieldPicker, type FieldPickerProps, type FieldPickerOption } from './FieldPicker'
|
|
29
|
+
|
|
30
|
+
export {
|
|
31
|
+
ReorderableList,
|
|
32
|
+
type ReorderableListProps,
|
|
33
|
+
type ReorderableListItem,
|
|
34
|
+
} from './ReorderableList'
|
|
35
|
+
|
|
36
|
+
export {
|
|
37
|
+
RelativeDateDefaultInput,
|
|
38
|
+
type RelativeDateDefaultInputProps,
|
|
39
|
+
} from './RelativeDateDefaultInput'
|
|
40
|
+
|
|
41
|
+
/** Filter and view components */
|
|
42
|
+
export { FilterBuilder, type FilterBuilderProps } from './FilterBuilder'
|
|
43
|
+
export { ViewCustomizer, type ViewCustomizerProps, type ViewCustomizerField } from './ViewCustomizer'
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { type JSX, splitProps } from 'solid-js'
|
|
2
|
+
import { Accordion as KobalteAccordion } from '@kobalte/core/accordion'
|
|
3
|
+
import type { AccordionContentProps as KobalteAccordionContentProps, AccordionTriggerProps as KobalteAccordionTriggerProps, AccordionItemProps as KobalteAccordionItemProps } from '@kobalte/core/accordion'
|
|
4
|
+
import { ChevronDown } from 'lucide-solid'
|
|
5
|
+
import { cn } from '../../utilities/classNames'
|
|
6
|
+
|
|
7
|
+
export const AccordionRoot = KobalteAccordion
|
|
8
|
+
export const AccordionItem = KobalteAccordion.Item
|
|
9
|
+
export const AccordionHeader = KobalteAccordion.Header
|
|
10
|
+
export const AccordionTrigger = KobalteAccordion.Trigger
|
|
11
|
+
export const AccordionContent = KobalteAccordion.Content
|
|
12
|
+
|
|
13
|
+
const accordionContentStyles = `
|
|
14
|
+
.accordion-content[data-expanded] {
|
|
15
|
+
animation: torchui-accordion-down 200ms ease-out;
|
|
16
|
+
}
|
|
17
|
+
.accordion-content[data-closed] {
|
|
18
|
+
animation: torchui-accordion-up 200ms ease-out;
|
|
19
|
+
}
|
|
20
|
+
@keyframes torchui-accordion-down {
|
|
21
|
+
from { height: 0; opacity: 0; }
|
|
22
|
+
to { height: var(--kb-accordion-content-height); opacity: 1; }
|
|
23
|
+
}
|
|
24
|
+
@keyframes torchui-accordion-up {
|
|
25
|
+
from { height: var(--kb-accordion-content-height); opacity: 1; }
|
|
26
|
+
to { height: 0; opacity: 0; }
|
|
27
|
+
}
|
|
28
|
+
`
|
|
29
|
+
|
|
30
|
+
export interface AccordionContentProps extends KobalteAccordionContentProps {
|
|
31
|
+
class?: string
|
|
32
|
+
children?: JSX.Element
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const ACCORDION_STYLE_ID = 'torchui-accordion-styles'
|
|
36
|
+
|
|
37
|
+
function ensureAccordionStyles() {
|
|
38
|
+
if (typeof document === 'undefined') return
|
|
39
|
+
if (document.getElementById(ACCORDION_STYLE_ID)) return
|
|
40
|
+
const style = document.createElement('style')
|
|
41
|
+
style.id = ACCORDION_STYLE_ID
|
|
42
|
+
style.textContent = accordionContentStyles
|
|
43
|
+
document.head.appendChild(style)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (typeof document !== 'undefined') ensureAccordionStyles()
|
|
47
|
+
|
|
48
|
+
export function AccordionContentStyled(props: AccordionContentProps) {
|
|
49
|
+
const [local, others] = splitProps(props, ['class', 'children'])
|
|
50
|
+
return (
|
|
51
|
+
<KobalteAccordion.Content
|
|
52
|
+
class={cn(
|
|
53
|
+
'accordion-content overflow-hidden border-b border-surface-border bg-surface-base/50',
|
|
54
|
+
local.class
|
|
55
|
+
)}
|
|
56
|
+
{...others}
|
|
57
|
+
>
|
|
58
|
+
<div class="px-4 py-3 text-sm text-ink-700">{local.children}</div>
|
|
59
|
+
</KobalteAccordion.Content>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface AccordionTriggerStyledProps extends KobalteAccordionTriggerProps {
|
|
64
|
+
class?: string
|
|
65
|
+
children?: JSX.Element
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function AccordionTriggerStyled(props: AccordionTriggerStyledProps) {
|
|
69
|
+
const [local, others] = splitProps(props, ['class', 'children'])
|
|
70
|
+
return (
|
|
71
|
+
<KobalteAccordion.Header as="h3" class="flex">
|
|
72
|
+
<KobalteAccordion.Trigger
|
|
73
|
+
class={cn(
|
|
74
|
+
'flex flex-1 items-center justify-between gap-2 rounded-t-lg px-4 py-3 text-left text-sm font-medium text-ink-800',
|
|
75
|
+
'hover:bg-ink-100 dark:hover:bg-ink-800',
|
|
76
|
+
'data-[expanded]:rounded-b-none data-[expanded]:bg-ink-100 dark:data-[expanded]:bg-ink-800',
|
|
77
|
+
'data-[expanded]:[&>svg]:rotate-180',
|
|
78
|
+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500',
|
|
79
|
+
'data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
|
80
|
+
local.class
|
|
81
|
+
)}
|
|
82
|
+
{...others}
|
|
83
|
+
>
|
|
84
|
+
{local.children}
|
|
85
|
+
<ChevronDown class="h-4 w-4 shrink-0 transition-transform duration-200" aria-hidden="true" />
|
|
86
|
+
</KobalteAccordion.Trigger>
|
|
87
|
+
</KobalteAccordion.Header>
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface AccordionItemStyledProps extends KobalteAccordionItemProps {
|
|
92
|
+
class?: string
|
|
93
|
+
children?: JSX.Element
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function AccordionItemStyled(props: AccordionItemStyledProps) {
|
|
97
|
+
const [local, others] = splitProps(props, ['class', 'children'])
|
|
98
|
+
return (
|
|
99
|
+
<KobalteAccordion.Item
|
|
100
|
+
class={cn(
|
|
101
|
+
'w-full rounded-lg border border-surface-border',
|
|
102
|
+
'data-[disabled]:opacity-60 data-[disabled]:cursor-not-allowed',
|
|
103
|
+
local.class
|
|
104
|
+
)}
|
|
105
|
+
{...others}
|
|
106
|
+
>
|
|
107
|
+
{local.children}
|
|
108
|
+
</KobalteAccordion.Item>
|
|
109
|
+
)
|
|
110
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import type { JSX } from 'solid-js'
|
|
2
|
+
import { splitProps } from 'solid-js'
|
|
3
|
+
import { cn } from '../../utilities/classNames'
|
|
4
|
+
|
|
5
|
+
export type AlertStatus = 'error' | 'success' | 'warning' | 'info'
|
|
6
|
+
|
|
7
|
+
export type AlertAppearance = 'subtle' | 'solid' | 'outline' | 'transparent'
|
|
8
|
+
|
|
9
|
+
export interface AlertProps extends Omit<JSX.HTMLAttributes<HTMLDivElement>, 'children' | 'title'> {
|
|
10
|
+
/** Semantic status: error, success, warning, or info. Ignored when colorClass is set. */
|
|
11
|
+
status?: AlertStatus
|
|
12
|
+
/** Visual style: subtle (default), solid, outline, or transparent. */
|
|
13
|
+
appearance?: AlertAppearance
|
|
14
|
+
/** Optional icon (e.g. from lucide-solid) shown at the start. */
|
|
15
|
+
icon?: JSX.Element
|
|
16
|
+
/** When true, shows a close button and calls onClose when clicked. */
|
|
17
|
+
closeable?: boolean
|
|
18
|
+
/** Called when the close button is clicked. Required when closeable is true. */
|
|
19
|
+
onClose?: () => void
|
|
20
|
+
/** Override default status colors. Provide Tailwind classes for border, background, and text (include dark: variants). */
|
|
21
|
+
colorClass?: string
|
|
22
|
+
/** Controls the ARIA live region behavior. 'assertive' → role="alert" (interrupts SR), 'polite' → role="status" (queued), 'off' → no role. Default 'polite'. */
|
|
23
|
+
ariaLive?: 'polite' | 'assertive' | 'off'
|
|
24
|
+
/** Optional CTAs (e.g. Resend, Upgrade) rendered at the end of the alert. */
|
|
25
|
+
actions?: JSX.Element
|
|
26
|
+
/** Optional heading (e.g. "Error" or "Session expired") shown above the message. */
|
|
27
|
+
title?: JSX.Element
|
|
28
|
+
class?: string
|
|
29
|
+
children?: JSX.Element
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type StatusAppearanceMap = Record<AlertStatus, Record<AlertAppearance, string>>
|
|
33
|
+
|
|
34
|
+
const statusAppearanceClasses: StatusAppearanceMap = {
|
|
35
|
+
error: {
|
|
36
|
+
subtle:
|
|
37
|
+
'border-danger-200 bg-danger-50 text-danger-900 dark:border-danger-800 dark:bg-danger-950 dark:text-danger-100',
|
|
38
|
+
solid:
|
|
39
|
+
'border-danger-500 bg-danger-500 text-white dark:border-danger-600 dark:bg-danger-600',
|
|
40
|
+
outline:
|
|
41
|
+
'border-danger-400 bg-transparent text-danger-700 dark:border-danger-500 dark:bg-transparent dark:text-danger-300',
|
|
42
|
+
transparent:
|
|
43
|
+
'border-transparent bg-danger-50 text-danger-800 dark:border-transparent dark:bg-danger-950 dark:text-danger-200',
|
|
44
|
+
},
|
|
45
|
+
success: {
|
|
46
|
+
subtle:
|
|
47
|
+
'border-success-200 bg-success-50 text-success-900 dark:border-success-800 dark:bg-success-950 dark:text-success-100',
|
|
48
|
+
solid:
|
|
49
|
+
'border-success-500 bg-success-500 text-white dark:border-success-600 dark:bg-success-600',
|
|
50
|
+
outline:
|
|
51
|
+
'border-success-400 bg-transparent text-success-700 dark:border-success-500 dark:bg-transparent dark:text-success-300',
|
|
52
|
+
transparent:
|
|
53
|
+
'border-transparent bg-success-50 text-success-800 dark:border-transparent dark:bg-success-950 dark:text-success-200',
|
|
54
|
+
},
|
|
55
|
+
warning: {
|
|
56
|
+
subtle:
|
|
57
|
+
'border-warning-200 bg-warning-50 text-warning-900 dark:border-warning-800 dark:bg-warning-950 dark:text-warning-100',
|
|
58
|
+
solid:
|
|
59
|
+
'border-warning-400 bg-warning-400 text-ink-900 dark:border-warning-500 dark:bg-warning-500',
|
|
60
|
+
outline:
|
|
61
|
+
'border-warning-400 bg-transparent text-warning-700 dark:border-warning-500 dark:bg-transparent dark:text-warning-300',
|
|
62
|
+
transparent:
|
|
63
|
+
'border-transparent bg-warning-50 text-warning-800 dark:border-transparent dark:bg-warning-950 dark:text-warning-200',
|
|
64
|
+
},
|
|
65
|
+
info: {
|
|
66
|
+
subtle:
|
|
67
|
+
'border-info-200 bg-info-50 text-info-900 dark:border-info-800 dark:bg-info-950 dark:text-info-100',
|
|
68
|
+
solid:
|
|
69
|
+
'border-info-500 bg-info-500 text-white dark:border-info-600 dark:bg-info-600',
|
|
70
|
+
outline:
|
|
71
|
+
'border-info-400 bg-transparent text-info-700 dark:border-info-500 dark:bg-transparent dark:text-info-300',
|
|
72
|
+
transparent:
|
|
73
|
+
'border-transparent bg-info-50 text-info-800 dark:border-transparent dark:bg-info-950 dark:text-info-200',
|
|
74
|
+
},
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Inline alert banner with status, appearance, optional icon, close, and CTAs. */
|
|
78
|
+
export function Alert(props: AlertProps): JSX.Element {
|
|
79
|
+
const [local, others] = splitProps(props, [
|
|
80
|
+
'status',
|
|
81
|
+
'appearance',
|
|
82
|
+
'icon',
|
|
83
|
+
'closeable',
|
|
84
|
+
'onClose',
|
|
85
|
+
'ariaLive',
|
|
86
|
+
'role',
|
|
87
|
+
'colorClass',
|
|
88
|
+
'actions',
|
|
89
|
+
'title',
|
|
90
|
+
'class',
|
|
91
|
+
'children',
|
|
92
|
+
'ref',
|
|
93
|
+
])
|
|
94
|
+
const status = () => local.status ?? 'error'
|
|
95
|
+
const appearance = () => local.appearance ?? 'subtle'
|
|
96
|
+
const colorClasses = () =>
|
|
97
|
+
local.colorClass ?? statusAppearanceClasses[status()][appearance()]
|
|
98
|
+
|
|
99
|
+
if (import.meta.env.DEV && local.closeable && !local.onClose) {
|
|
100
|
+
console.warn('Alert: closeable is true but onClose is not provided.')
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const liveMode = () => local.ariaLive ?? 'polite'
|
|
104
|
+
const roleAttr = () => {
|
|
105
|
+
const m = liveMode()
|
|
106
|
+
return m === 'assertive' ? 'alert' : m === 'polite' ? 'status' : undefined
|
|
107
|
+
}
|
|
108
|
+
const role = () => local.role ?? roleAttr()
|
|
109
|
+
const ariaLiveAttr = () => liveMode() === 'off' ? undefined : liveMode()
|
|
110
|
+
const showClose = () => local.closeable === true && local.onClose != null
|
|
111
|
+
const hasActions = () => showClose() || local.actions != null
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<div
|
|
115
|
+
ref={local.ref}
|
|
116
|
+
class={cn(
|
|
117
|
+
'flex w-full items-center gap-3 rounded-lg border px-4 py-3 text-sm',
|
|
118
|
+
colorClasses(),
|
|
119
|
+
local.class,
|
|
120
|
+
)}
|
|
121
|
+
{...others}
|
|
122
|
+
role={role()}
|
|
123
|
+
aria-live={ariaLiveAttr()}
|
|
124
|
+
>
|
|
125
|
+
{local.icon != null && (
|
|
126
|
+
<span class="shrink-0 [&>svg]:size-4" aria-hidden="true">
|
|
127
|
+
{local.icon}
|
|
128
|
+
</span>
|
|
129
|
+
)}
|
|
130
|
+
<div class="min-w-0 flex-1">
|
|
131
|
+
{local.title != null && (
|
|
132
|
+
<div class="font-semibold">{local.title}</div>
|
|
133
|
+
)}
|
|
134
|
+
{local.title != null && local.children != null && <div class="mt-0.5" />}
|
|
135
|
+
{local.children}
|
|
136
|
+
</div>
|
|
137
|
+
{hasActions() && (
|
|
138
|
+
<div class="flex shrink-0 items-center gap-2">
|
|
139
|
+
{local.actions}
|
|
140
|
+
{showClose() && (
|
|
141
|
+
<button
|
|
142
|
+
type="button"
|
|
143
|
+
onClick={local.onClose}
|
|
144
|
+
class="rounded p-1 opacity-70 hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-current focus:ring-offset-1 focus:ring-offset-transparent"
|
|
145
|
+
aria-label="Close"
|
|
146
|
+
>
|
|
147
|
+
<svg class="size-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
|
148
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
|
149
|
+
</svg>
|
|
150
|
+
</button>
|
|
151
|
+
)}
|
|
152
|
+
</div>
|
|
153
|
+
)}
|
|
154
|
+
</div>
|
|
155
|
+
)
|
|
156
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { JSX } from 'solid-js'
|
|
2
|
+
import { cn } from '../../utilities/classNames'
|
|
3
|
+
|
|
4
|
+
export type BlockQuoteJustify = 'start' | 'center' | 'end'
|
|
5
|
+
|
|
6
|
+
export interface BlockQuoteProps {
|
|
7
|
+
/** Quote content (paragraph(s) or text). */
|
|
8
|
+
children: JSX.Element | string
|
|
9
|
+
/** Optional attribution (e.g. "— Author Name"). Rendered in a footer. */
|
|
10
|
+
citation?: string | JSX.Element
|
|
11
|
+
/** Optional cite URL for the blockquote source. Maps to the cite attribute on <blockquote>. */
|
|
12
|
+
cite?: string
|
|
13
|
+
/** Optional icon (e.g. Quote from lucide-solid) shown at the start of the quote. */
|
|
14
|
+
icon?: JSX.Element
|
|
15
|
+
/** Optional avatar (e.g. Avatar or img) shown to the left of the quote. */
|
|
16
|
+
avatar?: JSX.Element
|
|
17
|
+
/** Text alignment: start (left), center, or end (right). Default: start. */
|
|
18
|
+
justify?: BlockQuoteJustify
|
|
19
|
+
/** When true, omit the left border. Default: false. */
|
|
20
|
+
noBorder?: boolean
|
|
21
|
+
/** Optional class for the blockquote root. */
|
|
22
|
+
class?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const justifyClass: Record<BlockQuoteJustify, string> = {
|
|
26
|
+
start: 'text-start',
|
|
27
|
+
center: 'text-center',
|
|
28
|
+
end: 'text-end',
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Semantic blockquote with optional icon, avatar, citation, and justification. */
|
|
32
|
+
export function BlockQuote(props: BlockQuoteProps): JSX.Element {
|
|
33
|
+
const justify = () => props.justify ?? 'start'
|
|
34
|
+
const alignClass = () => justifyClass[justify()]
|
|
35
|
+
const hasCitation = () =>
|
|
36
|
+
props.citation != null &&
|
|
37
|
+
(typeof props.citation !== 'string' || props.citation.trim() !== '')
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<blockquote
|
|
41
|
+
cite={props.cite}
|
|
42
|
+
class={cn(
|
|
43
|
+
'text-ink-700',
|
|
44
|
+
!props.noBorder && 'border-l-4 border-primary-500 pl-4',
|
|
45
|
+
props.class
|
|
46
|
+
)}
|
|
47
|
+
>
|
|
48
|
+
<div class={cn('flex gap-3', props.avatar && 'items-start')}>
|
|
49
|
+
{props.avatar && (
|
|
50
|
+
<div class="shrink-0" aria-hidden="true">
|
|
51
|
+
{props.avatar}
|
|
52
|
+
</div>
|
|
53
|
+
)}
|
|
54
|
+
<div class={cn('min-w-0 flex-1', alignClass())}>
|
|
55
|
+
{props.icon && (
|
|
56
|
+
<div class="mb-2 text-primary-500 [&>svg]:h-8 [&>svg]:w-8" aria-hidden="true">
|
|
57
|
+
{props.icon}
|
|
58
|
+
</div>
|
|
59
|
+
)}
|
|
60
|
+
<div class="[&>p]:mb-2 [&>p:last-child]:mb-0">{props.children}</div>
|
|
61
|
+
{hasCitation() && (
|
|
62
|
+
<footer class="mt-2 text-sm text-ink-500">
|
|
63
|
+
{props.citation}
|
|
64
|
+
</footer>
|
|
65
|
+
)}
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
</blockquote>
|
|
69
|
+
)
|
|
70
|
+
}
|