@vitavision/ui 0.10.0 → 0.11.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": "@vitavision/ui",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "The vitavision instrument design system: Tailwind v4 tokens, theme, and React primitives.",
5
5
  "license": "MIT OR Apache-2.0",
6
6
  "repository": {
@@ -12,7 +12,10 @@ import { cn, focusRing } from "./cn";
12
12
  */
13
13
  export type ButtonVariant = "primary" | "secondary" | "ghost" | "danger";
14
14
 
15
- /** A button's height and padding. `md` is the comfortable density's size, `sm` the compact one's. */
15
+ /**
16
+ * A control's height and padding, shared by `Button`, `SegmentedControl` and `ToggleChip`. For a
17
+ * button, `md` is the comfortable density's size and `sm` the compact one's.
18
+ */
16
19
  export type ButtonSize = "sm" | "md";
17
20
 
18
21
  const VARIANT_CLASSES: Record<ButtonVariant, string> = {
@@ -17,9 +17,21 @@
17
17
 
18
18
  import { useId } from "react";
19
19
 
20
+ import type { ButtonSize } from "./Button";
20
21
  import { useFieldDescription } from "./Field";
21
22
  import { cn } from "./cn";
22
23
 
24
+ /**
25
+ * The segment's own box, by size. `sm` is the toolbar size and the default. `md` sets the
26
+ * height explicitly rather than leaving it to padding plus line-height: the strip around it
27
+ * is `h-8` with a 1px border and `p-0.5`, which leaves 26px of content box, and `h-full`
28
+ * fills exactly that whatever the font's line height turns out to be.
29
+ */
30
+ const SEGMENT_SIZE_CLASSES: Record<ButtonSize, string> = {
31
+ sm: "px-2.5 py-1 text-xs",
32
+ md: "flex h-full items-center px-3.5 text-sm",
33
+ };
34
+
23
35
  /**
24
36
  * A choice among two or three, shown as a strip of segments (native radios in one group, so
25
37
  * arrow keys move between them).
@@ -28,6 +40,9 @@ import { cn } from "./cn";
28
40
  * or `defaultValue` while `value` is `""` — and choosing the segment that matches
29
41
  * `defaultValue` reports `""`, so the default stays defined in one place. Each segment
30
42
  * carries `data-state="checked"` or `"unchecked"`; the group carries `data-disabled`.
43
+ *
44
+ * Toolbar-sized by default. In a row with a comfortable-density `Input`, `Select` or
45
+ * `Button`, pass `size="md"` and the strip's outer height matches theirs.
31
46
  */
32
47
  export function SegmentedControl({
33
48
  value,
@@ -36,6 +51,7 @@ export function SegmentedControl({
36
51
  onValueChange,
37
52
  disabled = false,
38
53
  "aria-label": ariaLabel,
54
+ size = "sm",
39
55
  className,
40
56
  }: {
41
57
  /** `""` when the field is untouched. */
@@ -50,6 +66,13 @@ export function SegmentedControl({
50
66
  disabled?: boolean | undefined;
51
67
  /** Names the radio group. */
52
68
  "aria-label"?: string | undefined;
69
+ /**
70
+ * The strip's height: `sm` (the default) is the toolbar size, about 30px with `text-xs`
71
+ * labels; `md` is exactly 32px (`h-8`) outside, border included, with `text-sm` labels, the
72
+ * height of a comfortable-density `Input` or `Button`. Not derived from the density in
73
+ * force, so a consumer that never passes it renders as it always did.
74
+ */
75
+ size?: ButtonSize | undefined;
53
76
  /** Merged with the strip's own classes through `cn`. */
54
77
  className?: string | undefined;
55
78
  }) {
@@ -68,6 +91,7 @@ export function SegmentedControl({
68
91
  // its default `1 1 auto` gives up width first, and the segment labels are what pay for
69
92
  // it. Let the row scroll instead of squeezing the choice.
70
93
  "inline-flex w-fit max-w-full shrink-0 items-center gap-0.5 rounded-control border border-line-strong bg-raised p-0.5",
94
+ size === "md" && "h-8",
71
95
  disabled && "opacity-50",
72
96
  className,
73
97
  )}
@@ -82,7 +106,9 @@ export function SegmentedControl({
82
106
  // `whitespace-nowrap` or a two-word label's min-content width is its longest
83
107
  // *word*, and "Side by side" is free to stack onto three lines inside a
84
108
  // fixed-height strip that then clips it.
85
- "relative cursor-pointer whitespace-nowrap rounded-[0.3rem] px-2.5 py-1 text-xs font-medium transition-colors",
109
+ "relative cursor-pointer whitespace-nowrap rounded-[0.3rem]",
110
+ SEGMENT_SIZE_CLASSES[size],
111
+ "font-medium transition-colors",
86
112
  "has-focus-visible:outline-2 has-focus-visible:outline-offset-1 has-focus-visible:outline-signal",
87
113
  checked
88
114
  ? "bg-surface text-fg shadow-sm"
@@ -14,13 +14,32 @@
14
14
 
15
15
  import type { ReactNode } from "react";
16
16
 
17
+ import type { ButtonSize } from "./Button";
17
18
  import { cn, focusRing } from "./cn";
18
19
 
20
+ /**
21
+ * The chip's box, by size. `sm` is the toolbar size and the default. `md` sets the height
22
+ * explicitly (`h-8`, border included) rather than leaving it to padding plus line-height.
23
+ */
24
+ const SIZE_CLASSES: Record<ButtonSize, string> = {
25
+ sm: "px-2 py-1 text-xs",
26
+ md: "h-8 px-3 text-sm",
27
+ };
28
+
29
+ /** The swatch dot, by size: a little larger beside `text-sm` so it keeps its weight against the label. */
30
+ const SWATCH_SIZE_CLASSES: Record<ButtonSize, string> = {
31
+ sm: "size-2",
32
+ md: "size-2.5",
33
+ };
34
+
19
35
  /**
20
36
  * A dense on/off chip for a layer drawn on screen, with an optional swatch in the layer's
21
37
  * colour: a toolbar-sized `Switch` (`role="switch"`, `aria-checked`).
22
38
  *
23
39
  * Controlled. State is exposed as `data-state` (`checked`/`unchecked`).
40
+ *
41
+ * Toolbar-sized by default. In a row with a comfortable-density `Input`, `Select` or
42
+ * `Button`, pass `size="md"` and the chip's outer height matches theirs.
24
43
  */
25
44
  export function ToggleChip({
26
45
  checked,
@@ -29,6 +48,7 @@ export function ToggleChip({
29
48
  swatch,
30
49
  disabled = false,
31
50
  title,
51
+ size = "sm",
32
52
  className,
33
53
  }: {
34
54
  /** Whether the layer is shown. */
@@ -43,6 +63,14 @@ export function ToggleChip({
43
63
  disabled?: boolean | undefined;
44
64
  /** Why the control is disabled, or what the layer is. Never the only explanation. */
45
65
  title?: string | undefined;
66
+ /**
67
+ * The chip's height: `sm` (the default) is the toolbar size, about 26px with `text-xs`
68
+ * text; `md` is exactly 32px (`h-8`) outside, border included, with `text-sm` text and a
69
+ * slightly larger swatch, the height of a comfortable-density `Input` or `Button`. Not
70
+ * derived from the density in force, so a consumer that never passes it renders as it
71
+ * always did.
72
+ */
73
+ size?: ButtonSize | undefined;
46
74
  /** Merged with the chip's own classes through `cn`. */
47
75
  className?: string | undefined;
48
76
  }) {
@@ -56,7 +84,9 @@ export function ToggleChip({
56
84
  title={title}
57
85
  onClick={() => onCheckedChange(!checked)}
58
86
  className={cn(
59
- "inline-flex items-center gap-1.5 rounded-control border px-2 py-1 text-xs whitespace-nowrap transition-colors",
87
+ "inline-flex items-center gap-1.5 rounded-control border",
88
+ SIZE_CLASSES[size],
89
+ "whitespace-nowrap transition-colors",
60
90
  checked
61
91
  ? "border-line-strong bg-raised text-fg"
62
92
  : "border-line bg-transparent text-fg-muted hover:text-fg",
@@ -69,7 +99,8 @@ export function ToggleChip({
69
99
  <span
70
100
  aria-hidden
71
101
  className={cn(
72
- "size-2 shrink-0 rounded-full border transition-opacity",
102
+ SWATCH_SIZE_CLASSES[size],
103
+ "shrink-0 rounded-full border transition-opacity",
73
104
  checked ? "opacity-100" : "opacity-30",
74
105
  )}
75
106
  style={{ backgroundColor: swatch, borderColor: swatch }}