feat(console-web): add variant-based UI primitives
Introduce cva-driven Button, form, card, table, and segmented controls. Use clsx and tailwind-merge through a shared cn helper so callers can override component defaults without relying on global semantic CSS. Control sizes define minimum heights while icon-only actions retain stable square dimensions.
This commit is contained in:
+35
-91
@@ -8,8 +8,11 @@
|
||||
"name": "wasmeld-console-web",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"class-variance-authority": "0.7.1",
|
||||
"clsx": "2.1.1",
|
||||
"lucide-solid": "1.27.0",
|
||||
"solid-js": "1.9.14"
|
||||
"solid-js": "1.9.14",
|
||||
"tailwind-merge": "3.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "4.3.3",
|
||||
@@ -545,9 +548,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -565,9 +565,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -585,9 +582,6 @@
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -605,9 +599,6 @@
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -625,9 +616,6 @@
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -645,9 +633,6 @@
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -665,9 +650,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -685,9 +667,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -892,9 +871,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -912,9 +888,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -932,9 +905,6 @@
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -952,9 +922,6 @@
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -972,9 +939,6 @@
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -992,9 +956,6 @@
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1012,9 +973,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1032,9 +990,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1205,9 +1160,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1225,9 +1177,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1245,9 +1194,6 @@
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1265,9 +1211,6 @@
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1285,9 +1228,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1305,9 +1245,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1550,9 +1487,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1570,9 +1504,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1590,9 +1521,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1610,9 +1538,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2224,6 +2149,27 @@
|
||||
],
|
||||
"license": "CC-BY-4.0"
|
||||
},
|
||||
"node_modules/class-variance-authority": {
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
|
||||
"integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"clsx": "^2.1.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://polar.sh/cva"
|
||||
}
|
||||
},
|
||||
"node_modules/clsx": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/convert-source-map": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
||||
@@ -2565,9 +2511,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2589,9 +2532,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2613,9 +2553,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2637,9 +2574,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3040,6 +2974,16 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwind-merge": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.6.0.tgz",
|
||||
"integrity": "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/dcastil"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "4.3.3",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz",
|
||||
|
||||
@@ -13,8 +13,11 @@
|
||||
"format:check": "oxfmt --check ."
|
||||
},
|
||||
"dependencies": {
|
||||
"class-variance-authority": "0.7.1",
|
||||
"clsx": "2.1.1",
|
||||
"lucide-solid": "1.27.0",
|
||||
"solid-js": "1.9.14"
|
||||
"solid-js": "1.9.14",
|
||||
"tailwind-merge": "3.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "4.3.3",
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { splitProps, type JSX } from "solid-js";
|
||||
import { cn } from "../../lib/cn";
|
||||
|
||||
export const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 rounded-md border text-sm font-semibold transition-colors disabled:cursor-not-allowed disabled:opacity-45",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
primary: "border-brand bg-brand text-white hover:border-brand-strong hover:bg-brand-strong",
|
||||
secondary: "border-line bg-white text-ink hover:bg-canvas",
|
||||
danger: "border-coral-strong bg-white text-coral-strong hover:bg-coral-soft",
|
||||
},
|
||||
size: {
|
||||
default: "min-h-10 px-4 py-2",
|
||||
small: "min-h-9 px-4 py-1.5",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "secondary",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export type ButtonProps = JSX.ButtonHTMLAttributes<HTMLButtonElement> &
|
||||
VariantProps<typeof buttonVariants>;
|
||||
|
||||
/**
|
||||
* Standard text or icon-and-text command button.
|
||||
*
|
||||
* Size variants define a minimum target height instead of fixing the block
|
||||
* height, so wrapped labels and enlarged user fonts can grow without clipping.
|
||||
* The component defaults to `type="button"` so using it inside a form never
|
||||
* submits accidentally. Set `type="submit"` explicitly for form actions.
|
||||
*/
|
||||
export function Button(props: ButtonProps) {
|
||||
const [local, rest] = splitProps(props, ["variant", "size", "class", "children", "type"]);
|
||||
return (
|
||||
<button
|
||||
{...rest}
|
||||
data-slot="button"
|
||||
type={local.type ?? "button"}
|
||||
class={cn(buttonVariants({ variant: local.variant, size: local.size }), local.class)}
|
||||
>
|
||||
{local.children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export const iconButtonVariants = cva(
|
||||
"inline-flex shrink-0 items-center justify-center rounded-md border border-line bg-white text-muted transition-colors hover:bg-canvas hover:text-ink disabled:cursor-not-allowed disabled:opacity-45",
|
||||
{
|
||||
variants: {
|
||||
tone: {
|
||||
default: "",
|
||||
danger: "text-coral-strong",
|
||||
},
|
||||
size: {
|
||||
default: "size-9",
|
||||
small: "size-8",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
tone: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export type IconButtonProps = Omit<JSX.ButtonHTMLAttributes<HTMLButtonElement>, "aria-label"> &
|
||||
VariantProps<typeof iconButtonVariants> & {
|
||||
"aria-label": string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Square icon-only button for familiar toolbar and row actions.
|
||||
*
|
||||
* Callers must provide an accessible `aria-label`; `title` is recommended when
|
||||
* the icon's meaning benefits from a visible hover explanation.
|
||||
*/
|
||||
export function IconButton(props: IconButtonProps) {
|
||||
const [local, rest] = splitProps(props, ["tone", "size", "class", "children", "type"]);
|
||||
return (
|
||||
<button
|
||||
{...rest}
|
||||
data-slot="icon-button"
|
||||
type={local.type ?? "button"}
|
||||
class={cn(iconButtonVariants({ tone: local.tone, size: local.size }), local.class)}
|
||||
>
|
||||
{local.children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export type IconLinkProps = Omit<JSX.AnchorHTMLAttributes<HTMLAnchorElement>, "aria-label"> &
|
||||
Pick<VariantProps<typeof iconButtonVariants>, "size"> & {
|
||||
"aria-label": string;
|
||||
};
|
||||
|
||||
/** Anchor counterpart to IconButton for downloads and external navigation. */
|
||||
export function IconLink(props: IconLinkProps) {
|
||||
const [local, rest] = splitProps(props, ["size", "class", "children"]);
|
||||
return (
|
||||
<a
|
||||
{...rest}
|
||||
data-slot="icon-link"
|
||||
class={cn(iconButtonVariants({ size: local.size }), local.class)}
|
||||
>
|
||||
{local.children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { splitProps, type JSX } from "solid-js";
|
||||
import { Dynamic } from "solid-js/web";
|
||||
import { cn } from "../../lib/cn";
|
||||
|
||||
type CardElement = "article" | "aside" | "div" | "section";
|
||||
|
||||
export const cardVariants = cva("rounded-md border border-line bg-surface", {
|
||||
variants: {
|
||||
padding: {
|
||||
none: "",
|
||||
small: "p-4",
|
||||
medium: "p-5",
|
||||
},
|
||||
elevation: {
|
||||
flat: "",
|
||||
raised: "shadow-sm",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
padding: "none",
|
||||
elevation: "flat",
|
||||
},
|
||||
});
|
||||
|
||||
export type CardProps = JSX.HTMLAttributes<HTMLElement> &
|
||||
VariantProps<typeof cardVariants> & {
|
||||
as?: CardElement;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generic bordered surface.
|
||||
*
|
||||
* `as` preserves document semantics at the call site; Card only owns visual
|
||||
* framing and must not encode page or domain behavior.
|
||||
*/
|
||||
export function Card(props: CardProps) {
|
||||
const [local, rest] = splitProps(props, ["as", "padding", "elevation", "class", "children"]);
|
||||
return (
|
||||
<Dynamic
|
||||
component={local.as ?? "div"}
|
||||
{...rest}
|
||||
data-slot="card"
|
||||
class={cn(cardVariants({ padding: local.padding, elevation: local.elevation }), local.class)}
|
||||
>
|
||||
{local.children}
|
||||
</Dynamic>
|
||||
);
|
||||
}
|
||||
|
||||
export type CardHeaderProps = JSX.HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
/** Standard Card heading/action row. */
|
||||
export function CardHeader(props: CardHeaderProps) {
|
||||
const [local, rest] = splitProps(props, ["class", "children"]);
|
||||
return (
|
||||
<div
|
||||
{...rest}
|
||||
data-slot="card-header"
|
||||
class={cn(
|
||||
"flex min-h-16 items-center justify-between gap-4 border-b border-line px-5 py-3 [&_h2]:text-base [&_h2]:font-semibold [&_p]:mt-0.5 [&_p]:text-xs [&_p]:text-muted",
|
||||
local.class,
|
||||
)}
|
||||
>
|
||||
{local.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { splitProps, type JSX } from "solid-js";
|
||||
import { cn } from "../../lib/cn";
|
||||
|
||||
export type FieldProps = JSX.LabelHTMLAttributes<HTMLLabelElement>;
|
||||
|
||||
/**
|
||||
* Associates a label and form control while keeping field layout consistent.
|
||||
*
|
||||
* Keep validation and help text at the call site: those messages are domain
|
||||
* content, while Field only owns spacing and label typography.
|
||||
*/
|
||||
export function Field(props: FieldProps) {
|
||||
const [local, rest] = splitProps(props, ["class", "children"]);
|
||||
return (
|
||||
<label {...rest} data-slot="field" class={cn("block", local.class)}>
|
||||
{local.children}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
export type FieldLabelProps = JSX.HTMLAttributes<HTMLSpanElement>;
|
||||
|
||||
export function FieldLabel(props: FieldLabelProps) {
|
||||
const [local, rest] = splitProps(props, ["class", "children"]);
|
||||
return (
|
||||
<span
|
||||
{...rest}
|
||||
data-slot="field-label"
|
||||
class={cn("mb-1.5 block text-xs font-semibold text-muted", local.class)}
|
||||
>
|
||||
{local.children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { splitProps, type JSX } from "solid-js";
|
||||
import { cn } from "../../lib/cn";
|
||||
|
||||
export const inputVariants = cva(
|
||||
"w-full rounded-md border border-line bg-white px-3 text-sm text-ink outline-none transition-colors focus:border-brand disabled:cursor-not-allowed disabled:opacity-45 aria-invalid:border-coral-strong",
|
||||
{
|
||||
variants: {
|
||||
density: {
|
||||
default: "min-h-10 py-2",
|
||||
compact: "min-h-9 py-1.5",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
density: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export type InputProps = JSX.InputHTMLAttributes<HTMLInputElement> &
|
||||
VariantProps<typeof inputVariants>;
|
||||
|
||||
/** Native single-line input with a minimum, rather than fixed, control height. */
|
||||
export function Input(props: InputProps) {
|
||||
const [local, rest] = splitProps(props, ["density", "class"]);
|
||||
return (
|
||||
<input
|
||||
{...rest}
|
||||
data-slot="input"
|
||||
class={cn(inputVariants({ density: local.density }), local.class)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { splitProps, type JSX } from "solid-js";
|
||||
import { cn } from "../../lib/cn";
|
||||
|
||||
export function SegmentedControl(props: JSX.HTMLAttributes<HTMLDivElement>) {
|
||||
const [local, rest] = splitProps(props, ["class", "children"]);
|
||||
return (
|
||||
<div
|
||||
{...rest}
|
||||
data-slot="segmented-control"
|
||||
class={cn("inline-flex rounded-md border border-line bg-canvas p-1", local.class)}
|
||||
>
|
||||
{local.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const segmentVariants = cva(
|
||||
"min-h-8 rounded-sm px-3 py-1 text-xs font-semibold text-muted transition-colors",
|
||||
{
|
||||
variants: {
|
||||
active: {
|
||||
true: "bg-white text-ink shadow-sm",
|
||||
false: "hover:text-ink",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
active: false,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export type SegmentProps = JSX.ButtonHTMLAttributes<HTMLButtonElement> &
|
||||
VariantProps<typeof segmentVariants>;
|
||||
|
||||
/** One mutually exclusive option inside a SegmentedControl. */
|
||||
export function Segment(props: SegmentProps) {
|
||||
const [local, rest] = splitProps(props, ["active", "class", "children", "type"]);
|
||||
return (
|
||||
<button
|
||||
{...rest}
|
||||
data-slot="segment"
|
||||
type={local.type ?? "button"}
|
||||
aria-pressed={local.active === true}
|
||||
class={cn(segmentVariants({ active: local.active }), local.class)}
|
||||
>
|
||||
{local.children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { splitProps, type JSX } from "solid-js";
|
||||
import { cn } from "../../lib/cn";
|
||||
|
||||
export const selectVariants = cva(
|
||||
"w-full rounded-md border border-line bg-white px-3 text-sm text-ink outline-none transition-colors focus:border-brand disabled:cursor-not-allowed disabled:opacity-45 aria-invalid:border-coral-strong",
|
||||
{
|
||||
variants: {
|
||||
density: {
|
||||
default: "min-h-10 py-2",
|
||||
compact: "min-h-9 py-1.5",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
density: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export type SelectProps = JSX.SelectHTMLAttributes<HTMLSelectElement> &
|
||||
VariantProps<typeof selectVariants>;
|
||||
|
||||
/** Native select with browser behavior preserved and a content-growable height. */
|
||||
export function Select(props: SelectProps) {
|
||||
const [local, rest] = splitProps(props, ["density", "class", "children"]);
|
||||
return (
|
||||
<select
|
||||
{...rest}
|
||||
data-slot="select"
|
||||
class={cn(selectVariants({ density: local.density }), local.class)}
|
||||
>
|
||||
{local.children}
|
||||
</select>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { splitProps, type JSX } from "solid-js";
|
||||
import { cn } from "../../lib/cn";
|
||||
|
||||
export type TableProps = JSX.HTMLAttributes<HTMLTableElement> & {
|
||||
containerClass?: string;
|
||||
};
|
||||
|
||||
/** Responsive native table with a horizontal overflow boundary. */
|
||||
export function Table(props: TableProps) {
|
||||
const [local, rest] = splitProps(props, ["class", "containerClass", "children"]);
|
||||
return (
|
||||
<div data-slot="table-container" class={cn("w-full overflow-x-auto", local.containerClass)}>
|
||||
<table
|
||||
{...rest}
|
||||
data-slot="table"
|
||||
class={cn("w-full min-w-[760px] border-collapse text-left text-sm", local.class)}
|
||||
>
|
||||
{local.children}
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function TableHeader(props: JSX.HTMLAttributes<HTMLTableSectionElement>) {
|
||||
const [local, rest] = splitProps(props, ["class", "children"]);
|
||||
return (
|
||||
<thead {...rest} data-slot="table-header" class={cn(local.class)}>
|
||||
{local.children}
|
||||
</thead>
|
||||
);
|
||||
}
|
||||
|
||||
export function TableBody(props: JSX.HTMLAttributes<HTMLTableSectionElement>) {
|
||||
const [local, rest] = splitProps(props, ["class", "children"]);
|
||||
return (
|
||||
<tbody {...rest} data-slot="table-body" class={cn(local.class)}>
|
||||
{local.children}
|
||||
</tbody>
|
||||
);
|
||||
}
|
||||
|
||||
export function TableRow(props: JSX.HTMLAttributes<HTMLTableRowElement>) {
|
||||
const [local, rest] = splitProps(props, ["class", "children"]);
|
||||
return (
|
||||
<tr
|
||||
{...rest}
|
||||
data-slot="table-row"
|
||||
class={cn("transition-colors hover:bg-[#f8faf9] [&:last-child_td]:border-b-0", local.class)}
|
||||
>
|
||||
{local.children}
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
export function TableHead(props: JSX.ThHTMLAttributes<HTMLTableCellElement>) {
|
||||
const [local, rest] = splitProps(props, ["class", "children"]);
|
||||
return (
|
||||
<th
|
||||
{...rest}
|
||||
data-slot="table-head"
|
||||
class={cn(
|
||||
"border-b border-line bg-canvas px-4 py-2.5 text-[11px] font-semibold uppercase text-muted",
|
||||
local.class,
|
||||
)}
|
||||
>
|
||||
{local.children}
|
||||
</th>
|
||||
);
|
||||
}
|
||||
|
||||
export function TableCell(props: JSX.TdHTMLAttributes<HTMLTableCellElement>) {
|
||||
const [local, rest] = splitProps(props, ["class", "children"]);
|
||||
return (
|
||||
<td
|
||||
{...rest}
|
||||
data-slot="table-cell"
|
||||
class={cn("border-b border-line px-4 py-3 align-middle", local.class)}
|
||||
>
|
||||
{local.children}
|
||||
</td>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { splitProps, type JSX } from "solid-js";
|
||||
import { cn } from "../../lib/cn";
|
||||
|
||||
export const textareaVariants = cva(
|
||||
"min-h-36 w-full rounded-md border border-line bg-white px-3 py-3 text-sm text-ink outline-none transition-colors focus:border-brand disabled:cursor-not-allowed disabled:opacity-45 aria-invalid:border-coral-strong",
|
||||
{
|
||||
variants: {
|
||||
resize: {
|
||||
vertical: "resize-y",
|
||||
none: "resize-none",
|
||||
},
|
||||
typography: {
|
||||
mono: "font-mono",
|
||||
sans: "font-sans",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
resize: "vertical",
|
||||
typography: "mono",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export type TextareaProps = JSX.TextareaHTMLAttributes<HTMLTextAreaElement> &
|
||||
VariantProps<typeof textareaVariants>;
|
||||
|
||||
/** Standard multiline input with explicit resize and typography variants. */
|
||||
export function Textarea(props: TextareaProps) {
|
||||
const [local, rest] = splitProps(props, ["resize", "typography", "class", "children"]);
|
||||
return (
|
||||
<textarea
|
||||
{...rest}
|
||||
data-slot="textarea"
|
||||
class={cn(
|
||||
textareaVariants({ resize: local.resize, typography: local.typography }),
|
||||
local.class,
|
||||
)}
|
||||
>
|
||||
{local.children}
|
||||
</textarea>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
/**
|
||||
* Merges conditional Tailwind classes using the same pattern as shadcn.
|
||||
*
|
||||
* `clsx` evaluates conditional input while `tailwind-merge` lets a caller's
|
||||
* class override a component default from the same Tailwind class group:
|
||||
*
|
||||
* ```ts
|
||||
* cn("h-10 px-4", compact && "h-8", props.class)
|
||||
* ```
|
||||
*/
|
||||
export function cn(...inputs: ClassValue[]): string {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
Reference in New Issue
Block a user