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