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