@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,104 @@
1
+ /**
2
+ * CodeBlock Prism token colors. Import in your app:
3
+ * import '@torchui/solid/styles/code-block-tokens.css'
4
+ *
5
+ * Theme via CSS variables (e.g. in Tailwind @theme or :root / .dark):
6
+ * --code-token-default untokenized text + base token
7
+ * --code-token-comment comment, prolog, doctype
8
+ * --code-token-punctuation
9
+ * --code-token-keyword keyword, boolean
10
+ * --code-token-function function, class-name
11
+ * --code-token-string string, char, attr-value
12
+ * --code-token-number
13
+ * --code-token-property property, tag, symbol, selector, attr-name, builtin
14
+ * --code-token-operator operator, entity, url
15
+ * --code-token-variable regex, important, variable
16
+ *
17
+ * Set on :root (or @theme) for light; set on .dark for dark. For CodeBlock with dark prop
18
+ * to show dark tokens in light mode, set the same dark variables on .code-block-dark in your app:
19
+ * .code-block-dark pre[data-torchui="code-block"] { ...dark vars... }
20
+ */
21
+
22
+ /* Base rule with safe fallbacks */
23
+ pre[data-torchui="code-block"] {
24
+ color: var(--code-token-default, currentColor);
25
+ }
26
+ pre[data-torchui="code-block"] :is(.token, .token *) {
27
+ color: var(--code-token-default, currentColor);
28
+ }
29
+
30
+ /* Token-specific colors with fallbacks to default */
31
+ pre[data-torchui="code-block"] .token.comment,
32
+ pre[data-torchui="code-block"] .token.prolog,
33
+ pre[data-torchui="code-block"] .token.doctype {
34
+ color: var(--code-token-comment, var(--code-token-default, currentColor));
35
+ }
36
+
37
+ pre[data-torchui="code-block"] .token.punctuation {
38
+ color: var(--code-token-punctuation, var(--code-token-default, currentColor));
39
+ }
40
+
41
+ pre[data-torchui="code-block"] .token.keyword,
42
+ pre[data-torchui="code-block"] .token.boolean {
43
+ color: var(--code-token-keyword, var(--code-token-default, currentColor));
44
+ }
45
+
46
+ pre[data-torchui="code-block"] .token.function,
47
+ pre[data-torchui="code-block"] .token.class-name {
48
+ color: var(--code-token-function, var(--code-token-default, currentColor));
49
+ }
50
+
51
+ pre[data-torchui="code-block"] .token.string,
52
+ pre[data-torchui="code-block"] .token.char,
53
+ pre[data-torchui="code-block"] .token.attr-value {
54
+ color: var(--code-token-string, var(--code-token-default, currentColor));
55
+ }
56
+
57
+ pre[data-torchui="code-block"] .token.number {
58
+ color: var(--code-token-number, var(--code-token-default, currentColor));
59
+ }
60
+
61
+ pre[data-torchui="code-block"] .token.property,
62
+ pre[data-torchui="code-block"] .token.tag,
63
+ pre[data-torchui="code-block"] .token.symbol,
64
+ pre[data-torchui="code-block"] .token.selector,
65
+ pre[data-torchui="code-block"] .token.attr-name,
66
+ pre[data-torchui="code-block"] .token.builtin {
67
+ color: var(--code-token-property, var(--code-token-default, currentColor));
68
+ }
69
+
70
+ pre[data-torchui="code-block"] .token.operator,
71
+ pre[data-torchui="code-block"] .token.entity,
72
+ pre[data-torchui="code-block"] .token.url {
73
+ color: var(--code-token-operator, var(--code-token-default, currentColor));
74
+ }
75
+
76
+ pre[data-torchui="code-block"] .token.regex,
77
+ pre[data-torchui="code-block"] .token.important,
78
+ pre[data-torchui="code-block"] .token.variable {
79
+ color: var(--code-token-variable, var(--code-token-default, currentColor));
80
+ }
81
+
82
+ /* Additional common Prism token aliases */
83
+ pre[data-torchui="code-block"] .token.parameter,
84
+ pre[data-torchui="code-block"] .token.constant,
85
+ pre[data-torchui="code-block"] .token.namespace {
86
+ color: var(--code-token-variable, var(--code-token-default, currentColor));
87
+ }
88
+
89
+ pre[data-torchui="code-block"] .token.atrule {
90
+ color: var(--code-token-keyword, var(--code-token-default, currentColor));
91
+ }
92
+
93
+ pre[data-torchui="code-block"] .token.interpolation,
94
+ pre[data-torchui="code-block"] .token.interpolation-punctuation {
95
+ color: var(--code-token-string, var(--code-token-default, currentColor));
96
+ }
97
+
98
+ pre[data-torchui="code-block"] .token.inserted {
99
+ color: var(--code-token-string, var(--code-token-default, currentColor));
100
+ }
101
+
102
+ pre[data-torchui="code-block"] .token.deleted {
103
+ color: var(--code-token-operator, var(--code-token-default, currentColor));
104
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Prism syntax highlighting for CodeBlock. Supports many languages; add more
3
+ * prismjs/components/prism-* imports as needed. Apps style tokens via
4
+ * .code-block (follows app .dark), .code-block-dark (forced), .code-block-primary.
5
+ */
6
+ import Prism from 'prismjs'
7
+ import 'prismjs/components/prism-bash'
8
+ import 'prismjs/components/prism-css'
9
+ import 'prismjs/components/prism-diff'
10
+ import 'prismjs/components/prism-docker'
11
+ import 'prismjs/components/prism-git'
12
+ import 'prismjs/components/prism-go'
13
+ import 'prismjs/components/prism-graphql'
14
+ import 'prismjs/components/prism-ini'
15
+ import 'prismjs/components/prism-javascript'
16
+ import 'prismjs/components/prism-json'
17
+ import 'prismjs/components/prism-jsx'
18
+ import 'prismjs/components/prism-markdown'
19
+ import 'prismjs/components/prism-markup'
20
+ import 'prismjs/components/prism-python'
21
+ import 'prismjs/components/prism-ruby'
22
+ import 'prismjs/components/prism-rust'
23
+ import 'prismjs/components/prism-sass'
24
+ import 'prismjs/components/prism-scss'
25
+ import 'prismjs/components/prism-sql'
26
+ import 'prismjs/components/prism-toml'
27
+ import 'prismjs/components/prism-typescript'
28
+ import 'prismjs/components/prism-tsx'
29
+ import 'prismjs/components/prism-yaml'
30
+
31
+ const LANGUAGE_ALIASES: Record<string, string> = {
32
+ js: 'javascript',
33
+ ts: 'typescript',
34
+ jsx: 'jsx',
35
+ tsx: 'tsx',
36
+ sh: 'bash',
37
+ shell: 'bash',
38
+ zsh: 'bash',
39
+ html: 'markup',
40
+ xml: 'markup',
41
+ py: 'python',
42
+ rb: 'ruby',
43
+ rs: 'rust',
44
+ yml: 'yaml',
45
+ text: 'plaintext',
46
+ plain: 'plaintext',
47
+ txt: 'plaintext',
48
+ }
49
+
50
+ function normalizeLang(lang: string): string {
51
+ const key = lang.toLowerCase()
52
+ return LANGUAGE_ALIASES[key] ?? key
53
+ }
54
+
55
+ /**
56
+ * Highlight code string to HTML. Use this (and set el.innerHTML) for dynamic content.
57
+ * highlightElement() is for static page HTML; Prism.highlight() is the right API for programmatic use.
58
+ */
59
+ export function highlightCode(code: string, language: string): string {
60
+ const lang = normalizeLang(language)
61
+ const grammar = Prism.languages[lang]
62
+ if (!grammar) return escapeHtml(code)
63
+ return Prism.highlight(code, grammar, lang)
64
+ }
65
+
66
+ /** Escape text for safe insertion into HTML. Output is HTML-escaped text only — not safe for use in attribute contexts. */
67
+ function escapeHtml(text: string): string {
68
+ if (typeof document === 'undefined') {
69
+ return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;')
70
+ }
71
+ const el = document.createElement('div')
72
+ el.textContent = text
73
+ return el.innerHTML
74
+ }
75
+
76
+ /** Highlight a <code> element in place. Call after mount when content/language changes. */
77
+ export function highlightElement(element: HTMLElement, language: string): void {
78
+ const lang = normalizeLang(language)
79
+ element.classList.add(`language-${lang}`)
80
+ Prism.highlightElement(element, false)
81
+ }
@@ -0,0 +1,84 @@
1
+ import { type JSX, splitProps } from 'solid-js'
2
+ import { Collapsible as KobalteCollapsible, type CollapsibleContentProps as KobalteCollapsibleContentProps, type CollapsibleTriggerProps as KobalteCollapsibleTriggerProps } from '@kobalte/core/collapsible'
3
+ import { ChevronDown } from 'lucide-solid'
4
+ import { cn } from '../../utilities/classNames'
5
+
6
+ export const CollapsibleRoot = KobalteCollapsible
7
+ export const CollapsibleTrigger = KobalteCollapsible.Trigger
8
+ export const CollapsibleContent = KobalteCollapsible.Content
9
+
10
+ const COLLAPSIBLE_STYLE_ID = 'torchui-collapsible-styles'
11
+
12
+ const collapsibleStyles = `
13
+ .collapsible-content[data-expanded] {
14
+ animation: torchui-collapsible-down 200ms ease-out;
15
+ }
16
+ .collapsible-content[data-closed] {
17
+ animation: torchui-collapsible-up 200ms ease-out;
18
+ }
19
+ @keyframes torchui-collapsible-down {
20
+ from { height: 0; opacity: 0; }
21
+ to { height: var(--kb-collapsible-content-height); opacity: 1; }
22
+ }
23
+ @keyframes torchui-collapsible-up {
24
+ from { height: var(--kb-collapsible-content-height); opacity: 1; }
25
+ to { height: 0; opacity: 0; }
26
+ }
27
+ `
28
+
29
+ function ensureCollapsibleStyles() {
30
+ if (typeof document === 'undefined') return
31
+ if (document.getElementById(COLLAPSIBLE_STYLE_ID)) return
32
+ const style = document.createElement('style')
33
+ style.id = COLLAPSIBLE_STYLE_ID
34
+ style.textContent = collapsibleStyles
35
+ document.head.appendChild(style)
36
+ }
37
+
38
+ // Inject styles once on module load (client-side only)
39
+ // Note: In SSR, keyframes won't exist until client module runs, so first interaction may be unanimated
40
+ if (typeof document !== 'undefined') {
41
+ ensureCollapsibleStyles()
42
+ }
43
+
44
+ export interface CollapsibleContentProps extends KobalteCollapsibleContentProps {
45
+ class?: string
46
+ children?: JSX.Element
47
+ }
48
+
49
+ export function CollapsibleContentStyled(props: CollapsibleContentProps) {
50
+ const [local, others] = splitProps(props, ['class', 'children'])
51
+ return (
52
+ <KobalteCollapsible.Content
53
+ class={cn('collapsible-content overflow-hidden', local.class)}
54
+ {...others}
55
+ >
56
+ {local.children}
57
+ </KobalteCollapsible.Content>
58
+ )
59
+ }
60
+
61
+ export interface CollapsibleTriggerStyledProps extends KobalteCollapsibleTriggerProps {
62
+ class?: string
63
+ children?: JSX.Element
64
+ }
65
+
66
+ export function CollapsibleTriggerStyled(props: CollapsibleTriggerStyledProps) {
67
+ const [local, others] = splitProps(props, ['class', 'children'])
68
+ return (
69
+ <KobalteCollapsible.Trigger
70
+ class={cn(
71
+ 'flex w-full items-center justify-between gap-2 rounded-lg border border-surface-border bg-surface-base px-4 py-3 text-left text-sm font-medium text-ink-800',
72
+ 'hover:bg-surface-overlay',
73
+ 'data-[expanded]:rounded-b-none data-[expanded]:bg-surface-overlay',
74
+ 'data-[expanded]:[&>svg]:rotate-180',
75
+ 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500',
76
+ local.class
77
+ )}
78
+ {...others}
79
+ >
80
+ {local.children}
81
+ <ChevronDown class="h-4 w-4 shrink-0 transition-transform duration-200" aria-hidden="true" />
82
+ </KobalteCollapsible.Trigger>
83
+ )
84
+ }
@@ -0,0 +1,55 @@
1
+ import type { JSX, ParentComponent } from 'solid-js'
2
+ import { cn } from '../../utilities/classNames'
3
+
4
+ export type ContainerSize = 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'full'
5
+ export type ContainerAlign = 'start' | 'center' | 'end'
6
+
7
+ export interface ContainerProps {
8
+ /** Max width of the container; default md. full = no max-width constraint. Ignored when fluid is true. */
9
+ size?: ContainerSize
10
+ /** When true, container stretches to fill the width of its parent (no max-width, no centering). */
11
+ fluid?: boolean
12
+ /** Horizontal alignment within parent when not fluid. Default center. */
13
+ align?: ContainerAlign
14
+ /** Additional class for the wrapper. */
15
+ class?: string
16
+ children?: JSX.Element
17
+ }
18
+
19
+ const sizeClasses: Record<ContainerSize, string> = {
20
+ sm: 'max-w-3xl',
21
+ md: 'max-w-5xl',
22
+ lg: 'max-w-6xl',
23
+ xl: 'max-w-7xl',
24
+ '2xl': 'max-w-[90rem]',
25
+ full: 'max-w-full',
26
+ }
27
+
28
+ const alignClasses: Record<ContainerAlign, string> = {
29
+ start: 'ms-0 me-auto',
30
+ center: 'mx-auto',
31
+ end: 'ms-auto me-0',
32
+ }
33
+
34
+ /**
35
+ * Centered layout container with max-width and horizontal padding.
36
+ * No border or background by default—it only confines content (max-width, centering, padding).
37
+ * Use for page content, forms, or reading width. Set fluid to stretch to full parent width.
38
+ */
39
+ export const Container: ParentComponent<ContainerProps> = (props) => {
40
+ const size = () => props.size ?? 'md'
41
+ const fluid = () => props.fluid === true
42
+ const align = () => props.align ?? 'center'
43
+ return (
44
+ <div
45
+ class={cn(
46
+ 'w-full px-4 sm:px-6 lg:px-8',
47
+ !fluid() && alignClasses[align()],
48
+ !fluid() && size() !== 'full' && sizeClasses[size()],
49
+ props.class
50
+ )}
51
+ >
52
+ {props.children}
53
+ </div>
54
+ )
55
+ }
@@ -0,0 +1,64 @@
1
+ import { type JSX, splitProps } from 'solid-js'
2
+ import { Separator as KobalteSeparator } from '@kobalte/core/separator'
3
+ import { cn } from '../../utilities/classNames'
4
+
5
+ export type DividerStyle = 'solid' | 'dotted' | 'dashed'
6
+ export type DividerWeight = 'thin' | 'medium' | 'thick'
7
+
8
+ export interface DividerProps extends JSX.HTMLAttributes<HTMLDivElement> {
9
+ /** Optional label shown in the center (e.g. "or continue with") */
10
+ label?: string
11
+ /** Line style: solid (default), dotted, or dashed */
12
+ lineStyle?: DividerStyle
13
+ /** Line thickness: thin (1px), medium (2px), or thick (4px). Default thin. */
14
+ weight?: DividerWeight
15
+ }
16
+
17
+ const weightClasses: Record<DividerWeight, string> = {
18
+ thin: 'border-t',
19
+ medium: 'border-t-2',
20
+ thick: 'border-t-4',
21
+ }
22
+
23
+ const styleClasses: Record<DividerStyle, string> = {
24
+ solid: 'border-solid',
25
+ dotted: 'border-dotted',
26
+ dashed: 'border-dashed',
27
+ }
28
+
29
+ const lineBase = 'min-h-0 flex-1 shrink-0 border-surface-border'
30
+
31
+ export function Divider(props: DividerProps) {
32
+ const [local, others] = splitProps(props, ['label', 'lineStyle', 'weight', 'class'])
33
+
34
+ const lineStyle = () => local.lineStyle ?? 'solid'
35
+ const weight = () => local.weight ?? 'thin'
36
+
37
+ const lineClass = () =>
38
+ cn(lineBase, weightClasses[weight()], styleClasses[lineStyle()])
39
+
40
+ return (
41
+ <KobalteSeparator
42
+ as="div"
43
+ orientation="horizontal"
44
+ class={cn(
45
+ 'w-full my-6',
46
+ local.label ? 'flex items-center gap-4' : '',
47
+ local.class,
48
+ )}
49
+ {...others}
50
+ >
51
+ {local.label ? (
52
+ <>
53
+ <span class={lineClass()} />
54
+ <span class="shrink-0 whitespace-nowrap text-sm font-medium text-ink-500">
55
+ {local.label}
56
+ </span>
57
+ <span class={lineClass()} />
58
+ </>
59
+ ) : (
60
+ <span class={cn(lineClass(), 'block w-full')} />
61
+ )}
62
+ </KobalteSeparator>
63
+ )
64
+ }
@@ -0,0 +1,39 @@
1
+ import type { JSX } from 'solid-js'
2
+ import { Show, For } from 'solid-js'
3
+ import { cn } from '../../utilities/classNames'
4
+ import { Alert } from './Alert'
5
+
6
+ export interface FormProps extends Omit<JSX.FormHTMLAttributes<HTMLFormElement>, 'class'> {
7
+ /** Optional class for the form element */
8
+ class?: string
9
+ /** Form content (fields, sections, actions) */
10
+ children: JSX.Element
11
+ /** Optional form-level validation summary. When set, rendered at the top. Alert component provides alert semantics. Use for listing all field errors. */
12
+ errorSummary?: string[] | JSX.Element
13
+ }
14
+
15
+ /** Form wrapper with consistent spacing. Use with layout primitives and WizardActions for the button row. Supports errorSummary for validation summary. */
16
+ export function Form(props: FormProps): JSX.Element {
17
+ const { class: className, children, errorSummary, ...rest } = props
18
+ return (
19
+ <form class={cn('flex flex-col gap-6', className)} {...rest}>
20
+ <Show when={errorSummary && (Array.isArray(errorSummary) ? errorSummary.length > 0 : true)}>
21
+ <>
22
+ {Array.isArray(errorSummary) ? (
23
+ <Alert status="error" class="mb-0">
24
+ <p class="font-medium">Please fix the following:</p>
25
+ <ul class="mt-1 list-inside list-disc">
26
+ <For each={errorSummary}>
27
+ {(msg) => <li>{msg}</li>}
28
+ </For>
29
+ </ul>
30
+ </Alert>
31
+ ) : (
32
+ errorSummary
33
+ )}
34
+ </>
35
+ </Show>
36
+ {children}
37
+ </form>
38
+ )
39
+ }
@@ -0,0 +1,50 @@
1
+ import type { JSX } from 'solid-js'
2
+ import { Button } from '../actions'
3
+ import { cn } from '../../utilities/classNames'
4
+
5
+ export interface FormActionsProps {
6
+ /** Label for the back/secondary button */
7
+ backLabel: string
8
+ onBack: () => void
9
+ /** Label for the primary button */
10
+ primaryLabel: string
11
+ loading?: boolean
12
+ disabled?: boolean
13
+ class?: string
14
+ }
15
+
16
+ export interface FormActionsSubmitProps extends FormActionsProps {
17
+ /** Primary button type */
18
+ primaryType?: 'submit'
19
+ }
20
+
21
+ export interface FormActionsButtonProps extends FormActionsProps {
22
+ /** Primary button type */
23
+ primaryType: 'button'
24
+ /** Handler for primary button click (required when type='button') */
25
+ onPrimary: () => void
26
+ }
27
+
28
+ export type FormActionsAllProps = FormActionsSubmitProps | FormActionsButtonProps
29
+
30
+ /** Back + primary button row for forms (e.g. wizard steps). */
31
+ export function FormActions(props: FormActionsAllProps): JSX.Element {
32
+ const isSubmit = props.primaryType !== 'button'
33
+ return (
34
+ <div class={cn('flex gap-3 pt-2', props.class)}>
35
+ <Button type="button" variant="ghost" onClick={props.onBack} class="rounded-lg">
36
+ {props.backLabel}
37
+ </Button>
38
+ <Button
39
+ type={isSubmit ? 'submit' : 'button'}
40
+ variant="primary"
41
+ loading={props.loading ?? false}
42
+ disabled={props.disabled ?? false}
43
+ onClick={isSubmit ? undefined : (props.primaryType === 'button' ? props.onPrimary : undefined)}
44
+ class="rounded-lg py-2.5 font-semibold"
45
+ >
46
+ {props.primaryLabel}
47
+ </Button>
48
+ </div>
49
+ )
50
+ }
@@ -0,0 +1,53 @@
1
+ import type { JSX, ParentComponent } from 'solid-js'
2
+ import { cn } from '../../utilities/classNames'
3
+
4
+ export type GridCols = 1 | 2 | 3 | 4 | 5 | 6
5
+ export type GridGap = 'none' | 'sm' | 'md' | 'lg' | 'xl'
6
+
7
+ export interface GridProps {
8
+ /** Number of columns (1–6). Default 1. */
9
+ cols?: GridCols
10
+ /** Gap between items. Default md. */
11
+ gap?: GridGap
12
+ /** Additional class for the grid wrapper. */
13
+ class?: string
14
+ children?: JSX.Element
15
+ }
16
+
17
+ const colsClasses: Record<GridCols, string> = {
18
+ 1: 'grid-cols-1',
19
+ 2: 'grid-cols-2',
20
+ 3: 'grid-cols-3',
21
+ 4: 'grid-cols-4',
22
+ 5: 'grid-cols-5',
23
+ 6: 'grid-cols-6',
24
+ }
25
+
26
+ const gapClasses: Record<GridGap, string> = {
27
+ none: 'gap-0',
28
+ sm: 'gap-2',
29
+ md: 'gap-4',
30
+ lg: 'gap-6',
31
+ xl: 'gap-8',
32
+ }
33
+
34
+ /**
35
+ * CSS grid layout with configurable columns and gap.
36
+ * Use for card grids, form layouts, or equal-width columns.
37
+ */
38
+ export const Grid: ParentComponent<GridProps> = (props) => {
39
+ const cols = () => props.cols ?? 1
40
+ const gap = () => props.gap ?? 'md'
41
+ return (
42
+ <div
43
+ class={cn(
44
+ 'grid',
45
+ colsClasses[cols()],
46
+ gapClasses[gap()],
47
+ props.class
48
+ )}
49
+ >
50
+ {props.children}
51
+ </div>
52
+ )
53
+ }
@@ -0,0 +1,46 @@
1
+ import { Dynamic } from 'solid-js/web'
2
+ import type { JSX } from 'solid-js'
3
+ import { cn } from '../../utilities/classNames'
4
+
5
+ export interface PageHeadingProps {
6
+ /** Main heading text */
7
+ title: string
8
+ /** Optional description (plain string) */
9
+ description?: string
10
+ /** Optional description as JSX (e.g. with links or emphasis). Takes precedence over description. */
11
+ descriptionContent?: JSX.Element
12
+ /** Optional class for the description paragraph */
13
+ descriptionClass?: string
14
+ /** Optional class for the wrapper */
15
+ class?: string
16
+ /** Optional class for the heading element (h1/h2) */
17
+ titleClass?: string
18
+ /** Heading level: 1 or 2. Default 1. */
19
+ level?: 1 | 2
20
+ }
21
+
22
+ /** Page title + optional description. Use for consistent page headers. */
23
+ export function PageHeading(props: PageHeadingProps): JSX.Element {
24
+ const descClass = () =>
25
+ cn('text-[0.9375rem] text-ink-500', props.descriptionClass ?? 'mt-3')
26
+ const level = () => props.level ?? 1
27
+ return (
28
+ <div class={cn(props.class)}>
29
+ <Dynamic
30
+ component={level() === 2 ? 'h2' : 'h1'}
31
+ class={cn(
32
+ 'font-bold tracking-tight text-ink-900',
33
+ level() === 2 ? 'text-xl' : 'text-2xl',
34
+ props.titleClass
35
+ )}
36
+ >
37
+ {props.title}
38
+ </Dynamic>
39
+ {props.descriptionContent != null ? (
40
+ <p class={descClass()}>{props.descriptionContent}</p>
41
+ ) : props.description != null && props.description !== '' ? (
42
+ <p class={descClass()}>{props.description}</p>
43
+ ) : null}
44
+ </div>
45
+ )
46
+ }
@@ -0,0 +1,49 @@
1
+ import type { JSX } from 'solid-js'
2
+ import { cn } from '../../utilities/classNames'
3
+
4
+ export interface PromptWithActionProps {
5
+ /** Leading text (e.g. "Don't have an account?") */
6
+ prompt: string
7
+ /** Link or button label (e.g. "Sign up") */
8
+ actionLabel: string
9
+ /** Optional class for the wrapper */
10
+ class?: string
11
+ /** Optional class for the action link/button (e.g. primary link styling) */
12
+ actionClass?: string
13
+ }
14
+
15
+ export interface PromptWithActionLinkProps extends PromptWithActionProps {
16
+ /** Render as link with href */
17
+ href: string
18
+ onClick?: never
19
+ }
20
+
21
+ export interface PromptWithActionButtonProps extends PromptWithActionProps {
22
+ /** Render as button with click handler */
23
+ onClick: () => void
24
+ href?: never
25
+ }
26
+
27
+ export type PromptWithActionAllProps = PromptWithActionLinkProps | PromptWithActionButtonProps
28
+
29
+ const defaultActionClass =
30
+ 'cursor-pointer font-medium text-primary-500 hover:underline focus:rounded focus:outline-none focus:ring-2 focus:ring-primary-500/30 focus:ring-offset-1 dark:text-primary-400 dark:focus:ring-primary-500/40'
31
+
32
+ /** One line: prompt text plus a link or button action. */
33
+ export function PromptWithAction(props: PromptWithActionAllProps): JSX.Element {
34
+ const actionClass = () => cn(defaultActionClass, props.actionClass)
35
+ return (
36
+ <p class={cn('mb-7 text-[0.9375rem] text-ink-500', props.class)}>
37
+ {props.prompt}{' '}
38
+ {props.href != null ? (
39
+ <a href={props.href} class={actionClass()}>
40
+ {props.actionLabel}
41
+ </a>
42
+ ) : (
43
+ <button type="button" class={actionClass()} onClick={props.onClick}>
44
+ {props.actionLabel}
45
+ </button>
46
+ )}
47
+ </p>
48
+ )
49
+ }