@stasho/ds 0.4.0 → 0.5.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stasho/ds",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "stasho design system",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -24,6 +24,7 @@
24
24
  "exports": {
25
25
  "./alert": "./src/components/alert/alert.tsx",
26
26
  "./button": "./src/components/button/button.tsx",
27
+ "./icon-button": "./src/components/button/icon-button.tsx",
27
28
  "./checkbox": "./src/components/checkbox/checkbox.tsx",
28
29
  "./input": "./src/components/input/input.tsx",
29
30
  "./textarea": "./src/components/textarea/textarea.tsx",
@@ -101,6 +101,14 @@ const buttonVariants = cva(
101
101
  "dark:hover:bg-white/[0.04] dark:hover:text-white",
102
102
  "dark:disabled:text-white/30 dark:disabled:bg-transparent",
103
103
  ].join(" "),
104
+ link: [
105
+ // chrome-less inline text action: text-primary, underline on hover, no LED, no padding (zeroed per size below)
106
+ "bg-transparent font-normal underline-offset-2 text-primary",
107
+ "hover:underline active:translate-y-0",
108
+ "dark:text-accent",
109
+ "disabled:text-foreground/30 disabled:no-underline disabled:hover:no-underline",
110
+ "dark:disabled:text-white/30",
111
+ ].join(" "),
104
112
  },
