@stasho/ds 0.16.1 → 0.17.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.16.1",
3
+ "version": "0.17.0",
4
4
  "description": "stasho design system",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -45,6 +45,7 @@
45
45
  "./selectable-card": "./src/components/selectable-card/selectable-card.tsx",
46
46
  "./ui/skeleton": "./src/components/ui/skeleton.tsx",
47
47
  "./multi-select": "./src/components/multi-select/multi-select.tsx",
48
+ "./number-input": "./src/components/number-input/number-input.tsx",
48
49
  "./pagination": "./src/components/pagination/pagination.tsx",
49
50
  "./copyable-text": "./src/components/copyable-text/copyable-text.tsx",
50
51
  "./dialog": "./src/components/dialog/dialog.tsx",
@@ -89,7 +89,7 @@ const buttonVariants = cva(
89
89
  // dark (overrides): white chrome — outline is neutral, NOT accent (supersedes the outline half of Decision #82)
90
90
  "dark:text-white dark:border-white/40",
91
91
  "dark:hover:border-white",
92
- "dark:disabled:text-white/30 dark:disabled:border-white/10 dark:disabled:bg-transparent",
92
+ "dark:disabled:text-white/30 dark:disabled:border-white/10 dark:disabled:bg-neutral-900",
93
93
  ].join(" "),
