@nikala-ui/core 0.8.0 → 0.9.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.8.0",
3
+ "version": "0.9.1",
4
4
  "description": "Core component definitions, design tokens, and registry for Nikala UI",
5
5
  "type": "module",
6
6
  "private": false,
@@ -18,7 +18,7 @@
18
18
  "files": [
19
19
  {
20
20
  "path": "ui/command.tsx",
21
- "content": "import {\n createContext,\n useContext,\n createSignal,\n onMount,\n onCleanup,\n Show,\n splitProps,\n type Component,\n type JSX,\n type Accessor,\n} from \"solid-js\";\nimport { Dialog } from \"@kobalte/core/dialog\";\nimport { Search, ArrowUp, ArrowDown, CornerDownLeft } from \"lucide-solid\";\nimport { cn } from \"@/lib/cn\";\nimport { Kbd, KbdGroup } from \"../ui/kbd\";\nimport { InputGroup, InputGroupInput, InputGroupAddon } from \"../ui/input-group\";\nimport { List, ListGroup, ListHeader, ListItem, type ListItemProps } from \"../ui/list\";\n\n/* --- Command Context State --- */\ninterface CommandContextValue {\n search: Accessor<string>;\n setSearch: (value: string) => void;\n}\n\nconst CommandContext = createContext<CommandContextValue>();\n\nexport const useCommand = () => {\n const ctx = useContext(CommandContext);\n if (!ctx) {\n throw new Error(\"useCommand must be used within a <Command />\");\n }\n return ctx;\n};\n\n/* --- Root Command Container --- */\nexport interface CommandProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const Command: Component<CommandProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n const [search, setSearch] = createSignal(\"\");\n\n return (\n <CommandContext.Provider value={{ search, setSearch }}>\n <div\n class={cn(\n \"flex flex-col w-full h-full rounded-xl bg-popover text-popover-foreground overflow-hidden border border-border shadow-md\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </div>\n </CommandContext.Provider>\n );\n};\n\n/* --- Command Dialog (Modal) --- */\nexport interface CommandDialogProps {\n open?: boolean;\n onOpenChange?: (open: boolean) => void;\n enableHotkey?: boolean;\n children?: JSX.Element;\n class?: string;\n}\n\nexport const CommandDialog: Component<CommandDialogProps> = (props) => {\n const [internalOpen, setInternalOpen] = createSignal(false);\n\n const isOpen = () => (props.open !== undefined ? props.open : internalOpen());\n const setOpen = (val: boolean) => {\n if (props.open === undefined) setInternalOpen(val);\n if (typeof props.onOpenChange === \"function\") props.onOpenChange(val);\n };\n\n /* Listen for global Ctrl+K / Cmd+K hotkeys */\n onMount(() => {\n if (props.enableHotkey !== false) {\n const handleKeyDown = (e: KeyboardEvent) => {\n if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === \"k\") {\n e.preventDefault();\n setOpen(!isOpen());\n }\n };\n window.addEventListener(\"keydown\", handleKeyDown);\n onCleanup(() => window.removeEventListener(\"keydown\", handleKeyDown));\n }\n });\n\n return (\n <Dialog open={isOpen()} onOpenChange={setOpen}>\n <Dialog.Portal>\n <Dialog.Overlay class=\"fixed inset-0 z-50 bg-black/60 backdrop-blur-xs data-[expanded]:animate-in data-[closed]:animate-out data-[closed]:fade-out-0 data-[expanded]:fade-in-0\" />\n <div class=\"fixed inset-0 z-50 flex items-start justify-center pt-16 sm:pt-24 px-4\">\n <Dialog.Content class=\"w-full max-w-xl rounded-xl border border-border bg-popover text-popover-foreground shadow-2xl outline-none data-[expanded]:animate-in data-[closed]:animate-out data-[closed]:fade-out-0 data-[expanded]:fade-in-0 data-[closed]:zoom-out-95 data-[expanded]:zoom-in-95\">\n <Command class={props.class}>{props.children}</Command>\n </Dialog.Content>\n </div>\n </Dialog.Portal>\n </Dialog>\n );\n};\n\n/* --- Command Input --- */\nexport interface CommandInputProps\n extends JSX.InputHTMLAttributes<HTMLInputElement> {\n class?: string;\n}\n\nexport const CommandInput: Component<CommandInputProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"value\", \"onInput\"]);\n const { search, setSearch } = useCommand();\n\n return (\n <InputGroup class=\"border-0 border-b border-border rounded-none bg-transparent px-3 py-1 shadow-none focus-within:ring-0 focus-within:border-border\">\n <InputGroupAddon align=\"inline-start\">\n <Search class=\"w-4 h-4 text-muted-foreground\" />\n </InputGroupAddon>\n\n <InputGroupInput\n value={search()}\n onInput={(e) => {\n setSearch(e.currentTarget.value);\n if (typeof local.onInput === \"function\") {\n local.onInput(e);\n }\n }}\n placeholder=\"Type a command or search...\"\n class=\"h-11 text-base sm:text-sm font-medium\"\n {...rest}\n />\n </InputGroup>\n );\n};\n\n/* --- Command List Container --- */\nexport interface CommandListProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const CommandList: Component<CommandListProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <div\n class={cn(\"max-h-[330px] overflow-y-auto p-1.5 scrollbar-thin\", local.class)}\n {...rest}\n >\n <List>{local.children}</List>\n </div>\n );\n};\n\n/* --- Command Empty Block --- */\nexport interface CommandEmptyProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const CommandEmpty: Component<CommandEmptyProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n const { search } = useCommand();\n\n return (\n <Show when={search().trim().length > 0}>\n <div\n class={cn(\n \"py-8 text-center text-sm text-muted-foreground font-medium select-none\",\n local.class\n )}\n {...rest}\n >\n {local.children || `No results found for \"${search()}\".`}\n </div>\n </Show>\n );\n};\n\n/* --- Command Group --- */\nexport interface CommandGroupProps {\n heading: string;\n children?: JSX.Element;\n class?: string;\n}\n\nexport const CommandGroup: Component<CommandGroupProps> = (props) => {\n return (\n <ListGroup class={props.class}>\n <ListHeader title={props.heading} />\n {props.children}\n </ListGroup>\n );\n};\n\n/* --- Command Item --- */\nexport interface CommandItemProps extends ListItemProps {\n keywords?: string[];\n onSelect?: () => void;\n}\n\nexport const CommandItem: Component<CommandItemProps> = (props) => {\n const [local, rest] = splitProps(props, [\n \"title\",\n \"subtitle\",\n \"keywords\",\n \"onSelect\",\n \"onClick\",\n \"class\",\n ]);\n const { search } = useCommand();\n\n /* Auto fuzzy-filter item based on search query */\n const matchesSearch = () => {\n const query = search().toLowerCase().trim();\n if (!query) return true;\n\n const titleMatch = local.title?.toLowerCase().includes(query);\n const subtitleMatch = local.subtitle?.toLowerCase().includes(query);\n const keywordMatch = local.keywords?.some((k) =>\n k.toLowerCase().includes(query)\n );\n\n return Boolean(titleMatch || subtitleMatch || keywordMatch);\n };\n\n const handleClick = (e: MouseEvent) => {\n if (typeof local.onSelect === \"function\") {\n local.onSelect();\n }\n if (typeof local.onClick === \"function\") {\n local.onClick(e as any);\n }\n };\n\n return (\n <Show when={matchesSearch()}>\n <ListItem\n title={local.title}\n subtitle={local.subtitle}\n onClick={handleClick}\n class={local.class}\n {...rest}\n />\n </Show>\n );\n};\n\n/* --- Command Footer --- */\nexport interface CommandFooterProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const CommandFooter: Component<CommandFooterProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\"]);\n\n return (\n <div\n class={cn(\n \"flex items-center justify-between border-t border-border bg-muted/30 px-3 py-2 text-xs text-muted-foreground select-none\",\n local.class\n )}\n {...rest}\n >\n <div class=\"flex items-center gap-3\">\n <span class=\"flex items-center gap-1\">\n <KbdGroup>\n <Kbd size=\"sm\"><ArrowUp class=\"w-2.5 h-2.5\" /></Kbd>\n <Kbd size=\"sm\"><ArrowDown class=\"w-2.5 h-2.5\" /></Kbd>\n </KbdGroup>\n <span>Navigate</span>\n </span>\n\n <span class=\"flex items-center gap-1\">\n <Kbd size=\"sm\"><CornerDownLeft class=\"w-2.5 h-2.5\" /></Kbd>\n <span>Select</span>\n </span>\n </div>\n\n <span class=\"flex items-center gap-1\">\n <Kbd size=\"sm\">Esc</Kbd>\n <span>Close</span>\n </span>\n </div>\n );\n};",
21
+ "content": "import {\n createContext,\n useContext,\n createSignal,\n onMount,\n onCleanup,\n Show,\n splitProps,\n type Component,\n type JSX,\n type Accessor,\n} from \"solid-js\";\nimport { Dialog } from \"@kobalte/core/dialog\";\nimport { Search, ArrowUp, ArrowDown, CornerDownLeft } from \"lucide-solid\";\nimport { cn } from \"@/lib/cn\";\nimport { Kbd, KbdGroup } from \"../ui/kbd\";\nimport { InputGroup, InputGroupInput, InputGroupAddon } from \"../ui/input-group\";\nimport { List, ListGroup, ListHeader, ListItem, type ListItemProps } from \"../ui/list\";\n\n/* --- Command Context State --- */\ninterface CommandContextValue {\n search: Accessor<string>;\n setSearch: (value: string) => void;\n}\n\nconst CommandContext = createContext<CommandContextValue>();\n\nexport const useCommand = () => {\n const ctx = useContext(CommandContext);\n if (!ctx) {\n throw new Error(\"useCommand must be used within a <Command />\");\n }\n return ctx;\n};\n\n/* --- Root Command Container --- */\nexport interface CommandProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const Command: Component<CommandProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n const [search, setSearch] = createSignal(\"\");\n\n return (\n <CommandContext.Provider value={{ search, setSearch }}>\n <div\n class={cn(\n \"flex flex-col w-full h-full rounded-lg bg-popover text-popover-foreground overflow-hidden border border-border shadow-md\",\n local.class\n )}\n {...rest}\n >\n {local.children}\n </div>\n </CommandContext.Provider>\n );\n};\n\n/* --- Command Dialog (Modal) --- */\nexport interface CommandDialogProps {\n open?: boolean;\n onOpenChange?: (open: boolean) => void;\n enableHotkey?: boolean;\n children?: JSX.Element;\n class?: string;\n}\n\nexport const CommandDialog: Component<CommandDialogProps> = (props) => {\n const [internalOpen, setInternalOpen] = createSignal(false);\n\n const isOpen = () => (props.open !== undefined ? props.open : internalOpen());\n const setOpen = (val: boolean) => {\n if (props.open === undefined) setInternalOpen(val);\n if (typeof props.onOpenChange === \"function\") props.onOpenChange(val);\n };\n\n /* Listen for global Ctrl+K / Cmd+K hotkeys */\n onMount(() => {\n if (props.enableHotkey !== false) {\n const handleKeyDown = (e: KeyboardEvent) => {\n if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === \"k\") {\n e.preventDefault();\n setOpen(!isOpen());\n }\n };\n window.addEventListener(\"keydown\", handleKeyDown);\n onCleanup(() => window.removeEventListener(\"keydown\", handleKeyDown));\n }\n });\n\n return (\n <Dialog open={isOpen()} onOpenChange={setOpen}>\n <Dialog.Portal>\n <Dialog.Overlay class=\"fixed inset-0 z-50 bg-black/60 backdrop-blur-xs data-[expanded]:animate-in data-[closed]:animate-out data-[closed]:fade-out-0 data-[expanded]:fade-in-0\" />\n <div class=\"fixed inset-0 z-50 flex items-start justify-center pt-16 sm:pt-24 px-4\">\n <Dialog.Content class=\"w-full max-w-xl rounded-lg border border-border bg-popover text-popover-foreground shadow-2xl outline-none data-[expanded]:animate-in data-[closed]:animate-out data-[closed]:fade-out-0 data-[expanded]:fade-in-0 data-[closed]:zoom-out-95 data-[expanded]:zoom-in-95\">\n <Command class={props.class}>{props.children}</Command>\n </Dialog.Content>\n </div>\n </Dialog.Portal>\n </Dialog>\n );\n};\n\n/* --- Command Input --- */\nexport interface CommandInputProps\n extends JSX.InputHTMLAttributes<HTMLInputElement> {\n class?: string;\n}\n\nexport const CommandInput: Component<CommandInputProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"value\", \"onInput\"]);\n const { search, setSearch } = useCommand();\n\n return (\n <InputGroup class=\"border-0 border-b border-border rounded-none bg-transparent px-3 py-1 shadow-none focus-within:ring-0 focus-within:border-border\">\n <InputGroupAddon align=\"inline-start\">\n <Search class=\"w-4 h-4 text-muted-foreground\" />\n </InputGroupAddon>\n\n <InputGroupInput\n value={search()}\n onInput={(e) => {\n setSearch(e.currentTarget.value);\n if (typeof local.onInput === \"function\") {\n local.onInput(e);\n }\n }}\n placeholder=\"Type a command or search...\"\n class=\"h-11 text-base sm:text-sm font-medium\"\n {...rest}\n />\n </InputGroup>\n );\n};\n\n/* --- Command List Container --- */\nexport interface CommandListProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const CommandList: Component<CommandListProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n\n return (\n <div\n class={cn(\"max-h-[330px] overflow-y-auto p-1.5 scrollbar-thin\", local.class)}\n {...rest}\n >\n <List>{local.children}</List>\n </div>\n );\n};\n\n/* --- Command Empty Block --- */\nexport interface CommandEmptyProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const CommandEmpty: Component<CommandEmptyProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\", \"children\"]);\n const { search } = useCommand();\n\n return (\n <Show when={search().trim().length > 0}>\n <div\n class={cn(\n \"py-8 text-center text-sm text-muted-foreground font-medium select-none\",\n local.class\n )}\n {...rest}\n >\n {local.children || `No results found for \"${search()}\".`}\n </div>\n </Show>\n );\n};\n\n/* --- Command Group --- */\nexport interface CommandGroupProps {\n heading: string;\n children?: JSX.Element;\n class?: string;\n}\n\nexport const CommandGroup: Component<CommandGroupProps> = (props) => {\n return (\n <ListGroup class={props.class}>\n <ListHeader title={props.heading} />\n {props.children}\n </ListGroup>\n );\n};\n\n/* --- Command Item --- */\nexport interface CommandItemProps extends ListItemProps {\n keywords?: string[];\n onSelect?: () => void;\n}\n\nexport const CommandItem: Component<CommandItemProps> = (props) => {\n const [local, rest] = splitProps(props, [\n \"title\",\n \"subtitle\",\n \"keywords\",\n \"onSelect\",\n \"onClick\",\n \"class\",\n ]);\n const { search } = useCommand();\n\n /* Auto fuzzy-filter item based on search query */\n const matchesSearch = () => {\n const query = search().toLowerCase().trim();\n if (!query) return true;\n\n const titleMatch = local.title?.toLowerCase().includes(query);\n const subtitleMatch = local.subtitle?.toLowerCase().includes(query);\n const keywordMatch = local.keywords?.some((k) =>\n k.toLowerCase().includes(query)\n );\n\n return Boolean(titleMatch || subtitleMatch || keywordMatch);\n };\n\n const handleClick = (e: MouseEvent) => {\n if (typeof local.onSelect === \"function\") {\n local.onSelect();\n }\n if (typeof local.onClick === \"function\") {\n local.onClick(e as any);\n }\n };\n\n return (\n <Show when={matchesSearch()}>\n <ListItem\n title={local.title}\n subtitle={local.subtitle}\n onClick={handleClick}\n class={local.class}\n {...rest}\n />\n </Show>\n );\n};\n\n/* --- Command Footer --- */\nexport interface CommandFooterProps extends JSX.HTMLAttributes<HTMLDivElement> {\n class?: string;\n}\n\nexport const CommandFooter: Component<CommandFooterProps> = (props) => {\n const [local, rest] = splitProps(props, [\"class\"]);\n\n return (\n <div\n class={cn(\n \"flex items-center justify-between border-t border-border bg-muted/30 px-3 py-2 text-xs text-muted-foreground select-none\",\n local.class\n )}\n {...rest}\n >\n <div class=\"flex items-center gap-3\">\n <span class=\"flex items-center gap-1\">\n <KbdGroup>\n <Kbd size=\"sm\"><ArrowUp class=\"w-2.5 h-2.5\" /></Kbd>\n <Kbd size=\"sm\"><ArrowDown class=\"w-2.5 h-2.5\" /></Kbd>\n </KbdGroup>\n <span>Navigate</span>\n </span>\n\n <span class=\"flex items-center gap-1\">\n <Kbd size=\"sm\"><CornerDownLeft class=\"w-2.5 h-2.5\" /></Kbd>\n <span>Select</span>\n </span>\n </div>\n\n <span class=\"flex items-center gap-1\">\n <Kbd size=\"sm\">Esc</Kbd>\n <span>Close</span>\n </span>\n </div>\n );\n};",
22
22
  "type": "registry:ui"
23
23
  }
24
24
  ]
