@astherix/ui 0.40.0
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 +933 -0
- package/dist/chunk-IZEGCTZW.js +588 -0
- package/dist/chunk-IZEGCTZW.js.map +1 -0
- package/dist/cli.js +345 -0
- package/dist/controlStyles-Bco2sOHE.d.ts +4 -0
- package/dist/editor.d.ts +43 -0
- package/dist/editor.js +373 -0
- package/dist/editor.js.map +1 -0
- package/dist/index.d.ts +2470 -0
- package/dist/index.js +10582 -0
- package/dist/index.js.map +1 -0
- package/dist/theme.d.ts +78 -0
- package/dist/theme.js +287 -0
- package/dist/theme.js.map +1 -0
- package/package.json +69 -0
- package/theme.css +909 -0
- package/ui.theme.schema.json +462 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/lib/cn.ts","../src/components/button/Button.tsx","../src/components/input/Field.tsx","../src/components/input/Input.tsx","../src/components/input/controlStyles.ts"],"sourcesContent":["import { clsx, type ClassValue } from \"clsx\";\nimport { extendTailwindMerge } from \"tailwind-merge\";\n\n// Teach tailwind-merge about our custom tokens so overrides resolve correctly.\nconst twMerge = extendTailwindMerge({\n extend: {\n theme: {\n radius: [\"control\", \"control-sm\", \"control-lg\", \"modal\", \"card\"],\n },\n },\n});\n\n/** Merge class names, letting later Tailwind classes override earlier ones. */\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n","import * as React from \"react\";\nimport { Slot, Slottable } from \"@radix-ui/react-slot\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { cn } from \"../../lib/cn\";\n\n/*\n * The button's box-shadow is built from three layers, each set by a CSS variable:\n * --btn-ring a 1px outline (secondary variant)\n * --btn-edge the darker bottom edge that makes it look raised (`raised`)\n * --btn-drop the drop shadow under the button (`shadow`)\n * Keeping them separate lets `raised` and `shadow` be combined freely.\n * (\"0_0_#0000\" is an empty shadow. Class strings stay literal so Tailwind can find them.)\n */\nexport const buttonVariants = cva(\n [\n \"relative inline-flex items-center justify-center gap-2 whitespace-nowrap select-none\",\n \"font-medium tracking-[-0.005em] leading-none\",\n \"[--btn-ring:0_0_#0000] [--btn-edge:0_0_#0000] [--btn-drop:0_0_#0000]\",\n \"shadow-[var(--btn-ring),var(--btn-edge),var(--btn-drop)]\",\n \"transition-[background-color,box-shadow,color,translate] duration-100 ease-out\",\n \"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring\",\n \"disabled:pointer-events-none disabled:opacity-45\",\n \"aria-busy:cursor-progress\",\n \"motion-reduce:transition-none\",\n // SVG icons\n \"[&_svg]:pointer-events-none [&_svg]:size-[1.125em] [&_svg]:shrink-0\",\n // Icon fonts (Bootstrap Icons, Font Awesome, Glyphicons…) rendered as <i>\n \"[&_i]:pointer-events-none [&_i]:shrink-0 [&_i]:text-[1.125em] [&_i]:leading-none [&_i]:not-italic\",\n ],\n {\n variants: {\n variant: {\n primary: \"bg-primary text-primary-fg hover:bg-primary-hover [--btn-edge-color:var(--color-primary-edge)] [--ui-orbit-color:#fff] [--ui-progress-fill:color-mix(in_srgb,var(--color-primary-fg)_22%,transparent)] [--ui-progress-bar:var(--color-primary-fg)]\",\n secondary: [\n \"bg-secondary text-fg hover:bg-secondary-hover [--btn-edge-color:var(--color-border-strong)] [--ui-orbit-color:var(--color-primary)] [--ui-progress-fill:color-mix(in_srgb,var(--color-primary)_16%,transparent)] [--ui-progress-bar:var(--color-primary)]\",\n \"[--btn-ring:inset_0_0_0_1px_var(--color-border-strong)]\",\n ],\n ghost: \"bg-transparent text-fg hover:bg-secondary-hover active:bg-border [--ui-orbit-color:var(--color-primary)] [--ui-progress-fill:color-mix(in_srgb,var(--color-primary)_16%,transparent)] [--ui-progress-bar:var(--color-primary)]\",\n danger: \"bg-danger text-danger-fg hover:bg-danger-hover [--btn-edge-color:var(--color-danger-edge)] [--ui-orbit-color:#fff] [--ui-progress-fill:color-mix(in_srgb,var(--color-danger-fg)_22%,transparent)] [--ui-progress-bar:var(--color-danger-fg)]\",\n },\n // Raised: a 3px darker bottom edge makes the button look lifted. On press it drops\n // 2px and the edge shrinks to 1px, so it reads as pushed in. Flat buttons move 1px.\n raised: {\n true: [\n \"pb-[3px] [--btn-edge:inset_0_-3px_0_0_var(--btn-edge-color)]\",\n \"active:translate-y-[2px] active:[--btn-edge:inset_0_-1px_0_0_var(--btn-edge-color)]\",\n \"motion-reduce:active:translate-y-0\",\n ],\n false: \"active:translate-y-px motion-reduce:active:translate-y-0\",\n },\n // Drop shadow weight. Values are theme tokens (--ui-shadow-*), tuned for light and dark.\n // Pressing a button with a shadow lowers it to the lightest level.\n shadow: {\n none: \"\",\n sm: \"[--btn-drop:var(--ui-shadow-sm)]\",\n md: \"[--btn-drop:var(--ui-shadow-md)] active:[--btn-drop:var(--ui-shadow-sm)]\",\n lg: \"[--btn-drop:var(--ui-shadow-lg)] active:[--btn-drop:var(--ui-shadow-sm)]\",\n xl: \"[--btn-drop:var(--ui-shadow-xl)] active:[--btn-drop:var(--ui-shadow-sm)]\",\n },\n size: {\n sm: \"h-8 px-3 text-sm\",\n md: \"h-10 px-4 text-[0.9375rem]\",\n lg: \"h-12 px-5 text-base\",\n },\n // Corner radius. \"md\" follows --radius-control, so changing that token re-shapes\n // every default button in an app. With iconOnly, \"full\" makes a perfect circle.\n rounded: {\n none: \"rounded-none\",\n sm: \"rounded-control-sm\",\n md: \"rounded-control\",\n lg: \"rounded-control-lg\",\n full: \"rounded-full\",\n },\n iconOnly: {\n true: \"px-0\",\n false: \"\",\n },\n fullWidth: {\n true: \"w-full\",\n false: \"\",\n },\n },\n compoundVariants: [\n // Ghost buttons are always flat.\n {\n variant: \"ghost\",\n raised: true,\n class: \"pb-0 [--btn-edge:0_0_#0000] active:[--btn-edge:0_0_#0000] active:translate-y-px\",\n },\n { iconOnly: true, size: \"sm\", class: \"w-8\" },\n { iconOnly: true, size: \"md\", class: \"w-10\" },\n { iconOnly: true, size: \"lg\", class: \"w-12\" },\n // Pill buttons get a little more side padding so the label doesn't crowd the curve.\n { rounded: \"full\", iconOnly: false, size: \"sm\", class: \"px-4\" },\n { rounded: \"full\", iconOnly: false, size: \"md\", class: \"px-5\" },\n { rounded: \"full\", iconOnly: false, size: \"lg\", class: \"px-6\" },\n ],\n defaultVariants: {\n variant: \"primary\",\n raised: true,\n shadow: \"none\",\n size: \"md\",\n rounded: \"md\",\n iconOnly: false,\n fullWidth: false,\n },\n }\n);\n\nexport interface ButtonProps\n extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, \"onClick\">,\n VariantProps<typeof buttonVariants> {\n /**\n * Click handler. Return a Promise (e.g. from fetch or axios) and the button\n * shows its spinner until the Promise settles — no loading state to manage.\n */\n onClick?: (event: React.MouseEvent<HTMLButtonElement>) => unknown;\n /**\n * Render the child element instead of a <button>, keeping the button styles.\n * Use it for links: <Button asChild><Link href=\"/x\">Open</Link></Button>\n */\n asChild?: boolean;\n /**\n * Show the spinner on demand. Use it with state you already have, such as\n * Inertia's `form.processing` or React's `isPending` from useTransition.\n */\n loading?: boolean;\n /**\n * Where the spinner appears while loading.\n * - \"center\": replaces the label (the button keeps its width)\n * - \"start\" / \"end\": appears beside the label; takes the icon's place if there is one\n */\n spinnerPlacement?: \"center\" | \"start\" | \"end\";\n /**\n * How loading is shown.\n * - \"spinner\": a spinning icon (default)\n * - \"orbit\": experimental — a light runs around the button's edge and the label stays visible\n * - \"progress\": experimental — a progress fill or bar; the label stays visible\n */\n loadingIndicator?: \"spinner\" | \"orbit\" | \"progress\";\n /**\n * With loadingIndicator=\"progress\":\n * - \"fill\": a translucent fill sweeps across the button from left to right (default)\n * - \"bar\": a thin bar runs along the bottom edge\n */\n progressStyle?: \"fill\" | \"bar\";\n /**\n * With loadingIndicator=\"progress\": the real progress, 0–100 (e.g. from an upload).\n * Leave it out and the progress creeps forward on its own until the work finishes.\n */\n progress?: number;\n /** Label shown while loading, e.g. \"Saving…\". Used with \"start\"/\"end\" placement, \"orbit\" and \"progress\". */\n loadingLabel?: React.ReactNode;\n /**\n * Keep the spinner visible for at least this many milliseconds, so very fast\n * requests don't cause a flicker. Applies to Promise-based loading. Default 0.\n */\n minLoadingTime?: number;\n /**\n * Icon shown before the label. Pass an element (<DownloadIcon />) or an\n * icon-font class string, e.g. \"bi bi-download\" or \"fa-solid fa-download\".\n */\n leadingIcon?: IconInput;\n /** Icon shown after the label. Same options as `leadingIcon`. */\n trailingIcon?: IconInput;\n /**\n * The icon for an icon-only button (use with `iconOnly` and an `aria-label`).\n * Same options as `leadingIcon`; used instead of children.\n */\n icon?: IconInput;\n}\n\n/** An icon element, or a class string for an icon font. */\nexport type IconInput = React.ReactNode | string;\n\n/** Turns an icon-font class string into an <i>; passes elements through. */\nexport function renderIcon(icon: IconInput) {\n if (typeof icon === \"string\") {\n return icon.trim() ? <i className={icon} aria-hidden=\"true\" /> : null;\n }\n return icon;\n}\n\nfunction Spinner() {\n return (\n <svg\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n aria-hidden=\"true\"\n className=\"animate-spin motion-reduce:animate-none\"\n >\n <circle cx=\"8\" cy=\"8\" r=\"6.25\" stroke=\"currentColor\" strokeOpacity=\"0.25\" strokeWidth=\"1.5\" />\n <path d=\"M14.25 8A6.25 6.25 0 0 0 8 1.75\" stroke=\"currentColor\" strokeWidth=\"1.5\" strokeLinecap=\"round\" />\n </svg>\n );\n}\n\nfunction isPromiseLike(value: unknown): value is PromiseLike<unknown> {\n return !!value && typeof (value as PromiseLike<unknown>).then === \"function\";\n}\n\n/** Spinner that sits beside the label; its slot grows from zero width when there's no icon to replace. */\nfunction SideSlot({\n side,\n busy,\n icon,\n}: {\n side: \"start\" | \"end\";\n busy: boolean;\n icon: React.ReactNode;\n}) {\n if (icon) return <>{busy ? <Spinner /> : icon}</>;\n return (\n <span\n aria-hidden=\"true\"\n className={cn(\n \"inline-flex items-center justify-center overflow-hidden\",\n \"transition-[width,margin] duration-150 ease-out motion-reduce:transition-none\",\n busy ? \"w-[1.125em]\" : cn(\"w-0\", side === \"start\" ? \"-mr-2\" : \"-ml-2\")\n )}\n >\n {busy && <Spinner />}\n </span>\n );\n}\n\n/** How long the finish (jump to 100%, then fade) takes. */\nconst PROGRESS_FINISH_MS = 420;\n\n/**\n * Experimental progress indicator. Determinate when `value` is given; otherwise it\n * creeps toward ~92% (fast at first, then slower) until the work finishes. On finish\n * it runs to 100% and fades out.\n */\nfunction ProgressIndicator({\n kind,\n value,\n finishing,\n}: {\n kind: \"fill\" | \"bar\";\n value?: number;\n finishing: boolean;\n}) {\n const barRef = React.useRef<HTMLSpanElement>(null);\n const determinate = typeof value === \"number\" && Number.isFinite(value);\n\n React.useLayoutEffect(() => {\n const el = barRef.current;\n if (!el) return;\n if (!finishing) {\n // Loading started again before the finish ended: start fresh.\n el.style.animation = el.style.transition = el.style.opacity = \"\";\n if (!determinate) el.style.width = \"\";\n return;\n }\n // Freeze the current (possibly animated) width, then transition from there to 100%.\n const current = getComputedStyle(el).width;\n el.style.animation = \"none\";\n el.style.width = current;\n void el.getBoundingClientRect();\n el.style.transition = \"width 180ms ease-out, opacity 220ms ease 180ms\";\n el.style.width = \"100%\";\n el.style.opacity = \"0\";\n }, [finishing]);\n\n return (\n <span aria-hidden=\"true\" className=\"pointer-events-none absolute inset-0 overflow-hidden rounded-[inherit]\">\n <span\n ref={barRef}\n className={cn(\n \"absolute left-0\",\n kind === \"fill\"\n ? \"inset-y-0 bg-[var(--ui-progress-fill)]\"\n : \"bottom-0 h-[var(--ui-progress-height,3px)] bg-[var(--ui-progress-bar)]\",\n !determinate && \"animate-[ui-progress-creep_var(--ui-progress-duration,8s)_cubic-bezier(0.1,0.6,0.3,1)_forwards]\"\n )}\n style={\n determinate && !finishing\n ? { width: `${Math.min(100, Math.max(0, value))}%`, transition: \"width 200ms ease-out\" }\n : determinate\n ? { width: `${Math.min(100, Math.max(0, value))}%` }\n : undefined\n }\n />\n </span>\n );\n}\n\nexport const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(\n (\n {\n className,\n variant,\n size,\n rounded,\n raised,\n shadow,\n iconOnly,\n fullWidth,\n asChild = false,\n loading = false,\n spinnerPlacement = \"center\",\n loadingIndicator = \"spinner\",\n progressStyle = \"fill\",\n progress,\n loadingLabel,\n minLoadingTime = 0,\n leadingIcon,\n trailingIcon,\n icon,\n disabled,\n type,\n onClick,\n children,\n ...props\n },\n ref\n ) => {\n const [pending, setPending] = React.useState(false);\n const mounted = React.useRef(true);\n React.useEffect(() => {\n mounted.current = true;\n return () => {\n mounted.current = false;\n };\n }, []);\n\n const busy = loading || pending;\n\n // Progress indicator: when loading ends, keep it briefly so it can finish to 100%.\n const usesProgress = loadingIndicator === \"progress\";\n const [finishing, setFinishing] = React.useState(false);\n const wasBusy = React.useRef(busy);\n React.useEffect(() => {\n const ended = wasBusy.current && !busy;\n wasBusy.current = busy;\n if (busy) setFinishing(false);\n if (!ended || !usesProgress) return;\n setFinishing(true);\n const timer = window.setTimeout(() => setFinishing(false), PROGRESS_FINISH_MS);\n return () => window.clearTimeout(timer);\n }, [busy, usesProgress]);\n const classes = cn(buttonVariants({ variant, raised, shadow, size, rounded, iconOnly, fullWidth }), className);\n const lead = renderIcon(leadingIcon);\n const trail = renderIcon(trailingIcon);\n const content = icon !== undefined ? renderIcon(icon) : children;\n\n if (asChild) {\n return (\n <Slot ref={ref} className={classes} onClick={onClick as React.MouseEventHandler} {...props}>\n {lead}\n <Slottable>{children}</Slottable>\n {trail}\n </Slot>\n );\n }\n\n const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {\n // Ignore clicks while busy (this also stops a second form submit).\n if (busy) {\n event.preventDefault();\n return;\n }\n const result = onClick?.(event);\n if (!isPromiseLike(result)) return;\n\n setPending(true);\n const startedAt = Date.now();\n const stop = () => {\n const remaining = Math.max(0, minLoadingTime - (Date.now() - startedAt));\n setTimeout(() => {\n if (mounted.current) setPending(false);\n }, remaining);\n };\n // Stop on success or failure. Handle errors inside your onClick.\n Promise.resolve(result).then(stop, stop);\n };\n\n const orbit = loadingIndicator === \"orbit\";\n // Orbit and progress replace the spinner and keep the label visible.\n const keepsLabel = orbit || usesProgress;\n const centered = spinnerPlacement === \"center\" || iconOnly;\n const showSpinner = busy && !keepsLabel;\n const useLoadingLabel = busy && !iconOnly && loadingLabel !== undefined && (keepsLabel || !centered);\n const label = useLoadingLabel ? loadingLabel : content;\n\n return (\n <button\n ref={ref}\n // Default to \"button\" so a Button inside a form never submits by accident.\n type={type ?? \"button\"}\n className={classes}\n disabled={disabled}\n // While busy the button stays focusable (so keyboard focus isn't lost) but ignores clicks.\n aria-disabled={busy || undefined}\n aria-busy={busy || undefined}\n data-loading={busy ? \"\" : undefined}\n onClick={handleClick}\n {...props}\n >\n {orbit && busy && (\n // Experimental: a light running around the edge. The outer span adds the glow,\n // the inner span draws the light (see .ui-btn-orbit in theme.css).\n <span aria-hidden=\"true\" className=\"ui-btn-orbit-glow\">\n <span className=\"ui-btn-orbit\" />\n </span>\n )}\n {usesProgress && (busy || finishing) && (\n <ProgressIndicator\n kind={progressStyle}\n value={progress}\n finishing={!busy && finishing}\n />\n )}\n {centered && showSpinner && (\n <span className=\"absolute inset-0 flex items-center justify-center pb-[inherit]\">\n <Spinner />\n </span>\n )}\n <span className={cn(\"relative inline-flex items-center gap-2\", centered && showSpinner && \"opacity-0\")}>\n {centered || keepsLabel ? (\n <>\n {lead}\n {label}\n {trail}\n </>\n ) : (\n <>\n {spinnerPlacement === \"start\" ? <SideSlot side=\"start\" busy={showSpinner} icon={lead} /> : lead}\n {label}\n {spinnerPlacement === \"end\" ? <SideSlot side=\"end\" busy={showSpinner} icon={trail} /> : trail}\n </>\n )}\n </span>\n </button>\n );\n }\n);\n\nButton.displayName = \"Button\";\n","import * as React from \"react\";\nimport { cn } from \"../../lib/cn\";\n\n/*\n * Field wraps a form control with its label, description and error message, and wires\n * up the ids and ARIA attributes. Input and Textarea read this context automatically;\n * custom controls can use useField().\n */\n\nexport type FieldContextValue = {\n id: string;\n labelId: string;\n descriptionId?: string;\n errorId?: string;\n invalid: boolean;\n required: boolean;\n disabled: boolean;\n};\n\nconst FieldContext = React.createContext<FieldContextValue | null>(null);\n\n/** Read the surrounding Field's ids and state (returns null outside a Field). */\nexport function useField() {\n return React.useContext(FieldContext);\n}\n\n/** Props a control inside a Field should receive. Used by Input and Textarea. */\nexport function useFieldControlProps(props: {\n id?: string;\n invalid?: boolean;\n required?: boolean;\n disabled?: boolean;\n \"aria-describedby\"?: string;\n}) {\n const field = useField();\n const describedBy =\n [props[\"aria-describedby\"], field?.descriptionId, field?.errorId].filter(Boolean).join(\" \") || undefined;\n const invalid = props.invalid ?? field?.invalid ?? false;\n return {\n id: props.id ?? field?.id,\n required: props.required ?? field?.required,\n disabled: props.disabled ?? field?.disabled,\n invalid,\n \"aria-describedby\": describedBy,\n \"aria-invalid\": invalid || undefined,\n };\n}\n\nexport interface FieldProps extends Omit<React.HTMLAttributes<HTMLDivElement>, \"children\"> {\n /** The visible label. */\n label: React.ReactNode;\n /** Help text under the label. */\n description?: React.ReactNode;\n /** Error message. When set, the control is marked invalid. */\n error?: React.ReactNode;\n /** Marks the control as required and shows an asterisk. */\n required?: boolean;\n /** Shows \"(optional)\" after the label. Use it instead of `required` when most fields are required. */\n optional?: boolean;\n disabled?: boolean;\n /** Use your own id for the control. */\n id?: string;\n children: React.ReactNode;\n}\n\nexport function Field({\n label,\n description,\n error,\n required = false,\n optional = false,\n disabled = false,\n id: idProp,\n className,\n children,\n ...props\n}: FieldProps) {\n const autoId = React.useId();\n const id = idProp ?? `${autoId}-control`;\n const hasError = error !== undefined && error !== null && error !== false && error !== \"\";\n const value: FieldContextValue = {\n id,\n labelId: `${id}-label`,\n descriptionId: description ? `${id}-description` : undefined,\n errorId: hasError ? `${id}-error` : undefined,\n invalid: hasError,\n required,\n disabled,\n };\n\n return (\n <FieldContext.Provider value={value}>\n <div className={cn(\"grid content-start gap-1.5\", className)} {...props}>\n <label id={value.labelId} htmlFor={id} className=\"w-fit text-sm font-medium leading-snug text-fg\">\n {label}\n {required && (\n <span aria-hidden=\"true\" className=\"ml-0.5 text-danger\">\n *\n </span>\n )}\n {optional && <span className=\"ml-1 font-normal text-fg-muted\">(optional)</span>}\n </label>\n {description && (\n <p id={value.descriptionId} className=\"-mt-0.5 text-[0.8125rem] leading-relaxed text-fg-muted\">\n {description}\n </p>\n )}\n {children}\n {hasError && (\n <p id={value.errorId} className=\"flex items-start gap-1.5 text-[0.8125rem] leading-snug text-danger\">\n <svg viewBox=\"0 0 16 16\" aria-hidden=\"true\" className=\"mt-px size-3.5 shrink-0\" fill=\"currentColor\">\n <path d=\"M8 1.5a6.5 6.5 0 1 1 0 13 6.5 6.5 0 0 1 0-13Zm0 3a.75.75 0 0 0-.75.75v3a.75.75 0 0 0 1.5 0v-3A.75.75 0 0 0 8 4.5Zm0 6.25a.85.85 0 1 0 0 1.7.85.85 0 0 0 0-1.7Z\" />\n </svg>\n <span>{error}</span>\n </p>\n )}\n </div>\n </FieldContext.Provider>\n );\n}\n","import * as React from \"react\";\nimport { cva } from \"class-variance-authority\";\nimport { cn } from \"../../lib/cn\";\nimport { renderIcon, type IconInput } from \"../button/Button\";\nimport { useFieldControlProps } from \"./Field\";\nimport { controlFrame, type ControlRounded, type ControlSize } from \"./controlStyles\";\n\nconst inputFrame = cva(\"items-center\", {\n variants: {\n size: {\n sm: \"h-8 gap-1.5 px-2.5 text-sm\",\n md: \"h-10 gap-2 px-3 text-[0.9375rem]\",\n lg: \"h-12 gap-2.5 px-3.5 text-base\",\n },\n },\n defaultVariants: { size: \"md\" },\n});\n\nexport interface InputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, \"size\" | \"prefix\"> {\n /** Height matches Button sizes, so inputs and buttons line up in a row. */\n size?: ControlSize;\n rounded?: ControlRounded;\n /** Icon before the text: an element or an icon-font class string. */\n leadingIcon?: IconInput;\n /** Icon after the text: an element or an icon-font class string. */\n trailingIcon?: IconInput;\n /** Fixed text before the value, e.g. \"https://\" or \"$\". */\n prefix?: React.ReactNode;\n /** Fixed text after the value, e.g. \"kg\" or \"@example.com\". */\n suffix?: React.ReactNode;\n /** Marks the input as invalid. Inside a Field with an error this is set for you. */\n invalid?: boolean;\n /** Show a clear (×) button when the input has a value. */\n clearable?: boolean;\n /** Called after the clear button empties the input. */\n onClear?: () => void;\n /** For type=\"password\": show a button that reveals the password. Default true. */\n revealable?: boolean;\n /** Class for the outer frame. `className` goes on the <input> itself. */\n frameClassName?: string;\n /** Content layered exactly over the text box (same font and position), e.g. an inline completion. */\n overlay?: React.ReactNode;\n}\n\nconst EyeIcon = ({ off }: { off?: boolean }) => (\n <svg viewBox=\"0 0 20 20\" fill=\"none\" stroke=\"currentColor\" strokeWidth={1.6} strokeLinecap=\"round\" strokeLinejoin=\"round\" aria-hidden=\"true\">\n <path d=\"M2 10s3-5.5 8-5.5S18 10 18 10s-3 5.5-8 5.5S2 10 2 10Z\" />\n <circle cx=\"10\" cy=\"10\" r=\"2.5\" />\n {off && <path d=\"M3.5 3.5l13 13\" />}\n </svg>\n);\n\nconst ClearIcon = () => (\n <svg viewBox=\"0 0 20 20\" fill=\"none\" stroke=\"currentColor\" strokeWidth={1.7} strokeLinecap=\"round\" aria-hidden=\"true\">\n <path d=\"M6 6l8 8M14 6l-8 8\" />\n </svg>\n);\n\n/** Small icon button inside a control (clear, reveal). */\nexport function ControlButton({ className, ...props }: React.ButtonHTMLAttributes<HTMLButtonElement>) {\n return (\n <button\n type=\"button\"\n className={cn(\n \"-mr-1 grid size-7 shrink-0 cursor-pointer place-items-center rounded-control-sm text-fg-muted\",\n \"hover:bg-secondary-hover hover:text-fg focus-visible:outline-2 focus-visible:outline-ring\",\n \"[&_svg]:size-[1.05em]\",\n className\n )}\n {...props}\n />\n );\n}\n\n/** Set an input's value the way a person typing would, so React's onChange fires. */\nfunction setNativeValue(el: HTMLInputElement | HTMLTextAreaElement, value: string) {\n const proto = el instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;\n Object.getOwnPropertyDescriptor(proto, \"value\")?.set?.call(el, value);\n el.dispatchEvent(new Event(\"input\", { bubbles: true }));\n}\n\nexport { setNativeValue };\n\nexport const Input = React.forwardRef<HTMLInputElement, InputProps>(\n (\n {\n size = \"md\",\n rounded = \"md\",\n leadingIcon,\n trailingIcon,\n prefix,\n suffix,\n invalid: invalidProp,\n clearable = false,\n onClear,\n revealable = true,\n frameClassName,\n overlay,\n className,\n type = \"text\",\n id: idProp,\n required: requiredProp,\n disabled: disabledProp,\n readOnly,\n value,\n defaultValue,\n onChange,\n \"aria-describedby\": describedByProp,\n ...props\n },\n ref\n ) => {\n const { invalid, ...control } = useFieldControlProps({\n id: idProp,\n invalid: invalidProp,\n required: requiredProp,\n disabled: disabledProp,\n \"aria-describedby\": describedByProp,\n });\n\n const inputRef = React.useRef<HTMLInputElement | null>(null);\n const setRefs = (node: HTMLInputElement | null) => {\n inputRef.current = node;\n if (typeof ref === \"function\") ref(node);\n else if (ref) ref.current = node;\n };\n\n // Track whether there's a value, for the clear button (works controlled or not).\n const [hasValue, setHasValue] = React.useState(() => String(value ?? defaultValue ?? \"\") !== \"\");\n React.useEffect(() => {\n if (value !== undefined) setHasValue(String(value) !== \"\");\n }, [value]);\n\n const [revealed, setRevealed] = React.useState(false);\n const isPassword = type === \"password\";\n const effectiveType = isPassword && revealed ? \"text\" : type;\n\n const lead = renderIcon(leadingIcon);\n const trail = renderIcon(trailingIcon);\n const showClear = clearable && hasValue && !control.disabled && !readOnly;\n\n const inputEl = (\n <input\n ref={setRefs}\n type={effectiveType}\n readOnly={readOnly}\n value={value}\n defaultValue={defaultValue}\n onChange={(event) => {\n if (value === undefined) setHasValue(event.target.value !== \"\");\n onChange?.(event);\n }}\n className={cn(\n \"h-full w-0 min-w-0 flex-1 bg-transparent text-fg outline-none placeholder:text-fg-muted/70\",\n \"disabled:cursor-not-allowed [&::-webkit-search-cancel-button]:appearance-none\",\n className\n )}\n {...control}\n {...props}\n />\n );\n\n return (\n <div\n data-control-frame=\"\"\n data-invalid={invalid ? \"\" : undefined}\n data-disabled={control.disabled ? \"\" : undefined}\n data-readonly={readOnly ? \"\" : undefined}\n className={cn(\n controlFrame({ rounded }),\n inputFrame({ size }),\n rounded === \"full\" && (size === \"sm\" ? \"px-3.5\" : size === \"lg\" ? \"px-5\" : \"px-4\"),\n \"[&>svg]:size-[1.125em] [&>svg]:shrink-0 [&>svg]:text-fg-muted [&>i]:shrink-0 [&>i]:text-[1.125em] [&>i]:leading-none [&>i]:text-fg-muted\",\n frameClassName\n )}\n // Clicking the frame's padding, icons or affixes focuses the input.\n onMouseDown={(event) => {\n if (event.target !== inputRef.current && !(event.target as HTMLElement).closest(\"button\")) {\n event.preventDefault();\n inputRef.current?.focus();\n }\n }}\n >\n {lead}\n {prefix !== undefined && <span className=\"shrink-0 select-none whitespace-nowrap text-fg-muted\">{prefix}</span>}\n {overlay !== undefined ? (\n <span className=\"relative flex h-full min-w-0 flex-1\">\n {inputEl}\n {overlay}\n </span>\n ) : (\n inputEl\n )}\n {suffix !== undefined && <span className=\"shrink-0 select-none whitespace-nowrap text-fg-muted\">{suffix}</span>}\n {showClear && (\n <ControlButton\n aria-label=\"Clear\"\n onClick={() => {\n const el = inputRef.current;\n if (!el) return;\n setNativeValue(el, \"\");\n el.focus();\n onClear?.();\n }}\n >\n <ClearIcon />\n </ControlButton>\n )}\n {isPassword && revealable && !control.disabled && (\n <ControlButton\n aria-label={revealed ? \"Hide password\" : \"Show password\"}\n aria-pressed={revealed}\n onClick={() => setRevealed((r) => !r)}\n >\n <EyeIcon off={revealed} />\n </ControlButton>\n )}\n {trail}\n </div>\n );\n }\n);\nInput.displayName = \"Input\";\n","import { cva } from \"class-variance-authority\";\n\n/*\n * Shared frame for Input and Textarea. Controls read as slightly sunken (a soft inner\n * shadow at the top) — the counterpart to the raised Button.\n */\nexport const controlFrame = cva(\n [\n \"relative flex w-full border bg-surface text-fg transition-[border-color,box-shadow] duration-100\",\n \"border-border-strong shadow-[inset_0_1px_2px_rgb(var(--ui-shadow-color)/0.07)]\",\n \"hover:not-focus-within:border-fg-muted/60\",\n \"focus-within:border-ring focus-within:ring-3 focus-within:ring-ring/25\",\n \"data-[invalid]:border-danger data-[invalid]:hover:not-focus-within:border-danger data-[invalid]:focus-within:ring-danger/25\",\n \"data-[disabled]:cursor-not-allowed data-[disabled]:bg-secondary-hover data-[disabled]:opacity-70 data-[disabled]:hover:border-border-strong\",\n \"data-[readonly]:bg-secondary-hover/60\",\n \"motion-reduce:transition-none\",\n ],\n {\n variants: {\n rounded: {\n none: \"rounded-none\",\n sm: \"rounded-control-sm\",\n md: \"rounded-control\",\n lg: \"rounded-control-lg\",\n full: \"rounded-full\",\n },\n },\n defaultVariants: { rounded: \"md\" },\n }\n);\n\nexport type ControlRounded = \"none\" | \"sm\" | \"md\" | \"lg\" | \"full\";\nexport type ControlSize = \"sm\" | \"md\" | \"lg\";\n"],"mappings":"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","names":["React","jsx","jsxs","React","cva","cva","jsx","jsxs","cva","jsx","jsxs"]}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { readFileSync, writeFileSync, watch, existsSync, mkdirSync } from "fs";
|
|
5
|
+
import { dirname, resolve } from "path";
|
|
6
|
+
|
|
7
|
+
// src/theme/color.ts
|
|
8
|
+
function parseHex(hex) {
|
|
9
|
+
const m = hex.trim().match(/^#?([0-9a-f]{3}|[0-9a-f]{6})$/i);
|
|
10
|
+
if (!m) return null;
|
|
11
|
+
let h = m[1];
|
|
12
|
+
if (h.length === 3) h = h.split("").map((c) => c + c).join("");
|
|
13
|
+
return [0, 2, 4].map((i) => parseInt(h.slice(i, i + 2), 16) / 255);
|
|
14
|
+
}
|
|
15
|
+
function toHex([r, g, b]) {
|
|
16
|
+
return "#" + [r, g, b].map((v) => Math.round(Math.min(1, Math.max(0, v)) * 255).toString(16).padStart(2, "0")).join("");
|
|
17
|
+
}
|
|
18
|
+
var toLinear = (c) => c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
|
|
19
|
+
var fromLinear = (c) => c <= 31308e-7 ? 12.92 * c : 1.055 * c ** (1 / 2.4) - 0.055;
|
|
20
|
+
function rgbToOklch(rgb) {
|
|
21
|
+
const [r, g, b] = rgb.map(toLinear);
|
|
22
|
+
const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b);
|
|
23
|
+
const m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b);
|
|
24
|
+
const s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b);
|
|
25
|
+
const L = 0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s;
|
|
26
|
+
const A = 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s;
|
|
27
|
+
const B = 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s;
|
|
28
|
+
return [L, Math.hypot(A, B), (Math.atan2(B, A) * 180 / Math.PI + 360) % 360];
|
|
29
|
+
}
|
|
30
|
+
function oklchToRgbUnclamped([L, C, H]) {
|
|
31
|
+
const a = C * Math.cos(H * Math.PI / 180);
|
|
32
|
+
const b = C * Math.sin(H * Math.PI / 180);
|
|
33
|
+
const l = (L + 0.3963377774 * a + 0.2158037573 * b) ** 3;
|
|
34
|
+
const m = (L - 0.1055613458 * a - 0.0638541728 * b) ** 3;
|
|
35
|
+
const s = (L - 0.0894841775 * a - 1.291485548 * b) ** 3;
|
|
36
|
+
return [
|
|
37
|
+
fromLinear(4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s),
|
|
38
|
+
fromLinear(-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s),
|
|
39
|
+
fromLinear(-0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s)
|
|
40
|
+
];
|
|
41
|
+
}
|
|
42
|
+
function oklchToRgb([L, C, H]) {
|
|
43
|
+
let c = C;
|
|
44
|
+
for (let i = 0; i < 24; i++) {
|
|
45
|
+
const rgb = oklchToRgbUnclamped([L, c, H]);
|
|
46
|
+
if (rgb.every((v) => v >= -5e-4 && v <= 1.0005)) return rgb;
|
|
47
|
+
c *= 0.9;
|
|
48
|
+
}
|
|
49
|
+
return oklchToRgbUnclamped([L, 0, H]);
|
|
50
|
+
}
|
|
51
|
+
function luminance(rgb) {
|
|
52
|
+
const [r, g, b] = rgb.map(toLinear);
|
|
53
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
54
|
+
}
|
|
55
|
+
function contrast(a, b) {
|
|
56
|
+
const [x, y] = [luminance(a), luminance(b)].sort((p, q) => q - p);
|
|
57
|
+
return (x + 0.05) / (y + 0.05);
|
|
58
|
+
}
|
|
59
|
+
function adjust(hex, dL, chroma = 1) {
|
|
60
|
+
const rgb = parseHex(hex);
|
|
61
|
+
if (!rgb) return hex;
|
|
62
|
+
const [L, C, H] = rgbToOklch(rgb);
|
|
63
|
+
return toHex(oklchToRgb([Math.min(0.98, Math.max(0.05, L + dL)), C * chroma, H]));
|
|
64
|
+
}
|
|
65
|
+
function lightness(hex) {
|
|
66
|
+
const rgb = parseHex(hex);
|
|
67
|
+
return rgb ? rgbToOklch(rgb)[0] : 0.5;
|
|
68
|
+
}
|
|
69
|
+
function readableOn(hex) {
|
|
70
|
+
const rgb = parseHex(hex);
|
|
71
|
+
if (!rgb) return "#ffffff";
|
|
72
|
+
const white = [1, 1, 1];
|
|
73
|
+
const [, C, H] = rgbToOklch(rgb);
|
|
74
|
+
const darkHex = toHex(oklchToRgb([0.2, Math.min(C, 0.06), H]));
|
|
75
|
+
return contrast(rgb, white) >= 4.5 || contrast(rgb, white) >= contrast(rgb, parseHex(darkHex)) ? "#ffffff" : darkHex;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// src/theme/index.ts
|
|
79
|
+
function resolveColor(input) {
|
|
80
|
+
if (!input) return null;
|
|
81
|
+
const c = typeof input === "string" ? { light: input } : input;
|
|
82
|
+
if (!parseHex(c.light)) return null;
|
|
83
|
+
const L = lightness(c.light);
|
|
84
|
+
const dark = c.dark ?? adjust(c.light, Math.max(0, 0.72 - L), 0.95);
|
|
85
|
+
return {
|
|
86
|
+
light: c.light,
|
|
87
|
+
dark,
|
|
88
|
+
fg: c.foreground ?? readableOn(c.light),
|
|
89
|
+
darkFg: c.darkForeground ?? readableOn(dark)
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function surface(input) {
|
|
93
|
+
if (!input) return null;
|
|
94
|
+
return typeof input === "string" ? { light: input, dark: void 0 } : input;
|
|
95
|
+
}
|
|
96
|
+
var radiusPresets = {
|
|
97
|
+
none: { controlSm: "0", control: "0", controlLg: "0", card: "0", modal: "0" },
|
|
98
|
+
sm: { controlSm: "0.125rem", control: "0.25rem", controlLg: "0.375rem", card: "0.375rem", modal: "0.5rem" },
|
|
99
|
+
md: { controlSm: "0.1875rem", control: "0.375rem", controlLg: "0.75rem", card: "0.75rem", modal: "0.875rem" },
|
|
100
|
+
lg: { controlSm: "0.375rem", control: "0.625rem", controlLg: "1rem", card: "1rem", modal: "1.25rem" }
|
|
101
|
+
};
|
|
102
|
+
var densityUnits = { compact: 0.225, comfortable: 0.25, spacious: 0.28 };
|
|
103
|
+
function shadows(strength, dark) {
|
|
104
|
+
if (strength === 0) return { sm: "none", md: "none", lg: "none", xl: "none", modal: "none" };
|
|
105
|
+
const c = dark ? "0 0 0" : "var(--ui-shadow-color)";
|
|
106
|
+
const k = (a) => Math.min(1, +(a * strength * (dark ? 3 : 1)).toFixed(3));
|
|
107
|
+
return {
|
|
108
|
+
sm: `0 1px 2px rgb(${c} / ${k(0.14)})`,
|
|
109
|
+
md: `0 1px 2px rgb(${c} / ${k(0.12)}), 0 3px 8px -1px rgb(${c} / ${k(0.14)})`,
|
|
110
|
+
lg: `0 2px 4px rgb(${c} / ${k(0.1)}), 0 8px 18px -4px rgb(${c} / ${k(0.22)})`,
|
|
111
|
+
xl: `0 3px 6px rgb(${c} / ${k(0.1)}), 0 16px 32px -8px rgb(${c} / ${k(0.32)})`,
|
|
112
|
+
modal: `0 2px 6px rgb(${c} / ${k(0.08)}), 0 24px 56px -12px rgb(${c} / ${k(0.35)})`
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
var COLOR_ALIASES = [
|
|
116
|
+
"bg",
|
|
117
|
+
"surface",
|
|
118
|
+
"fg",
|
|
119
|
+
"fg-muted",
|
|
120
|
+
"border",
|
|
121
|
+
"border-strong",
|
|
122
|
+
"ring",
|
|
123
|
+
"primary",
|
|
124
|
+
"primary-hover",
|
|
125
|
+
"primary-edge",
|
|
126
|
+
"primary-fg",
|
|
127
|
+
"secondary",
|
|
128
|
+
"secondary-hover",
|
|
129
|
+
"danger",
|
|
130
|
+
"danger-hover",
|
|
131
|
+
"danger-edge",
|
|
132
|
+
"danger-fg",
|
|
133
|
+
"success",
|
|
134
|
+
"success-fg",
|
|
135
|
+
"warning",
|
|
136
|
+
"warning-fg",
|
|
137
|
+
"info",
|
|
138
|
+
"info-fg",
|
|
139
|
+
"tone-secondary",
|
|
140
|
+
"tone-secondary-fg",
|
|
141
|
+
"tone-tertiary",
|
|
142
|
+
"tone-tertiary-fg"
|
|
143
|
+
];
|
|
144
|
+
function getFontsHref(config) {
|
|
145
|
+
const fams = config.fonts?.googleFonts?.filter(Boolean);
|
|
146
|
+
if (!fams?.length) return null;
|
|
147
|
+
return `https://fonts.googleapis.com/css2?${fams.map((f) => `family=${f.trim().replace(/ /g, "+")}`).join("&")}&display=swap`;
|
|
148
|
+
}
|
|
149
|
+
function createThemeCss(config, options = {}) {
|
|
150
|
+
const { selector = ":root", darkSelector = ".dark", includeFontImport = false } = options;
|
|
151
|
+
const light = [];
|
|
152
|
+
const dark = [];
|
|
153
|
+
const set = (arr, name, value) => value && arr.push(` ${name}: ${value};`);
|
|
154
|
+
const c = config.colors ?? {};
|
|
155
|
+
const primary = resolveColor(c.primary);
|
|
156
|
+
if (primary) {
|
|
157
|
+
set(light, "--ui-primary", primary.light);
|
|
158
|
+
set(light, "--ui-primary-hover", adjust(primary.light, -0.07));
|
|
159
|
+
set(light, "--ui-primary-edge", adjust(primary.light, -0.26));
|
|
160
|
+
set(light, "--ui-primary-fg", primary.fg);
|
|
161
|
+
set(light, "--ui-ring", primary.light);
|
|
162
|
+
set(dark, "--ui-primary", primary.dark);
|
|
163
|
+
set(dark, "--ui-primary-hover", adjust(primary.dark, 0.07));
|
|
164
|
+
set(dark, "--ui-primary-edge", adjust(primary.dark, -0.16));
|
|
165
|
+
set(dark, "--ui-primary-fg", primary.darkFg);
|
|
166
|
+
set(dark, "--ui-ring", adjust(primary.dark, 0.06));
|
|
167
|
+
}
|
|
168
|
+
const tones = [
|
|
169
|
+
["secondary", "--ui-tone-secondary", false],
|
|
170
|
+
["tertiary", "--ui-tone-tertiary", false],
|
|
171
|
+
["success", "--ui-success", false],
|
|
172
|
+
["warning", "--ui-warning", false],
|
|
173
|
+
["danger", "--ui-danger", true],
|
|
174
|
+
["info", "--ui-info", false]
|
|
175
|
+
];
|
|
176
|
+
for (const [key, name, hasStates] of tones) {
|
|
177
|
+
const p = resolveColor(c[key]);
|
|
178
|
+
if (!p) continue;
|
|
179
|
+
set(light, name, p.light);
|
|
180
|
+
set(light, `${name}-fg`, p.fg);
|
|
181
|
+
set(dark, name, p.dark);
|
|
182
|
+
set(dark, `${name}-fg`, p.darkFg);
|
|
183
|
+
if (hasStates) {
|
|
184
|
+
set(light, `${name}-hover`, adjust(p.light, -0.07));
|
|
185
|
+
set(light, `${name}-edge`, adjust(p.light, -0.26));
|
|
186
|
+
set(dark, `${name}-hover`, adjust(p.dark, 0.07));
|
|
187
|
+
set(dark, `${name}-edge`, adjust(p.dark, -0.16));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
const surfaces = [
|
|
191
|
+
["background", ["--ui-bg"]],
|
|
192
|
+
["surface", ["--ui-surface", "--ui-secondary"]],
|
|
193
|
+
["foreground", ["--ui-fg"]],
|
|
194
|
+
["muted", ["--ui-fg-muted"]],
|
|
195
|
+
["border", ["--ui-border"]]
|
|
196
|
+
];
|
|
197
|
+
for (const [key, names] of surfaces) {
|
|
198
|
+
const s = surface(c[key]);
|
|
199
|
+
if (!s) continue;
|
|
200
|
+
names.forEach((n) => {
|
|
201
|
+
set(light, n, s.light);
|
|
202
|
+
set(dark, n, s.dark);
|
|
203
|
+
});
|
|
204
|
+
if (key === "border") {
|
|
205
|
+
set(light, "--ui-border-strong", parseHex(s.light) ? adjust(s.light, -0.08) : void 0);
|
|
206
|
+
set(dark, "--ui-border-strong", s.dark && parseHex(s.dark) ? adjust(s.dark, 0.08) : void 0);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
const f = config.fonts ?? {};
|
|
210
|
+
set(light, "--font-sans", f.sans);
|
|
211
|
+
set(light, "--font-heading", f.heading ?? (f.sans ? "var(--font-sans)" : void 0));
|
|
212
|
+
set(light, "--font-mono", f.mono);
|
|
213
|
+
if (config.radius) {
|
|
214
|
+
const r = typeof config.radius === "string" ? radiusPresets[config.radius] : config.radius;
|
|
215
|
+
if (r) {
|
|
216
|
+
set(light, "--radius-control-sm", r.controlSm);
|
|
217
|
+
set(light, "--radius-control", r.control);
|
|
218
|
+
set(light, "--radius-control-lg", r.controlLg);
|
|
219
|
+
set(light, "--radius-card", r.card);
|
|
220
|
+
set(light, "--radius-modal", r.modal);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (config.density !== void 0) {
|
|
224
|
+
const unit = typeof config.density === "number" ? 0.25 * config.density : densityUnits[config.density];
|
|
225
|
+
if (unit) set(light, "--spacing", `${+unit.toFixed(4)}rem`);
|
|
226
|
+
}
|
|
227
|
+
if (config.shadows) {
|
|
228
|
+
const k = { none: 0, subtle: 0.6, default: 1, strong: 1.6 }[config.shadows];
|
|
229
|
+
const l = shadows(k, false);
|
|
230
|
+
const d = shadows(k, true);
|
|
231
|
+
["sm", "md", "lg", "xl", "modal"].forEach((s) => {
|
|
232
|
+
set(light, `--ui-shadow-${s}`, l[s]);
|
|
233
|
+
set(dark, `--ui-shadow-${s}`, d[s]);
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
if (selector !== ":root" && (light.length || dark.length)) {
|
|
237
|
+
for (const name of COLOR_ALIASES) light.push(` --color-${name}: var(--ui-${name});`);
|
|
238
|
+
if (f.sans || f.heading) light.push(" font-family: var(--font-sans);");
|
|
239
|
+
}
|
|
240
|
+
const blocks = [];
|
|
241
|
+
const href = getFontsHref(config);
|
|
242
|
+
if (includeFontImport && href) blocks.push(`@import url("${href}");`);
|
|
243
|
+
blocks.push("/* Generated by @astherix/ui from your theme config. Import it after @astherix/ui/theme.css. */");
|
|
244
|
+
if (config.baseFontSize) blocks.push(`html {
|
|
245
|
+
font-size: ${config.baseFontSize};
|
|
246
|
+
}`);
|
|
247
|
+
if (light.length) blocks.push(`${selector} {
|
|
248
|
+
${light.join("\n")}
|
|
249
|
+
}`);
|
|
250
|
+
if (dark.length) {
|
|
251
|
+
const darkSel = selector === ":root" ? darkSelector : `${darkSelector} ${selector}, ${selector}${darkSelector}`;
|
|
252
|
+
blocks.push(`${darkSel} {
|
|
253
|
+
${dark.join("\n")}
|
|
254
|
+
}`);
|
|
255
|
+
}
|
|
256
|
+
return blocks.join("\n\n") + "\n";
|
|
257
|
+
}
|
|
258
|
+
function validateTheme(config) {
|
|
259
|
+
const problems = [];
|
|
260
|
+
if (!config || typeof config !== "object") return ["The theme must be a JSON object."];
|
|
261
|
+
const t = config;
|
|
262
|
+
const known = ["$schema", "colors", "fonts", "radius", "density", "shadows", "baseFontSize"];
|
|
263
|
+
Object.keys(t).forEach((k) => !known.includes(k) && problems.push(`Unknown setting "${k}". Known settings: ${known.slice(1).join(", ")}.`));
|
|
264
|
+
const checkColor = (name, v) => {
|
|
265
|
+
if (v === void 0) return;
|
|
266
|
+
const vals = typeof v === "string" ? [v] : v && typeof v === "object" ? Object.values(v) : [v];
|
|
267
|
+
vals.forEach((x) => {
|
|
268
|
+
if (typeof x !== "string" || !parseHex(x) && !/^(rgb|hsl|oklch|oklab|color|var)\(/.test(x))
|
|
269
|
+
problems.push(`colors.${name}: "${String(x)}" isn't a color. Use a hex value like "#16a34a".`);
|
|
270
|
+
});
|
|
271
|
+
};
|
|
272
|
+
Object.entries(t.colors ?? {}).forEach(([k, v]) => checkColor(k, v));
|
|
273
|
+
const derived = ["primary", "secondary", "tertiary", "success", "warning", "danger", "info"];
|
|
274
|
+
derived.forEach((k) => {
|
|
275
|
+
const v = t.colors?.[k];
|
|
276
|
+
const light = typeof v === "string" ? v : v?.light;
|
|
277
|
+
const alreadyReported = problems.some((p) => p.startsWith(`colors.${k}:`));
|
|
278
|
+
if (light && !parseHex(light) && !alreadyReported) problems.push(`colors.${k}: use a hex color (like "#16a34a") so hover and dark shades can be worked out.`);
|
|
279
|
+
});
|
|
280
|
+
if (typeof t.radius === "string" && !(t.radius in radiusPresets)) problems.push(`radius: use "none", "sm", "md" or "lg", or an object with exact values.`);
|
|
281
|
+
if (typeof t.density === "string" && !(t.density in densityUnits)) problems.push(`density: use "compact", "comfortable" or "spacious", or a number like 0.9.`);
|
|
282
|
+
if (typeof t.density === "number" && (t.density < 0.6 || t.density > 1.6)) problems.push(`density: ${t.density} is extreme. Use a number between 0.6 and 1.6.`);
|
|
283
|
+
if (t.shadows && !["none", "subtle", "default", "strong"].includes(t.shadows)) problems.push(`shadows: use "none", "subtle", "default" or "strong".`);
|
|
284
|
+
return problems;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// src/cli.ts
|
|
288
|
+
var [, , command, ...rest] = process.argv;
|
|
289
|
+
var arg = (name) => {
|
|
290
|
+
const i = rest.indexOf(name);
|
|
291
|
+
return i >= 0 ? rest[i + 1] : void 0;
|
|
292
|
+
};
|
|
293
|
+
function build(input, output) {
|
|
294
|
+
let config;
|
|
295
|
+
try {
|
|
296
|
+
config = JSON.parse(readFileSync(input, "utf8"));
|
|
297
|
+
} catch (e) {
|
|
298
|
+
console.error(`\u2717 Couldn't read ${input}: ${e.message}`);
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
const problems = validateTheme(config);
|
|
302
|
+
if (problems.length) {
|
|
303
|
+
console.error(`\u2717 ${input} has ${problems.length} problem${problems.length === 1 ? "" : "s"}:`);
|
|
304
|
+
problems.forEach((p) => console.error(` \u2022 ${p}`));
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
mkdirSync(dirname(output), { recursive: true });
|
|
308
|
+
writeFileSync(output, createThemeCss(config, { includeFontImport: true }));
|
|
309
|
+
console.log(`\u2713 Wrote ${output}`);
|
|
310
|
+
return true;
|
|
311
|
+
}
|
|
312
|
+
var starter = {
|
|
313
|
+
$schema: "./node_modules/@astherix/ui/ui.theme.schema.json",
|
|
314
|
+
colors: { primary: "#0d6efd", secondary: "#15803d", tertiary: "#ea580c" },
|
|
315
|
+
fonts: { sans: '"Schibsted Grotesk", ui-sans-serif, system-ui, sans-serif' },
|
|
316
|
+
radius: "md",
|
|
317
|
+
density: "comfortable",
|
|
318
|
+
shadows: "default"
|
|
319
|
+
};
|
|
320
|
+
if (command === "theme") {
|
|
321
|
+
const input = resolve(rest.find((a) => !a.startsWith("--") && a !== arg("--out")) ?? "ui.theme.json");
|
|
322
|
+
const output = resolve(arg("--out") ?? "ui-theme.css");
|
|
323
|
+
const ok = build(input, output);
|
|
324
|
+
if (rest.includes("--watch")) {
|
|
325
|
+
console.log(`Watching ${input} for changes\u2026`);
|
|
326
|
+
let timer;
|
|
327
|
+
watch(input, () => {
|
|
328
|
+
clearTimeout(timer);
|
|
329
|
+
timer = setTimeout(() => build(input, output), 80);
|
|
330
|
+
});
|
|
331
|
+
} else if (!ok) process.exit(1);
|
|
332
|
+
} else if (command === "init") {
|
|
333
|
+
const file = resolve(rest[0] ?? "ui.theme.json");
|
|
334
|
+
if (existsSync(file)) {
|
|
335
|
+
console.error(`\u2717 ${file} already exists.`);
|
|
336
|
+
process.exit(1);
|
|
337
|
+
}
|
|
338
|
+
writeFileSync(file, JSON.stringify(starter, null, 2) + "\n");
|
|
339
|
+
console.log(`\u2713 Wrote ${file}. Next: npx jm-ui theme ${rest[0] ?? "ui.theme.json"} --out <your css folder>/ui-theme.css`);
|
|
340
|
+
} else {
|
|
341
|
+
console.log(`jm-ui
|
|
342
|
+
|
|
343
|
+
jm-ui init [file] Write a starter ui.theme.json
|
|
344
|
+
jm-ui theme <file> --out <css> [--watch] Turn ui.theme.json into CSS variables`);
|
|
345
|
+
}
|
package/dist/editor.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { Editor as Editor$1 } from '@tiptap/react';
|
|
3
|
+
export { Editor as TiptapEditor } from '@tiptap/react';
|
|
4
|
+
import { C as ControlRounded } from './controlStyles-Bco2sOHE.js';
|
|
5
|
+
|
|
6
|
+
type EditorTool = "paragraph" | "heading2" | "heading3" | "bold" | "italic" | "underline" | "strike" | "code" | "link" | "bulletList" | "orderedList" | "blockquote" | "horizontalRule" | "undo" | "redo" | "|";
|
|
7
|
+
declare const defaultTools: EditorTool[];
|
|
8
|
+
interface EditorProps {
|
|
9
|
+
/** Controlled HTML value. */
|
|
10
|
+
value?: string;
|
|
11
|
+
/** Starting HTML when uncontrolled. */
|
|
12
|
+
defaultValue?: string;
|
|
13
|
+
/** Called with the new HTML on every change. An empty editor gives "". */
|
|
14
|
+
onChange?: (html: string) => void;
|
|
15
|
+
placeholder?: string;
|
|
16
|
+
/** Which toolbar buttons to show, in order. "|" adds a divider. */
|
|
17
|
+
toolbar?: EditorTool[];
|
|
18
|
+
/** Hard limit on characters. Shows a counter. */
|
|
19
|
+
maxLength?: number;
|
|
20
|
+
/** Show the character count (always shown with maxLength). */
|
|
21
|
+
showCount?: boolean;
|
|
22
|
+
/** Minimum height of the writing area, e.g. "8rem". Default "9rem". */
|
|
23
|
+
minHeight?: string;
|
|
24
|
+
/** Maximum height before the writing area scrolls, e.g. "24rem". */
|
|
25
|
+
maxHeight?: string;
|
|
26
|
+
rounded?: Exclude<ControlRounded, "full">;
|
|
27
|
+
invalid?: boolean;
|
|
28
|
+
disabled?: boolean;
|
|
29
|
+
/** Read-only: formatted text, no toolbar, no editing. */
|
|
30
|
+
readOnly?: boolean;
|
|
31
|
+
/** Adds a hidden input with this name holding the HTML, for normal form posts. */
|
|
32
|
+
name?: string;
|
|
33
|
+
id?: string;
|
|
34
|
+
autoFocus?: boolean;
|
|
35
|
+
/** Get the underlying Tiptap editor, e.g. to run commands or read JSON. */
|
|
36
|
+
onReady?: (editor: Editor$1) => void;
|
|
37
|
+
className?: string;
|
|
38
|
+
"aria-label"?: string;
|
|
39
|
+
"aria-describedby"?: string;
|
|
40
|
+
}
|
|
41
|
+
declare const Editor: React.ForwardRefExoticComponent<EditorProps & React.RefAttributes<HTMLDivElement>>;
|
|
42
|
+
|
|
43
|
+
export { Editor, type EditorProps, type EditorTool, defaultTools };
|