@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
@@ -0,0 +1,82 @@
1
+ import { readFileSync, readdirSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { describe, expect, test } from "bun:test";
4
+ const REGISTRY = join(import.meta.dir, "..", "registry", "components");
5
+ // The style dimension ("design" prop — React reserves `style`) shipped first on
6
+ // these components; each must offer the full design set.
7
+ const DESIGN_COMPONENTS = ["accordion", "alert", "avatar", "button", "card", "input"];
8
+ const DESIGNS = ["flat", "soft", "3d", "glass", "neo", "brutal", "gradient"];
9
+ // Purely presentational components must NOT carry 'use client'. Under swift-rust's
10
+ // SSR + islands model, a 'use client' component used directly in a server page is
11
+ // wrapped as an island and its children don't cross the boundary — hydration then
12
+ // wipes the children (an Alert rendered empty). Static SSR keeps children and ships
13
+ // zero JS. Only genuinely stateful components (accordion: useState/context) opt in.
14
+ const PRESENTATIONAL = ["button", "card", "input", "label", "avatar", "alert"];
15
+ const STATEFUL = ["accordion"];
16
+ describe("registry components", () => {
17
+ const files = readdirSync(REGISTRY).filter((f) => f.endsWith(".tsx"));
18
+ const firstLine = (slug) => readFileSync(join(REGISTRY, `${slug}.tsx`), "utf8").split("\n")[0]?.trim();
19
+ test("presentational components are server-renderable (no 'use client')", () => {
20
+ for (const slug of PRESENTATIONAL) {
21
+ expect({ slug, directive: firstLine(slug) }).not.toEqual({
22
+ slug,
23
+ directive: '"use client";',
24
+ });
25
+ }
26
+ });
27
+ test("stateful components declare 'use client'", () => {
28
+ for (const slug of STATEFUL) {
29
+ expect(firstLine(slug)).toBe('"use client";');
30
+ }
31
+ });
32
+ test("every component transpiles as TSX", () => {
33
+ const transpiler = new Bun.Transpiler({ loader: "tsx" });
34
+ for (const file of files) {
35
+ const src = readFileSync(join(REGISTRY, file), "utf8");
36
+ expect(() => transpiler.transformSync(src)).not.toThrow();
37
+ }
38
+ });
39
+ test("no Tailwind v3-only classnames (renamed or removed in v4)", () => {
40
+ // Utilities that v4 removed or renamed; using them warns or silently breaks.
41
+ const banned = [
42
+ /\boutline-none\b/, // → outline-hidden (v4's outline-none means something else)
43
+ /\bbg-gradient-to-/, // → bg-linear-to-*
44
+ /\bflex-shrink-/, // → shrink-*
45
+ /\bflex-grow\b/, // → grow
46
+ /\b(?:bg|text|border|ring|divide|placeholder)-opacity-\d/, // → color/NN syntax
47
+ /\boverflow-ellipsis\b/, // → text-ellipsis
48
+ /\bdecoration-(?:slice|clone)\b/, // → box-decoration-*
49
+ ];
50
+ for (const file of files) {
51
+ const src = readFileSync(join(REGISTRY, file), "utf8");
52
+ for (const re of banned) {
53
+ expect({ file, matches: re.test(src) ? re.source : null }).toEqual({
54
+ file,
55
+ matches: null,
56
+ });
57
+ }
58
+ }
59
+ });
60
+ test("design-dimension components expose every design style", () => {
61
+ for (const name of DESIGN_COMPONENTS) {
62
+ const src = readFileSync(join(REGISTRY, `${name}.tsx`), "utf8");
63
+ for (const design of DESIGNS) {
64
+ expect({ name, design, present: src.includes(`"${design}"`) }).toEqual({
65
+ name,
66
+ design,
67
+ present: true,
68
+ });
69
+ }
70
+ }
71
+ });
72
+ test("button exposes the full variant and size matrix", () => {
73
+ const src = readFileSync(join(REGISTRY, "button.tsx"), "utf8");
74
+ for (const v of ["default", "outline", "secondary", "ghost", "destructive", "link"]) {
75
+ expect(src).toContain(`${v}:`);
76
+ }
77
+ for (const s of ["xs", "sm", "md", "lg", "icon", "icon-xs", "icon-sm", "icon-md", "icon-lg"]) {
78
+ expect(src).toContain(`"${s}"`);
79
+ }
80
+ });
81
+ });
82
+ //# sourceMappingURL=registry.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.test.js","sourceRoot":"","sources":["../src/registry.test.ts"],"names":[],"mappings":"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"}
@@ -6,13 +6,15 @@ test("package exports core API", () => {
6
6
  expect(typeof mod.add).toBe("function");
7
7
  expect(typeof mod.list).toBe("function");
8
8
  });
9
- test("COMPONENTS map lists all 35 components", () => {
9
+ test("COMPONENTS map lists the full registry", () => {
10
10
  const components = mod.COMPONENTS;
11
- expect(Object.keys(components).length).toBe(35);
11
+ expect(Object.keys(components).length).toBeGreaterThanOrEqual(60);
12
12
  const button = components.button;
13
13
  expect(button).toBeDefined();
14
14
  expect(button?.files).toContain("button.tsx");
15
15
  expect(components["dropdown-menu"]).toBeDefined();
16
- expect(components["navigation-menu"]).toBeDefined();
16
+ expect(components["data-table"]).toBeDefined();
17
+ expect(components["code-block"]).toBeDefined();
18
+ expect(components.stepper).toBeDefined();
17
19
  });
18
20
  //# sourceMappingURL=smoke.test.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"smoke.test.js","sourceRoot":"","sources":["../src/smoke.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,UAAU,CAAC;AACxC,OAAO,KAAK,GAAG,MAAM,SAAS,CAAC;AAE/B,IAAI,CAAC,0BAA0B,EAAE,GAAG,EAAE;IACpC,MAAM,CAAC,GAAG,CAAC,CAAC,WAAW,EAAE,CAAC;IAC1B,MAAM,CAAC,OAAO,GAAG,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IACzC,MAAM,CAAC,OAAO,GAAG,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IACxC,MAAM,CAAC,OAAO,GAAG,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;AAC3C,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,wCAAwC,EAAE,GAAG,EAAE;IAClD,MAAM,UAAU,GAAG,GAAG,CAAC,UAAiD,CAAC;IACzE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAChD,MAAM,MAAM,GAAG,UAAU,CAAC,MAAM,CAAC;IACjC,MAAM,CAAC,MAAM,CAAC,CAAC,WAAW,EAAE,CAAC;IAC7B,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,SAAS,CAAC,YAAY,CAAC,CAAC;IAC9C,MAAM,CAAC,UAAU,CAAC,eAAe,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;IAClD,MAAM,CAAC,UAAU,CAAC,iBAAiB,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;AACtD,CAAC,CAAC,CAAC"}
1
+ {"version":3,"file":"smoke.test.js","sourceRoot":"","sources":["../src/smoke.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,UAAU,CAAC;AACxC,OAAO,KAAK,GAAG,MAAM,SAAS,CAAC;AAE/B,IAAI,CAAC,0BAA0B,EAAE,GAAG,EAAE;IACpC,MAAM,CAAC,GAAG,CAAC,CAAC,WAAW,EAAE,CAAC;IAC1B,MAAM,CAAC,OAAO,GAAG,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IACzC,MAAM,CAAC,OAAO,GAAG,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IACxC,MAAM,CAAC,OAAO,GAAG,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;AAC3C,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,wCAAwC,EAAE,GAAG,EAAE;IAClD,MAAM,UAAU,GAAG,GAAG,CAAC,UAAiD,CAAC;IACzE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,MAAM,CAAC,CAAC,sBAAsB,CAAC,EAAE,CAAC,CAAC;IAClE,MAAM,MAAM,GAAG,UAAU,CAAC,MAAM,CAAC;IACjC,MAAM,CAAC,MAAM,CAAC,CAAC,WAAW,EAAE,CAAC;IAC7B,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,SAAS,CAAC,YAAY,CAAC,CAAC;IAC9C,MAAM,CAAC,UAAU,CAAC,eAAe,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;IAClD,MAAM,CAAC,UAAU,CAAC,YAAY,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;IAC/C,MAAM,CAAC,UAAU,CAAC,YAAY,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;IAC/C,MAAM,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,WAAW,EAAE,CAAC;AAC3C,CAAC,CAAC,CAAC"}
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@swift-rust/ui",
3
- "version": "0.2.0",
4
- "description": "shadcn-style component registry for swift-rust.",
3
+ "version": "0.6.0",
4
+ "description": "swift-rust ui — a shadcn-style component registry with a third style dimension (variant × size × design) built for Tailwind v4.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://swift-rust.dev",
7
7
  "repository": {
8
8
  "type": "git",
9
- "url": "https://github.com/swift-rust/swift-rust.git",
9
+ "url": "https://github.com/colesites/swift-rust.git",
10
10
  "directory": "packages/ui"
11
11
  },
12
12
  "type": "module",
@@ -36,19 +36,19 @@
36
36
  "clean": "rm -rf dist .turbo"
37
37
  },
38
38
  "dependencies": {
39
- "@clack/prompts": "^0.7.0",
39
+ "@clack/prompts": "^1.5.1",
40
40
  "picocolors": "^1.0.1"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/bun": "^1.3.0",
44
- "clsx": "^2.1.0",
45
- "tailwind-merge": "^2.5.0",
44
+ "clsx": "^2.1.1",
45
+ "tailwind-merge": "^3.6.0",
46
46
  "typescript": "^6.0.0"
47
47
  },
48
48
  "publishConfig": {
49
49
  "access": "public"
50
50
  },
51
51
  "bugs": {
52
- "url": "https://github.com/swift-rust/swift-rust/issues"
52
+ "url": "https://github.com/colesites/swift-rust/issues"
53
53
  }
54
54
  }
@@ -2,23 +2,101 @@
2
2
  import * as React from "react";
3
3
  import { cn } from "@/lib/utils";
4
4
 
5
- export const AccordionContext = React.createContext<{
5
+ /**
6
+ * swift-rust ui · Accordion
7
+ *
8
+ * variant — default (divided), outline (separated items), secondary, ghost
9
+ * size — sm, default, lg
10
+ * design — flat, soft, 3d, glass, neo, brutal, gradient
11
+ *
12
+ * variant/size/design are set once on <Accordion> and flow to items via context.
13
+ */
14
+
15
+ export type AccordionVariant = "default" | "outline" | "secondary" | "ghost";
16
+ export type AccordionSize = "default" | "sm" | "lg";
17
+ export type AccordionDesign = "flat" | "soft" | "3d" | "glass" | "neo" | "brutal" | "gradient";
18
+
19
+ // Item chrome. The default variant is a classic divided list; the others render
20
+ // separated, surfaced items so designs (glass, brutal, …) have a card to paint.
21
+ const ITEM_VARIANTS: Record<AccordionVariant, string> = {
22
+ default: "border-b border-border last:border-b-0",
23
+ outline: "mb-2 rounded-lg border border-border px-4 last:mb-0",
24
+ secondary: "mb-2 rounded-lg bg-secondary px-4 text-secondary-foreground last:mb-0",
25
+ ghost: "rounded-lg px-2 hover:bg-secondary/50",
26
+ };
27
+
28
+ const TRIGGER_SIZES: Record<AccordionSize, string> = {
29
+ sm: "py-3 text-xs",
30
+ default: "py-4 text-sm",
31
+ lg: "py-5 text-base",
32
+ };
33
+
34
+ const CONTENT_SIZES: Record<AccordionSize, string> = {
35
+ sm: "pb-3 text-xs",
36
+ default: "pb-4 text-sm",
37
+ lg: "pb-5 text-base",
38
+ };
39
+
40
+ // Designs only decorate non-default variants' item surfaces (the divided list
41
+ // has no surface to paint).
42
+ const ITEM_DESIGNS: Record<AccordionDesign, string> = {
43
+ flat: "",
44
+ soft: "rounded-xl border-transparent bg-muted",
45
+ // Depth via a darker bottom lip + a top sheen — no drop shadow.
46
+ "3d": "border-transparent border-b-4 border-b-black/15 bg-linear-to-b from-white/20 to-transparent dark:border-b-black/40",
47
+ // Liquid glass panels.
48
+ glass:
49
+ "border-white/40 bg-white/15 backdrop-blur-xl backdrop-saturate-200 " +
50
+ "bg-linear-to-br from-white/30 via-white/10 to-white/5 " +
51
+ "dark:border-white/20 dark:bg-white/10 dark:from-white/15 dark:via-white/5 dark:to-transparent",
52
+ neo:
53
+ "border-transparent bg-background " +
54
+ "shadow-[5px_5px_10px_rgba(0,0,0,0.12),-5px_-5px_10px_rgba(255,255,255,0.8)] " +
55
+ "dark:shadow-[5px_5px_10px_rgba(0,0,0,0.6),-5px_-5px_10px_rgba(255,255,255,0.05)]",
56
+ brutal:
57
+ "rounded-none border-2 border-foreground shadow-[4px_4px_0_0_var(--color-foreground)]",
58
+ gradient:
59
+ "border-2 border-transparent " +
60
+ "[background:linear-gradient(var(--color-background),var(--color-background))_padding-box," +
61
+ "linear-gradient(135deg,#8b5cf6,#d946ef,#fb923c)_border-box]",
62
+ };
63
+
64
+ interface AccordionContextValue {
6
65
  openItems: Set<string>;
7
66
  toggle: (value: string) => void;
8
67
  type: "single" | "multiple";
9
- } | null>(null);
68
+ variant: AccordionVariant;
69
+ size: AccordionSize;
70
+ design: AccordionDesign;
71
+ }
72
+
73
+ export const AccordionContext = React.createContext<AccordionContextValue | null>(null);
74
+
75
+ function useAccordion(): AccordionContextValue {
76
+ const ctx = React.useContext(AccordionContext);
77
+ if (!ctx) throw new Error("Accordion components must be used inside <Accordion>");
78
+ return ctx;
79
+ }
80
+
81
+ export interface AccordionProps {
82
+ type?: "single" | "multiple";
83
+ defaultValue?: string | string[];
84
+ variant?: AccordionVariant;
85
+ size?: AccordionSize;
86
+ design?: AccordionDesign;
87
+ className?: string;
88
+ children: React.ReactNode;
89
+ }
10
90
 
11
91
  export function Accordion({
12
92
  type = "single",
13
93
  defaultValue,
94
+ variant = "default",
95
+ size = "default",
96
+ design = "flat",
14
97
  className,
15
98
  children,
16
- }: {
17
- type?: "single" | "multiple";
18
- defaultValue?: string | string[];
19
- className?: string;
20
- children: React.ReactNode;
21
- }) {
99
+ }: AccordionProps) {
22
100
  const [openItems, setOpenItems] = React.useState<Set<string>>(
23
101
  () => new Set(Array.isArray(defaultValue) ? defaultValue : defaultValue ? [defaultValue] : []),
24
102
  );
@@ -34,7 +112,7 @@ export function Accordion({
34
112
  });
35
113
  };
36
114
  return (
37
- <AccordionContext.Provider value={{ openItems, toggle, type }}>
115
+ <AccordionContext.Provider value={{ openItems, toggle, type, variant, size, design }}>
38
116
  <div className={className}>{children}</div>
39
117
  </AccordionContext.Provider>
40
118
  );
@@ -42,15 +120,28 @@ export function Accordion({
42
120
 
43
121
  export function AccordionItem({
44
122
  value,
123
+ disabled,
45
124
  className,
46
125
  children,
47
126
  }: {
48
127
  value: string;
128
+ disabled?: boolean;
49
129
  className?: string;
50
130
  children: React.ReactNode;
51
131
  }) {
132
+ const { openItems, variant, design } = useAccordion();
52
133
  return (
53
- <div data-value={value} className={cn("border-b border-[var(--ui-border)]", className)}>
134
+ <div
135
+ data-value={value}
136
+ data-disabled={disabled ? "" : undefined}
137
+ data-state={openItems.has(value) ? "open" : "closed"}
138
+ className={cn(
139
+ ITEM_VARIANTS[variant],
140
+ variant !== "default" && ITEM_DESIGNS[design],
141
+ variant !== "default" && design !== "flat" && "mb-3 last:mb-0",
142
+ className,
143
+ )}
144
+ >
54
145
  {children}
55
146
  </div>
56
147
  );
@@ -58,32 +149,43 @@ export function AccordionItem({
58
149
 
59
150
  export function AccordionTrigger({
60
151
  value,
152
+ disabled,
61
153
  className,
62
154
  children,
63
155
  }: {
64
156
  value: string;
157
+ disabled?: boolean;
65
158
  className?: string;
66
159
  children: React.ReactNode;
67
160
  }) {
68
- const ctx = React.useContext(AccordionContext)!;
161
+ const ctx = useAccordion();
69
162
  const open = ctx.openItems.has(value);
70
163
  return (
71
164
  <button
72
165
  type="button"
166
+ disabled={disabled}
167
+ aria-expanded={open}
168
+ data-state={open ? "open" : "closed"}
73
169
  onClick={() => ctx.toggle(value)}
74
170
  className={cn(
75
- "flex w-full items-center justify-between py-4 text-sm font-medium transition-all",
76
- "hover:text-[var(--ui-accent)]",
171
+ "flex w-full items-center justify-between gap-4 text-left font-medium transition-all",
172
+ "rounded-md hover:underline focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring/50",
173
+ "disabled:pointer-events-none disabled:opacity-50",
174
+ TRIGGER_SIZES[ctx.size],
77
175
  className,
78
176
  )}
79
177
  >
80
178
  {children}
81
179
  <svg
82
180
  viewBox="0 0 24 24"
83
- className={cn("h-4 w-4 shrink-0 transition-transform", open && "rotate-180")}
181
+ className={cn(
182
+ "size-4 shrink-0 text-muted-foreground transition-transform duration-200",
183
+ open && "rotate-180",
184
+ )}
84
185
  fill="none"
85
186
  stroke="currentColor"
86
187
  strokeWidth="2"
188
+ aria-hidden="true"
87
189
  >
88
190
  <path d="M6 9l6 6 6-6" strokeLinecap="round" strokeLinejoin="round" />
89
191
  </svg>
@@ -100,7 +202,14 @@ export function AccordionContent({
100
202
  className?: string;
101
203
  children: React.ReactNode;
102
204
  }) {
103
- const ctx = React.useContext(AccordionContext)!;
205
+ const ctx = useAccordion();
104
206
  if (!ctx.openItems.has(value)) return null;
105
- return <div className={cn("pb-4 text-sm text-[var(--ui-fg-muted)]", className)}>{children}</div>;
207
+ return (
208
+ <div
209
+ data-state="open"
210
+ className={cn("text-muted-foreground", CONTENT_SIZES[ctx.size], className)}
211
+ >
212
+ {children}
213
+ </div>
214
+ );
106
215
  }
@@ -0,0 +1,102 @@
1
+ "use client";
2
+ import * as React from "react";
3
+ import { cn } from "@/lib/utils";
4
+
5
+ const AlertDialogContext = React.createContext<{ open: boolean; setOpen: (v: boolean) => void } | null>(null);
6
+
7
+ export function AlertDialog({
8
+ open: controlled,
9
+ onOpenChange,
10
+ children,
11
+ }: {
12
+ open?: boolean;
13
+ onOpenChange?: (v: boolean) => void;
14
+ children: React.ReactNode;
15
+ }) {
16
+ const [internal, setInternal] = React.useState(false);
17
+ const open = controlled ?? internal;
18
+ const setOpen = (v: boolean) => {
19
+ if (controlled === undefined) setInternal(v);
20
+ onOpenChange?.(v);
21
+ };
22
+ return (
23
+ <AlertDialogContext.Provider value={{ open, setOpen }}>
24
+ {children}
25
+ {open ? <div className="fixed inset-0 z-50 bg-black/60 backdrop-blur-sm" aria-hidden /> : null}
26
+ </AlertDialogContext.Provider>
27
+ );
28
+ }
29
+
30
+ export function AlertDialogTrigger({ asChild, children }: { asChild?: boolean; children: React.ReactNode }) {
31
+ const ctx = React.useContext(AlertDialogContext);
32
+ if (asChild && React.isValidElement(children)) {
33
+ return React.cloneElement(children as React.ReactElement<{ onClick?: () => void }>, {
34
+ onClick: () => ctx?.setOpen(true),
35
+ });
36
+ }
37
+ return <button type="button" onClick={() => ctx?.setOpen(true)}>{children}</button>;
38
+ }
39
+
40
+ export function AlertDialogContent({ className, children }: { className?: string; children: React.ReactNode }) {
41
+ const ctx = React.useContext(AlertDialogContext);
42
+ if (!ctx?.open) return null;
43
+ return (
44
+ <div
45
+ role="alertdialog"
46
+ aria-modal
47
+ className={cn(
48
+ "fixed left-1/2 top-1/2 z-50 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl border border-border bg-background p-6 shadow-lg",
49
+ className,
50
+ )}
51
+ >
52
+ {children}
53
+ </div>
54
+ );
55
+ }
56
+
57
+ export function AlertDialogHeader({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
58
+ return <div className={cn("flex flex-col gap-2 text-left", className)} {...props} />;
59
+ }
60
+ export function AlertDialogFooter({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
61
+ return <div className={cn("flex justify-end gap-2 pt-2", className)} {...props} />;
62
+ }
63
+ export function AlertDialogTitle({ className, ...props }: React.HTMLAttributes<HTMLHeadingElement>) {
64
+ return <h2 className={cn("text-lg font-semibold", className)} {...props} />;
65
+ }
66
+ export function AlertDialogDescription({ className, ...props }: React.HTMLAttributes<HTMLParagraphElement>) {
67
+ return <p className={cn("text-sm text-muted-foreground", className)} {...props} />;
68
+ }
69
+ export function AlertDialogAction({ className, onClick, ...props }: React.ButtonHTMLAttributes<HTMLButtonElement>) {
70
+ const ctx = React.useContext(AlertDialogContext);
71
+ return (
72
+ <button
73
+ type="button"
74
+ onClick={(e) => {
75
+ onClick?.(e);
76
+ ctx?.setOpen(false);
77
+ }}
78
+ className={cn(
79
+ "inline-flex h-9 items-center justify-center rounded-lg bg-primary px-4 text-sm font-medium text-primary-foreground hover:bg-primary/90",
80
+ className,
81
+ )}
82
+ {...props}
83
+ />
84
+ );
85
+ }
86
+ export function AlertDialogCancel({ className, onClick, ...props }: React.ButtonHTMLAttributes<HTMLButtonElement>) {
87
+ const ctx = React.useContext(AlertDialogContext);
88
+ return (
89
+ <button
90
+ type="button"
91
+ onClick={(e) => {
92
+ onClick?.(e);
93
+ ctx?.setOpen(false);
94
+ }}
95
+ className={cn(
96
+ "inline-flex h-9 items-center justify-center rounded-lg border border-input bg-background px-4 text-sm font-medium hover:bg-secondary",
97
+ className,
98
+ )}
99
+ {...props}
100
+ />
101
+ );
102
+ }
@@ -1,34 +1,134 @@
1
- "use client";
2
1
  import * as React from "react";
3
2
  import { cn } from "@/lib/utils";
4
3
 
5
- type Tone = "default" | "success" | "warning" | "destructive" | "info";
6
- const TONES: Record<Tone, string> = {
7
- default: "bg-[var(--ui-surface-2)] text-[var(--ui-fg)]",
8
- success: "bg-[#dcfce7] text-[#166534]",
9
- warning: "bg-[#fef3c7] text-[#92400e]",
10
- destructive: "bg-[#fee2e2] text-[#991b1b]",
11
- info: "bg-[#dbeafe] text-[#1e40af]",
4
+ /**
5
+ * swift-rust ui · Alert
6
+ *
7
+ * variant — default, destructive, success, warning, info, outline, secondary
8
+ * size — sm, default, lg
9
+ * design flat, soft, 3d, glass, neo, brutal, gradient
10
+ *
11
+ * (`tone` is accepted as a deprecated alias of `variant`.)
12
+ */
13
+
14
+ export type AlertVariant =
15
+ | "default"
16
+ | "destructive"
17
+ | "success"
18
+ | "warning"
19
+ | "info"
20
+ | "outline"
21
+ | "secondary";
22
+ export type AlertSize = "default" | "sm" | "lg";
23
+ export type AlertDesign = "flat" | "soft" | "3d" | "glass" | "neo" | "brutal" | "gradient";
24
+
25
+ const VARIANTS: Record<AlertVariant, string> = {
26
+ default: "border-border bg-card text-card-foreground",
27
+ destructive:
28
+ "border-destructive/30 bg-destructive/10 text-destructive [&_[data-alert-description]]:text-destructive/90",
29
+ success:
30
+ "border-emerald-600/30 bg-emerald-500/10 text-emerald-700 dark:text-emerald-400 " +
31
+ "[&_[data-alert-description]]:text-emerald-700/90 dark:[&_[data-alert-description]]:text-emerald-400/90",
32
+ warning:
33
+ "border-amber-600/30 bg-amber-500/10 text-amber-700 dark:text-amber-400 " +
34
+ "[&_[data-alert-description]]:text-amber-700/90 dark:[&_[data-alert-description]]:text-amber-400/90",
35
+ info:
36
+ "border-sky-600/30 bg-sky-500/10 text-sky-700 dark:text-sky-400 " +
37
+ "[&_[data-alert-description]]:text-sky-700/90 dark:[&_[data-alert-description]]:text-sky-400/90",
38
+ outline: "border-2 border-border bg-transparent text-foreground",
39
+ secondary: "border-transparent bg-secondary text-secondary-foreground",
40
+ };
41
+
42
+ const SIZES: Record<AlertSize, string> = {
43
+ sm: "p-3 text-xs [&_[data-alert-title]]:text-sm",
44
+ default: "p-4 text-sm",
45
+ lg: "p-6 text-base",
46
+ };
47
+
48
+ const DESIGNS: Record<AlertDesign, string> = {
49
+ flat: "",
50
+ soft: "rounded-2xl border-transparent",
51
+ // Depth via a darker bottom lip + a top sheen — no drop shadow.
52
+ "3d": "border-b-4 border-b-black/15 bg-linear-to-b from-white/20 to-transparent dark:border-b-black/40",
53
+ // Liquid glass panel.
54
+ glass:
55
+ "border-white/40 bg-white/15 backdrop-blur-xl backdrop-saturate-200 " +
56
+ "bg-linear-to-br from-white/30 via-white/10 to-white/5 " +
57
+ "shadow-[inset_0_1px_1px_rgba(255,255,255,0.55),0_6px_24px_rgba(31,38,135,0.16)] " +
58
+ "dark:border-white/20 dark:bg-white/10 dark:from-white/15 dark:via-white/5 dark:to-transparent " +
59
+ "dark:shadow-[inset_0_1px_1px_rgba(255,255,255,0.2),0_6px_24px_rgba(0,0,0,0.4)]",
60
+ neo:
61
+ "border-transparent bg-background " +
62
+ "shadow-[6px_6px_12px_rgba(0,0,0,0.12),-6px_-6px_12px_rgba(255,255,255,0.8)] " +
63
+ "dark:shadow-[6px_6px_12px_rgba(0,0,0,0.6),-6px_-6px_12px_rgba(255,255,255,0.05)]",
64
+ brutal: "rounded-none border-2 border-foreground shadow-[4px_4px_0_0_var(--color-foreground)]",
65
+ gradient:
66
+ "border-2 border-transparent " +
67
+ "[background:linear-gradient(var(--color-card),var(--color-card))_padding-box," +
68
+ "linear-gradient(135deg,#8b5cf6,#d946ef,#fb923c)_border-box]",
12
69
  };
13
70
 
14
71
  export interface AlertProps extends React.HTMLAttributes<HTMLDivElement> {
15
- tone?: Tone;
72
+ variant?: AlertVariant;
73
+ /** @deprecated use `variant` */
74
+ tone?: AlertVariant;
75
+ size?: AlertSize;
76
+ design?: AlertDesign;
16
77
  }
17
78
 
18
- export function Alert({ className, tone = "default", ...props }: AlertProps) {
79
+ export function Alert({
80
+ className,
81
+ variant,
82
+ tone,
83
+ size = "default",
84
+ design = "flat",
85
+ ...props
86
+ }: AlertProps) {
87
+ const v = variant ?? tone ?? "default";
19
88
  return (
20
89
  <div
21
90
  role="alert"
22
- className={cn("relative w-full rounded-lg border border-[var(--ui-border)] p-4 text-sm", TONES[tone], className)}
91
+ className={cn(
92
+ "relative grid w-full grid-cols-[0_1fr] gap-y-0.5 rounded-lg border",
93
+ "has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-3 [&>svg]:size-4 [&>svg]:translate-y-0.5",
94
+ // Reserve room on the trailing edge when an AlertAction is present.
95
+ "has-[[data-alert-action]]:pr-16",
96
+ VARIANTS[v],
97
+ SIZES[size],
98
+ DESIGNS[design],
99
+ className,
100
+ )}
23
101
  {...props}
24
102
  />
25
103
  );
26
104
  }
27
105
 
28
106
  export function AlertTitle({ className, ...props }: React.HTMLAttributes<HTMLHeadingElement>) {
29
- return <h5 className={cn("mb-1 font-medium leading-none tracking-tight", className)} {...props} />;
107
+ return (
108
+ <h5
109
+ data-alert-title
110
+ className={cn("col-start-2 mb-1 font-medium leading-none tracking-tight", className)}
111
+ {...props}
112
+ />
113
+ );
30
114
  }
31
115
 
32
- export function AlertDescription({ className, ...props }: React.HTMLAttributes<HTMLParagraphElement>) {
33
- return <div className={cn("text-sm [&_p]:leading-relaxed", className)} {...props} />;
116
+ export function AlertDescription({
117
+ className,
118
+ ...props
119
+ }: React.HTMLAttributes<HTMLParagraphElement>) {
120
+ return (
121
+ <div
122
+ data-alert-description
123
+ className={cn("col-start-2 text-muted-foreground [&_p]:leading-relaxed", className)}
124
+ {...props}
125
+ />
126
+ );
127
+ }
128
+
129
+ /** A trailing action (button, link, …) pinned to the top-right of the alert. */
130
+ export function AlertAction({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
131
+ return (
132
+ <div data-alert-action className={cn("absolute right-3 top-3", className)} {...props} />
133
+ );
34
134
  }
@@ -0,0 +1,18 @@
1
+ import * as React from "react";
2
+ import { cn } from "@/lib/utils";
3
+
4
+ export interface AspectRatioProps extends React.HTMLAttributes<HTMLDivElement> {
5
+ ratio?: number;
6
+ }
7
+
8
+ export const AspectRatio = React.forwardRef<HTMLDivElement, AspectRatioProps>(
9
+ ({ ratio = 16 / 9, className, style, ...props }, ref) => (
10
+ <div
11
+ ref={ref}
12
+ className={cn("relative w-full overflow-hidden", className)}
13
+ style={{ aspectRatio: String(ratio), ...style }}
14
+ {...props}
15
+ />
16
+ ),
17
+ );
18
+ AspectRatio.displayName = "AspectRatio";