@@ -206,6 +206,17 @@
206
206
  "lucide-solid"
207
207
  ]
208
208
  },
209
+ {
210
+ "name": "progress",
211
+ "title": "Progress",
212
+ "description": "Displays an indicator showing the completion progress of a task or media playback, built on Kobalte primitives.",
213
+ "type": "registry:ui",
214
+ "dependencies": [
215
+ "clsx",
216
+ "tailwind-merge",
217
+ "@kobalte/core"
218
+ ]
219
+ },
209
220
  {
210
221
  "name": "radio-group",
211
222
  "title": "Radio Group",
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "progress",
3
+ "title": "Progress",
4
+ "description": "Displays an indicator showing the completion progress of a task or media playback, built on Kobalte primitives.",
5
+ "type": "registry:ui",
6
+ "dependencies": [
7
+ "clsx",
8
+ "tailwind-merge",
9
+ "@kobalte/core"
10
+ ],
11
+ "files": [
12
+ {
13
+ "path": "ui/progress.tsx",
14
+ "content": "import { splitProps, Show, type Component, type JSX } from \"solid-js\";\nimport { Progress as KobalteProgress } from \"@kobalte/core/progress\";\nimport { cn } from \"@/lib/cn\";\n\nexport interface ProgressProps\n extends Omit<\n JSX.HTMLAttributes<HTMLDivElement>,\n \"value\" | \"aria-valuenow\" | \"aria-valuemin\" | \"aria-valuemax\"\n > {\n /** The current numeric progress value. */\n value?: number;\n /** Minimum progress value. Defaults to 0. */\n minValue?: number;\n /** Maximum progress value. Defaults to 100. */\n maxValue?: number;\n /** Label text for accessibility and screen readers. */\n getValueLabel?: (params: { value: number; max: number }) => string;\n /** Custom label element to display above or beside progress bar. */\n label?: JSX.Element;\n /** Optional custom class for root container. */\n class?: string;\n /** Optional custom class for indicator fill bar. */\n indicatorClass?: string;\n}\n\n/**\n * Nikala UI Progress component for showing task completion status or media timeline positions.\n */\nexport const Progress: Component<ProgressProps> = (props) => {\n const [local, rest] = splitProps(props, [\n \"value\",\n \"minValue\",\n \"maxValue\",\n \"getValueLabel\",\n \"label\",\n \"class\",\n \"indicatorClass\",\n ]);\n\n return (\n <KobalteProgress\n value={local.value ?? 0}\n minValue={local.minValue ?? 0}\n maxValue={local.maxValue ?? 100}\n getValueLabel={local.getValueLabel}\n class={cn(\"flex w-full flex-col gap-1.5\", local.class)}\n {...rest}\n >\n <Show when={local.label}>\n <div class=\"flex justify-between text-xs font-medium text-muted-foreground\">\n <KobalteProgress.Label>{local.label}</KobalteProgress.Label>\n <KobalteProgress.ValueLabel class=\"font-mono text-foreground\" />\n </div>\n </Show>\n\n <KobalteProgress.Track class=\"relative h-2 w-full overflow-hidden rounded-full bg-primary/20\">\n <KobalteProgress.Fill\n class={cn(\n \"h-full w-[var(--kb-progress-fill-width)] bg-primary transition-all duration-300 ease-in-out\",\n local.indicatorClass\n )}\n />\n </KobalteProgress.Track>\n </KobalteProgress>\n );\n};\n",
15
+ "type": "registry:ui"
16
+ }
17
+ ]
18
+ }
@@ -46,7 +46,7 @@ export const Command: Component<CommandProps> = (props) => {
46
46
  <CommandContext.Provider value={{ search, setSearch }}>
47
47
  <div
48
48
  class={cn(
49
- "flex flex-col w-full h-full rounded-xl bg-popover text-popover-foreground overflow-hidden border border-border shadow-md",
49
+ "flex flex-col w-full h-full rounded-lg bg-popover text-popover-foreground overflow-hidden border border-border shadow-md",
50
50
  local.class
51
51
  )}
52
52
  {...rest}
@@ -94,7 +94,7 @@ export const CommandDialog: Component<CommandDialogProps> = (props) => {
94
94
  <Dialog.Portal>
95
95
  <Dialog.Overlay class="fixed inset-0 z-50 bg-black/60 backdrop-blur-xs data-[expanded]:animate-in data-[closed]:animate-out data-[closed]:fade-out-0 data-[expanded]:fade-in-0" />
96
96
  <div class="fixed inset-0 z-50 flex items-start justify-center pt-16 sm:pt-24 px-4">
97
- <Dialog.Content class="w-full max-w-xl rounded-xl border border-border bg-popover text-popover-foreground shadow-2xl outline-none data-[expanded]:animate-in data-[closed]:animate-out data-[closed]:fade-out-0 data-[expanded]:fade-in-0 data-[closed]:zoom-out-95 data-[expanded]:zoom-in-95">
97
+ <Dialog.Content class="w-full max-w-xl rounded-lg border border-border bg-popover text-popover-foreground shadow-2xl outline-none data-[expanded]:animate-in data-[closed]:animate-out data-[closed]:fade-out-0 data-[expanded]:fade-in-0 data-[closed]:zoom-out-95 data-[expanded]:zoom-in-95">
98
98
  <Command class={props.class}>{props.children}</Command>
99
99
  </Dialog.Content>
100
100
  </div>
@@ -0,0 +1,66 @@
1
+ import { splitProps, Show, type Component, type JSX } from "solid-js";
2
+ import { Progress as KobalteProgress } from "@kobalte/core/progress";
3
+ import { cn } from "@/lib/cn";
4
+
5
+ export interface ProgressProps
6
+ extends Omit<
7
+ JSX.HTMLAttributes<HTMLDivElement>,
8
+ "value" | "aria-valuenow" | "aria-valuemin" | "aria-valuemax"
9
+ > {
10
+ /** The current numeric progress value. */
11
+ value?: number;
12
+ /** Minimum progress value. Defaults to 0. */
13
+ minValue?: number;
14
+ /** Maximum progress value. Defaults to 100. */
15
+ maxValue?: number;
16
+ /** Label text for accessibility and screen readers. */
17
+ getValueLabel?: (params: { value: number; max: number }) => string;
18
+ /** Custom label element to display above or beside progress bar. */
19
+ label?: JSX.Element;
20
+ /** Optional custom class for root container. */
21
+ class?: string;
22
+ /** Optional custom class for indicator fill bar. */
23
+ indicatorClass?: string;
24
+ }
25
+
26
+ /**
27
+ * Nikala UI Progress component for showing task completion status or media timeline positions.
28
+ */
29
+ export const Progress: Component<ProgressProps> = (props) => {
30
+ const [local, rest] = splitProps(props, [
31
+ "value",
32
+ "minValue",
33
+ "maxValue",
34
+ "getValueLabel",
35
+ "label",
36
+ "class",
37
+ "indicatorClass",
38
+ ]);
39
+
40
+ return (
41
+ <KobalteProgress
42
+ value={local.value ?? 0}
43
+ minValue={local.minValue ?? 0}
44
+ maxValue={local.maxValue ?? 100}
45
+ getValueLabel={local.getValueLabel}
46
+ class={cn("flex w-full flex-col gap-1.5", local.class)}
47
+ {...rest}
48
+ >
49
+ <Show when={local.label}>
50
+ <div class="flex justify-between text-xs font-medium text-muted-foreground">
51
+ <KobalteProgress.Label>{local.label}</KobalteProgress.Label>
52
+ <KobalteProgress.ValueLabel class="font-mono text-foreground" />
53
+ </div>
54
+ </Show>
55
+
56
+ <KobalteProgress.Track class="relative h-2 w-full overflow-hidden rounded-full bg-primary/20">
57
+ <KobalteProgress.Fill
58
+ class={cn(
59
+ "h-full w-[var(--kb-progress-fill-width)] bg-primary transition-all duration-300 ease-in-out",
60
+ local.indicatorClass
61
+ )}
62
+ />
63
+ </KobalteProgress.Track>
64
+ </KobalteProgress>
65
+ );
66
+ };
@@ -164,4 +164,9 @@ export const COMPONENT_METADATA: Record<string, ComponentMeta> = {
164
164
  description: "Displays rich content in a portal layer triggered by a button, built on Kobalte primitives.",
165
165
  dependencies: ["clsx", "tailwind-merge", "@kobalte/core", "lucide-solid"],
166
166
  },
167
+ progress: {
168
+ title: "Progress",
169
+ description: "Displays an indicator showing the completion progress of a task or media playback, built on Kobalte primitives.",
170
+ dependencies: ["clsx", "tailwind-merge", "@kobalte/core"],
171
+ },
167
172
  };