@nikala-ui/core 0.9.12 → 0.10.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nikala-ui/core",
3
- "version": "0.9.12",
3
+ "version": "0.10.1",
4
4
  "description": "Core component definitions, design tokens, and registry for Nikala UI",
5
5
  "type": "module",
6
6
  "private": false,
@@ -8,10 +8,13 @@
8
8
  "tailwind-merge",
9
9
  "class-variance-authority"
10
10
  ],
11
+ "registryDependencies": [
12
+ "spinner"
13
+ ],
11
14
  "files": [
12
15
  {
13
16
  "path": "ui/button.tsx",
14
- "content": "import { splitProps, type Component, type JSX } from \"solid-js\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { cn } from \"@/lib/cn\";\n\n/**\n * Class variance authority configuration for button styling variants and sizes.\n */\nexport const buttonVariants = cva(\n \"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 cursor-pointer\",\n {\n variants: {\n variant: {\n default:\n \"bg-primary text-primary-foreground shadow hover:bg-primary/90 border border-primary/50\",\n destructive:\n \"bg-red-600 text-zinc-50 shadow-sm hover:bg-red-600/90 dark:bg-red-900 dark:text-zinc-50 dark:hover:bg-red-900/90\",\n outline:\n \"border border-input bg-background hover:bg-accent hover:text-accent-foreground\",\n secondary:\n \"bg-secondary text-secondary-foreground hover:bg-secondary/80 border border-border\",\n ghost:\n \"hover:bg-accent hover:text-accent-foreground\",\n link: \"text-primary underline-offset-4 hover:underline\",\n success: \"*:bg-green-600 text-zinc-50 shadow-sm hover:bg-green-600/90 dark:bg-green-900 dark:text-zinc-50 dark:hover:bg-green-900/90\",\n warning: \"*:bg-yellow-600 text-zinc-50 shadow-sm hover:bg-yellow-600/90 dark:bg-yellow-900 dark:text-zinc-50 dark:hover:bg-yellow-900/90\",\n info: \"*:bg-blue-600 text-zinc-50 shadow-sm hover:bg-blue-600/90 dark:bg-blue-900 dark:text-zinc-50 dark:hover:bg-blue-900/90\",\n },\n size: {\n xs: \"h-6 rounded-md px-2 text-xs\",\n default: \"h-9 px-4 py-2\",\n sm: \"h-8 rounded-md px-3 text-xs\",\n lg: \"h-10 rounded-md px-8\",\n icon: \"h-9 w-9\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"default\",\n },\n }\n);\n\n/**\n * Props interface for the Button component extending standard HTML button attributes.\n */\nexport interface ButtonProps\n extends JSX.ButtonHTMLAttributes<HTMLButtonElement>,\n VariantProps<typeof buttonVariants> {\n class?: string;\n}\n\n/**\n * Nikala UI Button component built for SolidJS with Tailwind CSS v4 styling.\n */\nexport const Button: Component<ButtonProps> = (props) => {\n // Use splitProps to preserve SolidJS reactivity for destructured props\n const [local, rest] = splitProps(props, [\"variant\", \"size\", \"class\", \"children\"]);\n\n return (\n <button\n class={cn(buttonVariants({ variant: local.variant, size: local.size }), local.class)}\n {...rest}\n >\n {local.children}\n </button>\n );\n};",
17
+ "content": "import { Show, splitProps, type Component, type JSX } from \"solid-js\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { cn } from \"@/lib/cn\";\nimport { Spinner } from \"./spinner\";\n\n/**\n * Class variance authority configuration for button styling variants and sizes.\n */\nexport const buttonVariants = cva(\n \"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 cursor-pointer\",\n {\n variants: {\n variant: {\n default:\n \"bg-primary text-primary-foreground shadow hover:bg-primary/90 border border-primary/50\",\n destructive:\n \"bg-red-600 text-zinc-50 shadow-sm hover:bg-red-600/90 dark:bg-red-900 dark:text-zinc-50 dark:hover:bg-red-900/90\",\n outline:\n \"border border-input bg-background hover:bg-accent hover:text-accent-foreground\",\n secondary:\n \"bg-secondary text-secondary-foreground hover:bg-secondary/80 border border-border\",\n ghost:\n \"hover:bg-accent hover:text-accent-foreground\",\n link: \"text-primary underline-offset-4 hover:underline\",\n success: \"*:bg-green-600 text-zinc-50 shadow-sm hover:bg-green-600/90 dark:bg-green-900 dark:text-zinc-50 dark:hover:bg-green-900/90\",\n warning: \"*:bg-yellow-600 text-zinc-50 shadow-sm hover:bg-yellow-600/90 dark:bg-yellow-900 dark:text-zinc-50 dark:hover:bg-yellow-900/90\",\n info: \"*:bg-blue-600 text-zinc-50 shadow-sm hover:bg-blue-600/90 dark:bg-blue-900 dark:text-zinc-50 dark:hover:bg-blue-900/90\",\n },\n size: {\n xs: \"h-6 rounded-md px-2 text-xs\",\n default: \"h-9 px-4 py-2\",\n sm: \"h-8 rounded-md px-3 text-xs\",\n lg: \"h-10 rounded-md px-8\",\n icon: \"h-9 w-9\",\n },\n },\n defaultVariants: {\n variant: \"default\",\n size: \"default\",\n },\n }\n);\n\n/**\n * Props interface for the Button component extending standard HTML button attributes.\n */\nexport interface ButtonProps\n extends JSX.ButtonHTMLAttributes<HTMLButtonElement>,\n VariantProps<typeof buttonVariants> {\n /** Shows a loading spinner and prevents repeated clicks while active. */\n loading?: boolean;\n class?: string;\n}\n\n/**\n * Nikala UI Button component built for SolidJS with Tailwind CSS v4 styling.\n */\nexport const Button: Component<ButtonProps> = (props) => {\n // Use splitProps to preserve SolidJS reactivity for destructured props\n const [local, rest] = splitProps(props, [\n \"variant\",\n \"size\",\n \"class\",\n \"children\",\n \"loading\",\n \"disabled\",\n ]);\n\n return (\n <button\n class={cn(buttonVariants({ variant: local.variant, size: local.size }), local.class)}\n disabled={local.disabled || local.loading}\n aria-busy={local.loading ? \"true\" : undefined}\n {...rest}\n >\n <Show when={local.loading}>\n <Spinner size=\"sm\" class=\"text-current\" />\n </Show>\n {local.children}\n </button>\n );\n};\n",
15
18
  "type": "registry:ui"
16
19
  }
17
20
  ]
