@spunto/design-system 0.1.0 → 0.2.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": "@spunto/design-system",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "Spunto's shared design system — warm/flame tokens, color constants, and UI primitives.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -49,11 +49,14 @@
49
49
  "tailwind-merge": "^3.6.0"
50
50
  },
51
51
  "peerDependencies": {
52
+ "@base-ui/react": "^1.6.0",
52
53
  "react": ">=19",
53
54
  "react-dom": ">=19"
54
55
  },
55
56
  "devDependencies": {
57
+ "@base-ui/react": "^1.6.0",
56
58
  "@types/react": "^19",
59
+ "@types/react-dom": "^19",
57
60
  "typescript": "^5"
58
61
  }
59
62
  }
@@ -1,29 +1,52 @@
1
- import * as React from "react"
1
+ import { mergeProps } from "@base-ui/react/merge-props"
2
+ import { useRender } from "@base-ui/react/use-render"
2
3
  import { cva, type VariantProps } from "class-variance-authority"
4
+
3
5
  import { cn } from "../utils"
4
6
 
5
- export const badgeVariants = cva(
6
- "inline-flex items-center gap-1 rounded-md border px-2 py-0.5 text-xs font-medium",
7
+ const badgeVariants = cva(
8
+ "group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!",
7
9
  {
8
10
  variants: {
9
11
  variant: {
10
- default: "border-transparent bg-primary/12 text-primary",
11
- secondary: "border-transparent bg-secondary text-secondary-foreground",
12
- outline: "border-border text-foreground",
13
- success: "border-transparent bg-emerald-500/12 text-emerald-600 dark:text-emerald-400",
14
- warning: "border-transparent bg-amber-500/15 text-amber-600 dark:text-amber-400",
15
- destructive: "border-transparent bg-destructive/12 text-destructive",
16
- muted: "border-transparent bg-muted text-muted-foreground",
12
+ default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
13
+ secondary:
14
+ "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
15
+ destructive:
16
+ "bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20",
17
+ outline:
18
+ "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
19
+ ghost:
20
+ "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
21
+ link: "text-primary underline-offset-4 hover:underline",
17
22
  },
18
23
  },
19
- defaultVariants: { variant: "default" },
20
- },
24
+ defaultVariants: {
25
+ variant: "default",
26
+ },
27
+ }
21
28
  )
22
29
 
