@swift-rust/ui 0.2.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/README.md +66 -0
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +89 -41
  4. package/dist/cli.js.map +1 -1
  5. package/dist/cli.test.d.ts +2 -0
  6. package/dist/cli.test.d.ts.map +1 -0
  7. package/dist/cli.test.js +36 -0
  8. package/dist/cli.test.js.map +1 -0
  9. package/dist/components.d.ts.map +1 -1
  10. package/dist/components.js +61 -32
  11. package/dist/components.js.map +1 -1
  12. package/dist/index.d.ts +1 -1
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js.map +1 -1
  15. package/dist/registry.test.d.ts +2 -0
  16. package/dist/registry.test.d.ts.map +1 -0
  17. package/dist/registry.test.js +82 -0
  18. package/dist/registry.test.js.map +1 -0
  19. package/dist/smoke.test.js +5 -3
  20. package/dist/smoke.test.js.map +1 -1
  21. package/package.json +7 -7
  22. package/registry/components/accordion.tsx +125 -16
  23. package/registry/components/alert-dialog.tsx +102 -0
  24. package/registry/components/alert.tsx +114 -14
  25. package/registry/components/aspect-ratio.tsx +18 -0
  26. package/registry/components/avatar.tsx +59 -7
  27. package/registry/components/badge.tsx +29 -14
  28. package/registry/components/breadcrumb.tsx +7 -13
  29. package/registry/components/button-group.tsx +28 -0
  30. package/registry/components/button.tsx +113 -28
  31. package/registry/components/calendar.tsx +92 -0
  32. package/registry/components/callout.tsx +14 -14
  33. package/registry/components/card.tsx +87 -12
  34. package/registry/components/carousel.tsx +41 -0
  35. package/registry/components/chart.tsx +50 -0
  36. package/registry/components/checkbox.tsx +5 -5
  37. package/registry/components/code-block.tsx +118 -0
  38. package/registry/components/code.tsx +2 -3
  39. package/registry/components/collapsible.tsx +60 -0
  40. package/registry/components/combobox.tsx +102 -0
  41. package/registry/components/command.tsx +5 -5
  42. package/registry/components/context-menu.tsx +81 -0
  43. package/registry/components/data-table.tsx +71 -0
  44. package/registry/components/date-picker.tsx +58 -0
  45. package/registry/components/dialog.tsx +2 -2
  46. package/registry/components/direction.tsx +17 -0
  47. package/registry/components/drawer.tsx +77 -0
  48. package/registry/components/dropdown-menu.tsx +5 -5
  49. package/registry/components/empty.tsx +34 -0
  50. package/registry/components/field.tsx +27 -0
  51. package/registry/components/file-upload.tsx +116 -0
  52. package/registry/components/form.tsx +3 -4
  53. package/registry/components/hover-card.tsx +59 -0
  54. package/registry/components/input-group.tsx +34 -0
  55. package/registry/components/input-otp.tsx +50 -0
  56. package/registry/components/input.tsx +71 -7
  57. package/registry/components/item.tsx +42 -0
  58. package/registry/components/kbd.tsx +3 -4
  59. package/registry/components/label.tsx +34 -4
  60. package/registry/components/menubar.tsx +60 -0
  61. package/registry/components/native-select.tsx +35 -0
  62. package/registry/components/navigation-menu.tsx +3 -3
  63. package/registry/components/pagination.tsx +4 -5
  64. package/registry/components/popover.tsx +1 -1
  65. package/registry/components/progress.tsx +10 -5
  66. package/registry/components/radio-group.tsx +9 -9
  67. package/registry/components/resizable.tsx +77 -0
  68. package/registry/components/scroll-area.tsx +20 -0
  69. package/registry/components/select.tsx +2 -3
  70. package/registry/components/separator.tsx +1 -2
  71. package/registry/components/sheet.tsx +1 -1
  72. package/registry/components/sidebar.tsx +72 -0
  73. package/registry/components/skeleton.tsx +1 -6
  74. package/registry/components/slider.tsx +6 -3
  75. package/registry/components/sonner.tsx +52 -0
  76. package/registry/components/spinner.tsx +19 -6
  77. package/registry/components/stepper.tsx +63 -0
  78. package/registry/components/switch.tsx +7 -6
  79. package/registry/components/table.tsx +2 -3
  80. package/registry/components/tabs.tsx +3 -3
  81. package/registry/components/textarea.tsx +42 -6
  82. package/registry/components/toast.tsx +2 -2
  83. package/registry/components/toggle-group.tsx +72 -0
  84. package/registry/components/toggle.tsx +45 -20
  85. package/registry/components/tooltip.tsx +4 -2
