@instruments/materia-ui 0.2.0 → 0.3.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.
Files changed (35) hide show
  1. package/dist/components/animated-number/number-digit.d.ts +5 -3
  2. package/dist/components/animated-number/number-digit.js +39 -16
  3. package/dist/components/animated-number/number-section.d.ts +7 -5
  4. package/dist/components/animated-number/number-section.js +81 -65
  5. package/dist/components/animated-number/number-symbol.js +2 -0
  6. package/dist/components/animated-number/target-widths.d.ts +7 -0
  7. package/dist/components/animated-number/target-widths.js +13 -1
  8. package/dist/components/button/button-variants.d.ts +2 -1
  9. package/dist/components/button/button-variants.js +2 -1
  10. package/dist/components/pill/pill-variants.d.ts +3 -0
  11. package/dist/components/pill/pill-variants.js +8 -1
  12. package/dist/components/segmented-control/segmented-control-indicator.js +1 -1
  13. package/dist/components/segmented-control/segmented-control-item.js +1 -1
  14. package/dist/components/segmented-control/segmented-control-root.js +1 -1
  15. package/dist/components/segmented-control/segmented-control-variants.js +1 -1
  16. package/dist/components/stepper/stepper-item.js +6 -2
  17. package/dist/components/stepper/stepper-variants.d.ts +1 -0
  18. package/dist/components/stepper/stepper-variants.js +11 -1
  19. package/dist/components/tab-navigation/tab-navigation-link.js +1 -1
  20. package/dist/components/tabs/tabs-indicator.js +1 -1
  21. package/dist/components/tabs/tabs-trigger.js +1 -1
  22. package/dist/components/tabs/tabs-variants.js +1 -1
  23. package/dist/data/docs-manifest.d.ts +1 -1
  24. package/dist/data/docs-manifest.js +5 -50
  25. package/dist/data/token-gallery.js +1 -1
  26. package/dist/styles/index.css +7 -1
  27. package/dist/tokens/emphasis.d.ts +5 -0
  28. package/dist/tokens/emphasis.js +5 -0
  29. package/dist/{components/segmented/segmented-variants.d.ts → tokens/segmented.d.ts} +12 -1
  30. package/dist/{components/segmented/segmented-variants.js → tokens/segmented.js} +11 -0
  31. package/package.json +5 -5
  32. package/dist/components/segmented/segmented-root.d.ts +0 -12
  33. package/dist/components/segmented/segmented-root.js +0 -12
  34. package/dist/components/segmented.d.ts +0 -1
  35. package/dist/components/segmented.js +0 -1
@@ -15,10 +15,12 @@ interface NumberDigitProps extends HTMLMotionProps<"span"> {
15
15
  * through every intermediate numeral in either direction, including across the
16
16
  * 9/0 wrap when `trend` asks for it. New leading digits enter with
17
17
  * `initialValue={0}` and roll up from zero; exiting digits roll to zero so
18
- * removal also animates. Per-digit width animates to the target glyph width so
19
- * proportional fonts stay aligned.
18
+ * removal also animates. The column's real width tweens to the target glyph
19
+ * width on the roll's timing, so proportional fonts stay aligned without a
20
+ * one-frame jump.
20
21
  *
21
- * Under reduced motion the FLIP is skipped: the digit snaps to its value.
22
+ * Under reduced motion the FLIP and the width tween are skipped: the digit
23
+ * snaps to its value and width.
22
24
  */
23
25
  export declare const NumberDigit: ({ value: _value, trend, ...rest }: NumberDigitProps) => import("react").JSX.Element;
24
26
  export {};
@@ -1,11 +1,11 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { animate, MotionConfigContext, PresenceContext, useIsPresent, useReducedMotion, } from "motion/react";
3
+ import { animate, MotionConfigContext, PresenceContext, useIsPresent, useMotionValue, useReducedMotion, } from "motion/react";
4
4
  import * as m from "motion/react-m";
5
- import { use, useEffect, useLayoutEffect, useRef, useState } from "react";
5
+ import { use, useEffect, useLayoutEffect, useRef } from "react";
6
6
  import { getWidthInEm } from "./get-width-in-em.js";
7
7
  import { maskHeight } from "./mask.js";
8
- import { targetWidths } from "./target-widths.js";
8
+ import { targetWidths, widthTiming } from "./target-widths.js";
9
9
  const mod = (n, base) => ((n % base) + base) % base;
10
10
  /** The nine digits above and below `value`, wrapping mod 10. */
11
11
  const ABOVE_OFFSETS = [9, 8, 7, 6, 5, 4, 3, 2, 1];
@@ -24,10 +24,12 @@ const digitFillStyle = {
24
24
  * through every intermediate numeral in either direction, including across the
25
25
  * 9/0 wrap when `trend` asks for it. New leading digits enter with
26
26
  * `initialValue={0}` and roll up from zero; exiting digits roll to zero so
27
- * removal also animates. Per-digit width animates to the target glyph width so
28
- * proportional fonts stay aligned.
27
+ * removal also animates. The column's real width tweens to the target glyph
28
+ * width on the roll's timing, so proportional fonts stay aligned without a
29
+ * one-frame jump.
29
30
  *
30
- * Under reduced motion the FLIP is skipped: the digit snaps to its value.
31
+ * Under reduced motion the FLIP and the width tween are skipped: the digit
32
+ * snaps to its value and width.
31
33
  */
32
34
  export const NumberDigit = ({ value: _value, trend = 0, ...rest }) => {
33
35
  const { transition } = use(MotionConfigContext);
@@ -45,15 +47,23 @@ export const NumberDigit = ({ value: _value, trend = 0, ...rest }) => {
45
47
  // Exit isn't a normal AnimatePresence exit because it must trigger a resize.
46
48
  const isPresent = useIsPresent();
47
49
  const value = isPresent ? _value : 0;
48
- // Set the width to the initial value's width immediately, so the next render
49
- // animates from it.
50
+ // The column's real layout width, tweened to each value's glyph width so a
51
+ // proportional font never jumps its neighbours. Natural until mounted.
52
+ const width = useMotionValue("auto");
53
+ // Set the width to the initial value's width before paint, so the first
54
+ // change tweens from it.
50
55
  useLayoutEffect(() => {
51
56
  const initialEl = numberRefs.current[initialValueRef.current];
52
57
  if (!scope.current || !initialEl) {
53
58
  return;
54
59
  }
55
- scope.current.style.width = getWidthInEm(initialEl);
56
- }, []);
60
+ const initialWidth = getWidthInEm(initialEl);
61
+ scope.current.style.width = initialWidth;
62
+ width.jump(initialWidth);
63
+ }, [width]);
64
+ useEffect(() => () => {
65
+ width.stop();
66
+ }, [width]);
57
67
  // FLIP the y in a layout effect. initialY is the pixel offset from the new
58
68
  // resting position, including any in-flight offset (inner vs outer rect top)
59
69
  // so mid-roll re-targets stay continuous.
@@ -83,24 +93,37 @@ export const NumberDigit = ({ value: _value, trend = 0, ...rest }) => {
83
93
  // (equivalent to the original's cleanup, without a returned teardown).
84
94
  prevValue.current = value;
85
95
  }, [value, trend, shouldReduceMotion, transition]);
86
- const [width, setWidth] = useState();
96
+ const sizedValue = useRef(null);
87
97
  useEffect(() => {
88
98
  const el = numberRefs.current[value];
89
- if (!el) {
99
+ if (!el || sizedValue.current === value) {
90
100
  return;
91
101
  }
102
+ sizedValue.current = value;
92
103
  const w = getWidthInEm(el);
93
104
  // Store the target width immediately, so the section resize can use it.
94
105
  if (ref.current) {
95
106
  targetWidths.set(ref.current, w);
96
107
  }
97
- // Trigger the layout animation by causing another render.
98
- setWidth(w);
99
- }, [value]);
108
+ if (shouldReduceMotion === true || width.get() === w) {
109
+ width.jump(w);
110
+ return;
111
+ }
112
+ // A change mid-tween retargets from the width it has reached.
113
+ animate(width, w, widthTiming(transition));
114
+ }, [value, shouldReduceMotion, transition, width]);
100
115
  const renderNumber = (digit) => (_jsx("span", { ref: (r) => {
101
116
  numberRefs.current[digit] = r;
102
117
  }, style: { display: "inline-block", padding: `calc(${maskHeight}/2) 0` }, children: digit }, digit));
