@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,166 @@
1
+ import type { JSX, ParentComponent } from 'solid-js'
2
+ import { splitProps } from 'solid-js'
3
+ import { Dynamic } from 'solid-js/web'
4
+ import { Avatar } from '../data-display/Avatar'
5
+ import { cn } from '../../utilities/classNames'
6
+
7
+ const cardBase =
8
+ 'rounded-xl border border-surface-border bg-surface-raised shadow-sm'
9
+
10
+ export interface CardProps extends Omit<JSX.HTMLAttributes<HTMLDivElement>, 'children'> {
11
+ children: JSX.Element
12
+ /** Horizontal layout: image (or first block) on the left, content on the right. Use with Card.Image (horizontal) + Card.Content. */
13
+ horizontal?: boolean
14
+ }
15
+
16
+ export type CardComponent = ParentComponent<CardProps> & {
17
+ Header: typeof CardHeader
18
+ Image: typeof CardImage
19
+ AvatarTitle: typeof CardAvatarTitle
20
+ Content: typeof CardContent
21
+ Body: typeof CardBody
22
+ }
23
+
24
+ /** Card root: rounded border, background, optional horizontal layout. Use with Card.Header, Card.Image, Card.AvatarTitle, Card.Body, Card.Content. */
25
+ export const Card: CardComponent = (props) => {
26
+ const [local, others] = splitProps(props, ['children', 'horizontal', 'class', 'ref'])
27
+ return (
28
+ <div
29
+ ref={local.ref}
30
+ class={cn(
31
+ cardBase,
32
+ local.horizontal ? 'flex flex-row overflow-hidden' : 'flex flex-col',
33
+ local.class,
34
+ )}
35
+ {...others}
36
+ >
37
+ {local.children}
38
+ </div>
39
+ )
40
+ }
41
+
42
+ export interface CardHeaderProps {
43
+ title: string
44
+ /** Optional action (e.g. button) on the right. */
45
+ action?: JSX.Element
46
+ /** Heading element to render. Default 'h3'. Use to maintain proper document outline in different contexts. */
47
+ as?: 'h2' | 'h3' | 'h4' | 'h5' | 'h6'
48
+ class?: string
49
+ }
50
+
51
+ /** Card header: title and optional action. Renders a bordered bottom. */
52
+ export function CardHeader(props: CardHeaderProps): JSX.Element {
53
+ const headingTag = () => props.as ?? 'h3'
54
+ return (
55
+ <div
56
+ class={cn(
57
+ 'flex shrink-0 items-center justify-between gap-3 border-b border-surface-border px-6 py-4',
58
+ props.class,
59
+ )}
60
+ >
61
+ <Dynamic
62
+ component={headingTag()}
63
+ class="text-base font-semibold text-ink-900"
64
+ >
65
+ {props.title}
66
+ </Dynamic>
67
+ {props.action}
68
+ </div>
69
+ )
70
+ }
71
+
72
+ export interface CardImageProps {
73
+ src: string
74
+ alt: string
75
+ /** Use for horizontal cards so the image has a fixed width and doesn't stretch. */
76
+ horizontal?: boolean
77
+ class?: string
78
+ imgClass?: string
79
+ }
80
+
81
+ /** Full-width image at top (or left when horizontal). Use horizontal=true inside a Card with horizontal. */
82
+ export function CardImage(props: CardImageProps): JSX.Element {
83
+ return (
84
+ <div
85
+ class={cn(
86
+ 'shrink-0 overflow-hidden',
87
+ props.horizontal ? 'w-36 self-stretch' : 'w-full rounded-t-xl',
88
+ props.class,
89
+ )}
90
+ >
91
+ <img
92
+ src={props.src}
93
+ alt={props.alt}
94
+ class={cn(
95
+ 'object-cover',
96
+ props.horizontal ? 'h-full min-h-0 w-full' : 'h-auto w-full',
97
+ props.imgClass,
98
+ )}
99
+ />
100
+ </div>
101
+ )
102
+ }
103
+
104
+ export interface CardAvatarTitleProps {
105
+ /** Display name shown next to the avatar. */
106
+ name: string
107
+ /** Optional image URL for the avatar. */
108
+ imageUrl?: string | null
109
+ /** Avatar size. */
110
+ avatarSize?: 'sm' | 'md'
111
+ class?: string
112
+ }
113
+
114
+ /** Row with Avatar and name. Use for "card with avatar and user name". */
115
+ export function CardAvatarTitle(props: CardAvatarTitleProps): JSX.Element {
116
+ return (
117
+ <div
118
+ class={cn(
119
+ 'flex shrink-0 items-center gap-3 px-6 pt-4 pb-0',
120
+ props.class,
121
+ )}
122
+ >
123
+ <Avatar
124
+ name={props.name}
125
+ imageUrl={props.imageUrl}
126
+ size={props.avatarSize ?? 'md'}
127
+ />
128
+ <span class="font-medium text-ink-900">
129
+ {props.name}
130
+ </span>
131
+ </div>
132
+ )
133
+ }
134
+
135
+ export interface CardContentProps {
136
+ children: JSX.Element
137
+ /** Use when card is horizontal so this block fills the remaining space. */
138
+ class?: string
139
+ }
140
+
141
+ /** Wrapper for the main content column. Use as the second child in horizontal cards (after Card.Image) so content fills remaining space. */
142
+ export const CardContent: ParentComponent<CardContentProps> = (props) => {
143
+ return (
144
+ <div class={cn('flex min-w-0 flex-1 flex-col', props.class)}>
145
+ {props.children}
146
+ </div>
147
+ )
148
+ }
149
+
150
+ export interface CardBodyProps {
151
+ children: JSX.Element
152
+ class?: string
153
+ }
154
+
155
+ /** Card body: padded content area. */
156
+ export const CardBody: ParentComponent<CardBodyProps> = (props) => {
157
+ return (
158
+ <div class={cn('flex-1 p-6 sm:p-8', props.class)}>{props.children}</div>
159
+ )
160
+ }
161
+
162
+ Card.Header = CardHeader
163
+ Card.Image = CardImage
164
+ Card.AvatarTitle = CardAvatarTitle
165
+ Card.Content = CardContent
166
+ Card.Body = CardBody
@@ -0,0 +1,477 @@
1
+ import { createSignal, createEffect, type JSX, Show, For, splitProps } from 'solid-js'
2
+ import { ChevronDown, ChevronUp } from 'lucide-solid'
3
+ import { Copy } from '../../actions/Copy'
4
+ import { CollapsibleRoot, CollapsibleTrigger, CollapsibleContentStyled } from '../Collapsible'
5
+ import { PopoverRoot, PopoverTrigger, PopoverContent } from '../../overlays/Popover'
6
+ import { cn } from '../../../utilities/classNames'
7
+ import { highlightElement } from './prism'
8
+
9
+ export interface CodeBlockLanguage {
10
+ /** Unique id for the tab (e.g. "js", "ts"). */
11
+ id: string
12
+ /** Tab label (e.g. "JavaScript", "TypeScript"). */
13
+ label: string
14
+ /** Code content for this variant. */
15
+ content: string
16
+ /** Prism language for syntax highlighting (e.g. "javascript", "typescript"). Defaults to id. */
17
+ language?: string
18
+ /** Optional icon (e.g. SVG). Pass a function to show the icon in both the trigger and the list (e.g. `() => <Icon name="JS" />`); a static element can only appear in one place. */
19
+ icon?: JSX.Element | (() => JSX.Element)
20
+ }
21
+
22
+ export interface CodeBlockProps extends Omit<JSX.HTMLAttributes<HTMLDivElement>, 'children'> {
23
+ /** Code content (single block). When used, language applies. */
24
+ content?: string
25
+ /** Alternate content (e.g. component-only snippet). When set, a toggle in the header switches between content and alternateContent. */
26
+ alternateContent?: string
27
+ /** Prism language for syntax highlighting when using content (e.g. "javascript", "typescript"). */
28
+ language?: string
29
+ /** Multiple language variants with a tab switcher. When set, content/language are ignored for initial display. */
30
+ languages?: CodeBlockLanguage[]
31
+ /** Optional filename or title shown in the header (e.g. "Table.jsx"). */
32
+ filename?: string
33
+ /** Optional label in the header (e.g. "Embed Code"). Shown with or instead of filename. */
34
+ label?: string
35
+ /** Optional icon or image (e.g. SVG) shown to the left of the filename/label in the header. */
36
+ headerIcon?: JSX.Element
37
+ /** Show line numbers. */
38
+ showLineNumbers?: boolean
39
+ /** Line numbers to highlight (e.g. [2, 3, 5]). */
40
+ highlightLines?: number[]
41
+ /** When true, force dark appearance regardless of app theme. When undefined, follow app light/dark (e.g. .dark on root). */
42
+ dark?: boolean
43
+ /** Use primary (brand) background. Overrides dark. */
44
+ primary?: boolean
45
+ /** Minimum height (e.g. "min-h-[120px]"). Default none (min-h-0) so short snippets don't reserve extra space. */
46
+ minHeight?: string
47
+ /** When true, code is in a collapsible section with a "Show code" / "Hide code" trigger. */
48
+ collapsible?: boolean
49
+ /** When collapsible, whether the code is open by default. Default false. */
50
+ defaultCodeOpen?: boolean
51
+ /** Label for the trigger when code is hidden. Default "Show code". */
52
+ collapsibleLabelShow?: string
53
+ /** Label for the trigger when code is visible. Default "Hide code". */
54
+ collapsibleLabelHide?: string
55
+ class?: string
56
+ preProps?: JSX.HTMLAttributes<HTMLPreElement>
57
+ }
58
+
59
+ function getHeaderTitle(props: CodeBlockProps): string | undefined {
60
+ if (props.filename != null && props.filename !== '') return props.filename
61
+ if (props.label != null && props.label !== '') return props.label
62
+ return undefined
63
+ }
64
+
65
+ /**
66
+ * Code block with optional copy button, language switcher, line numbers, and syntax highlighting (Prism).
67
+ * Use content + language for a single block, or languages for a tabbed multi-variant block.
68
+ * Import token styles in your app or tokens will be unstyled: `import '@torchui/solid/styles/code-block-tokens.css'`
69
+ * Language must match a Prism grammar in packages/ui/src/lib/prism.ts (or an alias there).
70
+
71
+ */
72
+ const CODE_BLOCK_PROP_KEYS = [
73
+ 'content',
74
+ 'alternateContent',
75
+ 'language',
76
+ 'languages',
77
+ 'filename',
78
+ 'label',
79
+ 'headerIcon',
80
+ 'showLineNumbers',
81
+ 'highlightLines',
82
+ 'dark',
83
+ 'primary',
84
+ 'minHeight',
85
+ 'collapsible',
86
+ 'defaultCodeOpen',
87
+ 'collapsibleLabelShow',
88
+ 'collapsibleLabelHide',
89
+ 'class',
90
+ 'preProps',
91
+ 'ref',
92
+ ] as const
93
+
94
+ export function CodeBlock(props: CodeBlockProps) {
95
+ const [local, others] = splitProps(props, [...CODE_BLOCK_PROP_KEYS])
96
+ const primary = () => local.primary === true
97
+ /** Follow app theme when not forcing dark. */
98
+ const themeAuto = () => !primary() && local.dark !== true
99
+ const dark = () => local.dark === true
100
+ const minHeight = () => local.minHeight ?? 'min-h-0'
101
+ const showLineNumbers = () => local.showLineNumbers === true
102
+ const highlightLines = () => local.highlightLines ?? []
103
+
104
+ const [selectedIndex, setSelectedIndex] = createSignal(0)
105
+ const [languageOpen, setLanguageOpen] = createSignal(false)
106
+ const [showAlternate, setShowAlternate] = createSignal(true)
107
+ const [codeOpen, setCodeOpen] = createSignal(local.defaultCodeOpen ?? false)
108
+ const collapsible = () => local.collapsible === true
109
+ const triggerClass = () =>
110
+ cn(
111
+ 'flex w-full items-center justify-center gap-2 rounded-none border-t border-surface-border py-2.5 text-sm font-medium',
112
+ 'text-ink-600 hover:text-ink-900 dark:hover:text-ink-100',
113
+ 'bg-surface-overlay hover:bg-surface-dim',
114
+ 'focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-500'
115
+ )
116
+
117
+ const currentContent = (): string => {
118
+ const langs = local.languages
119
+ if (langs && langs.length > 0) {
120
+ const idx = selectedIndex()
121
+ return langs[idx]?.content ?? langs[0].content
122
+ }
123
+ return local.content ?? ''
124
+ }
125
+
126
+ /** When alternateContent is set, this is the content actually displayed (full vs snippet). */
127
+ const effectiveContent = (): string => {
128
+ if (local.alternateContent != null && showAlternate()) return local.alternateContent
129
+ return currentContent()
130
+ }
131
+
132
+ const currentLanguage = (): string => {
133
+ const langs = local.languages
134
+ if (langs && langs.length > 0) {
135
+ const idx = selectedIndex()
136
+ const item = langs[idx] ?? langs[0]
137
+ return item?.language ?? item?.id ?? 'text'
138
+ }
139
+ return local.language ?? 'text'
140
+ }
141
+
142
+ /** Currently selected language item (for icon, etc.). */
143
+ const selectedLanguageItem = (): CodeBlockLanguage | undefined => {
144
+ const langs = local.languages ?? []
145
+ const idx = selectedIndex()
146
+ return langs[idx] ?? langs[0]
147
+ }
148
+
149
+ const hasHeader = () => {
150
+ const title = getHeaderTitle(local)
151
+ const hasLangs = local.languages && local.languages.length > 1
152
+ const hasAlternate = local.alternateContent != null && local.alternateContent.trim() !== ''
153
+ return !!(title || hasLangs || hasAlternate)
154
+ }
155
+
156
+ const lines = () => {
157
+ const text = effectiveContent()
158
+ return text.split(/\r?\n/)
159
+ }
160
+
161
+ const containerClass = () =>
162
+ primary()
163
+ ? 'border border-primary-600/80 bg-primary-600'
164
+ : themeAuto()
165
+ ? 'border border-surface-border bg-surface-base'
166
+ : dark()
167
+ ? 'border border-ink-800 bg-ink-900'
168
+ : 'border border-ink-200 bg-ink-50'
169
+
170
+ const headerBorderClass = () =>
171
+ primary()
172
+ ? 'border-primary-500/60'
173
+ : themeAuto()
174
+ ? 'border-surface-border'
175
+ : dark()
176
+ ? 'border-ink-800'
177
+ : 'border-ink-200'
178
+
179
+ const headerTextClass = () =>
180
+ primary()
181
+ ? 'text-white/90'
182
+ : themeAuto()
183
+ ? 'text-ink-500'
184
+ : dark()
185
+ ? 'text-ink-400'
186
+ : 'text-ink-500'
187
+
188
+ /* Minimal copy: icon only, no border/outline/ring, blends into header */
189
+ const copyButtonClass = () =>
190
+ primary()
191
+ ? '!border-0 !bg-transparent !shadow-none !ring-0 !ring-offset-0 text-white/70 hover:!bg-white/15 hover:text-white'
192
+ : themeAuto()
193
+ ? '!border-0 !bg-transparent !shadow-none !ring-0 !ring-offset-0 text-ink-500 hover:!bg-surface-overlay hover:text-ink-700 dark:hover:text-ink-200'
194
+ : dark()
195
+ ? '!border-0 !bg-transparent !shadow-none !ring-0 !ring-offset-0 text-ink-400 hover:!bg-ink-850 hover:text-ink-200'
196
+ : '!border-0 !bg-transparent !shadow-none !ring-0 !ring-offset-0 text-ink-500 hover:!bg-ink-200 hover:text-ink-700'
197
+
198
+ /* Line numbers: light grey, right-aligned, subtle separator (like reference) */
199
+ const lineNumClass = () =>
200
+ primary()
201
+ ? 'text-white/40'
202
+ : themeAuto()
203
+ ? 'text-ink-400'
204
+ : dark()
205
+ ? 'text-ink-500'
206
+ : 'text-ink-400'
207
+
208
+ const [codeEl, setCodeEl] = createSignal<HTMLElement | null>(null)
209
+ /** Use textContent + Prism.highlightElement() for safe dynamic content highlighting. */
210
+ createEffect(() => {
211
+ const el = codeEl()
212
+ if (!el) return
213
+
214
+ const content = effectiveContent()
215
+ const _lang = currentLanguage() // track language changes
216
+
217
+ // 1) SAFE: never interpret user content as HTML
218
+ el.textContent = content
219
+
220
+ // 2) Prism generates markup based on textContent (language class is declarative)
221
+ highlightElement(el, currentLanguage())
222
+ })
223
+ /* Token styling: .code-block (follows app .dark) or forced .code-block-dark / .code-block-primary */
224
+ const themeClass = () =>
225
+ primary()
226
+ ? 'code-block-primary'
227
+ : dark()
228
+ ? 'code-block-dark'
229
+ : 'code-block'
230
+
231
+ const codeContent = (
232
+ <>
233
+ <Show when={hasHeader()}>
234
+ <div
235
+ class={cn(
236
+ 'flex items-center justify-between gap-2 px-3 py-2 border-b',
237
+ headerBorderClass()
238
+ )}
239
+ >
240
+ <div class="min-w-0 flex-1 flex items-center gap-2 truncate">
241
+ <Show when={local.headerIcon}>
242
+ <span class="shrink-0 flex items-center [&>svg]:size-4 [&>img]:size-4" aria-hidden="true">
243
+ {local.headerIcon}
244
+ </span>
245
+ </Show>
246
+ <Show when={getHeaderTitle(local)}>
247
+ <span
248
+ class={cn(
249
+ 'text-xs font-medium truncate',
250
+ headerTextClass()
251
+ )}
252
+ >
253
+ {getHeaderTitle(local)}
254
+ </span>
255
+ </Show>
256
+ </div>
257
+ <div class="flex shrink-0 items-center gap-2">
258
+ <Show when={local.languages && local.languages.length > 1}>
259
+ <PopoverRoot open={languageOpen()} onOpenChange={setLanguageOpen} align="end">
260
+ <PopoverTrigger
261
+ as="button"
262
+ type="button"
263
+ aria-label="Language"
264
+ class={cn(
265
+ 'h-7 min-w-0 flex items-center gap-1.5 rounded border text-xs font-medium overflow-hidden cursor-pointer focus:outline-none focus:ring-2 focus:ring-inset focus:ring-primary-500',
266
+ primary()
267
+ ? 'bg-white/10 border-white/30 text-white hover:bg-white/15'
268
+ : themeAuto()
269
+ ? 'bg-surface-base border-surface-border text-ink-700 hover:bg-surface-overlay'
270
+ : dark()
271
+ ? 'bg-ink-800 border-ink-600 text-ink-300 hover:bg-ink-700'
272
+ : 'bg-ink-50 border-ink-200 text-ink-700 hover:bg-ink-100'
273
+ )}
274
+ >
275
+ <Show when={selectedLanguageItem()?.icon}>
276
+ <span class="shrink-0 flex items-center pl-2 [&>svg]:size-3.5 [&>img]:size-3.5 text-inherit" aria-hidden="true">
277
+ {(typeof selectedLanguageItem()!.icon === 'function'
278
+ ? (selectedLanguageItem()!.icon as () => JSX.Element)()
279
+ : selectedLanguageItem()!.icon) as JSX.Element}
280
+ </span>
281
+ </Show>
282
+ <span class="min-w-0 flex-1 truncate text-left py-1 pr-1 pl-1.5">
283
+ {selectedLanguageItem()?.label ?? ''}
284
+ </span>
285
+ <ChevronDown class="h-3.5 w-3.5 shrink-0 mr-1.5 opacity-70" aria-hidden="true" />
286
+ </PopoverTrigger>
287
+ <PopoverContent
288
+ class={cn(
289
+ 'min-w-0 p-1 max-h-60 overflow-auto',
290
+ primary()
291
+ ? 'bg-ink-900 border-white/20'
292
+ : themeAuto()
293
+ ? 'bg-surface-raised border-surface-border'
294
+ : dark()
295
+ ? 'bg-ink-900 border-ink-700'
296
+ : 'bg-ink-900 border-ink-700'
297
+ )}
298
+ >
299
+ <div class="flex flex-col" role="menu" aria-label="Language">
300
+ <For each={local.languages ?? []}>
301
+ {(item, idx) => (
302
+ <button
303
+ type="button"
304
+ role="menuitemradio"
305
+ aria-checked={selectedIndex() === idx()}
306
+ class={cn(
307
+ 'w-full flex items-center gap-2 rounded px-2 py-1.5 text-left text-xs font-medium transition-colors',
308
+ primary()
309
+ ? 'text-ink-200 hover:bg-white/10 hover:text-white'
310
+ : themeAuto()
311
+ ? 'text-ink-700 hover:bg-surface-overlay'
312
+ : dark()
313
+ ? 'text-ink-300 hover:bg-ink-800'
314
+ : 'text-ink-700 hover:bg-ink-100',
315
+ selectedIndex() === idx() && 'bg-primary-500/20 text-primary-600 dark:text-primary-400'
316
+ )}
317
+ onClick={() => {
318
+ setSelectedIndex(idx())
319
+ setLanguageOpen(false)
320
+ }}
321
+ >
322
+ <Show when={item.icon && (typeof item.icon === 'function' || selectedIndex() !== idx())}>
323
+ <span class="shrink-0 flex items-center [&>svg]:size-3.5 [&>img]:size-3.5 text-inherit" aria-hidden="true">
324
+ {typeof item.icon === 'function' ? (item.icon as () => JSX.Element)() : item.icon}
325
+ </span>
326
+ </Show>
327
+ <Show when={item.icon && typeof item.icon !== 'function' && selectedIndex() === idx()}>
328
+ <span class="w-3.5 shrink-0" aria-hidden="true" />
329
+ </Show>
330
+ <span class="min-w-0 truncate">{item.label}</span>
331
+ </button>
332
+ )}
333
+ </For>
334
+ </div>
335
+ </PopoverContent>
336
+ </PopoverRoot>
337
+ </Show>
338
+ <Show when={local.alternateContent != null && local.alternateContent.trim() !== ''}>
339
+ <button
340
+ type="button"
341
+ onClick={() => setShowAlternate((p) => !p)}
342
+ aria-pressed={showAlternate()}
343
+ class={cn(
344
+ 'flex items-center gap-1.5 shrink-0 px-2 py-1 text-xs font-medium rounded transition-colors',
345
+ primary()
346
+ ? 'text-white/80 hover:bg-white/15 hover:text-white'
347
+ : themeAuto()
348
+ ? 'text-ink-600 hover:bg-surface-overlay hover:text-ink-900 dark:hover:text-ink-100'
349
+ : dark()
350
+ ? 'text-ink-400 hover:bg-ink-850 hover:text-ink-100'
351
+ : 'text-ink-600 hover:bg-ink-200 hover:text-ink-800'
352
+ )}
353
+ >
354
+ {showAlternate() ? 'Full code' : 'Component only'}
355
+ </button>
356
+ </Show>
357
+ <Copy
358
+ text={effectiveContent()}
359
+ display="icon-only"
360
+ variant="ghost"
361
+ size="sm"
362
+ class={copyButtonClass()}
363
+ />
364
+ </div>
365
+ </div>
366
+ </Show>
367
+
368
+ <Show when={!hasHeader()}>
369
+ <div class="absolute top-4 right-4 z-10 opacity-0 transition-opacity group-hover:opacity-100 group-focus-within:opacity-100">
370
+ <Copy
371
+ text={effectiveContent()}
372
+ display="icon-only"
373
+ variant="ghost"
374
+ size="sm"
375
+ class={copyButtonClass()}
376
+ />
377
+ </div>
378
+ </Show>
379
+
380
+ <pre
381
+ data-torchui="code-block"
382
+ class={cn(
383
+ 'w-full py-3 px-4 text-sm font-mono whitespace-pre overflow-x-auto overflow-y-auto',
384
+ minHeight(),
385
+ themeClass(),
386
+ /* No text-* on pre: let Prism theme (or token CSS) color tokens; otherwise inherited color overrides .token */
387
+ showLineNumbers() && 'pl-0',
388
+ local.preProps?.class
389
+ )}
390
+ {...(local.preProps ? (() => {
391
+ const { class: _, ...rest } = local.preProps
392
+ return rest
393
+ })() : {})}
394
+ >
395
+ <Show
396
+ when={showLineNumbers()}
397
+ fallback={
398
+ <code ref={setCodeEl} class={cn('block leading-5', `language-${currentLanguage()}`)} />
399
+ }
400
+ >
401
+ <div class="flex min-w-0">
402
+ <div
403
+ class={cn(
404
+ 'select-none py-3 pr-3 pl-0 w-8 shrink-0 text-right text-xs font-mono tabular-nums',
405
+ lineNumClass()
406
+ )}
407
+ aria-hidden="true"
408
+ >
409
+ <For each={lines()}>
410
+ {(_, i) => {
411
+ const num = i() + 1
412
+ const isHighlighted = () => highlightLines().includes(num)
413
+ return (
414
+ <div
415
+ class={cn(
416
+ 'leading-5',
417
+ isHighlighted() &&
418
+ (primary()
419
+ ? 'bg-white/20 -mx-1 px-1 rounded'
420
+ : themeAuto()
421
+ ? 'bg-surface-dim -mx-1 px-1 rounded'
422
+ : dark()
423
+ ? 'bg-ink-850 -mx-1 px-1 rounded'
424
+ : 'bg-primary-100 dark:bg-primary-900/30 -mx-1 px-1 rounded')
425
+ )}
426
+ >
427
+ {num}
428
+ </div>
429
+ )
430
+ }}
431
+ </For>
432
+ </div>
433
+ <code
434
+ ref={setCodeEl}
435
+ class={cn('block flex-1 min-w-0 py-4 pr-4 pl-4 leading-6', `language-${currentLanguage()}`)}
436
+ />
437
+ </div>
438
+ </Show>
439
+ </pre>
440
+ </>
441
+ )
442
+
443
+ return (
444
+ <div
445
+ ref={local.ref}
446
+ data-torchui="code-block-container"
447
+ class={cn(
448
+ 'rounded-lg overflow-hidden relative',
449
+ !hasHeader() && !collapsible() && 'group',
450
+ containerClass(),
451
+ local.class
452
+ )}
453
+ {...others}
454
+ >
455
+ <Show when={collapsible()} fallback={codeContent}>
456
+ <CollapsibleRoot open={codeOpen()} onOpenChange={setCodeOpen}>
457
+ <CollapsibleTrigger class={triggerClass()}>
458
+ {codeOpen() ? (
459
+ <>
460
+ <ChevronUp class="h-4 w-4" aria-hidden="true" />
461
+ {local.collapsibleLabelHide ?? 'Hide code'}
462
+ </>
463
+ ) : (
464
+ <>
465
+ <ChevronDown class="h-4 w-4" aria-hidden="true" />
466
+ {local.collapsibleLabelShow ?? 'Show code'}
467
+ </>
468
+ )}
469
+ </CollapsibleTrigger>
470
+ <CollapsibleContentStyled>
471
+ <div class="border-t border-surface-border">{codeContent}</div>
472
+ </CollapsibleContentStyled>
473
+ </CollapsibleRoot>
474
+ </Show>
475
+ </div>
476
+ )
477
+ }