23
- export function Badge({
30
+ function Badge({
24
31
  className,
25
- variant,
32
+ variant = "default",
33
+ render,
26
34
  ...props
27
- }: React.HTMLAttributes<HTMLSpanElement> & VariantProps<typeof badgeVariants>) {
28
- return <span className={cn(badgeVariants({ variant }), className)} {...props} />
35
+ }: useRender.ComponentProps<"span"> & VariantProps<typeof badgeVariants>) {
36
+ return useRender({
37
+ defaultTagName: "span",
38
+ props: mergeProps<"span">(
39
+ {
40
+ className: cn(badgeVariants({ variant }), className),
41
+ },
42
+ props
43
+ ),
44
+ render,
45
+ state: {
46
+ slot: "badge",
47
+ variant,
48
+ },
49
+ })
29
50
  }
51
+
52
+ export { Badge, badgeVariants }
@@ -0,0 +1,39 @@
1
+ import { cva } from "class-variance-authority"
2
+
3
+ export const buttonVariants = cva(
4
+ "group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/40 active:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
5
+ {
6
+ variants: {
7
+ variant: {
8
+ default:
9
+ "bg-primary text-primary-foreground hover:bg-primary/85 border-primary/20",
10
+ outline:
11
+ "border-border bg-background hover:bg-accent hover:text-accent-foreground aria-expanded:bg-accent dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
12
+ secondary:
13
+ "bg-secondary text-secondary-foreground hover:bg-secondary/70 border-secondary/50",
14
+ ghost:
15
+ "hover:bg-accent hover:text-accent-foreground aria-expanded:bg-accent dark:hover:bg-muted/50",
16
+ destructive:
17
+ "bg-destructive/10 text-destructive hover:bg-destructive/20 border-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30",
18
+ link: "text-primary underline-offset-4 hover:underline",
19
+ },
20
+ size: {
21
+ default:
22
+ "h-8 gap-1.5 px-3 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
23
+ xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
24
+ sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
25
+ lg: "h-9 gap-1.5 px-4",
26
+ icon: "size-8",
27
+ "icon-xs":
28
+ "size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
29
+ "icon-sm":
30
+ "size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
31
+ "icon-lg": "size-9",
32
+ },
33
+ },
34
+ defaultVariants: {
35
+ variant: "default",
36
+ size: "default",
37
+ },
38
+ }
39
+ )
@@ -1,36 +1,24 @@
1
- import * as React from "react"
2
- import { cva, type VariantProps } from "class-variance-authority"
3
- import { cn } from "../utils"
1
+ "use client"
2
+
3
+ import { Button as ButtonPrimitive } from "@base-ui/react/button"
4
+ import { type VariantProps } from "class-variance-authority"
4
5
 
5
- export const buttonVariants = cva(
6
- "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0",
7
- {
8
- variants: {
9
- variant: {
10
- default: "bg-primary text-primary-foreground hover:opacity-90",
11
- secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
12
- outline: "border border-border bg-transparent hover:bg-accent hover:text-accent-foreground",
13
- ghost: "hover:bg-accent hover:text-accent-foreground",
14
- destructive: "bg-destructive text-white hover:opacity-90",
15
- },
16
- size: {
17
- default: "h-9 px-4 py-2",
18
- sm: "h-8 px-3 text-xs",
19
- lg: "h-10 px-6",
20
- icon: "size-9",
21
- },
22
- },
23
- defaultVariants: { variant: "default", size: "default" },
24
- },
25
- )
6
+ import { cn } from "../utils"
7
+ import { buttonVariants } from "./button-variants"
26
8
 
27
- export interface ButtonProps
28
- extends React.ButtonHTMLAttributes<HTMLButtonElement>,
29
- VariantProps<typeof buttonVariants> {}
9
+ function Button({
10
+ className,
11
+ variant = "default",
12
+ size = "default",
13
+ ...props
14
+ }: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) {
15
+ return (
16
+ <ButtonPrimitive
17
+ data-slot="button"
18
+ className={cn(buttonVariants({ variant, size, className }))}
19
+ {...props}
20
+ />
21
+ )
22
+ }
30
23
 
31
- export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
32
- ({ className, variant, size, ...props }, ref) => (
33
- <button ref={ref} className={cn(buttonVariants({ variant, size }), className)} {...props} />
34
- ),
35
- )
36
- Button.displayName = "Button"
24
+ export { Button, buttonVariants }
@@ -1,26 +1,103 @@
1
1
  import * as React from "react"
2
+
2
3
  import { cn } from "../utils"
3
4
 
4
- export function Card({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
5
- return <div className={cn("rounded-xl border border-border bg-card text-card-foreground", className)} {...props} />
5
+ function Card({
6
+ className,
7
+ size = "default",
8
+ ...props
9
+ }: React.ComponentProps<"div"> & { size?: "default" | "sm" }) {
10
+ return (
11
+ <div
12
+ data-slot="card"
13
+ data-size={size}
14
+ className={cn(
15
+ "group/card flex flex-col gap-4 overflow-hidden rounded-xl bg-card py-4 text-sm text-card-foreground ring-1 ring-foreground/10 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
16
+ className
17
+ )}
18
+ {...props}
19
+ />
20
+ )
21
+ }
22
+
23
+ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
24
+ return (
25
+ <div
26
+ data-slot="card-header"
27
+ className={cn(
28
+ "group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-4 group-data-[size=sm]/card:px-3 has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3",
29
+ className
30
+ )}
31
+ {...props}
32
+ />
33
+ )
34
+ }
35
+
36
+ function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
37
+ return (
38
+ <div
39
+ data-slot="card-title"
40
+ className={cn(
41
+ "text-base leading-snug font-medium group-data-[size=sm]/card:text-sm",
42
+ className
43
+ )}
44
+ {...props}
45
+ />
46
+ )
6
47
  }
7
48
 