103
- return (_jsx(m.span, { ...rest, "data-state": isPresent ? undefined : "exiting", layout: "position", ref: ref, style: { display: "inline-flex", justifyContent: "center", width }, children: _jsxs("span", { ref: scope, style: {
118
+ return (_jsx(m.span, { ...rest, "data-state": isPresent ? undefined : "exiting", layout: "position", ref: ref,
119
+ // Never shrinks: while the section's width tweens, the glyphs overflow
120
+ // it at their own widths rather than squeezing.
121
+ style: {
122
+ display: "inline-flex",
123
+ flexShrink: 0,
124
+ justifyContent: "center",
125
+ width,
126
+ }, children: _jsxs("span", { ref: scope, style: {
104
127
  alignItems: "center",
105
128
  display: "inline-flex",
106
129
  flexDirection: "column",
@@ -12,11 +12,13 @@ interface NumberSectionProps extends HTMLMotionProps<"span"> {
12
12
  }
13
13
  /**
14
14
  * A run of digits/symbols (pre, integer, fraction, post) sharing one anchor.
15
- * It holds a fixed measured width so a new glyph is appended at the end before
16
- * the layout animation begins - making it look like it was already there - then
17
- * re-measures by momentarily removing exiting children and applying the digits'
18
- * target widths (the exact resize trick), so reflow accounts for negative
19
- * margins between glyphs.
15
+ *
16
+ * Its real layout width is a Motion value: when the glyphs change it measures
17
+ * where the width will settle (exiting glyphs removed, digits at their target
18
+ * widths) and tweens to it on the roll's timing, so nothing laid out around the
19
+ * number jumps when a digit, separator, prefix or suffix arrives or leaves. In
20
+ * the meantime the glyphs hold to the section's anchor edge and overflow the
21
+ * other, which the digit mask clips. Under reduced motion the width snaps.
20
22
  */
21
23
  export declare const NumberSection: ({ parts, justify, mode, style, name, layoutDependency, trend, ...rest }: NumberSectionProps) => import("react").JSX.Element;
22
24
  export {};
@@ -1,93 +1,109 @@
1
1
  "use client";
2
- // Port of Motion+ AnimateNumber (motion-plus 2.0.0).
3
- "use client";
4
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
5
- import { AnimatePresence } from "motion/react";
3
+ import { animate, AnimatePresence, MotionConfigContext, useMotionValue, useReducedMotion, } from "motion/react";
6
4
  import * as m from "motion/react-m";
7
- import { useEffect, useMemo, useRef, useState } from "react";
5
+ import { use, useEffect, useMemo, useRef } from "react";
8
6
  import { isDigitPart } from "./format-parts.js";
9
7
  import { getWidthInEm } from "./get-width-in-em.js";
10
8
  import { NumberDigit } from "./number-digit.js";
11
9
  import { NumberSymbol } from "./number-symbol.js";
12
10
  import { SectionContext } from "./section-context.js";
13
- import { targetWidths } from "./target-widths.js";
11
+ import { targetWidths, widthTiming } from "./target-widths.js";
12
+ /**
13
+ * The section's natural width once the change lands, in `em`: exiting glyphs
14
+ * removed and each digit at its target width, measured at `max-content` and
15
+ * then put back as it was, in one synchronous pass so nothing paints between.
16
+ */
17
+ const measureTargetWidth = (section) => {
18
+ const undos = [];
19
+ // A static list: the loop removes children as it goes.
20
+ for (const child of section.querySelectorAll(":scope > *")) {
21
+ if (!(child instanceof HTMLElement)) {
22
+ continue;
23
+ }
24
+ if (child.dataset.state === "exiting") {
25
+ const next = child.nextSibling;
26
+ child.remove();
27
+ undos.push(() => {
28
+ if (next === null) {
29
+ section.append(child);
30
+ }
31
+ else {
32
+ next.before(child);
33
+ }
34
+ });
35
+ continue;
36
+ }
37
+ const target = targetWidths.get(child);
38
+ if (target === undefined) {
39
+ continue;
40
+ }
41
+ const current = child.style.width;
42
+ child.style.width = target;
43
+ undos.push(() => {
44
+ child.style.width = current;
45
+ });
46
+ }
47
+ const held = section.style.width;
48
+ section.style.width = "max-content";
49
+ const width = getWidthInEm(section);
50
+ section.style.width = held;
51
+ for (let i = undos.length - 1; i >= 0; i -= 1) {
52
+ undos[i]?.();
53
+ }
54
+ return width;
55
+ };
14
56
  /**
15
57
  * A run of digits/symbols (pre, integer, fraction, post) sharing one anchor.
16
- * It holds a fixed measured width so a new glyph is appended at the end before
17
- * the layout animation begins - making it look like it was already there - then
18
- * re-measures by momentarily removing exiting children and applying the digits'
19
- * target widths (the exact resize trick), so reflow accounts for negative
20
- * margins between glyphs.
58
+ *
59
+ * Its real layout width is a Motion value: when the glyphs change it measures
60
+ * where the width will settle (exiting glyphs removed, digits at their target
61
+ * widths) and tweens to it on the roll's timing, so nothing laid out around the
62
+ * number jumps when a digit, separator, prefix or suffix arrives or leaves. In
63
+ * the meantime the glyphs hold to the section's anchor edge and overflow the
64
+ * other, which the digit mask clips. Under reduced motion the width snaps.
21
65
  */
22
66
  export const NumberSection = ({ parts, justify = "left", mode, style, name, layoutDependency, trend, ...rest }) => {
23
67
  const ref = useRef(null);
24
68
  const context = useMemo(() => ({ justify }), [justify]);
25
- const measuredRef = useRef(null);
26
- // A fixed width for the section so new characters are added at the end
27
- // before the layout animation starts (they look like they were there).
28
- const [width, setWidth] = useState();
69
+ const { transition } = use(MotionConfigContext);
70
+ const shouldReduceMotion = useReducedMotion();
71
+ // Natural until mounted, then always an `em` length so it can tween.
72
+ const width = useMotionValue("auto");
29
73
  const partsSignature = parts.map((p) => p.value).join("");
30
- // On mount, fix the width to the first measured width.
31
74
  useEffect(() => {
32
- const measured = measuredRef.current;
33
- if (measured && ref.current) {
34
- ref.current.style.width = getWidthInEm(measured);
75
+ if (ref.current) {
76
+ width.set(getWidthInEm(ref.current));
35
77
  }
36
- }, []);
78
+ return () => {
79
+ width.stop();
80
+ };
81
+ }, [width]);
37
82
  const measuredSignature = useRef(partsSignature);
38
83
  useEffect(() => {
39
- const measured = measuredRef.current;
40
- if (!measured || measuredSignature.current === partsSignature) {
84
+ const section = ref.current;
85
+ if (!section || measuredSignature.current === partsSignature) {
41
86
  return;
42
87
  }
43
88
  measuredSignature.current = partsSignature;
44
- // Find the new width by removing exiting elements, measuring, then
45
- // re-adding them. We query the DOM because AnimatePresence popLayout
46
- // overwrites ref props.
47
- const undos = [];
48
- // Snapshot the children first - we mutate the live collection below.
49
- const children = [...measured.children];
50
- for (const child of children) {
51
- if (!(child instanceof HTMLElement)) {
52
- continue;
53
- }
54
- if (child.dataset.state === "exiting") {
55
- const next = child.nextSibling;
56
- child.remove();
57
- undos.push(() => {
58
- // insertBefore() appends if next is null.
59
- measuredRef.current?.insertBefore(child, next);
60
- });
61
- continue;
62
- }
63
- const newWidth = targetWidths.get(child);
64
- if (newWidth === undefined) {
65
- continue;
66
- }
67
- const oldWidth = child.style.width;
68
- child.style.width = newWidth;
69
- undos.push(() => {
70
- child.style.width = oldWidth;
71
- });
72
- }
73
- // Measure the resulting width, then undo immediately. This is a
74
- // measure-after-render pass (DOM already committed), not a render-phase
75
- // adjustment, so the section can hold a fixed width during the animation.
76
- setWidth(getWidthInEm(measured));
77
- for (let i = undos.length - 1; i >= 0; i -= 1) {
78
- undos[i]?.();
89
+ const target = measureTargetWidth(section);
90
+ if (shouldReduceMotion === true) {
91
+ width.jump(target);
92
+ return;
79
93
  }
80
- }, [partsSignature]);
81
- return (_jsx(SectionContext.Provider, { value: context, children: _jsx(m.span, { layoutDependency: layoutDependency, ...rest, className: `number-section-${name}`, ref: ref, style: {
94
+ // A change mid-tween retargets from the width it has reached. Not stopped
95
+ // when the effect re-runs: a fresh transition object from the caller must
96
+ // not freeze the width part-way.
97
+ animate(width, target, widthTiming(transition));
98
+ }, [partsSignature, shouldReduceMotion, transition, width]);
99
+ return (_jsx(SectionContext.Provider, { value: context, children: _jsxs(m.span, { layoutDependency: layoutDependency, ...rest, className: `number-section-${name}`, ref: ref, style: {
82
100
  ...style,
83
101
  display: "inline-flex",
84
102
  justifyContent: justify,
103
+ // The containing block for glyphs AnimatePresence pops out of flow.
104
+ position: "relative",
85
105
  width,
86
- }, children: _jsxs("span", { ref: measuredRef, style: {
87
- display: "inline-flex",
88
- justifyContent: "inherit",
89
- position: "relative",
90
- }, children: ["\u200B", _jsx(AnimatePresence, { anchorX: justify, initial: false, mode: mode ?? "sync", children: parts.map((part) => isDigitPart(part) ? (_jsx(NumberDigit, { animate: { opacity: 1 }, exit: { opacity: 0 }, initial: { opacity: 0 }, layoutDependency: layoutDependency, trend: trend, value: part.value }, part.key)) : (_jsx(NumberSymbol, { animate: { opacity: 1 }, exit: { opacity: 0 }, initial: { opacity: 0 }, layoutDependency: layoutDependency, partKey: part.key, type: part.type, children: part.value }, part.type === "literal"
91
- ? `${part.key}:${part.value}`
92
- : part.key))) })] }) }) }));
106
+ }, children: ["\u200B", _jsx(AnimatePresence, { anchorX: justify, initial: false, mode: mode ?? "sync", children: parts.map((part) => isDigitPart(part) ? (_jsx(NumberDigit, { animate: { opacity: 1 }, exit: { opacity: 0 }, initial: { opacity: 0 }, layoutDependency: layoutDependency, trend: trend, value: part.value }, part.key)) : (_jsx(NumberSymbol, { animate: { opacity: 1 }, exit: { opacity: 0 }, initial: { opacity: 0 }, layoutDependency: layoutDependency, partKey: part.key, type: part.type, children: part.value }, part.type === "literal"
107
+ ? `${part.key}:${part.value}`
108
+ : part.key))) })] }) }));
93
109
  };
@@ -16,6 +16,8 @@ export const NumberSymbol = ({ partKey: _partKey, type: _type, children, layoutD
16
16
  const { justify } = use(SectionContext);
17
17
  return (_jsx(m.span, { ...rest, "data-state": isPresent ? undefined : "exiting", layout: "position", layoutDependency: layoutDependency, style: {
18
18
  display: "inline-flex",
19
+ // Holds its width while the section's width tweens past it.
20
+ flexShrink: 0,
19
21
  justifyContent: justify,
20
22
  padding: `calc(${maskHeight}/2) 0`,
21
23
  position: "relative",
@@ -1,3 +1,4 @@
1
+ import type { Transition } from "motion/react";
1
2
  /**
2
3
  * Per-digit target widths, written by {@link NumberDigit} the instant a digit's
3
4
  * width changes and read by {@link NumberSection} when it re-measures its fixed
@@ -5,3 +6,9 @@
5
6
  * element.
6
7
  */
7
8
  export declare const targetWidths: WeakMap<Element, string>;
9
+ /**
10
+ * The timing for a tweened width: the layout curve when the transition is
11
+ * keyed per value (the token default), otherwise the transition itself (a
12
+ * caller's bespoke override), so widths move in step with the roll.
13
+ */
14
+ export declare const widthTiming: (transition: Transition | undefined) => Transition;
@@ -1,4 +1,3 @@
1
- // Port of Motion+ AnimateNumber (motion-plus 2.0.0).
2
1
  /**
3
2
  * Per-digit target widths, written by {@link NumberDigit} the instant a digit's
4
3
  * width changes and read by {@link NumberSection} when it re-measures its fixed
@@ -6,3 +5,16 @@
6
5
  * element.
7
6
  */
8
7
  export const targetWidths = new WeakMap();
8
+ /**
9
+ * The timing for a tweened width: the layout curve when the transition is
10
+ * keyed per value (the token default), otherwise the transition itself (a
11
+ * caller's bespoke override), so widths move in step with the roll.
12
+ */
13
+ export const widthTiming = (transition) => {
14
+ if (transition !== undefined &&
15
+ "layout" in transition &&
16
+ transition.layout !== undefined) {
17
+ return transition.layout;
18
+ }
19
+ return transition ?? {};
20
+ };
@@ -34,7 +34,8 @@ declare const getBaseSize: (size: ButtonSize) => ComponentSize;
34
34
  * - neutral + outline → the bordered button (old `outline` variant)
35
35
  * - neutral + ghost → the transparent button (old `ghost` variant)
36
36
  * - neutral + link → the inline link button (old `link` variant)
37
- * - neutral + transparent → the blurred translucent button
37
+ * - neutral + transparent → the blurred translucent button, for content
38
+ * over images only (see `tokens/emphasis`)
38
39
  */
39
40
  declare const buttonVariants: (props?: ({
40
41
  tone?: "brand" | "danger" | "neutral" | null | undefined;
@@ -156,7 +156,8 @@ const BRAND_TRANSPARENT = "transform-gpu border-transparent bg-brand/50 backdrop
156
156
  * - neutral + outline → the bordered button (old `outline` variant)
157
157
  * - neutral + ghost → the transparent button (old `ghost` variant)
158
158
  * - neutral + link → the inline link button (old `link` variant)
159
- * - neutral + transparent → the blurred translucent button
159
+ * - neutral + transparent → the blurred translucent button, for content
160
+ * over images only (see `tokens/emphasis`)
160
161
  */
161
162
  const buttonVariants = cva([
162
163
  "duration-quick text-paragraph-sm isolate inline-flex shrink-0 cursor-pointer items-center gap-2 border font-medium whitespace-nowrap transition ease-out",
@@ -22,6 +22,9 @@ export declare const DEFAULT_PILL_SIZE: ComponentSize;
22
22
  * - `neutral` + `solid` → the dark primary pill (old default/solid)
23
23
  * - `neutral` + `soft` → the grey secondary pill (old secondary/solid)
24
24
  * - `neutral` + `outline` → the bordered transparent pill (old `outline` variant)
25
+ *
26
+ * `neutral` + `transparent` is glass for content over images only (see
27
+ * `tokens/emphasis`).
25
28
  */
26
29
  export declare const pillVariants: (props?: ({
27
30
  tone?: "brand" | "danger" | "info" | "neutral" | "success" | "warning" | null | undefined;
@@ -30,8 +30,15 @@ export const DEFAULT_PILL_SIZE = "base";
30
30
  * - `neutral` + `solid` → the dark primary pill (old default/solid)
31
31
  * - `neutral` + `soft` → the grey secondary pill (old secondary/solid)
32
32
  * - `neutral` + `outline` → the bordered transparent pill (old `outline` variant)
33
+ *
34
+ * `neutral` + `transparent` is glass for content over images only (see
35
+ * `tokens/emphasis`).
33
36
  */
34
- export const pillVariants = cva(cn("inline-flex items-center justify-center border border-transparent font-normal whitespace-nowrap [&_svg]:-ms-px [&_svg]:shrink-0", focusRing(), "data-[hovered=true]:opacity-90"), {
37
+ export const pillVariants = cva(cn("inline-flex items-center justify-center border border-transparent font-normal whitespace-nowrap [&_svg]:-ms-px [&_svg]:shrink-0", focusRing(),
38
+ // Hover tints the fill with a 4% foreground wash laid over it as an image,
39
+ // so the text keeps its colour. 4% is the most every tone and emphasis takes
40
+ // and still reaches AA (styles/tone-contrast.test.ts, PILL_HOVER_WASH).
41
+ "data-[hovered=true]:from-foreground/4 data-[hovered=true]:to-foreground/4 data-[hovered=true]:bg-linear-to-b"), {
35
42
  compoundVariants: [
36
43
  /* ── Focus ring colours by tone ────────────────────────── */
37
44
  {
@@ -3,7 +3,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { springs } from "@instruments/materia-ui/tokens/motion";
4
4
  import { cn } from "@instruments/materia-ui/utils/cn";
5
5
  import * as m from "motion/react-m";
6
- import { SEGMENTED_INDICATOR_CLASS } from "../segmented/segmented-root.js";
6
+ import { SEGMENTED_INDICATOR_CLASS } from "../../tokens/segmented.js";
7
7
  import { useSegmentedControlContext } from "./segmented-control-context.js";
8
8
  /**
9
9
  * Animated active-segment indicator. A single instance is mounted under the
@@ -5,9 +5,9 @@ import { Dot } from "@instruments/materia-ui/components/dot";
5
5
  import { Icon } from "@instruments/materia-ui/components/icon";
6
6
  import { cn } from "@instruments/materia-ui/utils/cn";
7
7
  import { getResponsiveSizeOverrides } from "../../tokens/responsive.js";
8
+ import { SEGMENTED_TRIGGER_PADDING } from "../../tokens/segmented.js";
8
9
  import { warnMissingAriaLabel } from "../../utils/warn-missing-aria-label.js";
9
10
  import { Flex } from "../flex.js";
10
- import { SEGMENTED_TRIGGER_PADDING } from "../segmented/segmented-root.js";
11
11
  import { useSegmentedControlContext } from "./segmented-control-context.js";
12
12
  import { SegmentedControlIndicator } from "./segmented-control-indicator.js";
13
13
  import { segmentedControlItemVariants } from "./segmented-control-variants.js";
@@ -6,8 +6,8 @@ import { ScrollFade } from "@instruments/materia-ui/components/scroll-fade";
6
6
  import { cn } from "@instruments/materia-ui/utils/cn";
7
7
  import { useEffect, useId, useMemo, useRef, useState } from "react";
8
8
  import { getResponsiveSizeOverrides, resolveResponsiveBase, } from "../../tokens/responsive.js";
9
+ import { SEGMENTED_GAP, SEGMENTED_SHELL_BASE, SEGMENTED_SHELL_PADDING, } from "../../tokens/segmented.js";
9
10
  import { Flex } from "../flex.js";
10
- import { SEGMENTED_GAP, SEGMENTED_SHELL_BASE, SEGMENTED_SHELL_PADDING, } from "../segmented/segmented-root.js";
11
11
  import { SegmentedControlContext } from "./segmented-control-context.js";
12
12
  import { useSingleSelection } from "./use-single-selection.js";
13
13
  /**
@@ -2,8 +2,8 @@ import { focusRing } from "@instruments/materia-ui/tokens/focus-ring";
2
2
  import { cn } from "@instruments/materia-ui/utils/cn";
3
3
  import { cva } from "class-variance-authority";
4
4
  import { DISABLED } from "../../tokens/input-shell.js";
5
+ import { SEGMENTED_TRIGGER_PADDING, SEGMENTED_TRIGGER_TEXT, } from "../../tokens/segmented.js";
5
6
  import { typedEntries } from "../../utils/typed-entries.js";
6
- import { SEGMENTED_TRIGGER_PADDING, SEGMENTED_TRIGGER_TEXT, } from "../segmented/segmented-root.js";
7
7
  /**
8
8
  * Item padding lives in compound variants (mirroring `tabsTriggerVariants`) so a
9
9
  * single size dimension drives text (base `size` variant) and padding
@@ -58,7 +58,11 @@ export const StepperItem = ({ value, label, disabled = false, icon = Check, clas
58
58
  const circleSize = state === "completed"
59
59
  ? CIRCLE_SIZES[baseSize].completed
60
60
  : CIRCLE_SIZES[baseSize].default;
61
- return (_jsxs("div", { className: cn("relative", className), "data-slot": "stepper-item", "data-state": state, ...rest, children: [_jsx("div", { className: cn(stepperCircleContainerVariants({ size: baseSize }), getResponsiveSizeOverrides(size, STEPPER_CIRCLE_CONTAINER_SIZE_CLASSES)), children: _jsx(m.button, { "aria-current": state === "current" ? "step" : undefined, "aria-label": label !== undefined && label !== null && label !== ""
61
+ return (_jsxs("div", { className: cn("group/step relative", className), "data-slot": "stepper-item", "data-state": state, ...rest, children: [_jsx("div", { className: cn(stepperCircleContainerVariants({ size: baseSize }), getResponsiveSizeOverrides(size, STEPPER_CIRCLE_CONTAINER_SIZE_CLASSES)), children: _jsx(m.button, { "aria-current": state === "current" ? "step" : undefined, "aria-label": label !== undefined && label !== null && label !== ""
62
62
  ? `Step: ${label}`
63
- : `Step ${value + 1}`, className: cn(stepperCircleVariants({ size: baseSize, state }), isInteractive && "cursor-pointer", !isInteractive && "cursor-default"), disabled: disabled, layout: true, onClick: handleClick, onKeyDown: handleKeyDown, style: { borderRadius: 9999, height: circleSize, width: circleSize }, transition: springs.standard, type: "button", children: _jsx(m.span, { className: "flex", layout: true, children: _jsx(Icon, { className: stepperIconVariants({ size: baseSize, state }), icon: icon, size: iconSize }) }) }) }), label !== undefined && label !== null && label !== "" && (_jsx("span", { className: cn(stepperLabelVariants({ size: baseSize, state }), getResponsiveSizeOverrides(size, STEPPER_LABEL_SIZE_CLASSES)), children: label }))] }));
63
+ : `Step ${value + 1}`, className: cn(stepperCircleVariants({ size: baseSize, state }), isInteractive && "cursor-pointer", !isInteractive && "cursor-default"), disabled: disabled, layout: true, onClick: handleClick, onKeyDown: handleKeyDown, style: { borderRadius: 9999, height: circleSize, width: circleSize }, transition: springs.standard, type: "button", children: _jsx(m.span, { className: "flex", layout: true, children: _jsx(Icon, { className: stepperIconVariants({ size: baseSize, state }), icon: icon, size: iconSize }) }) }) }), label !== undefined && label !== null && label !== "" && (_jsx("span", { className: cn(stepperLabelVariants({
64
+ edge: value === 0 ? "start" : "center",
65
+ size: baseSize,
66
+ state,
67
+ }), getResponsiveSizeOverrides(size, STEPPER_LABEL_SIZE_CLASSES)), children: label }))] }));
64
68
  };
@@ -36,6 +36,7 @@ export declare const stepperIconVariants: (props?: ({
36
36
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
37
37
  /** Step label variants - absolutely positioned to not affect flex layout */
38
38
  export declare const stepperLabelVariants: (props?: ({
39
+ edge?: "center" | "start" | null | undefined;
39
40
  size?: StepperSize | null | undefined;
40
41
  state?: "completed" | "current" | "disabled" | "upcoming" | null | undefined;
41
42
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
@@ -105,12 +105,22 @@ export const stepperIconVariants = cva("transition-colors", {
105
105
  },
106
106
  });
107
107
  /** Step label variants - absolutely positioned to not affect flex layout */
108
- export const stepperLabelVariants = cva("text-caption absolute left-1/2 -translate-x-1/2 text-center whitespace-nowrap", {
108
+ // Labels centre under their circles, except at the ends: the first starts at
109
+ // its circle's left edge and the last ends at its circle's right edge, so no
110
+ // label hangs outside the stepper's box. The last step is found with CSS
111
+ // (`last-child` of the root, via the item's `group/step`) so it holds on the
112
+ // server render; the first is passed in as `edge`.
113
+ export const stepperLabelVariants = cva("text-caption absolute whitespace-nowrap group-last/step:right-0 group-last/step:left-auto group-last/step:translate-x-0 group-last/step:text-right", {
109
114
  defaultVariants: {
115
+ edge: "center",
110
116
  size: "base",
111
117
  state: "upcoming",
112
118
  },
113
119
  variants: {
120
+ edge: {
121
+ center: "left-1/2 -translate-x-1/2 text-center",
122
+ start: "left-0 text-left",
123
+ },
114
124
  size: STEPPER_LABEL_SIZE_CLASSES,
115
125
  state: {
116
126
  completed: "text-foreground",
@@ -4,9 +4,9 @@ import { mergeProps } from "@base-ui/react/merge-props";
4
4
  import { useRender } from "@base-ui/react/use-render";
5
5
  import { cn } from "@instruments/materia-ui/utils/cn";
6
6
  import { getResponsiveSizeOverrides, resolveResponsiveBase, } from "../../tokens/responsive.js";
7
+ import { SEGMENTED_ICON_SIZE, SEGMENTED_TRIGGER_TEXT, } from "../../tokens/segmented.js";
7
8
  import { Flex } from "../flex.js";
8
9
  import { Icon } from "../icon.js";
9
- import { SEGMENTED_ICON_SIZE, SEGMENTED_TRIGGER_TEXT, } from "../segmented/segmented-variants.js";
10
10
  import { useTabsContext } from "../tabs/tabs-context.js";
11
11
  import { TabsIndicator } from "../tabs/tabs-indicator.js";
12
12
  import { DEFAULT_TABS_SIZE, tabsTriggerVariants } from "../tabs/tabs-variants.js";
@@ -3,7 +3,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { springs } from "@instruments/materia-ui/tokens/motion";
4
4
  import { cn } from "@instruments/materia-ui/utils/cn";
5
5
  import * as m from "motion/react-m";
6
- import { SEGMENTED_INDICATOR_CLASS } from "../segmented/segmented-root.js";
6
+ import { SEGMENTED_INDICATOR_CLASS } from "../../tokens/segmented.js";
7
7
  import { useTabsContext } from "../tabs/tabs-context.js";
8
8
  const indicatorStyles = {
9
9
  line: "absolute inset-x-0 bottom-0 h-0.5 bg-primary dark:bg-gray-100",
@@ -3,10 +3,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { Tabs as TabsPrimitive } from "@base-ui/react/tabs";
4
4
  import { cn } from "@instruments/materia-ui/utils/cn";
5
5
  import { getResponsiveSizeOverrides, resolveResponsiveBase, } from "../../tokens/responsive.js";
6
+ import { SEGMENTED_DOT_SIZE, SEGMENTED_ICON_SIZE, SEGMENTED_TRIGGER_TEXT, } from "../../tokens/segmented.js";
6
7
  import { Dot } from "../dot.js";
7
8
  import { Flex } from "../flex.js";
8
9
  import { Icon } from "../icon.js";
9
- import { SEGMENTED_DOT_SIZE, SEGMENTED_ICON_SIZE, SEGMENTED_TRIGGER_TEXT, } from "../segmented/segmented-variants.js";
10
10
  import { useTabsContext } from "../tabs/tabs-context.js";
11
11
  import { TabsIndicator } from "../tabs/tabs-indicator.js";
12
12
  import { DEFAULT_TABS_SIZE, tabsTriggerVariants } from "../tabs/tabs-variants.js";
@@ -2,8 +2,8 @@ import { focusRing } from "@instruments/materia-ui/tokens/focus-ring";
2
2
  import { cn } from "@instruments/materia-ui/utils/cn";
3
3
  import { cva } from "class-variance-authority";
4
4
  import { DISABLED } from "../../tokens/input-shell.js";
5
+ import { SEGMENTED_GAP, SEGMENTED_SHELL_BASE, SEGMENTED_SHELL_PADDING, SEGMENTED_TRIGGER_PADDING, SEGMENTED_TRIGGER_TEXT, } from "../../tokens/segmented.js";
5
6
  import { typedEntries } from "../../utils/typed-entries.js";
6
- import { SEGMENTED_GAP, SEGMENTED_SHELL_BASE, SEGMENTED_SHELL_PADDING, SEGMENTED_TRIGGER_PADDING, SEGMENTED_TRIGGER_TEXT, } from "../segmented/segmented-root.js";
7
7
  const segmentedShellPaddingVariants = typedEntries(SEGMENTED_SHELL_PADDING).map(([size, className]) => ({
8
8
  className,
9
9
  size,
@@ -72,5 +72,5 @@ export interface DocsManifest {
72
72
  storybook: DocsStorybookInventory;
73
73
  }
74
74
  /** The slug of every documented component and composition. */
75
- export type DocsUnitSlug = "accordion" | "alert" | "alert-dialog" | "anchor" | "animated-number" | "aspect-ratio" | "attribute-card" | "avatar" | "badge" | "border-beam" | "box" | "breadcrumb" | "button" | "button-group" | "calendar" | "card" | "card-grid" | "carousel" | "center" | "chart" | "checkbox" | "checkbox-card" | "checkbox-field" | "code" | "collapsible" | "color-palette" | "color-wheel" | "combobox" | "command" | "container" | "context-menu" | "cookie-banner" | "country-combobox" | "country-select" | "data-grid" | "data-grid-column-filter" | "data-grid-column-header" | "data-grid-column-visibility" | "data-grid-pagination" | "data-grid-table" | "delivery-banner" | "description-list" | "dialog" | "document-list" | "dot" | "dot-matrix-loader" | "drawer" | "dropdown-menu" | "easing-curve" | "editorial-card" | "empty" | "empty-state" | "error-boundary" | "error-state" | "expand-button" | "expandable-text" | "fade-overlay" | "field" | "file-upload" | "flex" | "folder-tile" | "gallery" | "grid" | "group" | "heading" | "hotspot-indicator" | "hover-card" | "icon" | "icon-box" | "icon-card" | "icon-morph" | "identified-surface" | "input" | "input-card" | "input-group" | "input-otp" | "item" | "kbd" | "label" | "loading-state" | "location-input" | "logo" | "marquee-text" | "media-frame" | "mega-menu" | "menu-item" | "menubar" | "mobile-navigation" | "month-year-picker" | "morph-dialog" | "motion-config" | "native-select" | "navigation-menu" | "page-header" | "page-toolbar" | "pagination" | "phone-input" | "pill" | "pixel-loader" | "popover" | "product-card" | "product-specifications-selector" | "progress" | "progress-dots" | "prompt-panel" | "quantity-control" | "quick-view" | "radio" | "radio-card" | "radio-group" | "requirement-item" | "resizable" | "scrim" | "scroll-area" | "scroll-fade" | "scroll-frame" | "search-input" | "section" | "section-header" | "segmented" | "segmented-control" | "select" | "separator" | "settings-card" | "sheet" | "shimmer" | "shine" | "sidebar" | "sidepanel" | "simple-grid" | "site-footer" | "skeleton" | "slider" | "social-icon" | "space" | "spinner" | "stack" | "stagger" | "stepper" | "streaming-text" | "sub-navigation" | "success-tick-ripple" | "swatch" | "swatch-fan" | "swatch-group" | "swatch-selector" | "switch" | "tab-navigation" | "table" | "tabs" | "tag" | "tag-selector" | "text" | "textarea" | "thinking-loader" | "thinking-orb" | "thinking-trace" | "time-slot-picker" | "toast" | "toggle" | "toggle-group" | "tooltip" | "variant-selector" | "verge" | "visually-hidden" | "voice";
75
+ export type DocsUnitSlug = "accordion" | "alert" | "alert-dialog" | "anchor" | "animated-number" | "aspect-ratio" | "attribute-card" | "avatar" | "badge" | "border-beam" | "box" | "breadcrumb" | "button" | "button-group" | "calendar" | "card" | "card-grid" | "carousel" | "center" | "chart" | "checkbox" | "checkbox-card" | "checkbox-field" | "code" | "collapsible" | "color-palette" | "color-wheel" | "combobox" | "command" | "container" | "context-menu" | "cookie-banner" | "country-combobox" | "country-select" | "data-grid" | "data-grid-column-filter" | "data-grid-column-header" | "data-grid-column-visibility" | "data-grid-pagination" | "data-grid-table" | "delivery-banner" | "description-list" | "dialog" | "document-list" | "dot" | "dot-matrix-loader" | "drawer" | "dropdown-menu" | "easing-curve" | "editorial-card" | "empty" | "empty-state" | "error-boundary" | "error-state" | "expand-button" | "expandable-text" | "fade-overlay" | "field" | "file-upload" | "flex" | "folder-tile" | "gallery" | "grid" | "group" | "heading" | "hotspot-indicator" | "hover-card" | "icon" | "icon-box" | "icon-card" | "icon-morph" | "identified-surface" | "input" | "input-card" | "input-group" | "input-otp" | "item" | "kbd" | "label" | "loading-state" | "location-input" | "logo" | "marquee-text" | "media-frame" | "mega-menu" | "menu-item" | "menubar" | "mobile-navigation" | "month-year-picker" | "morph-dialog" | "motion-config" | "native-select" | "navigation-menu" | "page-header" | "page-toolbar" | "pagination" | "phone-input" | "pill" | "pixel-loader" | "popover" | "product-card" | "product-specifications-selector" | "progress" | "progress-dots" | "prompt-panel" | "quantity-control" | "quick-view" | "radio" | "radio-card" | "radio-group" | "requirement-item" | "resizable" | "scrim" | "scroll-area" | "scroll-fade" | "scroll-frame" | "search-input" | "section" | "section-header" | "segmented-control" | "select" | "separator" | "settings-card" | "sheet" | "shimmer" | "shine" | "sidebar" | "sidepanel" | "simple-grid" | "site-footer" | "skeleton" | "slider" | "social-icon" | "space" | "spinner" | "stack" | "stagger" | "stepper" | "streaming-text" | "sub-navigation" | "success-tick-ripple" | "swatch" | "swatch-fan" | "swatch-group" | "swatch-selector" | "switch" | "tab-navigation" | "table" | "tabs" | "tag" | "tag-selector" | "text" | "textarea" | "thinking-loader" | "thinking-orb" | "thinking-trace" | "time-slot-picker" | "toast" | "toggle" | "toggle-group" | "tooltip" | "variant-selector" | "verge" | "visually-hidden" | "voice";
76
76
  export declare const docsManifest: DocsManifest;
@@ -9393,51 +9393,6 @@ export const docsManifest = {
9393
9393
  uses: [],
9394
9394
  usedBy: [],
9395
9395
  },
9396
- {
9397
- slug: "segmented",
9398
- module: "components/segmented",
9399
- title: "Segmented",
9400
- category: "forms",
9401
- description: "Shared class-name token primitives for the segmented-control visual language.\n\nConsumed by SegmentedControl, Tabs, and TabNavigation so they render an\nidentical shell, padding, gap, and active indicator at any size.\nNot components - these are Tailwind class strings and size→class maps.",
9402
- exports: [
9403
- {
9404
- name: "SEGMENTED_GAP",
9405
- kind: "const",
9406
- props: [],
9407
- },
9408
- {
9409
- name: "SEGMENTED_INDICATOR_ACTIVE_CLASS",
9410
- kind: "const",
9411
- props: [],
9412
- },
9413
- {
9414
- name: "SEGMENTED_INDICATOR_CLASS",
9415
- kind: "const",
9416
- props: [],
9417
- },
9418
- {
9419
- name: "SEGMENTED_SHELL_BASE",
9420
- kind: "const",
9421
- props: [],
9422
- },
9423
- {
9424
- name: "SEGMENTED_SHELL_PADDING",
9425
- kind: "const",
9426
- props: [],
9427
- },
9428
- {
9429
- name: "SEGMENTED_TRIGGER_PADDING",
9430
- kind: "const",
9431
- props: [],
9432
- },
9433
- ],
9434
- bestPractices: [
9435
- "These are class-name strings and size→class maps, not components - apply them via `className`, never render them as JSX.",
9436
- 'Reuse these tokens to give a custom control the segmented "pill" shell; for actual segmented UIs prefer `SegmentedControl`; `Tabs` and `TabNavigation` consume the same tokens for panel and navigation switching.',
9437
- ],
9438
- uses: [],
9439
- usedBy: ["segmented-control", "tab-navigation", "tabs"],
9440
- },
9441
9396
  {
9442
9397
  slug: "segmented-control",
9443
9398
  module: "components/segmented-control",
@@ -9542,7 +9497,7 @@ export const docsManifest = {
9542
9497
  "Switching visible content panels is Tabs; navigating URLs is TabNavigation - SegmentedControl is an input that selects a value or mode.",
9543
9498
  "Icon-only items must carry an `aria-label`.",
9544
9499
  ],
9545
- uses: ["dot", "flex", "icon", "scroll-area", "scroll-fade", "segmented"],
9500
+ uses: ["dot", "flex", "icon", "scroll-area", "scroll-fade"],
9546
9501
  usedBy: ["color-palette"],
9547
9502
  anatomy: {
9548
9503
  children: [
@@ -11941,7 +11896,7 @@ export const docsManifest = {
11941
11896
  "Use `TabNavigation` when each tab is a route/URL (it renders a `<nav>` of links); for in-page panels that swap without navigating, use `Tabs`.",
11942
11897
  'It shares the Tabs look via context but defaults to `variant="line"`; mark the active link yourself (e.g. router `aria-current`) - there\'s no Base UI value tracking here.',
11943
11898
  ],
11944
- uses: ["flex", "icon", "segmented", "tabs"],
11899
+ uses: ["flex", "icon", "tabs"],
11945
11900
  usedBy: [],
11946
11901
  anatomy: {
11947
11902
  children: [
@@ -12183,7 +12138,7 @@ export const docsManifest = {
12183
12138
  "Reach for `Tabs` to swap in-page panels via `TabsContent`; when each tab is a route, use `TabNavigation` instead (renders links, no panels).",
12184
12139
  "Compose `TabsList`/`TabsTrigger`/`TabsContent` - shared `variant`/`size`/`fullWidth` flow through context, so set them on `Tabs`, not each trigger.",
12185
12140
  ],
12186
- uses: ["dot", "flex", "icon", "scroll-area", "segmented"],
12141
+ uses: ["dot", "flex", "icon", "scroll-area"],
12187
12142
  usedBy: ["tab-navigation"],
12188
12143
  anatomy: {
12189
12144
  children: [
@@ -23055,7 +23010,7 @@ export const docsManifest = {
23055
23010
  storybook: {
23056
23011
  addons: ["@storybook/addon-docs", "@storybook/react-vite"],
23057
23012
  anatomyFiles: 31,
23058
- storyFiles: 172,
23059
- storiesWithPlay: 64,
23013
+ storyFiles: 171,
23014
+ storiesWithPlay: 63,
23060
23015
  },
23061
23016
  };
@@ -65,7 +65,7 @@ export const SEMANTIC_COLORS = [
65
65
  class: "bg-brand-soft",
66
66
  key: "brand-soft",
67
67
  name: "Brand Soft",
68
- value: "#fffef5",
68
+ value: "#fffbd9",
69
69
  },
70
70
  {
71
71
  class: "bg-brand-accent",
@@ -233,7 +233,13 @@
233
233
  --color-info-foreground: #1e3a5f; /* blue-900 */
234
234
 
235
235
  --color-brand: #ffe600; /* Brand yellow, per the Figma base/brand token */
236
- --color-brand-soft: #fffef5; /* very soft brand yellow (code-only) */
236
+ /* Pale brand yellow (code-only), mixed from --color-brand so a re-brand
237
+ carries its soft fill with it: #fffbd9 for the default yellow. */
238
+ --color-brand-soft: color-mix(
239
+ in srgb,
240
+ var(--color-brand) 15%,
241
+ var(--color-white)
242
+ );
237
243
  --color-brand-accent: #dda600; /* per Figma base/brand accent - consumed at /40 for borders/accents */
238
244
  --color-brand-foreground: var(--color-gray-800); /* #1F2937 - per Figma */
239
245
  /* Brand-hued text on the page background, for brand buttons without a fill.
@@ -10,6 +10,11 @@
10
10
  * cva-backed components anchor their variant map `satisfies Record<Emphasis, …>`
11
11
  * so the fill vocabulary stays in lockstep with the canon.
12
12
  *
13
+ * `transparent` is a blurred half fill of the tone. Its brand and status tones
14
+ * are tuned to read on the page. Neutral transparent is glass for
15
+ * content over images: its white text reaches AA over a mid-to-dark photo or a
16
+ * Scrim, but not on the page or a pale image, so keep it to media.
17
+ *
13
18
  * `link` is deliberately NOT a member: it is a Button-only extension expressed
14
19
  * as `ButtonEmphasis = Emphasis | "link"`, so no other component inherits an
15
20
  * inline-link treatment it has no styling for.
@@ -10,6 +10,11 @@
10
10
  * cva-backed components anchor their variant map `satisfies Record<Emphasis, …>`
11
11
  * so the fill vocabulary stays in lockstep with the canon.
12
12
  *
13
+ * `transparent` is a blurred half fill of the tone. Its brand and status tones
14
+ * are tuned to read on the page. Neutral transparent is glass for
15
+ * content over images: its white text reaches AA over a mid-to-dark photo or a
16
+ * Scrim, but not on the page or a pale image, so keep it to media.
17
+ *
13
18
  * `link` is deliberately NOT a member: it is a Button-only extension expressed
14
19
  * as `ButtonEmphasis = Emphasis | "link"`, so no other component inherits an
15
20
  * inline-link treatment it has no styling for.
@@ -1,4 +1,15 @@
1
- import type { ComponentSize } from "../../tokens/size.js";
1
+ /**
2
+ * The segmented "pill" visual language: shell, padding, gap, trigger text and
3
+ * active-indicator class strings, with size-to-class maps.
4
+ *
5
+ * SegmentedControl, Tabs and TabNavigation all consume these, so they render
6
+ * an identical shell at any size. They are class strings, not components:
7
+ * apply them through `className`. For an actual segmented UI, use
8
+ * `SegmentedControl`.
9
+ *
10
+ * @module
11
+ */
12
+ import type { ComponentSize } from "./size.js";
2
13
  /**
3
14
  * Shared shell classes for segmented controls (Tabs default, ToggleGroup pill).
4
15
  *
@@ -1,3 +1,14 @@
1
+ /**
2
+ * The segmented "pill" visual language: shell, padding, gap, trigger text and
3
+ * active-indicator class strings, with size-to-class maps.
4
+ *
5
+ * SegmentedControl, Tabs and TabNavigation all consume these, so they render
6
+ * an identical shell at any size. They are class strings, not components:
7
+ * apply them through `className`. For an actual segmented UI, use
8
+ * `SegmentedControl`.
9
+ *
10
+ * @module
11
+ */
1
12
  /**
2
13
  * Shared shell classes for segmented controls (Tabs default, ToggleGroup pill).
3
14
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@instruments/materia-ui",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "Materia design system: React components, tokens and Tailwind v4 styles.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "repository": {
@@ -392,10 +392,6 @@
392
392
  "types": "./dist/components/section.d.ts",
393
393
  "default": "./dist/components/section.js"
394
394
  },
395
- "./components/segmented": {
396
- "types": "./dist/components/segmented.d.ts",
397
- "default": "./dist/components/segmented.js"
398
- },
399
395
  "./components/segmented-control": {
400
396
  "types": "./dist/components/segmented-control.d.ts",
401
397
  "default": "./dist/components/segmented-control.js"
@@ -792,6 +788,10 @@
792
788
  "types": "./dist/tokens/radius-tokens.d.ts",
793
789
  "default": "./dist/tokens/radius-tokens.js"
794
790
  },
791
+ "./tokens/segmented": {
792
+ "types": "./dist/tokens/segmented.d.ts",
793
+ "default": "./dist/tokens/segmented.js"
794
+ },
795
795
  "./tokens/shadows": {
796
796
  "types": "./dist/tokens/shadows.d.ts",
797
797
  "default": "./dist/tokens/shadows.js"
@@ -1,12 +0,0 @@
1
- /**
2
- * Shared class-name token primitives for the segmented-control visual language.
3
- *
4
- * Consumed by SegmentedControl, Tabs, and TabNavigation so they render an
5
- * identical shell, padding, gap, and active indicator at any size.
6
- * Not components - these are Tailwind class strings and size→class maps.
7
- *
8
- * @bestPractice These are class-name strings and size→class maps, not components - apply them via `className`, never render them as JSX.
9
- * @bestPractice Reuse these tokens to give a custom control the segmented "pill" shell; for actual segmented UIs prefer `SegmentedControl`; `Tabs` and `TabNavigation` consume the same tokens for panel and navigation switching.
10
- * @category forms
11
- */
12
- export { SEGMENTED_GAP, SEGMENTED_INDICATOR_ACTIVE_CLASS, SEGMENTED_INDICATOR_CLASS, SEGMENTED_SHELL_BASE, SEGMENTED_SHELL_PADDING, SEGMENTED_TRIGGER_PADDING, SEGMENTED_TRIGGER_TEXT, } from "./segmented-variants.js";
@@ -1,12 +0,0 @@
1
- /**
2
- * Shared class-name token primitives for the segmented-control visual language.
3
- *
4
- * Consumed by SegmentedControl, Tabs, and TabNavigation so they render an
5
- * identical shell, padding, gap, and active indicator at any size.
6
- * Not components - these are Tailwind class strings and size→class maps.
7
- *
8
- * @bestPractice These are class-name strings and size→class maps, not components - apply them via `className`, never render them as JSX.
9
- * @bestPractice Reuse these tokens to give a custom control the segmented "pill" shell; for actual segmented UIs prefer `SegmentedControl`; `Tabs` and `TabNavigation` consume the same tokens for panel and navigation switching.
10
- * @category forms
11
- */
12
- export { SEGMENTED_GAP, SEGMENTED_INDICATOR_ACTIVE_CLASS, SEGMENTED_INDICATOR_CLASS, SEGMENTED_SHELL_BASE, SEGMENTED_SHELL_PADDING, SEGMENTED_TRIGGER_PADDING, SEGMENTED_TRIGGER_TEXT, } from "./segmented-variants.js";
@@ -1 +0,0 @@
1
- export { SEGMENTED_GAP, SEGMENTED_INDICATOR_ACTIVE_CLASS, SEGMENTED_INDICATOR_CLASS, SEGMENTED_SHELL_BASE, SEGMENTED_SHELL_PADDING, SEGMENTED_TRIGGER_PADDING, } from "./segmented/segmented-variants.js";
@@ -1 +0,0 @@
1
- export { SEGMENTED_GAP, SEGMENTED_INDICATOR_ACTIVE_CLASS, SEGMENTED_INDICATOR_CLASS, SEGMENTED_SHELL_BASE, SEGMENTED_SHELL_PADDING, SEGMENTED_TRIGGER_PADDING, } from "./segmented/segmented-variants.js";