@@ -6,7 +6,7 @@
6
6
  "files": [
7
7
  {
8
8
  "path": "hooks/create-form.ts",
9
- "content": "import { createSignal, createMemo, type Accessor } from \"solid-js\";\n\nexport type FormErrors<T> = Partial<Record<keyof T, string>>;\nexport type FormTouched<T> = Partial<Record<keyof T, boolean>>;\n\nexport interface CreateFormOptions<T extends Record<string, any>> {\n /** Initial form field values object */\n initialValues: T;\n /** Custom validation function returning error messages object */\n validate?: (values: T) => FormErrors<T> | Promise<FormErrors<T>>;\n /** Submit handler callback invoked when validation succeeds */\n onSubmit?: (values: T) => void | Promise<void>;\n}\n\nexport interface CreateFormReturn<T extends Record<string, any>> {\n /** Accessor for current form field values */\n values: Accessor<T>;\n /** Accessor for form field validation error messages */\n errors: Accessor<FormErrors<T>>;\n /** Accessor for form field touched states */\n touched: Accessor<FormTouched<T>>;\n /** Accessor indicating if form is currently submitting */\n isSubmitting: Accessor<boolean>;\n /** Accessor indicating if form has zero validation errors */\n isValid: Accessor<boolean>;\n /** Accessor indicating if form values differ from initial values */\n isDirty: Accessor<boolean>;\n /** Update specific form field value */\n setFieldValue: <K extends keyof T>(field: K, value: T[K]) => void;\n /** Set specific form field validation error */\n setFieldError: <K extends keyof T>(field: K, error: string | undefined) => void;\n /** Set specific form field touched state */\n setFieldTouched: <K extends keyof T>(field: K, isTouched?: boolean) => void;\n /** Input change event listener helper factory function */\n handleChange: <K extends keyof T>(field: K) => (e: Event & { currentTarget: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement }) => void;\n /** Input blur event listener helper factory function */\n handleBlur: <K extends keyof T>(field: K) => () => void;\n /** Form onSubmit event handler */\n handleSubmit: (e?: Event) => void;\n /** Reset form values, errors, and touched states to initial values */\n resetForm: () => void;\n}\n\n/**\n * SolidJS reactive primitive for managing form field state, validation, errors, and submission.\n *\n * @param options Form configuration options including initialValues and validate function.\n */\nexport function createForm<T extends Record<string, any>>(\n options: CreateFormOptions<T>\n): CreateFormReturn<T> {\n const initialValues = { ...options.initialValues };\n\n const [values, setValues] = createSignal<T>({ ...initialValues });\n const [errors, setErrors] = createSignal<FormErrors<T>>({});\n const [touched, setTouched] = createSignal<FormTouched<T>>({});\n const [isSubmitting, setIsSubmitting] = createSignal(false);\n\n const isDirty = createMemo(() => {\n const current = values();\n return Object.keys(initialValues).some((key) => current[key] !== initialValues[key]);\n });\n\n const isValid = createMemo(() => {\n const errs = errors();\n return Object.keys(errs).length === 0;\n });\n\n const runValidation = async (currentValues: T): Promise<FormErrors<T>> => {\n if (!options.validate) return {};\n const result = await options.validate(currentValues);\n const newErrors = result || {};\n setErrors(() => newErrors);\n return newErrors;\n };\n\n const setFieldValue = <K extends keyof T>(field: K, value: T[K]) => {\n const next = { ...values(), [field]: value };\n setValues(() => next);\n runValidation(next);\n };\n\n const setFieldError = <K extends keyof T>(field: K, error: string | undefined) => {\n setErrors((prev) => {\n const next = { ...prev };\n if (error) next[field] = error;\n else delete next[field];\n return next;\n });\n };\n\n const setFieldTouched = <K extends keyof T>(field: K, isTouched = true) => {\n setTouched((prev) => ({ ...prev, [field]: isTouched }));\n };\n\n const handleChange = <K extends keyof T>(field: K) => {\n return (e: Event & { currentTarget: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement }) => {\n const val = e.currentTarget.value;\n setFieldValue(field, val as any);\n };\n };\n\n const handleBlur = <K extends keyof T>(field: K) => {\n return () => {\n setFieldTouched(field, true);\n };\n };\n\n const handleSubmit = async (e?: Event) => {\n e?.preventDefault();\n setIsSubmitting(true);\n\n // Mark all fields as touched on submit\n const allTouched = Object.keys(values()).reduce((acc, key) => {\n acc[key as keyof T] = true;\n return acc;\n }, {} as FormTouched<T>);\n setTouched(() => allTouched);\n\n const validationErrors = await runValidation(values());\n if (Object.keys(validationErrors).length === 0) {\n if (options.onSubmit) {\n await options.onSubmit(values());\n }\n }\n\n setIsSubmitting(false);\n };\n\n const resetForm = () => {\n setValues(() => ({ ...initialValues }));\n setErrors(() => ({}));\n setTouched(() => ({}));\n setIsSubmitting(false);\n };\n\n return {\n values,\n errors,\n touched,\n isSubmitting,\n isValid,\n isDirty,\n setFieldValue,\n setFieldError,\n setFieldTouched,\n handleChange,\n handleBlur,\n handleSubmit,\n resetForm,\n };\n}\n",
9
+ "content": "import { createSignal, createMemo, type Accessor } from \"solid-js\";\n\nexport type FormErrors<T> = Partial<Record<keyof T, string>>;\nexport type FormTouched<T> = Partial<Record<keyof T, boolean>>;\nexport type ValidateOn = \"change\" | \"blur\" | \"submit\";\n\nexport type FormInputEvent = Event & {\n currentTarget:\n | HTMLInputElement\n | HTMLTextAreaElement\n | HTMLSelectElement;\n};\n\nexport interface CreateFormOptions<T extends Record<string, any>> {\n /** Initial form field values object */\n initialValues: T;\n /** Custom validation function returning error messages object */\n validate?: (values: T) => FormErrors<T> | Promise<FormErrors<T>>;\n /** Event that triggers validation. Defaults to \"change\". */\n validateOn?: ValidateOn;\n /** Submit handler callback invoked when validation succeeds */\n onSubmit?: (values: T) => void | Promise<void>;\n}\n\nexport interface CreateFormReturn<T extends Record<string, any>> {\n /** Accessor for current form field values */\n values: Accessor<T>;\n /** Accessor for form field validation error messages */\n errors: Accessor<FormErrors<T>>;\n /** Accessor for form field touched states */\n touched: Accessor<FormTouched<T>>;\n /** Accessor indicating if form is currently submitting */\n isSubmitting: Accessor<boolean>;\n /** Accessor containing an exception thrown by onSubmit, if any */\n submitError: Accessor<unknown>;\n /** Accessor indicating if form has zero validation errors */\n isValid: Accessor<boolean>;\n /** Accessor indicating if form values differ from initial values */\n isDirty: Accessor<boolean>;\n /** Update specific form field value */\n setFieldValue: <K extends keyof T>(field: K, value: T[K]) => void;\n /** Set specific form field validation error */\n setFieldError: <K extends keyof T>(field: K, error: string | undefined) => void;\n /** Set specific form field touched state */\n setFieldTouched: <K extends keyof T>(field: K, isTouched?: boolean) => void;\n /** Input change event listener helper factory function */\n handleChange: <K extends keyof T>(field: K) => (e: FormInputEvent) => void;\n /** Input blur event listener helper factory function */\n handleBlur: <K extends keyof T>(field: K) => () => void;\n /** Form onSubmit event handler */\n handleSubmit: (e?: Event) => Promise<void>;\n /** Reset form values, errors, and touched states to initial values */\n resetForm: () => void;\n}\n\n/**\n * SolidJS reactive primitive for managing form field state, validation, errors, and submission.\n *\n * @param options Form configuration options including initialValues and validate function.\n */\nexport function createForm<T extends Record<string, any>>(\n options: CreateFormOptions<T>\n): CreateFormReturn<T> {\n const initialValues = { ...options.initialValues };\n\n const [values, setValues] = createSignal<T>({ ...initialValues });\n const [errors, setErrors] = createSignal<FormErrors<T>>({});\n const [touched, setTouched] = createSignal<FormTouched<T>>({});\n const [isSubmitting, setIsSubmitting] = createSignal(false);\n const [submitError, setSubmitError] = createSignal<unknown>();\n const validateOn = options.validateOn ?? \"change\";\n let validationSequence = 0;\n\n const isDirty = createMemo(() => {\n const current = values();\n return Object.keys(initialValues).some((key) => current[key] !== initialValues[key]);\n });\n\n const isValid = createMemo(() => {\n const errs = errors();\n return Object.keys(errs).length === 0;\n });\n\n const runValidation = async (currentValues: T): Promise<FormErrors<T>> => {\n const sequence = ++validationSequence;\n if (!options.validate) {\n setErrors(() => ({}));\n return {};\n }\n\n const result = await options.validate(currentValues);\n const newErrors = result || {};\n // Ignore stale async validations that finished after a newer request.\n if (sequence === validationSequence) {\n setErrors(() => newErrors);\n }\n return newErrors;\n };\n\n const setFieldValue = <K extends keyof T>(field: K, value: T[K]) => {\n const next = { ...values(), [field]: value };\n setValues(() => next);\n if (validateOn === \"change\") {\n void runValidation(next);\n }\n };\n\n const setFieldError = <K extends keyof T>(field: K, error: string | undefined) => {\n setErrors((prev) => {\n const next = { ...prev };\n if (error) next[field] = error;\n else delete next[field];\n return next;\n });\n };\n\n const setFieldTouched = <K extends keyof T>(field: K, isTouched = true) => {\n setTouched((prev) => ({ ...prev, [field]: isTouched }));\n };\n\n const handleChange = <K extends keyof T>(field: K) => {\n return (e: FormInputEvent) => {\n const target = e.currentTarget;\n let value: unknown = target.value;\n\n if (\n \"checked\" in target &&\n ((target as HTMLInputElement).type === \"checkbox\" ||\n (target as HTMLInputElement).type === \"radio\")\n ) {\n value = (target as HTMLInputElement).checked;\n } else if (\"selectedOptions\" in target && target.multiple) {\n value = Array.from(target.selectedOptions, (option) => option.value);\n }\n\n setFieldValue(field, value as T[K]);\n };\n };\n\n const handleBlur = <K extends keyof T>(field: K) => {\n return () => {\n setFieldTouched(field, true);\n if (validateOn === \"blur\") {\n void runValidation(values());\n }\n };\n };\n\n const handleSubmit = async (e?: Event) => {\n e?.preventDefault();\n setIsSubmitting(true);\n setSubmitError(undefined);\n\n // Mark all fields as touched on submit\n const allTouched = Object.keys(values()).reduce((acc, key) => {\n acc[key as keyof T] = true;\n return acc;\n }, {} as FormTouched<T>);\n setTouched(() => allTouched);\n\n try {\n const validationErrors = await runValidation(values());\n if (Object.keys(validationErrors).length === 0 && options.onSubmit) {\n try {\n await options.onSubmit(values());\n } catch (error) {\n setSubmitError(() => error);\n }\n }\n } finally {\n setIsSubmitting(false);\n }\n };\n\n const resetForm = () => {\n setValues(() => ({ ...initialValues }));\n setErrors(() => ({}));\n setTouched(() => ({}));\n setIsSubmitting(false);\n setSubmitError(undefined);\n validationSequence += 1;\n };\n\n return {\n values,\n errors,\n touched,\n isSubmitting,\n submitError,\n isValid,\n isDirty,\n setFieldValue,\n setFieldError,\n setFieldTouched,\n handleChange,\n handleBlur,\n handleSubmit,\n resetForm,\n };\n}\n",
10
10
  "type": "registry:hook"
11
11
  }
12
12
  ]
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "empty",
3
+ "title": "Empty",
4
+ "description": "A compound empty-state layout for collections, search results, and initial application states.",
5
+ "type": "registry:ui",
6
+ "dependencies": [
7
+ "clsx",
8
+ "tailwind-merge"
9
+ ],
10
+ "files": [
11
+ {
12
+ "path": "ui/empty.tsx",
13
+ "content": "import { splitProps, type Component, type JSX } from \"solid-js\";\nimport { cn } from \"@/lib/cn\";\n\nexport interface EmptyProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\n/** A centered layout for empty collections, search results, and initial states. */\nexport const Empty: Component<EmptyProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\"]);\n\n return (\n <div\n class={cn(\n \"flex min-h-64 w-full flex-col items-center justify-center rounded-lg border border-dashed border-border bg-card/30 p-6 text-center\",\n local.class\n )}\n {...rest}\n />\n );\n};\n\nexport interface EmptyIconProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const EmptyIcon: Component<EmptyIconProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\"]);\n\n return (\n <div\n aria-hidden=\"true\"\n class={cn(\n \"mb-4 flex size-12 items-center justify-center rounded-lg bg-muted text-muted-foreground [&_svg]:size-6\",\n local.class\n )}\n {...rest}\n />\n );\n};\n\nexport interface EmptyTitleProps extends JSX.HTMLAttributes<HTMLHeadingElement> {\n class?: string;\n}\n\nexport const EmptyTitle: Component<EmptyTitleProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\"]);\n\n return (\n <h3\n class={cn(\"text-lg font-semibold tracking-tight text-foreground\", local.class)}\n {...rest}\n />\n );\n};\n\nexport interface EmptyDescriptionProps\n extends JSX.HTMLAttributes<HTMLParagraphElement> {\n class?: string;\n}\n\nexport const EmptyDescription: Component<EmptyDescriptionProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\"]);\n\n return (\n <p\n class={cn(\"mt-1 max-w-sm text-sm text-muted-foreground\", local.class)}\n {...rest}\n />\n );\n};\n\nexport interface EmptyActionProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const EmptyAction: Component<EmptyActionProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\"]);\n\n return (\n <div class={cn(\"mt-5 flex items-center gap-2\", local.class)} {...rest} />\n );\n};\n",
14
+ "type": "registry:ui"
15
+ }
16
+ ]
17
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "field",
3
+ "title": "Field",
4
+ "description": "A consistent form field layout for labels, descriptions, and validation errors.",
5
+ "type": "registry:ui",
6
+ "dependencies": [
7
+ "clsx",
8
+ "tailwind-merge"
9
+ ],
10
+ "registryDependencies": [
11
+ "label"
12
+ ],
13
+ "files": [
14
+ {
15
+ "path": "ui/field.tsx",
16
+ "content": "import { splitProps, type Component, type JSX } from \"solid-js\";\nimport { cn } from \"@/lib/cn\";\nimport { Label, type LabelProps } from \"./label\";\n\nexport interface FieldProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\n/** A consistent layout wrapper for labels, controls, descriptions, and errors. */\nexport const Field: Component<FieldProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <div class={cn(\"grid w-full gap-1.5\", local.class)} {...rest}>\n {local.children}\n </div>\n );\n};\n\nexport interface FieldLabelProps extends LabelProps {\n class?: string;\n children?: JSX.Element;\n}\n\nexport const FieldLabel: Component<FieldLabelProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <Label class={cn(\"text-sm font-medium\", local.class)} {...rest}>\n {local.children}\n </Label>\n );\n};\n\nexport interface FieldDescriptionProps\n extends JSX.HTMLAttributes<HTMLParagraphElement> {\n class?: string;\n}\n\nexport const FieldDescription: Component<FieldDescriptionProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <p class={cn(\"text-sm text-muted-foreground\", local.class)} {...rest}>\n {local.children}\n </p>\n );\n};\n\nexport interface FieldErrorProps\n extends JSX.HTMLAttributes<HTMLParagraphElement> {\n class?: string;\n}\n\nexport const FieldError: Component<FieldErrorProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <p\n role=\"alert\"\n class={cn(\"text-sm font-medium text-destructive\", local.class)}\n {...rest}\n >\n {local.children}\n </p>\n );\n};\n",
17
+ "type": "registry:ui"
18
+ }
19
+ ]
20
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "form-message",
3
+ "title": "Form Message",
4
+ "description": "Validation message helper connected to createForm errors and touched state.",
5
+ "type": "registry:ui",
6
+ "registryDependencies": [
7
+ "field"
8
+ ],
9
+ "files": [
10
+ {
11
+ "path": "ui/form-message.tsx",
12
+ "content": "import { Show, splitProps, type JSX } from \"solid-js\";\nimport type { CreateFormReturn } from \"@nikala-ui/hooks\";\nimport { FieldError, type FieldErrorProps } from \"./field\";\n\ntype FormState<T extends Record<string, any>> = Pick<\n CreateFormReturn<T>,\n \"errors\" | \"touched\"\n>;\n\nexport interface FormMessageProps<\n T extends Record<string, any>,\n K extends keyof T,\n> extends Omit<FieldErrorProps, \"children\"> {\n form: FormState<T>;\n name: K;\n /** Show the message even before the field has been touched. */\n showUntouched?: boolean;\n}\n\n/** Displays a field's validation error from createForm when it should be visible. */\nexport function FormMessage<\n T extends Record<string, any>,\n K extends keyof T,\n>(props: FormMessageProps<T, K>): JSX.Element {\n const [local, rest] = splitProps(props, [\"form\", \"name\", \"showUntouched\", \"class\"]);\n const error = () => local.form.errors()[local.name] as string | undefined;\n const touched = () => local.form.touched()[local.name] === true;\n\n return (\n <Show when={error() && (local.showUntouched || touched())}>\n <FieldError class={local.class} {...rest}>\n {error()}\n </FieldError>\n </Show>\n );\n}\n",
13
+ "type": "registry:ui"
14
+ }
15
+ ]
16
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "form",
3
+ "title": "Form",
4
+ "description": "A semantic form layout wrapper designed to work with the createForm hook.",
5
+ "type": "registry:ui",
6
+ "dependencies": [
7
+ "clsx",
8
+ "tailwind-merge"
9
+ ],
10
+ "files": [
11
+ {
12
+ "path": "ui/form.tsx",
13
+ "content": "import { splitProps, type Component, type JSX } from \"solid-js\";\nimport { cn } from \"@/lib/cn\";\n\nexport interface FormProps extends JSX.FormHTMLAttributes<HTMLFormElement> {\n /** Indicates that the form is currently processing a submission. */\n loading?: boolean;\n class?: string;\n}\n\n/** A semantic form layout wrapper designed to work with the createForm hook. */\nexport const Form: Component<FormProps> = (props) => {\n const [local, rest] = splitProps(props, [\"loading\", \"class\"]);\n\n return (\n <form\n aria-busy={local.loading ? \"true\" : undefined}\n class={cn(\"w-full space-y-5\", local.class)}\n {...rest}\n />\n );\n};\n",
14
+ "type": "registry:ui"
15
+ }
16
+ ]
17
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "icon-button",
3
+ "title": "Icon Button",
4
+ "description": "An accessible square button intended for a single icon action.",
5
+ "type": "registry:ui",
6
+ "dependencies": [
7
+ "clsx",
8
+ "tailwind-merge"
9
+ ],
10
+ "registryDependencies": [
11
+ "button"
12
+ ],
13
+ "files": [
14
+ {
15
+ "path": "ui/icon-button.tsx",
16
+ "content": "import { splitProps, type Component, type JSX } from \"solid-js\";\nimport { Button, type ButtonProps } from \"./button\";\nimport { cn } from \"@/lib/cn\";\n\nexport interface IconButtonProps\n extends Omit<ButtonProps, \"size\" | \"children\"> {\n /** Accessible label for the icon-only button. */\n label: string;\n size?: \"sm\" | \"default\" | \"lg\";\n children?: JSX.Element;\n}\n\n/** An accessible square button intended for a single icon action. */\nexport const IconButton: Component<IconButtonProps> = (props) => {\n const [local, rest] = splitProps(props, [\"label\", \"size\", \"class\", \"children\"]);\n\n return (\n <Button\n size=\"icon\"\n aria-label={local.label}\n class={cn(\n {\n \"size-8\": local.size === \"sm\",\n \"size-9\": !local.size || local.size === \"default\",\n \"size-10\": local.size === \"lg\",\n },\n local.class\n )}\n {...rest}\n >\n {local.children}\n </Button>\n );\n};\n",
17
+ "type": "registry:ui"
18
+ }
19
+ ]
20
+ }
@@ -83,6 +83,9 @@
83
83
  "clsx",