8
- export function CardHeader({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
9
- return <div className={cn("flex flex-col gap-1.5 p-5", className)} {...props} />
49
+ function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
50
+ return (
51
+ <div
52
+ data-slot="card-description"
53
+ className={cn("text-sm text-muted-foreground", className)}
54
+ {...props}
55
+ />
56
+ )
10
57
  }
11
58
 
12
- export function CardTitle({ className, ...props }: React.HTMLAttributes<HTMLHeadingElement>) {
13
- return <h3 className={cn("font-semibold leading-none tracking-tight", className)} {...props} />
59
+ function CardAction({ className, ...props }: React.ComponentProps<"div">) {
60
+ return (
61
+ <div
62
+ data-slot="card-action"
63
+ className={cn(
64
+ "col-start-2 row-span-2 row-start-1 self-start justify-self-end",
65
+ className
66
+ )}
67
+ {...props}
68
+ />
69
+ )
14
70
  }
15
71
 
16
- export function CardDescription({ className, ...props }: React.HTMLAttributes<HTMLParagraphElement>) {
17
- return <p className={cn("text-sm text-muted-foreground", className)} {...props} />
72
+ function CardContent({ className, ...props }: React.ComponentProps<"div">) {
73
+ return (
74
+ <div
75
+ data-slot="card-content"
76
+ className={cn("px-4 group-data-[size=sm]/card:px-3", className)}
77
+ {...props}
78
+ />
79
+ )
18
80
  }
19
81
 
20
- export function CardContent({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
21
- return <div className={cn("p-5 pt-0", className)} {...props} />
82
+ function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
83
+ return (
84
+ <div
85
+ data-slot="card-footer"
86
+ className={cn(
87
+ "flex items-center rounded-b-xl border-t bg-muted/50 p-4 group-data-[size=sm]/card:p-3",
88
+ className
89
+ )}
90
+ {...props}
91
+ />
92
+ )
22
93
  }
23
94
 
24
- export function CardFooter({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
25
- return <div className={cn("flex items-center p-5 pt-0", className)} {...props} />
95
+ export {
96
+ Card,
97
+ CardHeader,
98
+ CardFooter,
99
+ CardTitle,
100
+ CardAction,
101
+ CardDescription,
102
+ CardContent,
26
103
  }
@@ -1,16 +1,20 @@
1
1
  import * as React from "react"
2
+ import { Input as InputPrimitive } from "@base-ui/react/input"
3
+
2
4
  import { cn } from "../utils"
3
5
 
4
- export const Input = React.forwardRef<HTMLInputElement, React.InputHTMLAttributes<HTMLInputElement>>(
5
- ({ className, ...props }, ref) => (
6
- <input
7
- ref={ref}
6
+ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
7
+ return (
8
+ <InputPrimitive
9
+ type={type}
10
+ data-slot="input"
8
11
  className={cn(
9
- "flex h-9 w-full rounded-lg border border-input bg-background px-3 py-1 text-sm shadow-none transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
10
- className,
12
+ "h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
13
+ className
11
14
  )}
12
15
  {...props}
13
16
  />
14
- ),
15
- )
16
- Input.displayName = "Input"
17
+ )
18
+ }
19
+
20
+ export { Input }
@@ -1,9 +1,20 @@
1
+ "use client"
2
+
1
3
  import * as React from "react"
4
+
2
5
  import { cn } from "../utils"
3
6
 
4
- export const Label = React.forwardRef<HTMLLabelElement, React.LabelHTMLAttributes<HTMLLabelElement>>(
5
- ({ className, ...props }, ref) => (
6
- <label ref={ref} className={cn("text-sm font-medium leading-none", className)} {...props} />
7
- ),
8
- )
9
- Label.displayName = "Label"
7
+ function Label({ className, ...props }: React.ComponentProps<"label">) {
8
+ return (
9
+ <label
10
+ data-slot="label"
11
+ className={cn(
12
+ "flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
13
+ className
14
+ )}
15
+ {...props}
16
+ />
17
+ )
18
+ }
19
+
20
+ export { Label }
@@ -1,41 +1,32 @@
1
1
  "use client"
2
2
 
3
- import * as React from "react"
3
+ import { Switch as SwitchPrimitive } from "@base-ui/react/switch"
4
+
4
5
  import { cn } from "../utils"
5
6
 
6
- export function Switch({
7
- checked,
8
- onCheckedChange,
7
+ function Switch({
9
8
  className,
10
- id,
11
- disabled,
12
- }: {
13
- checked: boolean
14
- onCheckedChange: (v: boolean) => void
15
- className?: string
16
- id?: string
17
- disabled?: boolean
9
+ size = "default",
10
+ ...props
11
+ }: SwitchPrimitive.Root.Props & {
12
+ size?: "sm" | "default"
18
13
  }) {
19
14
  return (
20
- <button
21
- type="button"
22
- role="switch"
23
- id={id}
24
- aria-checked={checked}
25
- disabled={disabled}
26
- onClick={() => onCheckedChange(!checked)}
15
+ <SwitchPrimitive.Root
16
+ data-slot="switch"
17
+ data-size={size}
27
18
  className={cn(
28
- "relative inline-flex h-5 w-9 shrink-0 items-center rounded-full transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50",
29
- checked ? "bg-primary" : "bg-input",
30
- className,
19
+ "peer group/switch relative inline-flex shrink-0 items-center rounded-full border border-transparent transition-all outline-none after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-[size=default]:h-[18.4px] data-[size=default]:w-[32px] data-[size=sm]:h-[14px] data-[size=sm]:w-[24px] dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:bg-primary data-unchecked:bg-input dark:data-unchecked:bg-input/80 data-disabled:cursor-not-allowed data-disabled:opacity-50",
20
+ className
31
21
  )}
22
+ {...props}
32
23
  >
33
- <span
34
- className={cn(
35
- "inline-block size-4 rounded-full bg-white shadow-sm transition-transform",
36
- checked ? "translate-x-4" : "translate-x-0.5",
37
- )}
24
+ <SwitchPrimitive.Thumb
25
+ data-slot="switch-thumb"
26
+ className="pointer-events-none block rounded-full bg-background ring-0 transition-transform group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 group-data-[size=default]/switch:data-checked:translate-x-[calc(100%-2px)] group-data-[size=sm]/switch:data-checked:translate-x-[calc(100%-2px)] dark:data-checked:bg-primary-foreground group-data-[size=default]/switch:data-unchecked:translate-x-0 group-data-[size=sm]/switch:data-unchecked:translate-x-0 dark:data-unchecked:bg-foreground"
38
27
  />
39
- </button>
28
+ </SwitchPrimitive.Root>
40
29
  )
41
30
  }
31
+
32
+ export { Switch }
@@ -1,16 +1,18 @@
1
1
  import * as React from "react"
2
+
2
3
  import { cn } from "../utils"
3
4
 
4
- export const Textarea = React.forwardRef<HTMLTextAreaElement, React.TextareaHTMLAttributes<HTMLTextAreaElement>>(
5
- ({ className, ...props }, ref) => (
5
+ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
6
+ return (
6
7
  <textarea
7
- ref={ref}
8
+ data-slot="textarea"
8
9
  className={cn(
9
- "flex min-h-[72px] w-full rounded-lg border border-input bg-background px-3 py-2 text-sm font-mono placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
10
- className,
10
+ "flex field-sizing-content min-h-16 w-full rounded-lg border border-input bg-transparent px-2.5 py-2 text-base transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
11
+ className
11
12
  )}
12
13
  {...props}
13
14
  />
14
- ),
15
- )
16
- Textarea.displayName = "Textarea"
15
+ )
16
+ }
17
+
18
+ export { Textarea }
package/src/index.ts CHANGED
@@ -4,8 +4,11 @@
4
4
  export { cn } from "./utils"
5
5
  export * from "./colors"
6
6
 
7
- export { Button, buttonVariants, type ButtonProps } from "./components/button"
8
- export { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from "./components/card"
7
+ export { Button } from "./components/button"
8
+ // `buttonVariants` is exported from its own directive-free module (not through the
9
+ // "use client" button.tsx) so it stays callable from React Server Components.
10
+ export { buttonVariants } from "./components/button-variants"
11
+ export { Card, CardHeader, CardTitle, CardDescription, CardAction, CardContent, CardFooter } from "./components/card"
9
12
  export { Input } from "./components/input"
10
13
  export { Textarea } from "./components/textarea"
11
14
  export { Label } from "./components/label"