@@ -1,15 +1,45 @@
1
- "use client";
2
1
  import * as React from "react";
3
2
  import { cn } from "@/lib/utils";
4
3
 
5
- export type LabelProps = React.LabelHTMLAttributes<HTMLLabelElement>;
4
+ /**
5
+ * swift-rust ui · Label
6
+ *
7
+ * variant — default, secondary (muted), destructive (error)
8
+ * size — xs, sm, default, md, lg
9
+ */
10
+
11
+ export type LabelVariant = "default" | "secondary" | "destructive";
12
+ export type LabelSize = "default" | "xs" | "sm" | "md" | "lg";
13
+
14
+ const VARIANTS: Record<LabelVariant, string> = {
15
+ default: "text-foreground",
16
+ secondary: "text-muted-foreground",
17
+ destructive: "text-destructive",
18
+ };
19
+
20
+ const SIZES: Record<LabelSize, string> = {
21
+ default: "text-sm",
22
+ xs: "text-xs",
23
+ sm: "text-xs",
24
+ md: "text-sm",
25
+ lg: "text-base",
26
+ };
27
+
28
+ export interface LabelProps extends React.LabelHTMLAttributes<HTMLLabelElement> {
29
+ variant?: LabelVariant;
30
+ size?: LabelSize;
31
+ }
6
32
 
