@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,338 @@
|
|
|
1
|
+
import { type JSX, Show, splitProps } from 'solid-js'
|
|
2
|
+
|
|
3
|
+
import { cn } from '../../utilities/classNames'
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
export interface StatCardProps extends JSX.HTMLAttributes<HTMLDivElement> {
|
|
8
|
+
|
|
9
|
+
label: string
|
|
10
|
+
|
|
11
|
+
/** Optional subtitle or category below the label (e.g. "Travel and tourism"). */
|
|
12
|
+
|
|
13
|
+
subtitle?: string
|
|
14
|
+
|
|
15
|
+
/** Optional icon (e.g. app logo) shown at top left. */
|
|
16
|
+
|
|
17
|
+
icon?: JSX.Element
|
|
18
|
+
|
|
19
|
+
/** Accessible label for the icon when it conveys meaning. When set, icon wrapper gets role="img" + aria-label. When omitted, the wrapper is transparent — the icon child controls its own accessibility. */
|
|
20
|
+
|
|
21
|
+
iconLabel?: string
|
|
22
|
+
|
|
23
|
+
/** Optional content at top right (e.g. Tag or "Connect" button). */
|
|
24
|
+
|
|
25
|
+
topRight?: JSX.Element
|
|
26
|
+
|
|
27
|
+
value?: string | number | null
|
|
28
|
+
|
|
29
|
+
helperText?: string
|
|
30
|
+
|
|
31
|
+
trendLabel?: string
|
|
32
|
+
|
|
33
|
+
/** Default: 'positive' (emerald). */
|
|
34
|
+
|
|
35
|
+
trendVariant?: 'positive' | 'neutral' | 'negative'
|
|
36
|
+
|
|
37
|
+
trendIcon?: JSX.Element
|
|
38
|
+
|
|
39
|
+
emptyText?: string
|
|
40
|
+
|
|
41
|
+
/** Optional chart or sparkline. Use chartPosition to place under the trend or to the right. */
|
|
42
|
+
|
|
43
|
+
chart?: JSX.Element
|
|
44
|
+
|
|
45
|
+
/** Where to render the chart: under the trend (default) or in a column to the right of the value/trend. */
|
|
46
|
+
|
|
47
|
+
chartPosition?: 'under' | 'right'
|
|
48
|
+
|
|
49
|
+
/** When set, the chart wrapper gets role="img" + aria-label instead of aria-hidden. The chart child should be decorative (aria-hidden) to avoid duplicate announcements. */
|
|
50
|
+
|
|
51
|
+
chartA11yLabel?: string
|
|
52
|
+
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
export function StatCard(props: StatCardProps) {
|
|
58
|
+
|
|
59
|
+
const [local, others] = splitProps(props, [
|
|
60
|
+
|
|
61
|
+
'label',
|
|
62
|
+
|
|
63
|
+
'subtitle',
|
|
64
|
+
|
|
65
|
+
'icon',
|
|
66
|
+
|
|
67
|
+
'topRight',
|
|
68
|
+
|
|
69
|
+
'value',
|
|
70
|
+
|
|
71
|
+
'helperText',
|
|
72
|
+
|
|
73
|
+
'trendLabel',
|
|
74
|
+
|
|
75
|
+
'trendVariant',
|
|
76
|
+
|
|
77
|
+
'trendIcon',
|
|
78
|
+
|
|
79
|
+
'emptyText',
|
|
80
|
+
|
|
81
|
+
'chart',
|
|
82
|
+
|
|
83
|
+
'chartPosition',
|
|
84
|
+
|
|
85
|
+
'chartA11yLabel',
|
|
86
|
+
|
|
87
|
+
'iconLabel',
|
|
88
|
+
|
|
89
|
+
'class',
|
|
90
|
+
|
|
91
|
+
])
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
const chartOnRight = () => local.chart != null && local.chartPosition === 'right'
|
|
96
|
+
|
|
97
|
+
const hasHeaderContent = () => local.icon != null || local.topRight != null
|
|
98
|
+
|
|
99
|
+
const contentMt = () => (hasHeaderContent() ? 'mt-5' : 'mt-2')
|
|
100
|
+
|
|
101
|
+
const hasValue = () => local.value != null && local.value !== ''
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
// Default: positive (emerald)
|
|
106
|
+
|
|
107
|
+
const trendClasses = () => {
|
|
108
|
+
|
|
109
|
+
if (local.trendVariant === 'negative') return 'text-red-600 dark:text-red-400'
|
|
110
|
+
|
|
111
|
+
if (local.trendVariant === 'neutral') return 'text-ink-600'
|
|
112
|
+
|
|
113
|
+
return 'text-emerald-600 dark:text-emerald-400'
|
|
114
|
+
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
/** Value or empty-state placeholder. `compact` uses text-2xl sm:text-3xl for the right-chart layout. */
|
|
120
|
+
|
|
121
|
+
const ValueOrEmpty = (p: { compact?: boolean }) => (
|
|
122
|
+
|
|
123
|
+
<Show
|
|
124
|
+
|
|
125
|
+
when={hasValue()}
|
|
126
|
+
|
|
127
|
+
fallback={
|
|
128
|
+
|
|
129
|
+
<div class="text-xs font-normal text-ink-400">
|
|
130
|
+
|
|
131
|
+
{local.emptyText ?? 'No data yet'}
|
|
132
|
+
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
>
|
|
138
|
+
|
|
139
|
+
<div class={cn(
|
|
140
|
+
|
|
141
|
+
'font-bold tracking-tight text-ink-900',
|
|
142
|
+
|
|
143
|
+
p.compact ? 'text-2xl sm:text-3xl' : 'text-3xl',
|
|
144
|
+
|
|
145
|
+
)}>
|
|
146
|
+
|
|
147
|
+
{local.value}
|
|
148
|
+
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
</Show>
|
|
152
|
+
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
/** Helper text + trend label/icon. `gap` controls top margin between value and trend. */
|
|
158
|
+
|
|
159
|
+
const TrendBlock = (p: { gap?: string }) => (
|
|
160
|
+
|
|
161
|
+
<>
|
|
162
|
+
|
|
163
|
+
<Show when={local.helperText}>
|
|
164
|
+
|
|
165
|
+
<div class="mt-1 text-sm text-ink-500">{local.helperText}</div>
|
|
166
|
+
|
|
167
|
+
</Show>
|
|
168
|
+
|
|
169
|
+
<Show when={local.trendLabel}>
|
|
170
|
+
|
|
171
|
+
<div class={cn(p.gap ?? 'mt-3', 'flex items-center gap-1.5 text-sm font-medium')}>
|
|
172
|
+
|
|
173
|
+
<Show when={local.trendIcon}>
|
|
174
|
+
|
|
175
|
+
<span class={cn('flex shrink-0', trendClasses())}>{local.trendIcon}</span>
|
|
176
|
+
|
|
177
|
+
</Show>
|
|
178
|
+
|
|
179
|
+
<span class={cn(trendClasses())}>{local.trendLabel}</span>
|
|
180
|
+
|
|
181
|
+
</div>
|
|
182
|
+
|
|
183
|
+
</Show>
|
|
184
|
+
|
|
185
|
+
</>
|
|
186
|
+
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
return (
|
|
192
|
+
|
|
193
|
+
<div
|
|
194
|
+
|
|
195
|
+
class={cn(
|
|
196
|
+
|
|
197
|
+
'rounded-2xl border border-surface-border bg-surface-raised p-5 shadow-sm',
|
|
198
|
+
|
|
199
|
+
local.class,
|
|
200
|
+
|
|
201
|
+
)}
|
|
202
|
+
|
|
203
|
+
{...others}
|
|
204
|
+
|
|
205
|
+
>
|
|
206
|
+
|
|
207
|
+
{/* Top row: optional icon + label, optional topRight */}
|
|
208
|
+
|
|
209
|
+
<div class="flex items-start justify-between gap-3">
|
|
210
|
+
|
|
211
|
+
<div class="min-w-0 flex-1">
|
|
212
|
+
|
|
213
|
+
<div class="flex items-center gap-2">
|
|
214
|
+
|
|
215
|
+
<Show when={local.icon}>
|
|
216
|
+
|
|
217
|
+
<span
|
|
218
|
+
|
|
219
|
+
class="flex h-9 w-9 shrink-0 items-center justify-center overflow-hidden rounded-lg text-[0]"
|
|
220
|
+
|
|
221
|
+
role={local.iconLabel ? 'img' : undefined}
|
|
222
|
+
|
|
223
|
+
aria-label={local.iconLabel}
|
|
224
|
+
|
|
225
|
+
>
|
|
226
|
+
|
|
227
|
+
{local.icon}
|
|
228
|
+
|
|
229
|
+
</span>
|
|
230
|
+
|
|
231
|
+
</Show>
|
|
232
|
+
|
|
233
|
+
<div class="min-w-0">
|
|
234
|
+
|
|
235
|
+
<div class="text-sm font-semibold text-ink-700">{local.label}</div>
|
|
236
|
+
|
|
237
|
+
<Show when={local.subtitle}>
|
|
238
|
+
|
|
239
|
+
<div class="mt-0.5 text-xs text-ink-500">{local.subtitle}</div>
|
|
240
|
+
|
|
241
|
+
</Show>
|
|
242
|
+
|
|
243
|
+
</div>
|
|
244
|
+
|
|
245
|
+
</div>
|
|
246
|
+
|
|
247
|
+
</div>
|
|
248
|
+
|
|
249
|
+
<Show when={local.topRight}>
|
|
250
|
+
|
|
251
|
+
<div class="shrink-0">{local.topRight}</div>
|
|
252
|
+
|
|
253
|
+
</Show>
|
|
254
|
+
|
|
255
|
+
</div>
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
{/* Main content: when chart on right, use two columns; otherwise single column */}
|
|
260
|
+
|
|
261
|
+
<Show
|
|
262
|
+
|
|
263
|
+
when={chartOnRight()}
|
|
264
|
+
|
|
265
|
+
fallback={
|
|
266
|
+
|
|
267
|
+
<>
|
|
268
|
+
|
|
269
|
+
<div class={contentMt()}>
|
|
270
|
+
|
|
271
|
+
<ValueOrEmpty />
|
|
272
|
+
|
|
273
|
+
</div>
|
|
274
|
+
|
|
275
|
+
<TrendBlock />
|
|
276
|
+
|
|
277
|
+
<Show when={local.chart != null && local.chartPosition !== 'right'}>
|
|
278
|
+
|
|
279
|
+
<div
|
|
280
|
+
|
|
281
|
+
class="mt-3 h-10 w-full min-w-0"
|
|
282
|
+
|
|
283
|
+
aria-hidden={local.chartA11yLabel ? undefined : true}
|
|
284
|
+
|
|
285
|
+
role={local.chartA11yLabel ? 'img' : undefined}
|
|
286
|
+
|
|
287
|
+
aria-label={local.chartA11yLabel}
|
|
288
|
+
|
|
289
|
+
>
|
|
290
|
+
|
|
291
|
+
{local.chart}
|
|
292
|
+
|
|
293
|
+
</div>
|
|
294
|
+
|
|
295
|
+
</Show>
|
|
296
|
+
|
|
297
|
+
</>
|
|
298
|
+
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
>
|
|
302
|
+
|
|
303
|
+
<div class={cn(contentMt(), 'flex gap-4')}>
|
|
304
|
+
|
|
305
|
+
<div class="min-w-0 flex-1">
|
|
306
|
+
|
|
307
|
+
<ValueOrEmpty compact />
|
|
308
|
+
|
|
309
|
+
<TrendBlock gap="mt-2" />
|
|
310
|
+
|
|
311
|
+
</div>
|
|
312
|
+
|
|
313
|
+
<div
|
|
314
|
+
|
|
315
|
+
class="h-14 w-24 shrink-0 sm:w-32"
|
|
316
|
+
|
|
317
|
+
aria-hidden={local.chartA11yLabel ? undefined : true}
|
|
318
|
+
|
|
319
|
+
role={local.chartA11yLabel ? 'img' : undefined}
|
|
320
|
+
|
|
321
|
+
aria-label={local.chartA11yLabel}
|
|
322
|
+
|
|
323
|
+
>
|
|
324
|
+
|
|
325
|
+
{local.chart}
|
|
326
|
+
|
|
327
|
+
</div>
|
|
328
|
+
|
|
329
|
+
</div>
|
|
330
|
+
|
|
331
|
+
</Show>
|
|
332
|
+
|
|
333
|
+
</div>
|
|
334
|
+
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
}
|
|
338
|
+
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { type JSX, Show, splitProps, createContext, useContext } from 'solid-js'
|
|
2
|
+
import { cn } from '../../utilities/classNames'
|
|
3
|
+
|
|
4
|
+
type TableSection = 'head' | 'body' | 'foot'
|
|
5
|
+
|
|
6
|
+
const TableContext = createContext<boolean>(false)
|
|
7
|
+
/** Which table section a row lives in. Hover/stripe only apply to 'body' rows. */
|
|
8
|
+
const TableSectionContext = createContext<TableSection | undefined>(undefined)
|
|
9
|
+
|
|
10
|
+
export interface TableProps extends JSX.HTMLAttributes<HTMLTableElement> {
|
|
11
|
+
/** When true, even rows get a subtle background (striped). Default: false.
|
|
12
|
+
* Note: striping uses CSS `even:` which counts every `<tr>` in the `<tbody>`,
|
|
13
|
+
* including group headers, add-form rows, etc. If you mix data rows with non-data
|
|
14
|
+
* rows, stripes will be based on DOM order, not logical data-row index. */
|
|
15
|
+
striped?: boolean
|
|
16
|
+
/** Accessible caption for the table. Rendered as a visually-hidden `<caption>` by default. Pass JSX for a visible caption. */
|
|
17
|
+
caption?: JSX.Element | string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function Table(props: TableProps) {
|
|
21
|
+
const [local, others] = splitProps(props, ['class', 'striped', 'caption', 'children'])
|
|
22
|
+
const striped = () => local.striped === true
|
|
23
|
+
return (
|
|
24
|
+
<TableContext.Provider value={striped()}>
|
|
25
|
+
<table
|
|
26
|
+
class={cn('w-full text-sm text-ink-900', local.class)}
|
|
27
|
+
{...others}
|
|
28
|
+
>
|
|
29
|
+
<Show when={local.caption != null}>
|
|
30
|
+
<caption class={typeof local.caption === 'string' ? 'sr-only' : undefined}>
|
|
31
|
+
{local.caption}
|
|
32
|
+
</caption>
|
|
33
|
+
</Show>
|
|
34
|
+
{local.children}
|
|
35
|
+
</table>
|
|
36
|
+
</TableContext.Provider>
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface TableHeaderProps
|
|
41
|
+
extends JSX.HTMLAttributes<HTMLTableSectionElement> {}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Renders `<thead>` with sticky positioning.
|
|
45
|
+
* Sticky header requires a vertically scrollable ancestor (e.g. `max-h-* overflow-y-auto`).
|
|
46
|
+
*/
|
|
47
|
+
export function TableHeader(props: TableHeaderProps) {
|
|
48
|
+
const [local, others] = splitProps(props, ['class', 'children'])
|
|
49
|
+
return (
|
|
50
|
+
<thead
|
|
51
|
+
class={cn(
|
|
52
|
+
'sticky top-0 z-10 border-b border-surface-border bg-surface-raised',
|
|
53
|
+
local.class,
|
|
54
|
+
)}
|
|
55
|
+
{...others}
|
|
56
|
+
>
|
|
57
|
+
<TableSectionContext.Provider value={'head'}>{local.children}</TableSectionContext.Provider>
|
|
58
|
+
</thead>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface TableBodyProps
|
|
63
|
+
extends JSX.HTMLAttributes<HTMLTableSectionElement> {}
|
|
64
|
+
|
|
65
|
+
export function TableBody(props: TableBodyProps) {
|
|
66
|
+
const [local, others] = splitProps(props, ['class', 'children'])
|
|
67
|
+
return (
|
|
68
|
+
<tbody
|
|
69
|
+
class={cn(
|
|
70
|
+
'divide-y divide-surface-border bg-surface-base',
|
|
71
|
+
local.class,
|
|
72
|
+
)}
|
|
73
|
+
{...others}
|
|
74
|
+
>
|
|
75
|
+
<TableSectionContext.Provider value={'body'}>{local.children}</TableSectionContext.Provider>
|
|
76
|
+
</tbody>
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface TableFooterProps
|
|
81
|
+
extends JSX.HTMLAttributes<HTMLTableSectionElement> {}
|
|
82
|
+
|
|
83
|
+
export function TableFooter(props: TableFooterProps) {
|
|
84
|
+
const [local, others] = splitProps(props, ['class', 'children'])
|
|
85
|
+
return (
|
|
86
|
+
<tfoot
|
|
87
|
+
class={cn(
|
|
88
|
+
'border-t border-surface-border bg-surface-raised',
|
|
89
|
+
local.class,
|
|
90
|
+
)}
|
|
91
|
+
{...others}
|
|
92
|
+
>
|
|
93
|
+
<TableSectionContext.Provider value={'foot'}>{local.children}</TableSectionContext.Provider>
|
|
94
|
+
</tfoot>
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface TableRowProps extends JSX.HTMLAttributes<HTMLTableRowElement> {
|
|
99
|
+
/** When false, hover background is not applied (e.g. for empty state, loading, or group header rows). Default true for body rows. */
|
|
100
|
+
hover?: boolean
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function TableRow(props: TableRowProps) {
|
|
104
|
+
const [local, others] = splitProps(props, ['class', 'hover'])
|
|
105
|
+
const striped = useContext(TableContext) ?? false
|
|
106
|
+
const section = useContext(TableSectionContext)
|
|
107
|
+
const inBody = section === 'body'
|
|
108
|
+
const allowHover = () => local.hover !== false && inBody
|
|
109
|
+
return (
|
|
110
|
+
<tr
|
|
111
|
+
class={cn(
|
|
112
|
+
'transition-colors',
|
|
113
|
+
inBody && striped ? 'even:bg-surface-overlay' : '',
|
|
114
|
+
allowHover() ? 'hover:bg-surface-dim/60' : '',
|
|
115
|
+
local.class,
|
|
116
|
+
)}
|
|
117
|
+
{...others}
|
|
118
|
+
/>
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export interface TableHeadProps
|
|
123
|
+
extends JSX.ThHTMLAttributes<HTMLTableCellElement> {}
|
|
124
|
+
|
|
125
|
+
export function TableHead(props: TableHeadProps) {
|
|
126
|
+
const [local, others] = splitProps(props, ['class', 'scope'])
|
|
127
|
+
return (
|
|
128
|
+
<th
|
|
129
|
+
scope={local.scope ?? 'col'}
|
|
130
|
+
class={cn(
|
|
131
|
+
'px-4 py-3 text-left text-[11px] font-semibold uppercase tracking-[0.12em] text-ink-500',
|
|
132
|
+
local.class,
|
|
133
|
+
)}
|
|
134
|
+
{...others}
|
|
135
|
+
/>
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export interface TableCellProps
|
|
140
|
+
extends JSX.TdHTMLAttributes<HTMLTableCellElement> {}
|
|
141
|
+
|
|
142
|
+
export function TableCell(props: TableCellProps) {
|
|
143
|
+
const [local, others] = splitProps(props, ['class'])
|
|
144
|
+
return (
|
|
145
|
+
<td class={cn('px-4 py-3 align-middle text-sm text-ink-900', local.class)} {...others} />
|
|
146
|
+
)
|
|
147
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { type JSX, splitProps, Show, createMemo } from 'solid-js'
|
|
2
|
+
import { cn } from '../../utilities/classNames'
|
|
3
|
+
|
|
4
|
+
export type TagVariant =
|
|
5
|
+
| 'neutral'
|
|
6
|
+
| 'primary'
|
|
7
|
+
| 'success'
|
|
8
|
+
| 'warning'
|
|
9
|
+
| 'danger'
|
|
10
|
+
| 'info'
|
|
11
|
+
|
|
12
|
+
export type TagSize = 'sm' | 'md'
|
|
13
|
+
|
|
14
|
+
export interface TagProps extends Omit<JSX.HTMLAttributes<HTMLSpanElement>, 'color'> {
|
|
15
|
+
/** Semantic color variant. Ignored when color is set. Default: neutral. */
|
|
16
|
+
variant?: TagVariant
|
|
17
|
+
/** Tag size. Default: md. */
|
|
18
|
+
size?: TagSize
|
|
19
|
+
/** CSS color for a status indicator dot before children (e.g. "#22c55e"). */
|
|
20
|
+
statusColor?: string
|
|
21
|
+
/** Accessible label for the status dot (e.g. "Active"). Rendered as sr-only text. When omitted, the dot is purely decorative. */
|
|
22
|
+
statusLabel?: string
|
|
23
|
+
/** Arbitrary CSS color for a fully custom tag. Sets bg (10% opacity), border (25% opacity), and text color. Overrides variant. */
|
|
24
|
+
color?: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const tagVariants: Record<TagVariant, string> = {
|
|
28
|
+
neutral:
|
|
29
|
+
'bg-ink-100 text-ink-700 border-ink-200',
|
|
30
|
+
primary:
|
|
31
|
+
'bg-primary-50 text-primary-700 border-primary-100 dark:bg-primary-500/20 dark:text-primary-200 dark:border-primary-500/40',
|
|
32
|
+
success:
|
|
33
|
+
'bg-success-50 text-success-700 border-success-100 dark:bg-success-950 dark:text-success-200 dark:border-success-800',
|
|
34
|
+
warning:
|
|
35
|
+
'bg-warning-50 text-warning-700 border-warning-100 dark:bg-warning-950 dark:text-warning-200 dark:border-warning-800',
|
|
36
|
+
danger:
|
|
37
|
+
'bg-danger-50 text-danger-700 border-danger-100 dark:bg-danger-950 dark:text-danger-200 dark:border-danger-800',
|
|
38
|
+
info:
|
|
39
|
+
'bg-info-50 text-info-700 border-info-100 dark:bg-info-950 dark:text-info-200 dark:border-info-800',
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const tagSizes: Record<TagSize, string> = {
|
|
43
|
+
sm: 'px-2 py-0.5 text-[11px]',
|
|
44
|
+
md: 'px-2.5 py-0.5 text-xs',
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function Tag(props: TagProps) {
|
|
48
|
+
const [local, others] = splitProps(props, ['variant', 'size', 'statusColor', 'statusLabel', 'color', 'class', 'style', 'children'])
|
|
49
|
+
const variant = () => local.variant ?? 'neutral'
|
|
50
|
+
const size = () => local.size ?? 'md'
|
|
51
|
+
|
|
52
|
+
const customStyle = createMemo((): JSX.CSSProperties => {
|
|
53
|
+
const c = local.color
|
|
54
|
+
if (!c) return {}
|
|
55
|
+
return {
|
|
56
|
+
'background-color': `color-mix(in srgb, ${c} 10%, transparent)`,
|
|
57
|
+
'border-color': `color-mix(in srgb, ${c} 25%, transparent)`,
|
|
58
|
+
color: c,
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
const mergedStyle = createMemo((): JSX.CSSProperties => {
|
|
63
|
+
const base = customStyle()
|
|
64
|
+
const s = local.style
|
|
65
|
+
return typeof s === 'object' && s != null ? { ...base, ...s } as JSX.CSSProperties : base
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<span
|
|
70
|
+
class={cn(
|
|
71
|
+
'inline-flex items-center gap-1 rounded-full border font-medium',
|
|
72
|
+
!local.color && tagVariants[variant()],
|
|
73
|
+
tagSizes[size()],
|
|
74
|
+
local.class,
|
|
75
|
+
)}
|
|
76
|
+
style={mergedStyle()}
|
|
77
|
+
{...others}
|
|
78
|
+
>
|
|
79
|
+
<Show when={local.statusColor}>
|
|
80
|
+
<span
|
|
81
|
+
class="size-2 shrink-0 rounded-full ring-1 ring-ink-200/80 dark:ring-ink-500/80"
|
|
82
|
+
style={{ 'background-color': local.statusColor }}
|
|
83
|
+
aria-hidden={local.statusLabel ? undefined : 'true'}
|
|
84
|
+
role={local.statusLabel ? 'img' : undefined}
|
|
85
|
+
aria-label={local.statusLabel}
|
|
86
|
+
/>
|
|
87
|
+
</Show>
|
|
88
|
+
{local.children}
|
|
89
|
+
</span>
|
|
90
|
+
)
|
|
91
|
+
}
|