105
113
  size: {
106
114
  xs: "py-[6px] px-3 text-[11px] gap-1.5",
@@ -115,6 +123,10 @@ const buttonVariants = cva(
115
123
  { variant: "outline", size: "sm", class: "py-[6px] px-[13px]" },
116
124
  { variant: "outline", size: "md", class: "py-[8px] px-[17px]" },
117
125
  { variant: "outline", size: "lg", class: "py-[12px] px-[23px]" },
126
+ { variant: "link", size: "xs", class: "p-0" },
127
+ { variant: "link", size: "sm", class: "p-0" },
128
+ { variant: "link", size: "md", class: "p-0" },
129
+ { variant: "link", size: "lg", class: "p-0" },
118
130
  ],
119
131
  defaultVariants: {
120
132
  variant: "primary",
@@ -152,6 +164,8 @@ const ledColorClass: Record<Variant, string> = {
152
164
  outline: "bg-primary/35 text-primary dark:bg-accent/50 dark:text-accent",
153
165
  // ghost: LED is never rendered for ghost, so this entry is a sentinel.
154
166
  ghost: "",
167
+ // link: LED is never rendered for link, so this entry is a sentinel.
168
+ link: "",
155
169
  };
156
170
 
157
171
  // iconLeft glow treatment per variant — applied to the wrapper span.
@@ -163,6 +177,7 @@ const iconGlowClass: Record<Variant, string> = {
163
177
  success: "text-success-foreground",
164
178
  outline: "text-primary dark:text-accent [filter:drop-shadow(0_0_4px_currentColor)]",
165
179
  ghost: "text-foreground/60 dark:text-white/60",
180
+ link: "text-primary dark:text-accent",
166
181
  };
167
182
 
168
183
  type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> &
@@ -199,8 +214,8 @@ const Button = forwardRef<HTMLButtonElement, ButtonProps>(
199
214
  );
200
215
 
201
216
  const leadingSlot = (() => {
202
- // Loading replaces everything in the leading slot (except on ghost).
203
- if (loading && v !== "ghost") {
217
+ // Loading replaces everything in the leading slot (except on ghost/link).
218
+ if (loading && v !== "ghost" && v !== "link") {
204
219
  return (
205
220
  <span
206
221
  data-led-chase
@@ -241,8 +256,8 @@ const Button = forwardRef<HTMLButtonElement, ButtonProps>(
241
256
  </span>
242
257
  );
243
258
  }
244
- // Resting state with static LED (non-ghost variants).
245
- if (v !== "ghost") {
259
+ // Resting state with static LED (non-ghost/link variants).
260
+ if (v !== "ghost" && v !== "link") {
246
261
  return (
247
262
  <span
248
263
  data-led
@@ -0,0 +1,51 @@
1
+ import { forwardRef, type ButtonHTMLAttributes, type ReactNode } from "react";
2
+ import { type VariantProps } from "class-variance-authority";
3
+ import { cn } from "@ac/lib/cn";
4
+ import { buttonVariants } from "./button";
5
+
6
+ // Square inset per size. The glyph dimensions are consumer-controlled (e.g. <Bell size={18} />);
7
+ // the button stays square because the padding is symmetric.
8
+ const squareSizeClass = {
9
+ xs: "p-1",
10
+ sm: "p-1.5",
11
+ md: "p-2",
12
+ lg: "p-2.5",
13
+ } as const;
14
+
15
+ type Variant = NonNullable<VariantProps<typeof buttonVariants>["variant"]>;
16
+ type Size = keyof typeof squareSizeClass;
17
+
18
+ // aria-label is required: an icon-only button must carry an accessible name.
19
+ type IconButtonProps = Omit<
20
+ ButtonHTMLAttributes<HTMLButtonElement>,
21
+ "aria-label"
22
+ > & {
23
+ variant?: Variant;
24
+ size?: Size;
25
+ "aria-label": string;
26
+ children: ReactNode;
27
+ };
28
+
29
+ const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(
30
+ ({ variant = "ghost", size = "md", className, children, ...rest }, ref) => (
31
+ <button
32
+ ref={ref}
33
+ type="button"
34
+ className={cn(
35
+ // reuse Button's color/chassis; tailwind-merge collapses the default
36
+ // rectangular size padding into the square p-* appended after it.
37
+ buttonVariants({ variant }),
38
+ "justify-center shrink-0",
39
+ squareSizeClass[size],
40
+ className,
41
+ )}
42
+ {...rest}
43
+ >
44
+ {children}
45
+ </button>
46
+ ),
47
+ );
48
+
49
+ IconButton.displayName = "IconButton";
50
+
51
+ export { IconButton, type IconButtonProps };
@@ -8,21 +8,22 @@ const switchVariants = cva(
8
8
  "peer inline-flex shrink-0 cursor-pointer",
9
9
  "items-center rounded-[2px]",
10
10
  "border border-edge bg-muted dark:bg-neutral-900",
11
- "shadow-[inset_0_1px_0_rgba(255,255,255,0.06),inset_0_-1px_0_rgba(0,0,0,0.4)]",
11
+ "[box-shadow:inset_0_1px_0_rgba(255,255,255,0.7),inset_0_-1px_0_rgba(0,0,0,0.10)]",
12
+ "dark:[box-shadow:inset_0_1px_0_rgba(255,255,255,0.06),inset_0_-1px_0_rgba(0,0,0,0.4)]",
12
13
  "focus-visible:outline-2 focus-visible:outline-accent focus-visible:outline-offset-2",
13
- "disabled:bg-muted dark:disabled:bg-background",
14
- "disabled:border-edge/50 disabled:shadow-none",
14
+ "disabled:bg-edge dark:disabled:bg-background",
15
+ "disabled:border-edge-hover dark:disabled:border-edge/50 disabled:[box-shadow:none]",
15
16
  "disabled:cursor-not-allowed",
16
- "disabled:data-[state=checked]:border-edge/50",
17
+ "disabled:data-[state=checked]:border-edge-hover dark:disabled:data-[state=checked]:border-edge/50",
17
18
  "data-[state=checked]:border-accent/30",
18
19
  "transition-colors",
19
20
  ].join(" "),
20
21
  {
21
22
  variants: {
22
23
  size: {
23
- xs: "h-4 w-7",
24
- sm: "h-5 w-9",
25
- md: "h-6 w-11",
24
+ xs: "h-[18px] w-8",
25
+ sm: "h-[22px] w-10",
26
+ md: "h-[26px] w-[47px]",
26
27
  },
27
28
  },
28
29
  defaultVariants: {
@@ -45,9 +46,9 @@ const thumbVariants = cva(
45
46
  {
46
47
  variants: {
47
48
  size: {
48
- xs: "size-3 data-[state=checked]:translate-x-[12px]",
49
- sm: "size-4 data-[state=checked]:translate-x-[16px]",
50
- md: "size-5 data-[state=checked]:translate-x-[20px]",
49
+ xs: "size-3 data-[state=checked]:translate-x-[16px]",
50
+ sm: "size-4 data-[state=checked]:translate-x-[20px]",
51
+ md: "size-5 data-[state=checked]:translate-x-[23px]",
51
52
  },
52
53
  },
53
54
  defaultVariants: {