@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,61 @@
|
|
|
1
|
+
import type { JSX } from 'solid-js'
|
|
2
|
+
import { Show, splitProps, createUniqueId } from 'solid-js'
|
|
3
|
+
import { cn } from '../../utilities/classNames'
|
|
4
|
+
|
|
5
|
+
export interface EmptyStateProps extends JSX.HTMLAttributes<HTMLDivElement> {
|
|
6
|
+
title: string
|
|
7
|
+
description?: string
|
|
8
|
+
icon?: JSX.Element
|
|
9
|
+
actions?: JSX.Element
|
|
10
|
+
/** When true, sets role="status" + aria-live="polite" so screen readers announce the empty state. Default: false. */
|
|
11
|
+
announce?: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function EmptyState(props: EmptyStateProps) {
|
|
15
|
+
const [local, rest] = splitProps(props, [
|
|
16
|
+
'title',
|
|
17
|
+
'description',
|
|
18
|
+
'icon',
|
|
19
|
+
'actions',
|
|
20
|
+
'announce',
|
|
21
|
+
'class',
|
|
22
|
+
])
|
|
23
|
+
|
|
24
|
+
const uid = createUniqueId()
|
|
25
|
+
const titleId = `empty-title-${uid}`
|
|
26
|
+
const descId = `empty-desc-${uid}`
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div
|
|
30
|
+
{...rest}
|
|
31
|
+
class={cn(
|
|
32
|
+
'flex flex-col items-center justify-center gap-4 px-6 py-16 text-center',
|
|
33
|
+
local.class
|
|
34
|
+
)}
|
|
35
|
+
role={local.announce ? 'status' : undefined}
|
|
36
|
+
aria-live={local.announce ? 'polite' : undefined}
|
|
37
|
+
aria-labelledby={local.announce ? titleId : undefined}
|
|
38
|
+
aria-describedby={local.announce && local.description ? descId : undefined}
|
|
39
|
+
>
|
|
40
|
+
<Show when={local.icon}>
|
|
41
|
+
<div
|
|
42
|
+
class="flex h-16 w-16 items-center justify-center rounded-full bg-surface-overlay text-ink-400"
|
|
43
|
+
aria-hidden="true"
|
|
44
|
+
>
|
|
45
|
+
{local.icon}
|
|
46
|
+
</div>
|
|
47
|
+
</Show>
|
|
48
|
+
<div class="space-y-1">
|
|
49
|
+
<h3 id={titleId} class="text-base font-semibold text-ink-900">{local.title}</h3>
|
|
50
|
+
<Show when={local.description}>
|
|
51
|
+
<p id={descId} class="max-w-sm text-sm text-ink-500">{local.description}</p>
|
|
52
|
+
</Show>
|
|
53
|
+
</div>
|
|
54
|
+
<Show when={local.actions}>
|
|
55
|
+
<div class="flex flex-wrap items-center justify-center gap-2">
|
|
56
|
+
{local.actions}
|
|
57
|
+
</div>
|
|
58
|
+
</Show>
|
|
59
|
+
</div>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { type JSX, Show, splitProps, createSignal, onCleanup, createEffect } from 'solid-js'
|
|
2
|
+
import { Image as KobalteImage } from '@kobalte/core/image'
|
|
3
|
+
import { cn } from '../../utilities/classNames'
|
|
4
|
+
|
|
5
|
+
export interface ImageProps extends Omit<JSX.ImgHTMLAttributes<HTMLImageElement>, 'loading'> {
|
|
6
|
+
/** Image source URL */
|
|
7
|
+
src: string
|
|
8
|
+
/** Alternative text for accessibility */
|
|
9
|
+
alt: string
|
|
10
|
+
/** Fallback source to try if main src fails */
|
|
11
|
+
fallbackSrc?: string
|
|
12
|
+
/** Show loading skeleton while image loads */
|
|
13
|
+
showSkeleton?: boolean
|
|
14
|
+
/** Custom fallback content (overrides skeleton) */
|
|
15
|
+
fallback?: JSX.Element
|
|
16
|
+
/** Delay before showing fallback to avoid flash */
|
|
17
|
+
fallbackDelay?: number
|
|
18
|
+
/** Aspect ratio class (e.g. 'aspect-square', 'aspect-video') */
|
|
19
|
+
aspectRatio?: string
|
|
20
|
+
/** Object fit class */
|
|
21
|
+
objectFit?: 'contain' | 'cover' | 'fill' | 'none' | 'scale-down'
|
|
22
|
+
/** Intuitive scaling aliases - easier to remember than objectFit */
|
|
23
|
+
scale?: 'contain' | 'cover' | 'stretch' | 'none' | 'scale-down' | 'portrait' | 'landscape' | 'square'
|
|
24
|
+
/** Smart scaling constraints */
|
|
25
|
+
scalingConstraints?: {
|
|
26
|
+
/** Maximum width the image should scale to */
|
|
27
|
+
maxWidth?: string
|
|
28
|
+
/** Maximum height the image should scale to */
|
|
29
|
+
maxHeight?: string
|
|
30
|
+
}
|
|
31
|
+
/** Object position class */
|
|
32
|
+
objectPosition?: string
|
|
33
|
+
/** Border radius class */
|
|
34
|
+
rounded?: string
|
|
35
|
+
/** Whether to lazy load the image */
|
|
36
|
+
lazy?: boolean
|
|
37
|
+
/** Content to overlay on top of the image */
|
|
38
|
+
overlay?: JSX.Element
|
|
39
|
+
/** Overlay position class */
|
|
40
|
+
overlayPosition?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'center' | 'full'
|
|
41
|
+
/** Show overlay on hover only */
|
|
42
|
+
overlayOnHover?: boolean
|
|
43
|
+
/** Callback when image loads successfully */
|
|
44
|
+
onLoad?: () => void
|
|
45
|
+
/** Callback when image fails to load */
|
|
46
|
+
onError?: () => void
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Image component with loading states, error handling, and accessibility features.
|
|
51
|
+
* Built on top of Kobalte's Image primitive for enhanced accessibility.
|
|
52
|
+
*/
|
|
53
|
+
export function Image(props: ImageProps) {
|
|
54
|
+
const [local, others] = splitProps(props, [
|
|
55
|
+
'src',
|
|
56
|
+
'alt',
|
|
57
|
+
'fallbackSrc',
|
|
58
|
+
'showSkeleton',
|
|
59
|
+
'fallback',
|
|
60
|
+
'fallbackDelay',
|
|
61
|
+
'aspectRatio',
|
|
62
|
+
'objectFit',
|
|
63
|
+
'scale',
|
|
64
|
+
'scalingConstraints',
|
|
65
|
+
'objectPosition',
|
|
66
|
+
'rounded',
|
|
67
|
+
'lazy',
|
|
68
|
+
'overlay',
|
|
69
|
+
'overlayPosition',
|
|
70
|
+
'overlayOnHover',
|
|
71
|
+
'onLoad',
|
|
72
|
+
'onError',
|
|
73
|
+
'class',
|
|
74
|
+
])
|
|
75
|
+
|
|
76
|
+
const [showFallback, setShowFallback] = createSignal(false)
|
|
77
|
+
const [imageLoaded, setImageLoaded] = createSignal(false)
|
|
78
|
+
const [activeSrc, setActiveSrc] = createSignal(local.src)
|
|
79
|
+
const [hasTriedFallback, setHasTriedFallback] = createSignal(false)
|
|
80
|
+
let fallbackTimeout: ReturnType<typeof setTimeout> | undefined
|
|
81
|
+
|
|
82
|
+
// React to src prop changes
|
|
83
|
+
createEffect(() => {
|
|
84
|
+
const newSrc = local.src
|
|
85
|
+
setActiveSrc(newSrc)
|
|
86
|
+
setImageLoaded(false)
|
|
87
|
+
setShowFallback(false)
|
|
88
|
+
setHasTriedFallback(false)
|
|
89
|
+
|
|
90
|
+
// Clear any pending timeout when src changes
|
|
91
|
+
if (fallbackTimeout) {
|
|
92
|
+
clearTimeout(fallbackTimeout)
|
|
93
|
+
fallbackTimeout = undefined
|
|
94
|
+
}
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
// Cleanup timeout on unmount
|
|
98
|
+
onCleanup(() => {
|
|
99
|
+
if (fallbackTimeout) {
|
|
100
|
+
clearTimeout(fallbackTimeout)
|
|
101
|
+
}
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
// Scaling logic - convert scale aliases to object-fit values
|
|
105
|
+
const effectiveObjectFit = () => {
|
|
106
|
+
// Priority: scale > objectFit
|
|
107
|
+
if (local.scale) {
|
|
108
|
+
return mapScaleToObjectFit(local.scale)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return local.objectFit
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const mapScaleToObjectFit = (scale: 'contain' | 'cover' | 'stretch' | 'none' | 'scale-down' | 'portrait' | 'landscape' | 'square'): 'contain' | 'cover' | 'fill' | 'none' | 'scale-down' => {
|
|
115
|
+
switch (scale) {
|
|
116
|
+
case 'stretch':
|
|
117
|
+
return 'fill'
|
|
118
|
+
case 'portrait':
|
|
119
|
+
return 'contain' // Fit portrait image within container maintaining aspect ratio
|
|
120
|
+
case 'landscape':
|
|
121
|
+
return 'contain' // Fit landscape image within container maintaining aspect ratio
|
|
122
|
+
case 'square':
|
|
123
|
+
return 'cover' // Force to fill square area
|
|
124
|
+
default:
|
|
125
|
+
return scale
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Generate inline styles for constraints (Tailwind JIT won't see dynamic classes)
|
|
130
|
+
const constraintStyles = () => {
|
|
131
|
+
if (!local.scalingConstraints) return {}
|
|
132
|
+
|
|
133
|
+
const styles: JSX.CSSProperties = {}
|
|
134
|
+
if (local.scalingConstraints.maxWidth) {
|
|
135
|
+
styles['max-width'] = local.scalingConstraints.maxWidth
|
|
136
|
+
}
|
|
137
|
+
if (local.scalingConstraints.maxHeight) {
|
|
138
|
+
styles['max-height'] = local.scalingConstraints.maxHeight
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return styles
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const handleLoad = () => {
|
|
145
|
+
setImageLoaded(true)
|
|
146
|
+
setShowFallback(false)
|
|
147
|
+
if (fallbackTimeout) {
|
|
148
|
+
clearTimeout(fallbackTimeout)
|
|
149
|
+
fallbackTimeout = undefined
|
|
150
|
+
}
|
|
151
|
+
local.onLoad?.()
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const handleError = () => {
|
|
155
|
+
// Try fallback source if available and we haven't tried it yet
|
|
156
|
+
if (local.fallbackSrc && !hasTriedFallback() && activeSrc() !== local.fallbackSrc) {
|
|
157
|
+
setHasTriedFallback(true)
|
|
158
|
+
setImageLoaded(false) // Reset loading state for proper transitions
|
|
159
|
+
setShowFallback(false) // Hide fallback UI while fallback loads
|
|
160
|
+
// Clear any existing timeout when switching to fallback
|
|
161
|
+
if (fallbackTimeout) {
|
|
162
|
+
clearTimeout(fallbackTimeout)
|
|
163
|
+
fallbackTimeout = undefined
|
|
164
|
+
}
|
|
165
|
+
setActiveSrc(local.fallbackSrc)
|
|
166
|
+
// Let the image try loading the fallback
|
|
167
|
+
return
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Show fallback after delay (only applies when there's no fallbackSrc)
|
|
171
|
+
if (local.fallbackDelay) {
|
|
172
|
+
fallbackTimeout = setTimeout(() => setShowFallback(true), local.fallbackDelay)
|
|
173
|
+
} else {
|
|
174
|
+
setShowFallback(true)
|
|
175
|
+
}
|
|
176
|
+
local.onError?.()
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const containerClass = () =>
|
|
180
|
+
cn(
|
|
181
|
+
'relative overflow-hidden',
|
|
182
|
+
local.overlayOnHover && 'group',
|
|
183
|
+
local.aspectRatio || ((local.scale || local.objectFit) ? 'w-full h-full' : 'w-full h-auto'),
|
|
184
|
+
local.rounded || 'rounded-lg',
|
|
185
|
+
local.class
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
const imageClass = () => {
|
|
189
|
+
const fit = effectiveObjectFit()
|
|
190
|
+
return cn(
|
|
191
|
+
'w-full h-full transition-opacity duration-300',
|
|
192
|
+
imageLoaded() ? 'opacity-100' : 'opacity-0',
|
|
193
|
+
fit && `object-${fit}`,
|
|
194
|
+
local.objectPosition
|
|
195
|
+
)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const skeletonClass = () =>
|
|
199
|
+
cn(
|
|
200
|
+
'absolute inset-0 bg-ink-200 animate-pulse',
|
|
201
|
+
local.rounded || 'rounded-lg'
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
const overlayPositionClass = () => {
|
|
205
|
+
const position = local.overlayPosition || 'bottom-right'
|
|
206
|
+
const baseClasses = 'absolute pointer-events-none'
|
|
207
|
+
|
|
208
|
+
switch (position) {
|
|
209
|
+
case 'top-left':
|
|
210
|
+
return `${baseClasses} top-2 left-2`
|
|
211
|
+
case 'top-right':
|
|
212
|
+
return `${baseClasses} top-2 right-2`
|
|
213
|
+
case 'bottom-left':
|
|
214
|
+
return `${baseClasses} bottom-2 left-2`
|
|
215
|
+
case 'bottom-right':
|
|
216
|
+
return `${baseClasses} bottom-2 right-2`
|
|
217
|
+
case 'center':
|
|
218
|
+
return `${baseClasses} top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2`
|
|
219
|
+
case 'full':
|
|
220
|
+
return 'absolute inset-0'
|
|
221
|
+
default:
|
|
222
|
+
return `${baseClasses} bottom-2 right-2`
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const overlayClass = () =>
|
|
227
|
+
cn(
|
|
228
|
+
overlayPositionClass(),
|
|
229
|
+
local.overlayOnHover && 'opacity-0 group-hover:opacity-100 transition-opacity duration-200',
|
|
230
|
+
!local.overlayOnHover && 'opacity-100'
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
return (
|
|
234
|
+
<div class={containerClass()} style={constraintStyles()}>
|
|
235
|
+
{/* Independent skeleton overlay - shows during initial load and fallback retry */}
|
|
236
|
+
<Show when={local.showSkeleton && !imageLoaded() && !showFallback()}>
|
|
237
|
+
<div class={skeletonClass()} />
|
|
238
|
+
</Show>
|
|
239
|
+
|
|
240
|
+
<Show when={activeSrc()} keyed>
|
|
241
|
+
{(src) => (
|
|
242
|
+
<KobalteImage onLoadingStatusChange={(status) => {
|
|
243
|
+
if (status === 'loaded') handleLoad()
|
|
244
|
+
else if (status === 'error') handleError()
|
|
245
|
+
}}>
|
|
246
|
+
<KobalteImage.Img
|
|
247
|
+
{...others}
|
|
248
|
+
src={src}
|
|
249
|
+
alt={local.alt}
|
|
250
|
+
loading={local.lazy ? 'lazy' : 'eager'}
|
|
251
|
+
class={imageClass()}
|
|
252
|
+
/>
|
|
253
|
+
<KobalteImage.Fallback>
|
|
254
|
+
<Show when={showFallback()}>
|
|
255
|
+
{local.fallback || (
|
|
256
|
+
<div class="flex items-center justify-center w-full h-full bg-surface-dim border border-surface-border">
|
|
257
|
+
<div class="text-center p-4">
|
|
258
|
+
<svg class="w-12 h-12 mx-auto mb-2 text-ink-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
259
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
|
260
|
+
</svg>
|
|
261
|
+
<p class="text-sm text-ink-500">Failed to load image</p>
|
|
262
|
+
</div>
|
|
263
|
+
</div>
|
|
264
|
+
)}
|
|
265
|
+
</Show>
|
|
266
|
+
</KobalteImage.Fallback>
|
|
267
|
+
</KobalteImage>
|
|
268
|
+
)}
|
|
269
|
+
</Show>
|
|
270
|
+
<Show when={local.overlay && imageLoaded()}>
|
|
271
|
+
<div class={overlayClass()}>
|
|
272
|
+
{local.overlay}
|
|
273
|
+
</div>
|
|
274
|
+
</Show>
|
|
275
|
+
</div>
|
|
276
|
+
)
|
|
277
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { type JSX, For, Show, splitProps } from 'solid-js'
|
|
2
|
+
import { cn } from '../../utilities/classNames'
|
|
3
|
+
|
|
4
|
+
export type KbdVariant = 'default' | 'flat'
|
|
5
|
+
export type KbdSize = 'sm' | 'md' | 'lg'
|
|
6
|
+
|
|
7
|
+
export interface KbdProps extends JSX.HTMLAttributes<HTMLElement> {
|
|
8
|
+
/** Visual style. default = raised key with bottom border; flat = simple outlined. Default: default. */
|
|
9
|
+
variant?: KbdVariant
|
|
10
|
+
/** Size. Default: md. */
|
|
11
|
+
size?: KbdSize
|
|
12
|
+
children?: JSX.Element
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface KbdShortcutProps {
|
|
16
|
+
/** Ordered list of key labels to display (e.g. [KEY.Cmd, 'K']). */
|
|
17
|
+
keys: string[]
|
|
18
|
+
/** Passed to each Kbd. Default: default. */
|
|
19
|
+
variant?: KbdVariant
|
|
20
|
+
/** Passed to each Kbd. Default: md. */
|
|
21
|
+
size?: KbdSize
|
|
22
|
+
/** Separator rendered between keys. Default: '+'. */
|
|
23
|
+
separator?: string
|
|
24
|
+
class?: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Common special key symbols for use with Kbd and KbdShortcut. */
|
|
28
|
+
export const KEY = {
|
|
29
|
+
Cmd: '⌘',
|
|
30
|
+
Shift: '⇧',
|
|
31
|
+
Option: '⌥',
|
|
32
|
+
Alt: '⌥',
|
|
33
|
+
Ctrl: '⌃',
|
|
34
|
+
Enter: '↵',
|
|
35
|
+
Backspace: '⌫',
|
|
36
|
+
Delete: '⌦',
|
|
37
|
+
Escape: 'Esc',
|
|
38
|
+
Tab: '⇥',
|
|
39
|
+
Up: '↑',
|
|
40
|
+
Down: '↓',
|
|
41
|
+
Left: '←',
|
|
42
|
+
Right: '→',
|
|
43
|
+
} as const
|
|
44
|
+
|
|
45
|
+
const variantClasses: Record<KbdVariant, string> = {
|
|
46
|
+
default: [
|
|
47
|
+
'bg-surface-raised border border-surface-border border-b-2',
|
|
48
|
+
'shadow-sm dark:shadow-none',
|
|
49
|
+
].join(' '),
|
|
50
|
+
flat: 'bg-surface-overlay border border-surface-border',
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const sizeClasses: Record<KbdSize, string> = {
|
|
54
|
+
sm: 'h-5 min-w-5 px-1 text-[10px] rounded',
|
|
55
|
+
md: 'h-6 min-w-6 px-1.5 text-[11px] rounded',
|
|
56
|
+
lg: 'h-7 min-w-7 px-2 text-xs rounded-md',
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Displays a single keyboard key.
|
|
61
|
+
* Use KbdShortcut for multi-key combinations.
|
|
62
|
+
*/
|
|
63
|
+
export function Kbd(props: KbdProps) {
|
|
64
|
+
const [local, others] = splitProps(props, ['variant', 'size', 'class', 'children'])
|
|
65
|
+
const variant = () => local.variant ?? 'default'
|
|
66
|
+
const size = () => local.size ?? 'md'
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<kbd
|
|
70
|
+
class={cn(
|
|
71
|
+
'inline-flex items-center justify-center font-mono font-medium leading-none text-ink-600',
|
|
72
|
+
variantClasses[variant()],
|
|
73
|
+
sizeClasses[size()],
|
|
74
|
+
local.class,
|
|
75
|
+
)}
|
|
76
|
+
{...others}
|
|
77
|
+
>
|
|
78
|
+
{local.children}
|
|
79
|
+
</kbd>
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Displays a keyboard shortcut as a sequence of Kbd keys separated by a delimiter.
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* <KbdShortcut keys={[KEY.Cmd, 'K']} />
|
|
88
|
+
* <KbdShortcut keys={['Ctrl', 'Shift', 'P']} separator="+" />
|
|
89
|
+
*/
|
|
90
|
+
export function KbdShortcut(props: KbdShortcutProps) {
|
|
91
|
+
const sep = () => props.separator ?? '+'
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<span class={cn('inline-flex items-center gap-0.5', props.class)}>
|
|
95
|
+
<For each={props.keys}>
|
|
96
|
+
{(key, i) => (
|
|
97
|
+
<>
|
|
98
|
+
<Show when={i() > 0}>
|
|
99
|
+
<span
|
|
100
|
+
class="mx-0.5 select-none font-sans text-[10px] text-ink-400"
|
|
101
|
+
aria-hidden="true"
|
|
102
|
+
>
|
|
103
|
+
{sep()}
|
|
104
|
+
</span>
|
|
105
|
+
</Show>
|
|
106
|
+
<Kbd variant={props.variant} size={props.size}>
|
|
107
|
+
{key}
|
|
108
|
+
</Kbd>
|
|
109
|
+
</>
|
|
110
|
+
)}
|
|
111
|
+
</For>
|
|
112
|
+
</span>
|
|
113
|
+
)
|
|
114
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { type JSX, splitProps } from 'solid-js'
|
|
2
|
+
import { cn } from '../../utilities/classNames'
|
|
3
|
+
import { Avatar } from './Avatar'
|
|
4
|
+
import type { SizeKey, AvatarShape, AvatarColor } from '../../types/avatar-types'
|
|
5
|
+
|
|
6
|
+
export interface PersonaProps extends Omit<JSX.HTMLAttributes<HTMLDivElement>, 'children'> {
|
|
7
|
+
/** Display name (primary text); also used for avatar initials. */
|
|
8
|
+
name: string
|
|
9
|
+
/** Optional image URL for the avatar. */
|
|
10
|
+
imageUrl?: string | null
|
|
11
|
+
/** Optional secondary line (e.g. role, email). */
|
|
12
|
+
secondary?: string
|
|
13
|
+
/** Avatar and text size. Default: md. */
|
|
14
|
+
size?: SizeKey
|
|
15
|
+
/** Avatar shape passed through to Avatar. Default: circle. */
|
|
16
|
+
shape?: AvatarShape
|
|
17
|
+
/** Avatar color passed through to Avatar. Default: neutral. */
|
|
18
|
+
color?: AvatarColor
|
|
19
|
+
/** Optional content after the text block (e.g. actions). */
|
|
20
|
+
children?: JSX.Element
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const sizeStyles: Record<SizeKey, { gap: string; name: string; secondary: string }> = {
|
|
24
|
+
sm: { gap: 'gap-2', name: 'text-sm', secondary: 'text-xs' },
|
|
25
|
+
md: { gap: 'gap-3', name: 'text-sm', secondary: 'text-sm' },
|
|
26
|
+
lg: { gap: 'gap-4', name: 'text-base', secondary: 'text-sm' },
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A row combining Avatar with primary name and optional secondary text.
|
|
31
|
+
* Use in lists, dropdowns, or cards for a compact user/profile display.
|
|
32
|
+
*/
|
|
33
|
+
export function Persona(props: PersonaProps) {
|
|
34
|
+
const [local, others] = splitProps(props, [
|
|
35
|
+
'name',
|
|
36
|
+
'imageUrl',
|
|
37
|
+
'secondary',
|
|
38
|
+
'size',
|
|
39
|
+
'shape',
|
|
40
|
+
'color',
|
|
41
|
+
'class',
|
|
42
|
+
'children',
|
|
43
|
+
])
|
|
44
|
+
|
|
45
|
+
const size = () => local.size ?? 'md'
|
|
46
|
+
const styles = () => sizeStyles[size()]
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<div
|
|
50
|
+
class={cn(
|
|
51
|
+
'flex items-center min-w-0',
|
|
52
|
+
styles().gap,
|
|
53
|
+
local.class,
|
|
54
|
+
)}
|
|
55
|
+
{...others}
|
|
56
|
+
>
|
|
57
|
+
<Avatar
|
|
58
|
+
decorative
|
|
59
|
+
name={local.name}
|
|
60
|
+
imageUrl={local.imageUrl}
|
|
61
|
+
size={size()}
|
|
62
|
+
shape={local.shape}
|
|
63
|
+
color={local.color}
|
|
64
|
+
/>
|
|
65
|
+
<div class="min-w-0 flex-1">
|
|
66
|
+
<div class={cn('font-medium text-ink-900 truncate', styles().name)}>
|
|
67
|
+
{local.name}
|
|
68
|
+
</div>
|
|
69
|
+
{local.secondary && (
|
|
70
|
+
<div class={cn('text-ink-500 truncate', styles().secondary)}>
|
|
71
|
+
{local.secondary}
|
|
72
|
+
</div>
|
|
73
|
+
)}
|
|
74
|
+
</div>
|
|
75
|
+
{local.children}
|
|
76
|
+
</div>
|
|
77
|
+
)
|
|
78
|
+
}
|