7
33
  export const Label = React.forwardRef<HTMLLabelElement, LabelProps>(
8
- ({ className, ...props }, ref) => (
34
+ ({ className, variant = "default", size = "default", ...props }, ref) => (
9
35
  <label
10
36
  ref={ref}
11
37
  className={cn(
12
- "text-sm font-medium leading-none text-[var(--ui-fg)] peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
38
+ "flex select-none items-center gap-2 font-medium leading-none",
39
+ "peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
40
+ "group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50",
41
+ VARIANTS[variant],
42
+ SIZES[size],
13
43
  className,
14
44
  )}
15
45
  {...props}
@@ -0,0 +1,60 @@
1
+ "use client";
2
+ import * as React from "react";
3
+ import { cn } from "@/lib/utils";
4
+
5
+ const MenubarContext = React.createContext<{ openId: string | null; setOpenId: (id: string | null) => void } | null>(null);
6
+
7
+ export function Menubar({ className, children }: { className?: string; children: React.ReactNode }) {
8
+ const [openId, setOpenId] = React.useState<string | null>(null);
9
+ const ref = React.useRef<HTMLDivElement>(null);
10
+ React.useEffect(() => {
11
+ const onClick = (e: MouseEvent) => {
12
+ if (ref.current && !ref.current.contains(e.target as Node)) setOpenId(null);
13
+ };
14
+ document.addEventListener("mousedown", onClick);
15
+ return () => document.removeEventListener("mousedown", onClick);
16
+ }, []);
17
+ return (
18
+ <MenubarContext.Provider value={{ openId, setOpenId }}>
19
+ <div ref={ref} className={cn("flex items-center gap-0.5 rounded-lg border border-border bg-background p-1", className)}>
20
+ {children}
21
+ </div>
22
+ </MenubarContext.Provider>
23
+ );
24
+ }
25
+
26
+ export function MenubarMenu({ value, label, children }: { value: string; label: string; children: React.ReactNode }) {
27
+ const ctx = React.useContext(MenubarContext);
28
+ const open = ctx?.openId === value;
29
+ return (
30
+ <div className="relative">
31
+ <button
32
+ type="button"
33
+ onClick={() => ctx?.setOpenId(open ? null : value)}
34
+ onMouseEnter={() => ctx?.openId && ctx.setOpenId(value)}
35
+ className={cn("rounded-md px-3 py-1.5 text-sm font-medium transition-colors hover:bg-muted", open && "bg-muted")}
36
+ >
37
+ {label}
38
+ </button>
39
+ {open && (
40
+ <div role="menu" className="absolute left-0 z-50 mt-1 min-w-[12rem] rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md">
41
+ {children}
42
+ </div>
43
+ )}
44
+ </div>
45
+ );
46
+ }
47
+
48
+ export function MenubarItem({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
49
+ return (
50
+ <div role="menuitem" className={cn("flex cursor-pointer select-none items-center justify-between gap-2 rounded-sm px-2 py-1.5 text-sm hover:bg-muted", className)} {...props} />
51
+ );
52
+ }
53
+
54
+ export function MenubarSeparator() {
55
+ return <div className="my-1 h-px bg-border" />;
56
+ }
57
+
58
+ export function MenubarShortcut({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) {
59
+ return <span className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)} {...props} />;
60
+ }
@@ -0,0 +1,35 @@
1
+ import * as React from "react";
2
+ import { cn } from "@/lib/utils";
3
+
4
+ // A native <select> with a chevron and the design-system token styling.
5
+ export type NativeSelectProps = React.SelectHTMLAttributes<HTMLSelectElement>;
6
+
7
+ export const NativeSelect = React.forwardRef<HTMLSelectElement, NativeSelectProps>(
8
+ ({ className, children, ...props }, ref) => (
9
+ <div className="relative inline-flex w-full">
10
+ <select
11
+ ref={ref}
12
+ className={cn(
13
+ "h-9 w-full appearance-none rounded-lg border border-input bg-background px-3 pr-8 text-sm shadow-xs",
14
+ "focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring/50",
15
+ "disabled:cursor-not-allowed disabled:opacity-50",
16
+ className,
17
+ )}
18
+ {...props}
19
+ >
20
+ {children}
21
+ </select>
22
+ <svg
23
+ viewBox="0 0 24 24"
24
+ className="pointer-events-none absolute right-2.5 top-1/2 size-4 -translate-y-1/2 text-muted-foreground"
25
+ fill="none"
26
+ stroke="currentColor"
27
+ strokeWidth="2"
28
+ aria-hidden
29
+ >
30
+ <path d="m6 9 6 6 6-6" strokeLinecap="round" strokeLinejoin="round" />
31
+ </svg>
32
+ </div>
33
+ ),
34
+ );
35
+ NativeSelect.displayName = "NativeSelect";
@@ -34,7 +34,7 @@ export function NavigationMenuTrigger({ children }: { children: React.ReactNode
34
34
  return (
35
35
  <button
36
36
  type="button"
37
- className="inline-flex h-9 items-center gap-1 rounded-md px-3 py-1 text-sm hover:bg-[var(--ui-surface-2)]"
37
+ className="inline-flex h-9 items-center gap-1 rounded-md px-3 py-1 text-sm hover:bg-muted"
38
38
  >
39
39
  {children}
40
40
  <svg viewBox="0 0 24 24" className="h-3 w-3" fill="none" stroke="currentColor" strokeWidth="2">
@@ -50,7 +50,7 @@ export function NavigationMenuContent({ value, className, children }: { value: s
50
50
  return (
51
51
  <div
52
52
  className={cn(
53
- "absolute left-0 top-full mt-1 min-w-[200px] rounded-md border border-[var(--ui-border)] bg-[var(--ui-surface)] p-2 shadow-md",
53
+ "absolute left-0 top-full mt-1 min-w-[200px] rounded-md border border-border bg-popover p-2 shadow-md",
54
54
  className,
55
55
  )}
56
56
  >
@@ -64,7 +64,7 @@ export function NavigationMenuLink({ href, children, className }: { href: string
64
64
  <a
65
65
  href={href}
66
66
  className={cn(
67
- "block rounded-sm px-2 py-1.5 text-sm text-[var(--ui-fg-muted)] hover:bg-[var(--ui-surface-2)] hover:text-[var(--ui-fg)]",
67
+ "block rounded-sm px-2 py-1.5 text-sm text-muted-foreground hover:bg-muted hover:text-foreground",
68
68
  className,
69
69
  )}
70
70
  >
@@ -1,4 +1,3 @@
1
- "use client";
2
1
  import * as React from "react";
3
2
  import { cn } from "@/lib/utils";
4
3
 
@@ -22,13 +21,13 @@ export function Pagination({ page, total, onChange, className }: PaginationProps
22
21
  type="button"
23
22
  onClick={() => onChange(Math.max(1, page - 1))}
24
23
  disabled={page === 1}
25
- className="inline-flex h-8 items-center gap-1 rounded-md px-2 text-sm hover:bg-[var(--ui-surface-2)] disabled:opacity-50"
24
+ className="inline-flex h-8 items-center gap-1 rounded-md px-2 text-sm hover:bg-muted disabled:opacity-50"
26
25
  >
27
26
 
28
27
  </button>
29
28
  {pages.map((p, i) =>
30
29
  p === "ellipsis" ? (
31
- <span key={`e${i}`} className="px-2 text-[var(--ui-fg-subtle)]">…</span>
30
+ <span key={`e${i}`} className="px-2 text-muted-foreground">…</span>
32
31
  ) : (
33
32
  <button
34
33
  key={p}
@@ -36,7 +35,7 @@ export function Pagination({ page, total, onChange, className }: PaginationProps
36
35
  onClick={() => onChange(p)}
37
36
  className={cn(
38
37
  "h-8 w-8 rounded-md text-sm transition-colors",
39
- p === page ? "bg-[var(--ui-fg)] text-[var(--ui-bg)]" : "hover:bg-[var(--ui-surface-2)]",
38
+ p === page ? "bg-primary text-primary-foreground" : "hover:bg-muted",
40
39
  )}
41
40
  >
42
41
  {p}
@@ -47,7 +46,7 @@ export function Pagination({ page, total, onChange, className }: PaginationProps
47
46
  type="button"
48
47
  onClick={() => onChange(Math.min(total, page + 1))}
49
48
  disabled={page === total}
50
- className="inline-flex h-8 items-center gap-1 rounded-md px-2 text-sm hover:bg-[var(--ui-surface-2)] disabled:opacity-50"
49
+ className="inline-flex h-8 items-center gap-1 rounded-md px-2 text-sm hover:bg-muted disabled:opacity-50"
51
50
  >
52
51
 
53
52
  </button>
@@ -46,7 +46,7 @@ export function PopoverContent({
46
46
  return (
47
47
  <div
48
48
  className={cn(
49
- "absolute z-50 mt-1 w-72 rounded-md border border-[var(--ui-border)] bg-[var(--ui-surface)] p-4 text-sm shadow-md",
49
+ "absolute z-50 mt-1 w-72 rounded-md border border-border bg-popover p-4 text-sm shadow-md",
50
50
  align === "end" && "right-0",
51
51
  align === "center" && "left-1/2 -translate-x-1/2",
52
52
  className,
@@ -1,20 +1,25 @@
1
- "use client";
2
1
  import * as React from "react";
3
2
  import { cn } from "@/lib/utils";
4
3
 
5
- export const Progress = React.forwardRef<HTMLDivElement, { value: number; max?: number; className?: string }>(
4
+ export interface ProgressProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "children"> {
5
+ value: number;
6
+ max?: number;
7
+ }
8
+
9
+ export const Progress = React.forwardRef<HTMLDivElement, ProgressProps>(
6
10
  ({ className, value, max = 100, ...props }, ref) => (
7
11
  <div
8
12
  ref={ref}
9
13
  role="progressbar"
10
14
  aria-valuenow={value}
15
+ aria-valuemin={0}
11
16
  aria-valuemax={max}
12
- className={cn("relative h-2 w-full overflow-hidden rounded-full bg-[var(--ui-surface-2)]", className)}
17
+ className={cn("relative h-2 w-full overflow-hidden rounded-full bg-secondary", className)}
13
18
  {...props}
14
19
  >
15
20
  <div
16
- className="h-full bg-[var(--ui-accent)] transition-all"
17
- style={{ width: `${Math.min(100, (value / max) * 100)}%` }}
21
+ className="h-full rounded-full bg-primary transition-all"
22
+ style={{ width: `${Math.min(100, Math.max(0, (value / max) * 100))}%` }}
18
23
  />
19
24
  </div>
20
25
  ),
@@ -31,7 +31,7 @@ export function RadioGroup({
31
31
  };
32
32
  return (
33
33
  <RadioGroupContext.Provider value={{ value, setValue, name }}>
34
- <div role="radiogroup" className={cn("space-y-1", className)}>
34
+ <div role="radiogroup" className={cn("grid gap-1", className)}>
35
35
  {children}
36
36
  </div>
37
37
  </RadioGroupContext.Provider>
@@ -47,29 +47,29 @@ export function RadioGroupItem({
47
47
  label?: React.ReactNode;
48
48
  className?: string;
49
49
  }) {
50
- const ctx = React.useContext(RadioGroupContext)!;
51
- const active = ctx.value === value;
50
+ const ctx = React.useContext(RadioGroupContext);
51
+ const active = ctx?.value === value;
52
52
  return (
53
53
  <label
54
54
  className={cn(
55
- "flex cursor-pointer items-center gap-2 rounded-md p-2 text-sm transition-colors hover:bg-[var(--ui-surface-2)]",
55
+ "flex cursor-pointer items-center gap-2 rounded-md p-2 text-sm transition-colors hover:bg-muted",
56
56
  className,
57
57
  )}
58
58
  >
59
59
  <span
60
60
  className={cn(
61
- "flex h-4 w-4 items-center justify-center rounded-full border",
62
- active ? "border-[var(--ui-accent)]" : "border-[var(--ui-border-strong)]",
61
+ "flex size-4 items-center justify-center rounded-full border transition-colors",
62
+ active ? "border-primary" : "border-input",
63
63
  )}
64
64
  >
65
- {active ? <span className="h-2 w-2 rounded-full bg-[var(--ui-accent)]" /> : null}
65
+ {active ? <span className="size-2 rounded-full bg-primary" /> : null}
66
66
  </span>
67
67
  <input
68
68
  type="radio"
69
- name={ctx.name}
69
+ name={ctx?.name}
70
70
  value={value}
71
71
  checked={active}
72
- onChange={() => ctx.setValue(value)}
72
+ onChange={() => ctx?.setValue(value)}
73
73
  className="sr-only"
74
74
  />
75
75
  {label}
@@ -0,0 +1,77 @@
1
+ "use client";
2
+ import * as React from "react";
3
+ import { cn } from "@/lib/utils";
4
+
5
+ const ResizableContext = React.createContext<{ pct: number; setPct: (n: number) => void } | null>(null);
6
+
7
+ export function ResizablePanelGroup({
8
+ defaultSize = 50,
9
+ className,
10
+ children,
11
+ }: {
12
+ defaultSize?: number;
13
+ className?: string;
14
+ children: React.ReactNode;
15
+ }) {
16
+ const [pct, setPct] = React.useState(defaultSize);
17
+ const ref = React.useRef<HTMLDivElement>(null);
18
+
19
+ const onPointerDown = (e: React.PointerEvent) => {
20
+ e.preventDefault();
21
+ const el = ref.current;
22
+ if (!el) return;
23
+ const move = (ev: PointerEvent) => {
24
+ const rect = el.getBoundingClientRect();
25
+ const next = ((ev.clientX - rect.left) / rect.width) * 100;
26
+ setPct(Math.min(85, Math.max(15, next)));
27
+ };
28
+ const up = () => {
29
+ document.removeEventListener("pointermove", move);
30
+ document.removeEventListener("pointerup", up);
31
+ };
32
+ document.addEventListener("pointermove", move);
33
+ document.addEventListener("pointerup", up);
34
+ };
35
+
36
+ return (
37
+ <ResizableContext.Provider value={{ pct, setPct }}>
38
+ <div ref={ref} className={cn("flex w-full overflow-hidden rounded-lg border border-border", className)}>
39
+ {React.Children.map(children, (child) => {
40
+ if (React.isValidElement(child) && (child.type as { __srHandle?: boolean }).__srHandle) {
41
+ return React.cloneElement(child as React.ReactElement<{ onPointerDown?: (e: React.PointerEvent) => void }>, { onPointerDown });
42
+ }
43
+ return child;
44
+ })}
45
+ </div>
46
+ </ResizableContext.Provider>
47
+ );
48
+ }
49
+
50
+ export function ResizablePanel({
51
+ index = 0,
52
+ className,
53
+ children,
54
+ }: {
55
+ index?: number;
56
+ className?: string;
57
+ children: React.ReactNode;
58
+ }) {
59
+ const ctx = React.useContext(ResizableContext);
60
+ const pct = ctx?.pct ?? 50;
61
+ return (
62
+ <div className={cn("min-w-0 overflow-auto p-4", className)} style={{ flexBasis: `${index === 0 ? pct : 100 - pct}%` }}>
63
+ {children}
64
+ </div>
65
+ );
66
+ }
67
+
68
+ export function ResizableHandle({ onPointerDown }: { onPointerDown?: (e: React.PointerEvent) => void }) {
69
+ return (
70
+ <div
71
+ role="separator"
72
+ onPointerDown={onPointerDown}
73
+ className="relative w-px shrink-0 cursor-col-resize bg-border after:absolute after:inset-y-0 after:-left-1 after:w-3 hover:bg-primary"
74
+ />
75
+ );
76
+ }
77
+ ResizableHandle.__srHandle = true;
@@ -0,0 +1,20 @@
1
+ import * as React from "react";
2
+ import { cn } from "@/lib/utils";
3
+
4
+ export const ScrollArea = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
5
+ ({ className, ...props }, ref) => (
6
+ <div
7
+ ref={ref}
8
+ className={cn(
9
+ "relative overflow-auto",
10
+ "[scrollbar-width:thin] [scrollbar-color:var(--color-border-strong)_transparent]",
11
+ "[&::-webkit-scrollbar]:w-2 [&::-webkit-scrollbar]:h-2",
12
+ "[&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-border-strong",
13
+ "[&::-webkit-scrollbar-track]:bg-transparent",
14
+ className,
15
+ )}
16
+ {...props}
17
+ />
18
+ ),
19
+ );
20
+ ScrollArea.displayName = "ScrollArea";
@@ -1,4 +1,3 @@
1
- "use client";
2
1
  import * as React from "react";
3
2
  import { cn } from "@/lib/utils";
4
3
 
@@ -7,8 +6,8 @@ export const Select = React.forwardRef<HTMLSelectElement, React.SelectHTMLAttrib
7
6
  <select
8
7
  ref={ref}
9
8
  className={cn(
10
- "flex h-9 w-full rounded-md border border-[var(--ui-border)] bg-[var(--ui-surface)] px-3 py-1 text-sm shadow-sm",
11
- "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--ui-accent)] focus-visible:ring-offset-1",
9
+ "flex h-9 w-full rounded-md border border-border bg-popover px-3 py-1 text-sm shadow-sm",
10
+ "focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1",
12
11
  "disabled:cursor-not-allowed disabled:opacity-50",
13
12
  className,
14
13
  )}
@@ -1,4 +1,3 @@
1
- "use client";
2
1
  import * as React from "react";
3
2
  import { cn } from "@/lib/utils";
4
3
 
@@ -12,7 +11,7 @@ export const Separator = React.forwardRef<HTMLDivElement, SeparatorProps>(
12
11
  ref={ref}
13
12
  role="separator"
14
13
  className={cn(
15
- "shrink-0 bg-[var(--ui-border)]",
14
+ "shrink-0 bg-border",
16
15
  orientation === "horizontal" ? "h-px w-full" : "h-full w-px",
17
16
  className,
18
17
  )}
@@ -36,7 +36,7 @@ export function Sheet({
36
36
  <div
37
37
  role="dialog"
38
38
  className={cn(
39
- "fixed z-50 bg-[var(--ui-surface)] p-6 shadow-lg transition-transform",
39
+ "fixed z-50 bg-background p-6 shadow-lg transition-transform",
40
40
  sideClass,
41
41
  )}
42
42
  >
@@ -0,0 +1,72 @@
1
+ "use client";
2
+ import * as React from "react";
3
+ import { cn } from "@/lib/utils";
4
+
5
+ const SidebarContext = React.createContext<{ open: boolean; toggle: () => void } | null>(null);
6
+
7
+ export function SidebarProvider({ defaultOpen = true, children }: { defaultOpen?: boolean; children: React.ReactNode }) {
8
+ const [open, setOpen] = React.useState(defaultOpen);
9
+ return (
10
+ <SidebarContext.Provider value={{ open, toggle: () => setOpen((o) => !o) }}>
11
+ <div className="flex w-full">{children}</div>
12
+ </SidebarContext.Provider>
13
+ );
14
+ }
15
+
16
+ export function Sidebar({ className, children }: { className?: string; children: React.ReactNode }) {
17
+ const ctx = React.useContext(SidebarContext);
18
+ return (
19
+ <aside
20
+ data-state={ctx?.open ? "open" : "collapsed"}
21
+ className={cn(
22
+ "flex shrink-0 flex-col gap-2 overflow-hidden border-r border-border bg-card p-2 text-card-foreground transition-[width] duration-200",
23
+ ctx?.open ? "w-60" : "w-0 border-r-0 p-0",
24
+ className,
25
+ )}
26
+ >
27
+ {children}
28
+ </aside>
29
+ );
30
+ }
31
+
32
+ export function SidebarHeader({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
33
+ return <div className={cn("flex items-center gap-2 px-2 py-1.5", className)} {...props} />;
34
+ }
35
+ export function SidebarContent({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
36
+ return <div className={cn("flex flex-1 flex-col gap-1 overflow-y-auto", className)} {...props} />;
37
+ }
38
+ export function SidebarGroupLabel({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
39
+ return <div className={cn("px-2 pt-3 text-[0.7rem] font-semibold uppercase tracking-widest text-muted-foreground", className)} {...props} />;
40
+ }
41
+ export function SidebarItem({
42
+ active,
43
+ className,
44
+ ...props
45
+ }: React.HTMLAttributes<HTMLButtonElement> & { active?: boolean }) {
46
+ return (
47
+ <button
48
+ type="button"
49
+ data-active={active}
50
+ className={cn(
51
+ "flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm transition-colors [&_svg]:size-4",
52
+ active ? "bg-muted font-medium text-foreground" : "text-muted-foreground hover:bg-muted hover:text-foreground",
53
+ className,
54
+ )}
55
+ {...props}
56
+ />
57
+ );
58
+ }
59
+ export function SidebarTrigger({ className, ...props }: React.ButtonHTMLAttributes<HTMLButtonElement>) {
60
+ const ctx = React.useContext(SidebarContext);
61
+ return (
62
+ <button
63
+ type="button"
64
+ aria-label="Toggle sidebar"
65
+ onClick={() => ctx?.toggle()}
66
+ className={cn("inline-flex size-9 items-center justify-center rounded-lg border border-border hover:bg-muted", className)}
67
+ {...props}
68
+ >
69
+ <svg viewBox="0 0 24 24" className="size-4" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden><rect x="3" y="3" width="18" height="18" rx="2" /><path d="M9 3v18" /></svg>
70
+ </button>
71
+ );
72
+ }
@@ -1,14 +1,9 @@
1
- "use client";
2
1
  import * as React from "react";
3
2
  import { cn } from "@/lib/utils";
4
3
 
5
4
  export const Skeleton = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
6
5
  ({ className, ...props }, ref) => (
7
- <div
8
- ref={ref}
9
- className={cn("animate-pulse rounded-md bg-[var(--ui-surface-2)]", className)}
10
- {...props}
11
- />
6
+ <div ref={ref} className={cn("animate-pulse rounded-md bg-muted", className)} {...props} />
12
7
  ),
13
8
  );
14
9
  Skeleton.displayName = "Skeleton";
@@ -1,14 +1,17 @@
1
- "use client";
2
1
  import * as React from "react";
3
2
  import { cn } from "@/lib/utils";
4
3
 
5
- export const Slider = React.forwardRef<HTMLInputElement, React.InputHTMLAttributes<HTMLInputElement>>(
4
+ export type SliderProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, "type">;
5
+
6
+ export const Slider = React.forwardRef<HTMLInputElement, SliderProps>(
6
7
  ({ className, ...props }, ref) => (
7
8
  <input
8
9
  type="range"
9
10
  ref={ref}
10
11
  className={cn(
11
- "h-1.5 w-full cursor-pointer appearance-none rounded-full bg-[var(--ui-surface-2)] accent-[var(--ui-accent)]",
12
+ "h-1.5 w-full cursor-pointer appearance-none rounded-full bg-secondary accent-primary",
13
+ "focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring/50",
14
+ "disabled:cursor-not-allowed disabled:opacity-50",
12
15
  className,
13
16
  )}
14
17
  {...props}
@@ -0,0 +1,52 @@
1
+ "use client";
2
+ import * as React from "react";
3
+ import { cn } from "@/lib/utils";
4
+
5
+ export interface SonnerToast {
6
+ id: number;
7
+ title: string;
8
+ description?: string;
9
+ tone?: "default" | "success" | "error";
10
+ }
11
+
12
+ const listeners = new Set<(t: SonnerToast) => void>();
13
+ let counter = 0;
14
+
15
+ export function toast(title: string, opts?: { description?: string; tone?: SonnerToast["tone"] }) {
16
+ const t: SonnerToast = { id: ++counter, title, description: opts?.description, tone: opts?.tone ?? "default" };
17
+ for (const l of listeners) l(t);
18
+ }
19
+
20
+ const TONES: Record<NonNullable<SonnerToast["tone"]>, string> = {
21
+ default: "border-border bg-popover text-popover-foreground",
22
+ success: "border-emerald-600/40 bg-popover text-popover-foreground",
23
+ error: "border-destructive/40 bg-popover text-popover-foreground",
24
+ };
25
+
26
+ export function Toaster({ duration = 3500 }: { duration?: number }) {
27
+ const [toasts, setToasts] = React.useState<SonnerToast[]>([]);
28
+ React.useEffect(() => {
29
+ const add = (t: SonnerToast) => {
30
+ setToasts((prev) => [...prev, t]);
31
+ setTimeout(() => setToasts((prev) => prev.filter((x) => x.id !== t.id)), duration);
32
+ };
33
+ listeners.add(add);
34
+ return () => {
35
+ listeners.delete(add);
36
+ };
37
+ }, [duration]);
38
+
39
+ return (
40
+ <div className="pointer-events-none fixed bottom-4 right-4 z-[100] flex w-80 flex-col gap-2">
41
+ {toasts.map((t) => (
42
+ <div
43
+ key={t.id}
44
+ className={cn("pointer-events-auto rounded-lg border p-4 shadow-lg", TONES[t.tone ?? "default"])}
45
+ >
46
+ <p className="text-sm font-medium">{t.title}</p>
47
+ {t.description ? <p className="mt-1 text-sm text-muted-foreground">{t.description}</p> : null}
48
+ </div>
49
+ ))}
50
+ </div>
51
+ );
52
+ }
@@ -1,15 +1,28 @@
1
- "use client";
2
1
  import * as React from "react";
3
2
  import { cn } from "@/lib/utils";
4
3
 
5
- export const Spinner = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement> & { size?: number }>(
6
- ({ className, size = 16, ...props }, ref) => (
7
- <div
4
+ export type SpinnerSize = "sm" | "default" | "lg";
5
+ const SIZES: Record<SpinnerSize, string> = {
6
+ sm: "size-4 border-2",
7
+ default: "size-5 border-2",
8
+ lg: "size-8 border-[3px]",
9
+ };
10
+
11
+ export interface SpinnerProps extends React.HTMLAttributes<HTMLSpanElement> {
12
+ size?: SpinnerSize;
13
+ }
14
+
15
+ export const Spinner = React.forwardRef<HTMLSpanElement, SpinnerProps>(
16
+ ({ className, size = "default", ...props }, ref) => (
17
+ <span
8
18
  ref={ref}
9
19
  role="status"
10
20
  aria-label="Loading"
11
- className={cn("inline-block animate-spin rounded-full border-2 border-current border-t-transparent", className)}
12
- style={{ width: size, height: size }}
21
+ className={cn(
22
+ "inline-block animate-spin rounded-full border-current border-t-transparent text-muted-foreground",
23
+ SIZES[size],
24
+ className,
25
+ )}
13
26
  {...props}
14
27
  />
15
28
  ),