@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,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
+ }