@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,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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"')
|
|
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
|
+
}
|