84
84
  "tailwind-merge",
85
85
  "class-variance-authority"
86
+ ],
87
+ "registryDependencies": [
88
+ "spinner"
86
89
  ]
87
90
  },
88
91
  {
@@ -194,6 +197,48 @@
194
197
  "scroll-area"
195
198
  ]
196
199
  },
200
+ {
201
+ "name": "empty",
202
+ "title": "Empty",
203
+ "description": "A compound empty-state layout for collections, search results, and initial application states.",
204
+ "type": "registry:ui",
205
+ "dependencies": [
206
+ "clsx",
207
+ "tailwind-merge"
208
+ ]
209
+ },
210
+ {
211
+ "name": "field",
212
+ "title": "Field",
213
+ "description": "A consistent form field layout for labels, descriptions, and validation errors.",
214
+ "type": "registry:ui",
215
+ "dependencies": [
216
+ "clsx",
217
+ "tailwind-merge"
218
+ ],
219
+ "registryDependencies": [
220
+ "label"
221
+ ]
222
+ },
223
+ {
224
+ "name": "form-message",
225
+ "title": "Form Message",
226
+ "description": "Validation message helper connected to createForm errors and touched state.",
227
+ "type": "registry:ui",
228
+ "registryDependencies": [
229
+ "field"
230
+ ]
231
+ },
232
+ {
233
+ "name": "form",
234
+ "title": "Form",
235
+ "description": "A semantic form layout wrapper designed to work with the createForm hook.",
236
+ "type": "registry:ui",
237
+ "dependencies": [
238
+ "clsx",
239
+ "tailwind-merge"
240
+ ]
241
+ },
197
242
  {
198
243
  "name": "hover-card",
199
244
  "title": "Hover Card",
@@ -205,6 +250,19 @@
205
250
  "@kobalte/core"
206
251
  ]