94
94
  ghost: [
95
95
  // light (base): foreground text, surface hover
@@ -4,20 +4,22 @@ import { Command } from "cmdk";
4
4
  import { cva, type VariantProps } from "class-variance-authority";
5
5
  import { CaretDown, Check } from "@phosphor-icons/react";
6
6
  import { cn } from "@ac/lib/cn";
7
+ import {
8
+ fieldChassis,
9
+ fieldDisabled,
10
+ fieldError,
11
+ fieldFocus,
12
+ fieldTriggerHover,
13
+ } from "@ac/lib/field-chassis";
7
14
 
8
15
  const triggerVariants = cva(
9
16
  [
10
17
  "inline-flex items-center justify-between",
11
18
  "w-full font-sans text-foreground",
12
- "bg-background dark:bg-surface",
13
- "border border-edge rounded-sm",
14
- "hover:border-edge-hover",
15
- "focus-visible:outline-none",
16
- "focus-visible:border-accent-700 dark:focus-visible:border-accent",
17
- "disabled:bg-muted dark:disabled:bg-background",
18
- "disabled:border-edge/50",
19
- "disabled:text-foreground/30",
20
- "disabled:cursor-not-allowed",
19
+ fieldChassis,
20
+ fieldTriggerHover,
21
+ fieldFocus,
22
+ fieldDisabled,
21
23
  "transition-colors",
22
24
  ].join(" "),
23
25
  {
@@ -87,8 +89,7 @@ const Combobox = forwardRef<HTMLButtonElement, ComboboxProps>(
87
89
  aria-invalid={error || undefined}
88
90
  className={cn(
89
91
  triggerVariants({ size }),
90
- error &&
91
- "border-error hover:border-error focus-visible:border-error dark:focus-visible:border-error",
92
+ error && fieldError,
92
93
  !selectedLabel &&
93
94
  (disabled ? "text-muted-foreground/50" : "text-muted-foreground"),
94
95
  className,
@@ -1,20 +1,21 @@
1
1
  import { forwardRef, type InputHTMLAttributes } from "react";
2
2
  import { cva, type VariantProps } from "class-variance-authority";
3
3
  import { cn } from "@ac/lib/cn";
4
+ import {
5
+ fieldChassis,
6
+ fieldDisabled,
7
+ fieldError,
8
+ fieldFocus,
9
+ } from "@ac/lib/field-chassis";
4
10
 
5
11
  const inputVariants = cva(
6
12
  [
7
13
  "w-full font-sans text-foreground",
8
- "bg-background dark:bg-surface",
9
- "border border-edge rounded-sm",
14
+ fieldChassis,
10
15
  "placeholder:text-muted-foreground",
11
- "focus-visible:outline-none",
12
- "focus-visible:border-accent-700 dark:focus-visible:border-accent",
13
- "disabled:bg-muted dark:disabled:bg-background",
14
- "disabled:border-edge/50",
15
- "disabled:text-foreground/30",
16
+ fieldFocus,
17
+ fieldDisabled,
16
18
  "disabled:placeholder:text-muted-foreground/50",
17
- "disabled:cursor-not-allowed",
18
19
  "transition-colors",
19
20
  ].join(" "),
20
21
  {
@@ -42,8 +43,7 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
42
43
  ref={ref}
43
44
  className={cn(
44
45
  inputVariants({ size }),
45
- error &&
46
- "border-error hover:border-error focus-visible:border-error dark:focus-visible:border-error",
46
+ error && fieldError,
47
47
  className,
48
48
  )}
49
49
  aria-invalid={error || undefined}
@@ -4,16 +4,24 @@ import { Command } from "cmdk";
4
4
  import { cva, type VariantProps } from "class-variance-authority";
5
5
  import { CaretDown, Check, X } from "@phosphor-icons/react";
6
6
  import { cn } from "@ac/lib/cn";
7
+ import {
8
+ fieldChassis,
9
+ fieldError,
10
+ fieldFocus,
11
+ fieldTriggerHover,
12
+ } from "@ac/lib/field-chassis";
7
13
 
8
14
  const triggerVariants = cva(
9
15
  [
10
16
  "inline-flex items-center gap-1.5",
11
17
  "w-full font-sans text-foreground",
12
- "bg-background dark:bg-surface",
13
- "border border-edge rounded-sm",
14
- "hover:border-edge-hover",
15
- "focus-visible:outline-none",
16
- "focus-visible:border-accent-700 dark:focus-visible:border-accent",
18
+ fieldChassis,
19
+ fieldTriggerHover,
20
+ fieldFocus,
21
+ // MultiSelect's trigger is a role="button" div, not a native disabled
22
+ // control — it uses aria-disabled: instead of the shared fieldDisabled
23
+ // (disabled:) constant. Kept component-side; see docs/ARCHITECTURE.md
24
+ // § Shared Chassis Constants (`field-chassis.ts`).
17
25
  "aria-disabled:bg-muted dark:aria-disabled:bg-background",
18
26
  "aria-disabled:border-edge/50",
19
27
  "aria-disabled:text-foreground/30",
@@ -141,8 +149,7 @@ const MultiSelect = forwardRef<HTMLDivElement, MultiSelectProps>(
141
149
  className={cn(
142
150
  triggerVariants({ size }),
143
151
  "cursor-pointer",
144
- error &&
145
- "border-error hover:border-error focus-visible:border-error dark:focus-visible:border-error",
152
+ error && fieldError,
146
153
  !hasSelection &&
147
154
  (disabled
148
155
  ? "text-muted-foreground/50"
@@ -0,0 +1,103 @@
1
+ import { forwardRef, useRef, type InputHTMLAttributes } from "react";
2
+ import { CaretDown, CaretUp } from "@phosphor-icons/react";
3
+ import { cn } from "@ac/lib/cn";
4
+ import { fieldChassis } from "@ac/lib/field-chassis";
5
+
6
+ type NumberInputProps = Omit<InputHTMLAttributes<HTMLInputElement>, "size"> & {
7
+ size?: "sm" | "md";
8
+ error?: boolean;
9
+ };
10
+
11
+ const sizeClasses = {
12
+ sm: "py-1.5 pl-4 text-sm",
13
+ md: "py-2 pl-5 text-base",
14
+ };
15
+
16
+ const stepButtonClasses =
17
+ "flex items-center justify-center cursor-pointer transition-colors text-foreground/60 hover:text-accent disabled:text-foreground/30 disabled:cursor-not-allowed disabled:hover:text-foreground/30";
18
+
19
+ const NumberInput = forwardRef<HTMLInputElement, NumberInputProps>(
20
+ ({ size = "md", error = false, disabled = false, className, ...rest }, ref) => {
21
+ const inputRef = useRef<HTMLInputElement>(null);
22
+
23
+ const setRefs = (node: HTMLInputElement | null) => {
24
+ inputRef.current = node;
25
+ if (typeof ref === "function") {
26
+ ref(node);
27
+ } else if (ref) {
28
+ ref.current = node;
29
+ }
30
+ };
31
+
32
+ const step = (direction: "up" | "down") => {
33
+ const input = inputRef.current;
34
+ if (!input || disabled) return;
35
+ if (direction === "up") {
36
+ input.stepUp();
37
+ } else {
38
+ input.stepDown();
39
+ }
40
+ input.dispatchEvent(new Event("change", { bubbles: true }));
41
+ };
42
+
43
+ return (
44
+ <div
45
+ className={cn(
46
+ "flex w-full items-center font-sans text-foreground",
47
+ fieldChassis,
48
+ "transition-colors",
49
+ "focus-within:border-accent-700 dark:focus-within:border-accent",
50
+ disabled &&
51
+ "bg-muted dark:bg-background border-edge/50 cursor-not-allowed",
52
+ error && [
53
+ "border-error",
54
+ "focus-within:border-error dark:focus-within:border-error",
55
+ ],
56
+ sizeClasses[size],
57
+ className,
58
+ )}
59
+ >
60
+ <input
61
+ ref={setRefs}
62
+ disabled={disabled}
63
+ className={cn(
64
+ "w-full bg-transparent focus-visible:outline-none",
65
+ "[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none",
66
+ "placeholder:text-muted-foreground",
67
+ "disabled:placeholder:text-muted-foreground/50",
68
+ disabled && "text-foreground/30 cursor-not-allowed",
69
+ )}
70
+ aria-invalid={error || undefined}
71
+ {...rest}
72
+ type="number"
73
+ />
74
+ <div className="flex flex-col pr-1 -my-0.5">
75
+ <button
76
+ type="button"
77
+ tabIndex={-1}
78
+ disabled={disabled}
79
+ aria-label="Increase value"
80
+ className={stepButtonClasses}
81
+ onClick={() => step("up")}
82
+ >
83
+ <CaretUp weight="bold" className="size-3" />
84
+ </button>
85
+ <button
86
+ type="button"
87
+ tabIndex={-1}
88
+ disabled={disabled}
89
+ aria-label="Decrease value"
90
+ className={stepButtonClasses}
91
+ onClick={() => step("down")}
92
+ >
93
+ <CaretDown weight="bold" className="size-3" />
94
+ </button>
95
+ </div>
96
+ </div>
97
+ );
98
+ },
99
+ );
100
+
101
+ NumberInput.displayName = "NumberInput";
102
+
103
+ export { NumberInput, type NumberInputProps };
@@ -3,20 +3,22 @@ import { Select as SelectPrimitive } from "radix-ui";
3
3
  import { cva, type VariantProps } from "class-variance-authority";
4
4
  import { CaretDown, Check } from "@phosphor-icons/react";
5
5
  import { cn } from "@ac/lib/cn";
6
+ import {
7
+ fieldChassis,
8
+ fieldDisabled,
9
+ fieldError,
10
+ fieldFocus,
11
+ fieldTriggerHover,
12
+ } from "@ac/lib/field-chassis";
6
13
 
7
14
  const triggerVariants = cva(
8
15
  [
9
16
  "inline-flex items-center justify-between",
10
17
  "w-full font-sans text-foreground",
11
- "bg-background dark:bg-surface",
12
- "border border-edge rounded-sm",
13
- "hover:border-edge-hover",
14
- "focus-visible:outline-none",
15
- "focus-visible:border-accent-700 dark:focus-visible:border-accent",
16
- "disabled:bg-muted dark:disabled:bg-background",
17
- "disabled:border-edge/50",
18
- "disabled:text-foreground/30",
19
- "disabled:cursor-not-allowed",
18
+ fieldChassis,
19
+ fieldTriggerHover,
20
+ fieldFocus,
21
+ fieldDisabled,
20
22
  "transition-colors",
21
23
  "data-[placeholder]:text-muted-foreground",
22
24
  "disabled:data-[placeholder]:text-muted-foreground/50",
@@ -76,8 +78,7 @@ const Select = forwardRef<HTMLButtonElement, SelectProps>(
76
78
  aria-invalid={error || undefined}
77
79
  className={cn(
78
80
  triggerVariants({ size }),
79
- error &&
80
- "border-error hover:border-error focus-visible:border-error dark:focus-visible:border-error",
81
+ error && fieldError,
81
82
  className,
82
83
  )}
83
84
  >
@@ -1,20 +1,21 @@
1
1
  import { forwardRef, type TextareaHTMLAttributes } from "react";
2
2
  import { cva, type VariantProps } from "class-variance-authority";
3
3
  import { cn } from "@ac/lib/cn";
4
+ import {
5
+ fieldChassis,
6
+ fieldDisabled,
7
+ fieldError,
8
+ fieldFocus,
9
+ } from "@ac/lib/field-chassis";
4
10
 
5
11
  const textareaVariants = cva(
6
12
  [
7
13
  "w-full font-sans text-foreground",
8
- "bg-background dark:bg-surface",
9
- "border border-edge rounded-sm",
14
+ fieldChassis,
10
15
  "placeholder:text-muted-foreground",
11
- "focus-visible:outline-none",
12
- "focus-visible:border-accent-700 dark:focus-visible:border-accent",
13
- "disabled:bg-muted dark:disabled:bg-background",
14
- "disabled:border-edge/50",
15
- "disabled:text-foreground/30",
16
+ fieldFocus,
17
+ fieldDisabled,
16
18
  "disabled:placeholder:text-muted-foreground/50",
17
- "disabled:cursor-not-allowed",
18
19
  "resize-y transition-colors",
19
20
  ].join(" "),
20
21
  {
@@ -46,8 +47,7 @@ const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
46
47
  rows={rows}
47
48
  className={cn(
48
49
  textareaVariants({ size }),
49
- error &&
50
- "border-error hover:border-error focus-visible:border-error dark:focus-visible:border-error",
50
+ error && fieldError,
51
51
  className,
52
52
  )}
53
53
  aria-invalid={error || undefined}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Shared "flat slot" chassis classes for form controls (Input, Textarea, and
3
+ * the Select / Combobox / MultiSelect triggers). Extracted from the subsets
4
+ * that are genuinely identical across those five components — see
5
+ * docs/SKIN-PRINCIPLES.md for the underlying rules these encode.
6
+ *
7
+ * Not every component consumes every constant; some classes stay
8
+ * component-side because they diverge (e.g. MultiSelect is a `role="button"`
9
+ * div and uses `aria-disabled:` instead of `disabled:`, so it does not
10
+ * consume `fieldDisabled`).
11
+ */
12
+
13
+ /** Rest-state surface: fill + hairline border + radius floor. */
14
+ export const fieldChassis =
15
+ "bg-background dark:bg-surface border border-edge rounded-sm";
16
+
17
+ /** Cyan hairline focus pair. */
18
+ export const fieldFocus =
19
+ "focus-visible:outline-none focus-visible:border-accent-700 dark:focus-visible:border-accent";
20
+
21
+ /**
22
+ * Flat-sink disabled cluster for controls using the native `disabled:`
23
+ * pseudo-class (Input, Textarea, Select, Combobox).
24
+ */
25
+ export const fieldDisabled =
26
+ "disabled:bg-muted dark:disabled:bg-background disabled:border-edge/50 disabled:text-foreground/30 disabled:cursor-not-allowed";
27
+
28
+ /** Error rail applied on top of the chassis when a control is invalid. */
29
+ export const fieldError =
30
+ "border-error hover:border-error focus-visible:border-error dark:focus-visible:border-error";
31
+
32
+ /** Dropdown-trigger hover cue (Select, Combobox, MultiSelect only). */
33
+ export const fieldTriggerHover = "hover:border-edge-hover";