@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,200 @@
|
|
|
1
|
+
import { type JSX, Show, For, splitProps } from 'solid-js'
|
|
2
|
+
import { cn } from '../../utilities/classNames'
|
|
3
|
+
|
|
4
|
+
export type TimelineItemStatus = 'completed' | 'active' | 'pending' | 'error'
|
|
5
|
+
export type TimelineVariant = 'default' | 'compact' | 'outlined'
|
|
6
|
+
export type TimelineConnector = 'solid' | 'dashed' | 'dotted'
|
|
7
|
+
|
|
8
|
+
export interface TimelineItem {
|
|
9
|
+
id?: string
|
|
10
|
+
/** Main heading for the event */
|
|
11
|
+
title: JSX.Element
|
|
12
|
+
/** Secondary description text */
|
|
13
|
+
description?: JSX.Element
|
|
14
|
+
/** Timestamp or label shown beside the title */
|
|
15
|
+
time?: JSX.Element
|
|
16
|
+
/** Custom icon/content inside the dot. If omitted, a default dot or status icon renders. */
|
|
17
|
+
icon?: JSX.Element
|
|
18
|
+
/** Controls the dot color and default icon. Default: 'pending'. */
|
|
19
|
+
status?: TimelineItemStatus
|
|
20
|
+
/** Extra content slot rendered below description */
|
|
21
|
+
content?: JSX.Element
|
|
22
|
+
/** Override the dot color with any Tailwind bg class e.g. 'bg-purple-500' */
|
|
23
|
+
color?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface TimelineProps extends Omit<JSX.HTMLAttributes<HTMLDivElement>, 'children'> {
|
|
27
|
+
/** Timeline entries */
|
|
28
|
+
items: TimelineItem[]
|
|
29
|
+
/** Visual style. Default: 'default'. */
|
|
30
|
+
variant?: TimelineVariant
|
|
31
|
+
/** Connector line style. Default: 'solid'. */
|
|
32
|
+
connector?: TimelineConnector
|
|
33
|
+
/** Show connector line between items. Default: true. */
|
|
34
|
+
showConnector?: boolean
|
|
35
|
+
/** Place timestamp to the left of the connector. Default: false. */
|
|
36
|
+
timeLeft?: boolean
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const statusDotClass: Record<TimelineItemStatus, string> = {
|
|
40
|
+
completed: 'bg-success-500 text-white',
|
|
41
|
+
active: 'bg-primary-500 text-white ring-4 ring-primary-500/20',
|
|
42
|
+
pending: 'bg-surface-border text-ink-400 border-2 border-ink-300',
|
|
43
|
+
error: 'bg-danger-500 text-white',
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const statusConnectorClass: Record<TimelineItemStatus, string> = {
|
|
47
|
+
completed: 'border-success-300 dark:border-success-700',
|
|
48
|
+
active: 'border-primary-300 dark:border-primary-700',
|
|
49
|
+
pending: 'border-ink-200 dark:border-ink-700',
|
|
50
|
+
error: 'border-danger-300 dark:border-danger-700',
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function CheckIcon() {
|
|
54
|
+
return (
|
|
55
|
+
<svg class="h-3 w-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
|
56
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M5 13l4 4L19 7" />
|
|
57
|
+
</svg>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function XIcon() {
|
|
62
|
+
return (
|
|
63
|
+
<svg class="h-3 w-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
|
64
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M6 18L18 6M6 6l12 12" />
|
|
65
|
+
</svg>
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function DefaultDotIcon(props: { status: TimelineItemStatus }) {
|
|
70
|
+
if (props.status === 'completed') return <CheckIcon />
|
|
71
|
+
if (props.status === 'error') return <XIcon />
|
|
72
|
+
if (props.status === 'active') {
|
|
73
|
+
return <span class="block h-2 w-2 rounded-full bg-white" />
|
|
74
|
+
}
|
|
75
|
+
return null
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function Timeline(props: TimelineProps) {
|
|
79
|
+
const [local, others] = splitProps(props, [
|
|
80
|
+
'items',
|
|
81
|
+
'variant',
|
|
82
|
+
'connector',
|
|
83
|
+
'showConnector',
|
|
84
|
+
'timeLeft',
|
|
85
|
+
'class',
|
|
86
|
+
])
|
|
87
|
+
|
|
88
|
+
const variant = () => local.variant ?? 'default'
|
|
89
|
+
const connector = () => local.connector ?? 'solid'
|
|
90
|
+
const showConnector = () => local.showConnector !== false
|
|
91
|
+
const timeLeft = () => local.timeLeft ?? false
|
|
92
|
+
|
|
93
|
+
const compact = () => variant() === 'compact'
|
|
94
|
+
const outlined = () => variant() === 'outlined'
|
|
95
|
+
|
|
96
|
+
const connectorBorderStyle = () => {
|
|
97
|
+
const style = connector()
|
|
98
|
+
if (style === 'dashed') return 'border-dashed'
|
|
99
|
+
if (style === 'dotted') return 'border-dotted'
|
|
100
|
+
return 'border-solid'
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<div
|
|
105
|
+
class={cn('relative', local.class)}
|
|
106
|
+
{...others}
|
|
107
|
+
>
|
|
108
|
+
<ol class="list-none">
|
|
109
|
+
<For each={local.items}>
|
|
110
|
+
{(item, idx) => {
|
|
111
|
+
const isLast = () => idx() === local.items.length - 1
|
|
112
|
+
const status = () => item.status ?? 'pending'
|
|
113
|
+
const dotClass = () => item.color
|
|
114
|
+
? `${item.color} text-white`
|
|
115
|
+
: statusDotClass[status()]
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<li class={cn('relative flex', timeLeft() ? 'flex-row-reverse' : 'flex-row', compact() ? 'gap-3' : 'gap-4')}>
|
|
119
|
+
{/* Left side: timestamp (when timeLeft) */}
|
|
120
|
+
<Show when={timeLeft() && item.time}>
|
|
121
|
+
<div class="w-28 shrink-0 pt-0.5 text-right">
|
|
122
|
+
<span class="text-xs text-ink-400">{item.time}</span>
|
|
123
|
+
</div>
|
|
124
|
+
</Show>
|
|
125
|
+
|
|
126
|
+
{/* Center: dot + connector */}
|
|
127
|
+
<div class={cn('flex flex-col items-center', timeLeft() ? 'mx-0' : '')}>
|
|
128
|
+
{/* Dot */}
|
|
129
|
+
<div
|
|
130
|
+
class={cn(
|
|
131
|
+
'relative z-10 flex shrink-0 items-center justify-center rounded-full transition-all',
|
|
132
|
+
compact() ? 'h-5 w-5' : 'h-8 w-8',
|
|
133
|
+
dotClass(),
|
|
134
|
+
outlined() && 'ring-2 ring-offset-2 ring-offset-surface-base',
|
|
135
|
+
)}
|
|
136
|
+
aria-hidden="true"
|
|
137
|
+
>
|
|
138
|
+
<Show when={item.icon} fallback={<DefaultDotIcon status={status()} />}>
|
|
139
|
+
<span class={cn('flex items-center justify-center', compact() ? '[&>svg]:h-2.5 [&>svg]:w-2.5' : '[&>svg]:h-4 [&>svg]:w-4')}>
|
|
140
|
+
{item.icon}
|
|
141
|
+
</span>
|
|
142
|
+
</Show>
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
{/* Connector line */}
|
|
146
|
+
<Show when={showConnector() && !isLast()}>
|
|
147
|
+
<div
|
|
148
|
+
class={cn(
|
|
149
|
+
'flex-1 border-l-2 my-1',
|
|
150
|
+
connectorBorderStyle(),
|
|
151
|
+
item.color
|
|
152
|
+
? 'border-ink-200 dark:border-ink-700'
|
|
153
|
+
: statusConnectorClass[status()],
|
|
154
|
+
)}
|
|
155
|
+
style={{ 'min-height': compact() ? '1rem' : '1.5rem' }}
|
|
156
|
+
aria-hidden="true"
|
|
157
|
+
/>
|
|
158
|
+
</Show>
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
{/* Right side: content */}
|
|
162
|
+
<div
|
|
163
|
+
class={cn(
|
|
164
|
+
'min-w-0 flex-1',
|
|
165
|
+
!isLast() ? (compact() ? 'pb-4' : 'pb-6') : 'pb-0',
|
|
166
|
+
timeLeft() ? 'text-right' : '',
|
|
167
|
+
)}
|
|
168
|
+
>
|
|
169
|
+
{/* Title row */}
|
|
170
|
+
<div class={cn('flex items-start gap-2', timeLeft() ? 'flex-row-reverse' : 'flex-row')}>
|
|
171
|
+
<span class={cn('font-medium text-ink-900 leading-none', compact() ? 'text-sm' : 'text-sm')}>
|
|
172
|
+
{item.title}
|
|
173
|
+
</span>
|
|
174
|
+
<Show when={!timeLeft() && item.time}>
|
|
175
|
+
<span class="shrink-0 text-xs text-ink-400 mt-0.5">{item.time}</span>
|
|
176
|
+
</Show>
|
|
177
|
+
</div>
|
|
178
|
+
|
|
179
|
+
{/* Description */}
|
|
180
|
+
<Show when={item.description}>
|
|
181
|
+
<div class={cn('mt-1 text-ink-500 leading-relaxed', compact() ? 'text-xs' : 'text-sm')}>
|
|
182
|
+
{item.description}
|
|
183
|
+
</div>
|
|
184
|
+
</Show>
|
|
185
|
+
|
|
186
|
+
{/* Extra content slot */}
|
|
187
|
+
<Show when={item.content}>
|
|
188
|
+
<div class="mt-2">
|
|
189
|
+
{item.content}
|
|
190
|
+
</div>
|
|
191
|
+
</Show>
|
|
192
|
+
</div>
|
|
193
|
+
</li>
|
|
194
|
+
)
|
|
195
|
+
}}
|
|
196
|
+
</For>
|
|
197
|
+
</ol>
|
|
198
|
+
</div>
|
|
199
|
+
)
|
|
200
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { type JSX, Show, For, createSignal, splitProps } from 'solid-js'
|
|
2
|
+
import { ChevronRight } from 'lucide-solid'
|
|
3
|
+
import { cn } from '../../utilities/classNames'
|
|
4
|
+
|
|
5
|
+
export interface TreeNode {
|
|
6
|
+
/** Unique identifier */
|
|
7
|
+
id: string
|
|
8
|
+
/** Display label */
|
|
9
|
+
label: JSX.Element
|
|
10
|
+
/** Optional icon shown before the label */
|
|
11
|
+
icon?: JSX.Element
|
|
12
|
+
/** Child nodes */
|
|
13
|
+
children?: TreeNode[]
|
|
14
|
+
/** Prevents selection and interaction */
|
|
15
|
+
disabled?: boolean
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface TreeViewProps extends Omit<JSX.HTMLAttributes<HTMLDivElement>, 'children' | 'onSelect'> {
|
|
19
|
+
/** Tree data */
|
|
20
|
+
nodes: TreeNode[]
|
|
21
|
+
/** Controlled selected node id */
|
|
22
|
+
selected?: string
|
|
23
|
+
/** Callback when a node is selected */
|
|
24
|
+
onSelect?: (id: string) => void
|
|
25
|
+
/** Default selected node id (uncontrolled) */
|
|
26
|
+
defaultSelected?: string
|
|
27
|
+
/** Controlled expanded node ids */
|
|
28
|
+
expanded?: string[]
|
|
29
|
+
/** Callback when expanded state changes */
|
|
30
|
+
onExpandedChange?: (ids: string[]) => void
|
|
31
|
+
/** Default expanded ids (uncontrolled) */
|
|
32
|
+
defaultExpanded?: string[]
|
|
33
|
+
/** Pixels of indentation per level. Default: 16. */
|
|
34
|
+
indent?: number
|
|
35
|
+
/** Show connecting lines between nodes. Default: true. */
|
|
36
|
+
showLines?: boolean
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function TreeView(props: TreeViewProps) {
|
|
40
|
+
const [local, others] = splitProps(props, [
|
|
41
|
+
'nodes',
|
|
42
|
+
'selected',
|
|
43
|
+
'onSelect',
|
|
44
|
+
'defaultSelected',
|
|
45
|
+
'expanded',
|
|
46
|
+
'onExpandedChange',
|
|
47
|
+
'defaultExpanded',
|
|
48
|
+
'indent',
|
|
49
|
+
'showLines',
|
|
50
|
+
'class',
|
|
51
|
+
])
|
|
52
|
+
|
|
53
|
+
const [internalSelected, setInternalSelected] = createSignal<string | undefined>(
|
|
54
|
+
local.defaultSelected,
|
|
55
|
+
)
|
|
56
|
+
const [internalExpanded, setInternalExpanded] = createSignal<string[]>(
|
|
57
|
+
local.defaultExpanded ?? [],
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
const selectedId = () => local.selected ?? internalSelected()
|
|
61
|
+
const expandedIds = () => local.expanded ?? internalExpanded()
|
|
62
|
+
|
|
63
|
+
const isSelected = (id: string) => selectedId() === id
|
|
64
|
+
const isExpanded = (id: string) => expandedIds().includes(id)
|
|
65
|
+
|
|
66
|
+
const handleSelect = (id: string) => {
|
|
67
|
+
if (local.selected === undefined) setInternalSelected(id)
|
|
68
|
+
local.onSelect?.(id)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const toggleExpand = (id: string) => {
|
|
72
|
+
const current = expandedIds()
|
|
73
|
+
const next = current.includes(id) ? current.filter((x) => x !== id) : [...current, id]
|
|
74
|
+
if (local.expanded === undefined) setInternalExpanded(next)
|
|
75
|
+
local.onExpandedChange?.(next)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const indent = () => local.indent ?? 16
|
|
79
|
+
const showLines = () => local.showLines !== false
|
|
80
|
+
|
|
81
|
+
const renderNodes = (nodes: TreeNode[], level = 0): JSX.Element => (
|
|
82
|
+
<ul role={level === 0 ? 'tree' : 'group'} class="list-none">
|
|
83
|
+
<For each={nodes}>
|
|
84
|
+
{(node) => {
|
|
85
|
+
const hasChildren = () => (node.children?.length ?? 0) > 0
|
|
86
|
+
const expanded = () => isExpanded(node.id)
|
|
87
|
+
const selected = () => isSelected(node.id)
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<li role="treeitem" aria-expanded={hasChildren() ? expanded() : undefined} aria-selected={selected()}>
|
|
91
|
+
<button
|
|
92
|
+
type="button"
|
|
93
|
+
disabled={node.disabled}
|
|
94
|
+
onClick={() => {
|
|
95
|
+
if (node.disabled) return
|
|
96
|
+
if (hasChildren()) toggleExpand(node.id)
|
|
97
|
+
handleSelect(node.id)
|
|
98
|
+
}}
|
|
99
|
+
onKeyDown={(e: KeyboardEvent) => {
|
|
100
|
+
if (node.disabled) return
|
|
101
|
+
if (e.key === 'ArrowRight' && hasChildren() && !expanded()) {
|
|
102
|
+
e.preventDefault()
|
|
103
|
+
toggleExpand(node.id)
|
|
104
|
+
} else if (e.key === 'ArrowLeft' && hasChildren() && expanded()) {
|
|
105
|
+
e.preventDefault()
|
|
106
|
+
toggleExpand(node.id)
|
|
107
|
+
} else if (e.key === 'Enter' || e.key === ' ') {
|
|
108
|
+
e.preventDefault()
|
|
109
|
+
if (hasChildren()) toggleExpand(node.id)
|
|
110
|
+
handleSelect(node.id)
|
|
111
|
+
}
|
|
112
|
+
}}
|
|
113
|
+
class={cn(
|
|
114
|
+
'flex w-full items-center gap-1.5 rounded-md px-2 py-1.5 text-left text-sm transition-colors',
|
|
115
|
+
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500/50',
|
|
116
|
+
selected()
|
|
117
|
+
? 'bg-primary-50 text-primary-700 font-medium dark:bg-primary-500/15 dark:text-primary-400'
|
|
118
|
+
: 'text-ink-700 hover:bg-surface-overlay hover:text-ink-900 dark:hover:text-ink-900',
|
|
119
|
+
node.disabled && 'cursor-not-allowed opacity-40',
|
|
120
|
+
)}
|
|
121
|
+
style={{ 'padding-left': `${level * indent() + 8}px` }}
|
|
122
|
+
>
|
|
123
|
+
{/* Expand/collapse chevron */}
|
|
124
|
+
<span class={cn('flex h-4 w-4 shrink-0 items-center justify-center')}>
|
|
125
|
+
<Show when={hasChildren()} fallback={<span class="h-4 w-4" />}>
|
|
126
|
+
<ChevronRight
|
|
127
|
+
class={cn(
|
|
128
|
+
'h-3.5 w-3.5 text-ink-400 transition-transform duration-150',
|
|
129
|
+
expanded() && 'rotate-90',
|
|
130
|
+
)}
|
|
131
|
+
aria-hidden="true"
|
|
132
|
+
/>
|
|
133
|
+
</Show>
|
|
134
|
+
</span>
|
|
135
|
+
|
|
136
|
+
{/* Node icon */}
|
|
137
|
+
<Show when={node.icon}>
|
|
138
|
+
<span class={cn('flex h-4 w-4 shrink-0 items-center justify-center [&>svg]:h-4 [&>svg]:w-4', selected() ? 'text-primary-500' : 'text-ink-500')}>
|
|
139
|
+
{node.icon}
|
|
140
|
+
</span>
|
|
141
|
+
</Show>
|
|
142
|
+
|
|
143
|
+
{/* Label */}
|
|
144
|
+
<span class="min-w-0 truncate">{node.label}</span>
|
|
145
|
+
</button>
|
|
146
|
+
|
|
147
|
+
{/* Children */}
|
|
148
|
+
<Show when={hasChildren() && expanded()}>
|
|
149
|
+
<div
|
|
150
|
+
class={cn(
|
|
151
|
+
showLines() && level < 2
|
|
152
|
+
? 'relative ml-[calc(var(--indent-offset)+8px)] border-l border-ink-200 dark:border-ink-700 pl-0'
|
|
153
|
+
: '',
|
|
154
|
+
)}
|
|
155
|
+
style={{ '--indent-offset': `${level * indent() + 14}px` } as any}
|
|
156
|
+
>
|
|
157
|
+
{renderNodes(node.children!, level + 1)}
|
|
158
|
+
</div>
|
|
159
|
+
</Show>
|
|
160
|
+
</li>
|
|
161
|
+
)
|
|
162
|
+
}}
|
|
163
|
+
</For>
|
|
164
|
+
</ul>
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
return (
|
|
168
|
+
<div class={cn('select-none', local.class)} {...others}>
|
|
169
|
+
{renderNodes(local.nodes)}
|
|
170
|
+
</div>
|
|
171
|
+
)
|
|
172
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { type JSX, createSignal, Show, splitProps } from 'solid-js'
|
|
2
|
+
import { cn } from '../../utilities/classNames'
|
|
3
|
+
|
|
4
|
+
export interface VideoProps {
|
|
5
|
+
/** Video source URL */
|
|
6
|
+
src: string
|
|
7
|
+
/** Poster image URL shown before playback */
|
|
8
|
+
poster?: string
|
|
9
|
+
/** Show native browser controls. Default: true */
|
|
10
|
+
controls?: boolean
|
|
11
|
+
/** Autoplay the video. Default: false. Note: forces muted=true to satisfy browser autoplay policy. */
|
|
12
|
+
autoplay?: boolean
|
|
13
|
+
/** Mute the video. Default: false */
|
|
14
|
+
muted?: boolean
|
|
15
|
+
/** Loop the video. Default: false */
|
|
16
|
+
loop?: boolean
|
|
17
|
+
/** CSS aspect-ratio value e.g. '16/9', '4/3', '1/1', '9/16'. Default: '16/9' */
|
|
18
|
+
aspectRatio?: string
|
|
19
|
+
/** Max width of the container (CSS value). Default: '100%' */
|
|
20
|
+
width?: string
|
|
21
|
+
/** Max height of the container (CSS value). Rarely needed with aspect-ratio. */
|
|
22
|
+
height?: string
|
|
23
|
+
/** Fallback content to render when the video fails to load */
|
|
24
|
+
fallback?: JSX.Element
|
|
25
|
+
/** Preload strategy. Default: 'metadata' */
|
|
26
|
+
preload?: 'none' | 'metadata' | 'auto'
|
|
27
|
+
/** Whether the video fills its container. Default: true */
|
|
28
|
+
fluid?: boolean
|
|
29
|
+
class?: string
|
|
30
|
+
/** Forwarded to the <video> element */
|
|
31
|
+
videoClass?: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function Video(props: VideoProps) {
|
|
35
|
+
const [local, others] = splitProps(props, [
|
|
36
|
+
'src', 'poster', 'controls', 'autoplay', 'muted', 'loop',
|
|
37
|
+
'aspectRatio', 'width', 'height', 'fallback', 'preload',
|
|
38
|
+
'fluid', 'class', 'videoClass',
|
|
39
|
+
])
|
|
40
|
+
|
|
41
|
+
const [error, setError] = createSignal(false)
|
|
42
|
+
|
|
43
|
+
const aspectRatio = () => local.aspectRatio ?? '16/9'
|
|
44
|
+
const fluid = () => local.fluid !== false
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<div
|
|
48
|
+
class={cn('relative overflow-hidden rounded-xl', local.class)}
|
|
49
|
+
style={{
|
|
50
|
+
'aspect-ratio': aspectRatio(),
|
|
51
|
+
'max-width': local.width ?? (fluid() ? '100%' : undefined),
|
|
52
|
+
'max-height': local.height,
|
|
53
|
+
}}
|
|
54
|
+
>
|
|
55
|
+
<Show
|
|
56
|
+
when={!error()}
|
|
57
|
+
fallback={
|
|
58
|
+
<div class="absolute inset-0 flex items-center justify-center bg-surface-raised">
|
|
59
|
+
<Show
|
|
60
|
+
when={local.fallback}
|
|
61
|
+
fallback={
|
|
62
|
+
<div class="flex flex-col items-center gap-3 p-6 text-center">
|
|
63
|
+
<div class="flex h-12 w-12 items-center justify-center rounded-full bg-surface-overlay">
|
|
64
|
+
<svg class="h-6 w-6 text-ink-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
|
65
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15.75 10.5l4.72-4.72a.75.75 0 011.28.53v11.38a.75.75 0 01-1.28.53l-4.72-4.72M12 18.75H4.5a2.25 2.25 0 01-2.25-2.25V9m12.841 9.091L16.5 19.5m-1.409-1.409c.407-.164.841-.26 1.298-.26H19.5m-14.25 0A2.25 2.25 0 013 16.5V9.75m0 0A2.25 2.25 0 015.25 7.5H12M3 9.75l9-4.5" />
|
|
66
|
+
</svg>
|
|
67
|
+
</div>
|
|
68
|
+
<div>
|
|
69
|
+
<p class="text-sm font-medium text-ink-700">Video unavailable</p>
|
|
70
|
+
<p class="mt-0.5 text-xs text-ink-400">This video could not be loaded.</p>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
}
|
|
74
|
+
>
|
|
75
|
+
{local.fallback}
|
|
76
|
+
</Show>
|
|
77
|
+
</div>
|
|
78
|
+
}
|
|
79
|
+
>
|
|
80
|
+
<video
|
|
81
|
+
class={cn('h-full w-full object-cover', local.videoClass)}
|
|
82
|
+
src={local.src}
|
|
83
|
+
poster={local.poster}
|
|
84
|
+
controls={local.controls !== false}
|
|
85
|
+
autoplay={local.autoplay}
|
|
86
|
+
muted={local.autoplay ? true : local.muted}
|
|
87
|
+
loop={local.loop}
|
|
88
|
+
preload={local.preload ?? 'metadata'}
|
|
89
|
+
onError={() => setError(true)}
|
|
90
|
+
playsinline
|
|
91
|
+
/>
|
|
92
|
+
</Show>
|
|
93
|
+
</div>
|
|
94
|
+
)
|
|
95
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { AvatarShape, SizeKey } from '../../types/avatar-types'
|
|
2
|
+
|
|
3
|
+
export const shapeClasses: Record<AvatarShape, string> = {
|
|
4
|
+
circle: 'rounded-full',
|
|
5
|
+
rounded: 'rounded-lg',
|
|
6
|
+
square: 'rounded-none',
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const avatarSizeClasses: Record<SizeKey, string> = {
|
|
10
|
+
sm: 'h-8 w-8 text-sm',
|
|
11
|
+
md: 'h-10 w-10 text-sm',
|
|
12
|
+
lg: 'h-12 w-12 text-base',
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Neutral avatar background/text — shared between Avatar and AvatarGroup overflow pill. */
|
|
16
|
+
export const neutralColorClass = 'bg-ink-200 text-ink-600 dark:bg-ink-700 dark:text-ink-200'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Initials from a full name: first letter of first name + first letter of last name.
|
|
20
|
+
* Treats the last word as last name and everything before as first name.
|
|
21
|
+
* "Jenny Lee Smith" → "JS", "Alex" → "A", "Alex Chen" → "AC".
|
|
22
|
+
*/
|
|
23
|
+
export function getInitials(name: string): string {
|
|
24
|
+
const trimmed = name.trim()
|
|
25
|
+
if (!trimmed) return '?'
|
|
26
|
+
const parts = trimmed.split(/\s+/).filter(Boolean)
|
|
27
|
+
if (parts.length === 0) return '?'
|
|
28
|
+
if (parts.length === 1) return parts[0].charAt(0).toUpperCase()
|
|
29
|
+
const first = parts[0].charAt(0)
|
|
30
|
+
const last = parts[parts.length - 1].charAt(0)
|
|
31
|
+
return (first + last).toUpperCase()
|
|
32
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/** Data Display (sidebar) */
|
|
2
|
+
export { Avatar } from './Avatar'
|
|
3
|
+
export type { AvatarProps, AvatarShape, AvatarRing, AvatarColor, AvatarBadgePlacement } from './Avatar'
|
|
4
|
+
export type { SizeKey } from '../../types/avatar-types'
|
|
5
|
+
|
|
6
|
+
export { ColorSwatch } from './ColorSwatch'
|
|
7
|
+
export type { ColorSwatchProps, ColorSwatchVariant } from './ColorSwatch'
|
|
8
|
+
|
|
9
|
+
export { Persona } from './Persona'
|
|
10
|
+
export type { PersonaProps } from './Persona'
|
|
11
|
+
|
|
12
|
+
export { Badge } from './Badge'
|
|
13
|
+
export type { BadgeProps, BadgeVariant, BadgeSize } from './Badge'
|
|
14
|
+
|
|
15
|
+
export { AvatarGroup } from './AvatarGroup'
|
|
16
|
+
export type { AvatarGroupProps, AvatarGroupItem, AvatarStacking } from './AvatarGroup'
|
|
17
|
+
|
|
18
|
+
export { TablePanel } from '../layout/TablePanel'
|
|
19
|
+
export type { TablePanelProps } from '../layout/TablePanel'
|
|
20
|
+
|
|
21
|
+
export { TableView, useTableView, emptyFilterGroup } from '../layout/TableView'
|
|
22
|
+
export type { TableViewProps, TableViewContextValue } from '../layout/TableView'
|
|
23
|
+
export type { TableViewConfig, ViewConfig } from '../layout/TableView/types'
|
|
24
|
+
|
|
25
|
+
export { Carousel } from './Carousel'
|
|
26
|
+
export type { CarouselProps, CarouselSlide } from './Carousel'
|
|
27
|
+
export { Chart } from '../charts/Chart'
|
|
28
|
+
export type { ChartProps, ChartData, ChartDataset, ChartType, ScatterPoint, BubblePoint } from '../charts/Chart'
|
|
29
|
+
export { EmptyState } from './EmptyState'
|
|
30
|
+
export type { EmptyStateProps } from './EmptyState'
|
|
31
|
+
export { Tag } from './Tag'
|
|
32
|
+
export type { TagProps, TagVariant, TagSize } from './Tag'
|
|
33
|
+
|
|
34
|
+
export { Kbd, KbdShortcut, KEY } from './Kbd'
|
|
35
|
+
export type { KbdProps, KbdShortcutProps, KbdVariant, KbdSize } from './Kbd'
|
|
36
|
+
|
|
37
|
+
export { StatCard } from './StatCard'
|
|
38
|
+
export type { StatCardProps } from './StatCard'
|
|
39
|
+
|
|
40
|
+
export { Timeline } from './Timeline'
|
|
41
|
+
export type { TimelineProps, TimelineItem, TimelineItemStatus, TimelineVariant, TimelineConnector } from './Timeline'
|
|
42
|
+
|
|
43
|
+
export { TreeView } from './TreeView'
|
|
44
|
+
export type { TreeViewProps, TreeNode } from './TreeView'
|
|
45
|
+
|
|
46
|
+
export { Video } from './Video'
|
|
47
|
+
export type { VideoProps } from './Video'
|
|
48
|
+
export { Sparkline } from '../charts/Sparkline'
|
|
49
|
+
export type { SparklineProps } from '../charts/Sparkline'
|
|
50
|
+
export { DataTable, TABLE_CONTAINER_CLASS } from './DataTable'
|
|
51
|
+
export type {
|
|
52
|
+
ColumnDef,
|
|
53
|
+
DataTableProps,
|
|
54
|
+
DataTablePagination,
|
|
55
|
+
DataTablePagingProps,
|
|
56
|
+
DataTableGroupByProps,
|
|
57
|
+
DataTableSearchProps,
|
|
58
|
+
DataTableButtonProps,
|
|
59
|
+
DataTableAddRowProps,
|
|
60
|
+
DataTableEditModalProps,
|
|
61
|
+
DataTableDeleteDialogProps,
|
|
62
|
+
} from './DataTable'
|
|
63
|
+
export {
|
|
64
|
+
Table,
|
|
65
|
+
TableBody,
|
|
66
|
+
TableCell,
|
|
67
|
+
TableFooter,
|
|
68
|
+
TableHead,
|
|
69
|
+
TableHeader,
|
|
70
|
+
TableRow,
|
|
71
|
+
} from './Table'
|
|
72
|
+
export type {
|
|
73
|
+
TableBodyProps,
|
|
74
|
+
TableCellProps,
|
|
75
|
+
TableFooterProps,
|
|
76
|
+
TableHeadProps,
|
|
77
|
+
TableHeaderProps,
|
|
78
|
+
TableProps,
|
|
79
|
+
TableRowProps,
|
|
80
|
+
} from './Table'
|
|
81
|
+
|