207
252
  },
253
+ {
254
+ "name": "icon-button",
255
+ "title": "Icon Button",
256
+ "description": "An accessible square button intended for a single icon action.",
257
+ "type": "registry:ui",
258
+ "dependencies": [
259
+ "clsx",
260
+ "tailwind-merge"
261
+ ],
262
+ "registryDependencies": [
263
+ "button"
264
+ ]
265
+ },
208
266
  {
209
267
  "name": "input-group",
210
268
  "title": "Input Group",
@@ -423,6 +481,28 @@
423
481
  "@kobalte/core"
424
482
  ]
425
483
  },
484
+ {
485
+ "name": "spinner",
486
+ "title": "Spinner",
487
+ "description": "An accessible animated loading indicator for asynchronous UI states.",
488
+ "type": "registry:ui",
489
+ "dependencies": [
490
+ "clsx",
491
+ "tailwind-merge",
492
+ "class-variance-authority"
493
+ ]
494
+ },
495
+ {
496
+ "name": "status",
497
+ "title": "Status",
498
+ "description": "A compact semantic status indicator with a color dot and label.",
499
+ "type": "registry:ui",
500
+ "dependencies": [
501
+ "clsx",
502
+ "tailwind-merge",
503
+ "class-variance-authority"
504
+ ]
505
+ },
426
506
  {
427
507
  "name": "switch",
428
508
  "title": "Switch",
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "spinner",
3
+ "title": "Spinner",
4
+ "description": "An accessible animated loading indicator for asynchronous UI states.",
5
+ "type": "registry:ui",
6
+ "dependencies": [
7
+ "clsx",
8
+ "tailwind-merge",
9
+ "class-variance-authority"
10
+ ],
11
+ "files": [
12
+ {
13
+ "path": "ui/spinner.tsx",
14
+ "content": "import { splitProps, type Component, type JSX } from \"solid-js\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { cn } from \"@/lib/cn\";\n\nexport const spinnerVariants = cva(\n \"inline-block animate-spin rounded-full border-2 border-current border-r-transparent text-primary\",\n {\n variants: {\n size: {\n sm: \"size-3\",\n default: \"size-4\",\n lg: \"size-6\",\n },\n },\n defaultVariants: {\n size: \"default\",\n },\n }\n);\n\nexport interface SpinnerProps\n extends Omit<JSX.HTMLAttributes<HTMLSpanElement>, \"role\">,\n VariantProps<typeof spinnerVariants> {\n /** Accessible text announced while the spinner is active. */\n label?: string;\n class?: string;\n}\n\n/** A compact, accessible loading indicator for async UI states. */\nexport const Spinner: Component<SpinnerProps> = (props) => {\n const [local, rest] = splitProps(props, [\"size\", \"label\", \"class\"]);\n\n return (\n <span\n role=\"status\"\n aria-label={local.label || \"Loading\"}\n class={cn(spinnerVariants({ size: local.size }), local.class)}\n {...rest}\n />\n );\n};\n",
15
+ "type": "registry:ui"
16
+ }
17
+ ]
18
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "status",
3
+ "title": "Status",
4
+ "description": "A compact semantic status indicator with a color dot and label.",
5
+ "type": "registry:ui",
6
+ "dependencies": [
7
+ "clsx",
8
+ "tailwind-merge",
9
+ "class-variance-authority"
10
+ ],
11
+ "files": [
12
+ {
13
+ "path": "ui/status.tsx",
14
+ "content": "import { Show, splitProps, type Component, type JSX } from \"solid-js\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { cn } from \"@/lib/cn\";\n\nexport const statusVariants = cva(\n \"inline-flex items-center gap-2 text-sm font-medium text-foreground\",\n {\n variants: {\n variant: {\n neutral: \"text-muted-foreground\",\n success: \"text-emerald-700 dark:text-emerald-400\",\n warning: \"text-amber-700 dark:text-amber-400\",\n error: \"text-destructive\",\n info: \"text-blue-700 dark:text-blue-400\",\n },\n size: {\n sm: \"text-xs gap-1.5\",\n default: \"text-sm gap-2\",\n },\n bordered: {\n true: \"rounded-md border px-2 py-1\",\n false: \"\",\n },\n borderVariant: {\n neutral: \"border-muted-foreground/30\",\n success: \"border-emerald-500/30\",\n warning: \"border-amber-500/30\",\n error: \"border-destructive/30\",\n info: \"border-blue-500/30\",\n },\n },\n defaultVariants: {\n variant: \"neutral\",\n size: \"default\",\n bordered: false,\n borderVariant: \"neutral\",\n },\n }\n);\n\nexport const statusDotVariants = cva(\"size-2 shrink-0 rounded-lg\", {\n variants: {\n variant: {\n neutral: \"bg-muted-foreground\",\n success: \"bg-emerald-500\",\n warning: \"bg-amber-500\",\n error: \"bg-destructive\",\n info: \"bg-blue-500\",\n },\n size: {\n sm: \"size-1.5\",\n default: \"size-2\",\n },\n animation: {\n none: \"\",\n pulse: \"animate-pulse\",\n ping: \"\",\n },\n },\n defaultVariants: {\n variant: \"neutral\",\n size: \"default\",\n animation: \"none\",\n },\n});\n\nexport interface StatusProps\n extends JSX.HTMLAttributes<HTMLSpanElement>,\n VariantProps<typeof statusVariants> {\n class?: string;\n animation?: \"none\" | \"pulse\" | \"ping\";\n}\n\n/** A compact status indicator combining a semantic color dot and label. */\nexport const Status: Component<StatusProps> = (props) => {\n const [local, rest] = splitProps(props, [\n \"variant\",\n \"size\",\n \"class\",\n \"children\",\n \"animation\",\n \"bordered\",\n ]);\n\n return (\n <span\n role=\"status\"\n class={cn(\n statusVariants({\n variant: local.variant,\n size: local.size,\n bordered: local.bordered,\n borderVariant: local.variant,\n }),\n local.class\n )}\n {...rest}\n >\n <span class=\"relative flex shrink-0\" aria-hidden=\"true\">\n <Show when={local.animation === \"ping\"}>\n <span\n class={cn(\n \"absolute inline-flex size-full animate-ping rounded-lg opacity-75\",\n statusDotVariants({ variant: local.variant, size: local.size })\n )}\n />\n </Show>\n <span\n class={statusDotVariants({\n variant: local.variant,\n size: local.size,\n animation: local.animation === \"ping\" ? \"none\" : local.animation,\n })}\n />\n </span>\n {local.children}\n </span>\n );\n};\n",
15
+ "type": "registry:ui"
16
+ }
17
+ ]
18
+ }
@@ -1,6 +1,7 @@
1
- import { splitProps, type Component, type JSX } from "solid-js";
1
+ import { Show, splitProps, type Component, type JSX } from "solid-js";
2
2
  import { cva, type VariantProps } from "class-variance-authority";
3
3
  import { cn } from "@/lib/cn";
4
+ import { Spinner } from "./spinner";
4
5
 
5
6
  /**
6
7
  * Class variance authority configuration for button styling variants and sizes.
@@ -46,6 +47,8 @@ export const buttonVariants = cva(
46
47
  export interface ButtonProps
47
48
  extends JSX.ButtonHTMLAttributes<HTMLButtonElement>,
48
49
  VariantProps<typeof buttonVariants> {
50
+ /** Shows a loading spinner and prevents repeated clicks while active. */
51
+ loading?: boolean;
49
52
  class?: string;
50
53
  }
51
54
 
@@ -54,14 +57,26 @@ export interface ButtonProps
54
57
  */
55
58
  export const Button: Component<ButtonProps> = (props) => {
56
59
  // Use splitProps to preserve SolidJS reactivity for destructured props
57
- const [local, rest] = splitProps(props, ["variant", "size", "class", "children"]);
60
+ const [local, rest] = splitProps(props, [
61
+ "variant",
62
+ "size",
63
+ "class",
64
+ "children",
65
+ "loading",
66
+ "disabled",
67
+ ]);
58
68
 
59
69
  return (
60
70
  <button
61
71
  class={cn(buttonVariants({ variant: local.variant, size: local.size }), local.class)}
72
+ disabled={local.disabled || local.loading}
73
+ aria-busy={local.loading ? "true" : undefined}
62
74
  {...rest}
63
75
  >
76
+ <Show when={local.loading}>
77
+ <Spinner size="sm" class="text-current" />
78
+ </Show>
64
79
  {local.children}
65
80
  </button>
66
81
  );
67
- };
82
+ };
@@ -0,0 +1,83 @@
1
+ import { splitProps, type Component, type JSX } from "solid-js";
2
+ import { cn } from "@/lib/cn";
3
+
4
+ export interface EmptyProps extends JSX.HTMLAttributes<HTMLDivElement> {
5
+ class?: string;
6
+ }
7
+
8
+ /** A centered layout for empty collections, search results, and initial states. */
9
+ export const Empty: Component<EmptyProps> = (props) => {
10
+ const [local, rest] = splitProps(props, ["class"]);
11
+
12
+ return (
13
+ <div
14
+ class={cn(
15
+ "flex min-h-64 w-full flex-col items-center justify-center rounded-lg border border-dashed border-border bg-card/30 p-6 text-center",
16
+ local.class
17
+ )}
18
+ {...rest}
19
+ />
20
+ );
21
+ };
22
+
23
+ export interface EmptyIconProps extends JSX.HTMLAttributes<HTMLDivElement> {
24
+ class?: string;
25
+ }
26
+
27
+ export const EmptyIcon: Component<EmptyIconProps> = (props) => {
28
+ const [local, rest] = splitProps(props, ["class"]);
29
+
30
+ return (
31
+ <div
32
+ aria-hidden="true"
33
+ class={cn(
34
+ "mb-4 flex size-12 items-center justify-center rounded-lg bg-muted text-muted-foreground [&_svg]:size-6",
35
+ local.class
36
+ )}
37
+ {...rest}
38
+ />
39
+ );
40
+ };
41
+
42
+ export interface EmptyTitleProps extends JSX.HTMLAttributes<HTMLHeadingElement> {
43
+ class?: string;
44
+ }
45
+
46
+ export const EmptyTitle: Component<EmptyTitleProps> = (props) => {
47
+ const [local, rest] = splitProps(props, ["class"]);
48
+
49
+ return (
50
+ <h3
51
+ class={cn("text-lg font-semibold tracking-tight text-foreground", local.class)}
52
+ {...rest}
53
+ />
54
+ );
55
+ };
56
+
57
+ export interface EmptyDescriptionProps
58
+ extends JSX.HTMLAttributes<HTMLParagraphElement> {
59
+ class?: string;
60
+ }
61
+
62
+ export const EmptyDescription: Component<EmptyDescriptionProps> = (props) => {
63
+ const [local, rest] = splitProps(props, ["class"]);
64
+
65
+ return (
66
+ <p
67
+ class={cn("mt-1 max-w-sm text-sm text-muted-foreground", local.class)}
68
+ {...rest}
69
+ />
70
+ );
71
+ };
72
+
73
+ export interface EmptyActionProps extends JSX.HTMLAttributes<HTMLDivElement> {
74
+ class?: string;
75
+ }
76
+
77
+ export const EmptyAction: Component<EmptyActionProps> = (props) => {
78
+ const [local, rest] = splitProps(props, ["class"]);
79
+
80
+ return (
81
+ <div class={cn("mt-5 flex items-center gap-2", local.class)} {...rest} />
82
+ );
83
+ };
@@ -0,0 +1,67 @@
1
+ import { splitProps, type Component, type JSX } from "solid-js";
2
+ import { cn } from "@/lib/cn";
3
+ import { Label, type LabelProps } from "./label";
4
+
5
+ export interface FieldProps extends JSX.HTMLAttributes<HTMLDivElement> {
6
+ class?: string;
7
+ }
8
+
9
+ /** A consistent layout wrapper for labels, controls, descriptions, and errors. */
10
+ export const Field: Component<FieldProps> = (props) => {
11
+ const [local, rest] = splitProps(props, ["class", "children"]);
12
+
13
+ return (
14
+ <div class={cn("grid w-full gap-1.5", local.class)} {...rest}>
15
+ {local.children}
16
+ </div>
17
+ );
18
+ };
19
+
20
+ export interface FieldLabelProps extends LabelProps {
21
+ class?: string;
22
+ children?: JSX.Element;
23
+ }
24
+
25
+ export const FieldLabel: Component<FieldLabelProps> = (props) => {
26
+ const [local, rest] = splitProps(props, ["class", "children"]);
27
+
28
+ return (
29
+ <Label class={cn("text-sm font-medium", local.class)} {...rest}>
30
+ {local.children}
31
+ </Label>
32
+ );
33
+ };
34
+
35
+ export interface FieldDescriptionProps
36
+ extends JSX.HTMLAttributes<HTMLParagraphElement> {
37
+ class?: string;
38
+ }
39
+
40
+ export const FieldDescription: Component<FieldDescriptionProps> = (props) => {
41
+ const [local, rest] = splitProps(props, ["class", "children"]);
42
+
43
+ return (
44
+ <p class={cn("text-sm text-muted-foreground", local.class)} {...rest}>
45
+ {local.children}
46
+ </p>
47
+ );
48
+ };
49
+
50
+ export interface FieldErrorProps
51
+ extends JSX.HTMLAttributes<HTMLParagraphElement> {
52
+ class?: string;
53
+ }
54
+
55
+ export const FieldError: Component<FieldErrorProps> = (props) => {
56
+ const [local, rest] = splitProps(props, ["class", "children"]);
57
+
58
+ return (
59
+ <p
60
+ role="alert"
61
+ class={cn("text-sm font-medium text-destructive", local.class)}
62
+ {...rest}
63
+ >
64
+ {local.children}
65
+ </p>
66
+ );
67
+ };
@@ -0,0 +1,36 @@
1
+ import { Show, splitProps, type JSX } from "solid-js";
2
+ import type { CreateFormReturn } from "@nikala-ui/hooks";
3
+ import { FieldError, type FieldErrorProps } from "./field";
4
+
5
+ type FormState<T extends Record<string, any>> = Pick<
6
+ CreateFormReturn<T>,
7
+ "errors" | "touched"
8
+ >;
9
+
10
+ export interface FormMessageProps<
11
+ T extends Record<string, any>,
12
+ K extends keyof T,
13
+ > extends Omit<FieldErrorProps, "children"> {
14
+ form: FormState<T>;
15
+ name: K;
16
+ /** Show the message even before the field has been touched. */
17
+ showUntouched?: boolean;
18
+ }
19
+
20
+ /** Displays a field's validation error from createForm when it should be visible. */
21
+ export function FormMessage<
22
+ T extends Record<string, any>,
23
+ K extends keyof T,
24
+ >(props: FormMessageProps<T, K>): JSX.Element {
25
+ const [local, rest] = splitProps(props, ["form", "name", "showUntouched", "class"]);
26
+ const error = () => local.form.errors()[local.name] as string | undefined;
27
+ const touched = () => local.form.touched()[local.name] === true;
28
+
29
+ return (
30
+ <Show when={error() && (local.showUntouched || touched())}>
31
+ <FieldError class={local.class} {...rest}>
32
+ {error()}
33
+ </FieldError>
34
+ </Show>
35
+ );
36
+ }
@@ -0,0 +1,21 @@
1
+ import { splitProps, type Component, type JSX } from "solid-js";
2
+ import { cn } from "@/lib/cn";
3
+
4
+ export interface FormProps extends JSX.FormHTMLAttributes<HTMLFormElement> {
5
+ /** Indicates that the form is currently processing a submission. */
6
+ loading?: boolean;
7
+ class?: string;
8
+ }
9
+
10
+ /** A semantic form layout wrapper designed to work with the createForm hook. */
11
+ export const Form: Component<FormProps> = (props) => {
12
+ const [local, rest] = splitProps(props, ["loading", "class"]);
13
+
14
+ return (
15
+ <form
16
+ aria-busy={local.loading ? "true" : undefined}
17
+ class={cn("w-full space-y-5", local.class)}
18
+ {...rest}
19
+ />
20
+ );
21
+ };
@@ -0,0 +1,34 @@
1
+ import { splitProps, type Component, type JSX } from "solid-js";
2
+ import { Button, type ButtonProps } from "./button";
3
+ import { cn } from "@/lib/cn";
4
+
5
+ export interface IconButtonProps
6
+ extends Omit<ButtonProps, "size" | "children"> {
7
+ /** Accessible label for the icon-only button. */
8
+ label: string;
9
+ size?: "sm" | "default" | "lg";
10
+ children?: JSX.Element;
11
+ }
12
+
13
+ /** An accessible square button intended for a single icon action. */
14
+ export const IconButton: Component<IconButtonProps> = (props) => {
15
+ const [local, rest] = splitProps(props, ["label", "size", "class", "children"]);
16
+
17
+ return (
18
+ <Button
19
+ size="icon"
20
+ aria-label={local.label}
21
+ class={cn(
22
+ {
23
+ "size-8": local.size === "sm",
24
+ "size-9": !local.size || local.size === "default",
25
+ "size-10": local.size === "lg",
26
+ },
27
+ local.class
28
+ )}
29
+ {...rest}
30
+ >
31
+ {local.children}
32
+ </Button>
33
+ );
34
+ };
@@ -0,0 +1,41 @@
1
+ import { splitProps, type Component, type JSX } from "solid-js";
2
+ import { cva, type VariantProps } from "class-variance-authority";
3
+ import { cn } from "@/lib/cn";
4
+
5
+ export const spinnerVariants = cva(
6
+ "inline-block animate-spin rounded-full border-2 border-current border-r-transparent text-primary",
7
+ {
8
+ variants: {
9
+ size: {
10
+ sm: "size-3",
11
+ default: "size-4",
12
+ lg: "size-6",
13
+ },
14
+ },
15
+ defaultVariants: {
16
+ size: "default",
17
+ },
18
+ }
19
+ );
20
+
21
+ export interface SpinnerProps
22
+ extends Omit<JSX.HTMLAttributes<HTMLSpanElement>, "role">,
23
+ VariantProps<typeof spinnerVariants> {
24
+ /** Accessible text announced while the spinner is active. */
25
+ label?: string;
26
+ class?: string;
27
+ }
28
+
29
+ /** A compact, accessible loading indicator for async UI states. */
30
+ export const Spinner: Component<SpinnerProps> = (props) => {
31
+ const [local, rest] = splitProps(props, ["size", "label", "class"]);
32
+
33
+ return (
34
+ <span
35
+ role="status"
36
+ aria-label={local.label || "Loading"}
37
+ class={cn(spinnerVariants({ size: local.size }), local.class)}
38
+ {...rest}
39
+ />
40
+ );
41
+ };
@@ -0,0 +1,119 @@
1
+ import { Show, splitProps, type Component, type JSX } from "solid-js";
2
+ import { cva, type VariantProps } from "class-variance-authority";
3
+ import { cn } from "@/lib/cn";
4
+
5
+ export const statusVariants = cva(
6
+ "inline-flex items-center gap-2 text-sm font-medium text-foreground",
7
+ {
8
+ variants: {
9
+ variant: {
10
+ neutral: "text-muted-foreground",
11
+ success: "text-emerald-700 dark:text-emerald-400",
12
+ warning: "text-amber-700 dark:text-amber-400",
13
+ error: "text-destructive",
14
+ info: "text-blue-700 dark:text-blue-400",
15
+ },
16
+ size: {
17
+ sm: "text-xs gap-1.5",
18
+ default: "text-sm gap-2",
19
+ },
20
+ bordered: {
21
+ true: "rounded-md border px-2 py-1",
22
+ false: "",
23
+ },
24
+ borderVariant: {
25
+ neutral: "border-muted-foreground/30",
26
+ success: "border-emerald-500/30",
27
+ warning: "border-amber-500/30",
28
+ error: "border-destructive/30",
29
+ info: "border-blue-500/30",
30
+ },
31
+ },
32
+ defaultVariants: {
33
+ variant: "neutral",
34
+ size: "default",
35
+ bordered: false,
36
+ borderVariant: "neutral",
37
+ },
38
+ }
39
+ );
40
+
41
+ export const statusDotVariants = cva("size-2 shrink-0 rounded-lg", {
42
+ variants: {
43
+ variant: {
44
+ neutral: "bg-muted-foreground",
45
+ success: "bg-emerald-500",
46
+ warning: "bg-amber-500",
47
+ error: "bg-destructive",
48
+ info: "bg-blue-500",
49
+ },
50
+ size: {
51
+ sm: "size-1.5",
52
+ default: "size-2",
53
+ },
54
+ animation: {
55
+ none: "",
56
+ pulse: "animate-pulse",
57
+ ping: "",
58
+ },
59
+ },
60
+ defaultVariants: {
61
+ variant: "neutral",
62
+ size: "default",
63
+ animation: "none",
64
+ },
65
+ });
66
+
67
+ export interface StatusProps
68
+ extends JSX.HTMLAttributes<HTMLSpanElement>,
69
+ VariantProps<typeof statusVariants> {
70
+ class?: string;
71
+ animation?: "none" | "pulse" | "ping";
72
+ }
73
+
74
+ /** A compact status indicator combining a semantic color dot and label. */
75
+ export const Status: Component<StatusProps> = (props) => {
76
+ const [local, rest] = splitProps(props, [
77
+ "variant",
78
+ "size",
79
+ "class",
80
+ "children",
81
+ "animation",
82
+ "bordered",
83
+ ]);
84
+
85
+ return (
86
+ <span
87
+ role="status"
88
+ class={cn(
89
+ statusVariants({
90
+ variant: local.variant,
91
+ size: local.size,
92
+ bordered: local.bordered,
93
+ borderVariant: local.variant,
94
+ }),
95
+ local.class
96
+ )}
97
+ {...rest}
98
+ >
99
+ <span class="relative flex shrink-0" aria-hidden="true">
100
+ <Show when={local.animation === "ping"}>
101
+ <span
102
+ class={cn(
103
+ "absolute inline-flex size-full animate-ping rounded-lg opacity-75",
104
+ statusDotVariants({ variant: local.variant, size: local.size })
105
+ )}
106
+ />
107
+ </Show>
108
+ <span
109
+ class={statusDotVariants({
110
+ variant: local.variant,
111
+ size: local.size,
112
+ animation: local.animation === "ping" ? "none" : local.animation,
113
+ })}
114
+ />
115
+ </span>
116
+ {local.children}
117
+ </span>
118
+ );
119
+ };
@@ -14,12 +14,50 @@ export const COMPONENT_METADATA: Record<string, ComponentMeta> = {
14
14
  title: "Button",
15
15
  description: "An interactive button component with variant and size options.",
16
16
  dependencies: ["clsx", "tailwind-merge", "class-variance-authority"],
17
+ registryDependencies: ["spinner"],
17
18
  },
18
19
  input: {
19
20
  title: "Input",
20
21
  description: "A standard text input field with styling variants.",
21
22
  dependencies: ["clsx", "tailwind-merge"],
22
23
  },
24
+ spinner: {
25
+ title: "Spinner",
26
+ description: "An accessible animated loading indicator for asynchronous UI states.",
27
+ dependencies: ["clsx", "tailwind-merge", "class-variance-authority"],
28
+ },
29
+ empty: {
30
+ title: "Empty",
31
+ description: "A compound empty-state layout for collections, search results, and initial application states.",
32
+ dependencies: ["clsx", "tailwind-merge"],
33
+ },
34
+ status: {
35
+ title: "Status",
36
+ description: "A compact semantic status indicator with a color dot and label.",
37
+ dependencies: ["clsx", "tailwind-merge", "class-variance-authority"],
38
+ },
39
+ "icon-button": {
40
+ title: "Icon Button",
41
+ description: "An accessible square button intended for a single icon action.",
42
+ dependencies: ["clsx", "tailwind-merge"],
43
+ registryDependencies: ["button"],
44
+ },
45
+ form: {
46
+ title: "Form",
47
+ description: "A semantic form layout wrapper designed to work with the createForm hook.",
48
+ dependencies: ["clsx", "tailwind-merge"],
49
+ },
50
+ "form-message": {
51
+ title: "Form Message",
52
+ description: "Validation message helper connected to createForm errors and touched state.",
53
+ registryDependencies: ["field"],
54
+ },
55
+ field: {
56
+ title: "Field",
57
+ description: "A consistent form field layout for labels, descriptions, and validation errors.",
58
+ dependencies: ["clsx", "tailwind-merge"],
59
+ registryDependencies: ["label"],
60
+ },
23
61
  card: {
24
62
  title: "Card",
25
63
  description: "A versatile container component with header, content, and footer sections.",
@@ -399,4 +437,4 @@ export const HOOK_METADATA: Record<string, ComponentMeta> = {
399
437
  title: "createScrollIntoView",
400
438
  description: "SolidJS reactive primitive for scrolling a target element into view smooth or auto behavior",
401
439
  },
402
- };
440
+ };