@instruments/materia-ui 0.1.0 → 0.3.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/agent-docs.md +4 -3
- package/dist/components/animated-number/number-digit.d.ts +5 -3
- package/dist/components/animated-number/number-digit.js +39 -16
- package/dist/components/animated-number/number-section.d.ts +7 -5
- package/dist/components/animated-number/number-section.js +81 -65
- package/dist/components/animated-number/number-symbol.js +2 -0
- package/dist/components/animated-number/target-widths.d.ts +7 -0
- package/dist/components/animated-number/target-widths.js +13 -1
- package/dist/components/button/button-variants.js +53 -8
- package/dist/components/button-group/button-group-root.js +8 -6
- package/dist/components/calendar/calendar-root.js +39 -13
- package/dist/components/chart/chart-tooltip.js +7 -5
- package/dist/components/empty/empty-media.js +5 -5
- package/dist/components/input-group/input-group-textarea.d.ts +3 -2
- package/dist/components/input-group/input-group-textarea.js +8 -4
- package/dist/components/pill/pill-variants.js +21 -10
- package/dist/components/progress/progress-root.d.ts +7 -5
- package/dist/components/progress/progress-root.js +26 -5
- package/dist/components/radio-group/radio-group-root.d.ts +7 -3
- package/dist/components/radio-group/radio-group-root.js +14 -4
- package/dist/components/segmented-control/segmented-control-indicator.js +1 -1
- package/dist/components/segmented-control/segmented-control-item.js +1 -1
- package/dist/components/segmented-control/segmented-control-root.js +1 -1
- package/dist/components/segmented-control/segmented-control-variants.js +1 -1
- package/dist/components/stepper/stepper-item.js +6 -2
- package/dist/components/stepper/stepper-variants.d.ts +1 -0
- package/dist/components/stepper/stepper-variants.js +11 -1
- package/dist/components/tab-navigation/tab-navigation-link.js +1 -1
- package/dist/components/tabs/tabs-indicator.js +1 -1
- package/dist/components/tabs/tabs-trigger.js +1 -1
- package/dist/components/tabs/tabs-variants.js +1 -1
- package/dist/components/textarea/textarea-root.d.ts +1 -1
- package/dist/components/textarea/textarea-root.js +6 -9
- package/dist/components/textarea/use-textarea-rows.d.ts +22 -0
- package/dist/components/textarea/use-textarea-rows.js +43 -0
- package/dist/compositions/data-grid-pagination/data-grid-pagination-root.d.ts +8 -6
- package/dist/compositions/data-grid-pagination/data-grid-pagination-root.js +11 -20
- package/dist/compositions/product-card/product-card-color-count.js +26 -26
- package/dist/compositions/product-card/product-card-compact-arm.js +2 -2
- package/dist/compositions/product-card/product-card-content-types.d.ts +5 -0
- package/dist/compositions/product-card/product-card-full-arm.js +2 -2
- package/dist/compositions/product-card/product-card-provider-types.d.ts +10 -0
- package/dist/compositions/product-card/product-card-provider.js +1 -1
- package/dist/compositions/product-card/product-card-swatch-selector.js +4 -3
- package/dist/data/docs-manifest.d.ts +1 -1
- package/dist/data/docs-manifest.js +29 -74
- package/dist/styles/index.css +17 -6
- package/dist/{components/segmented/segmented-variants.d.ts → tokens/segmented.d.ts} +12 -1
- package/dist/{components/segmented/segmented-variants.js → tokens/segmented.js} +11 -0
- package/lint/tokens.mjs +1 -0
- package/package.json +5 -5
- package/dist/components/segmented/segmented-root.d.ts +0 -12
- package/dist/components/segmented/segmented-root.js +0 -12
- package/dist/components/segmented.d.ts +0 -1
- package/dist/components/segmented.js +0 -1
package/agent-docs.md
CHANGED
|
@@ -1259,10 +1259,11 @@ The component renders only the field stack - no card, heading, or CTA chrome. Co
|
|
|
1259
1259
|
|
|
1260
1260
|
`@instruments/materia-ui/components/progress`
|
|
1261
1261
|
|
|
1262
|
-
Linear progress bar showing completion from 0 to 100
|
|
1262
|
+
Linear progress bar showing completion from 0 to 100. Pass `value={null}` when the amount of work is unknown: a short bar slides across the track on a loop, and under reduced motion it holds still in the middle. Built on Base UI Progress for accessible semantics.
|
|
1263
1263
|
|
|
1264
|
-
- Use `Progress` for determinate 0–100 completion
|
|
1264
|
+
- Use `Progress` for determinate 0–100 completion and `value={null}` when the total is unknown; position in a stepped flow is `ProgressDots`.
|
|
1265
1265
|
- For a ring instead of a bar use `ProgressCircle` - it needs an explicit `label` for its native `<progress>` accessible name.
|
|
1266
|
+
- Mount `ReducedMotionProvider` at the app root - the indeterminate bar slides via `m.*` (LazyMotion).
|
|
1266
1267
|
|
|
1267
1268
|
## ProgressDots
|
|
1268
1269
|
|
|
@@ -1945,7 +1946,7 @@ The typography primitive for body copy - renders a `<p>` (or any element via `as
|
|
|
1945
1946
|
Renders a textarea element for multi-line text input (Base UI `Field.Control` rendering a `<textarea>`). Inside a Field it is the field's control: the FieldLabel targets it, FieldDescription/FieldError describe it, and it inherits the field's invalid state.
|
|
1946
1947
|
|
|
1947
1948
|
- Inside a `Field`, let invalid state cascade like `Input` - omit `hasError`; pass it only to force error styling on a standalone `Textarea`.
|
|
1948
|
-
- It auto-grows with content (`field-sizing-content`); set `rows` for
|
|
1949
|
+
- It auto-grows with content (`field-sizing-content`); set `rows` for a minimum height in lines rather than pinning height via CSS.
|
|
1949
1950
|
- Give it a label - inside a `Field` a `FieldLabel` targets it with no ids to wire; standalone, point a `Label`'s `htmlFor` at its `id`. A placeholder is not an accessible name.
|
|
1950
1951
|
|
|
1951
1952
|
## ThinkingLoader
|
|
@@ -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.
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
//
|
|
49
|
-
//
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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,
|
|
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
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
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
|
|
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
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
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
|
|
26
|
-
|
|
27
|
-
//
|
|
28
|
-
const
|
|
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
|
-
|
|
33
|
-
|
|
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
|
|
40
|
-
if (!
|
|
84
|
+
const section = ref.current;
|
|
85
|
+
if (!section || measuredSignature.current === partsSignature) {
|
|
41
86
|
return;
|
|
42
87
|
}
|
|
43
88
|
measuredSignature.current = partsSignature;
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
81
|
-
|
|
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:
|
|
87
|
-
|
|
88
|
-
|
|
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
|
+
};
|
|
@@ -114,18 +114,37 @@ const NEUTRAL_SOFT = "border-border bg-secondary text-secondary-foreground";
|
|
|
114
114
|
const NEUTRAL_SOFT_GRADIENT = "before:bg-sheen-bright hover:before:bg-sheen-hover active:before:bg-none disabled:before:bg-none data-disabled:before:bg-none data-[hovered=true]:before:bg-sheen-hover data-[pressed=true]:before:bg-none dark:before:bg-sheen-dark";
|
|
115
115
|
const NEUTRAL_OUTLINE = "border-border bg-background text-foreground hover:bg-muted hover:text-accent-foreground data-[hovered=true]:bg-muted data-[hovered=true]:text-accent-foreground";
|
|
116
116
|
const NEUTRAL_GHOST = "border-transparent bg-transparent text-foreground shadow-none hover:bg-muted hover:text-accent-foreground data-[hovered=true]:bg-muted data-[hovered=true]:text-accent-foreground";
|
|
117
|
-
|
|
118
|
-
|
|
117
|
+
/* ── Link, shared by every tone: no fill at rest or on hover, so each tone
|
|
118
|
+
* only picks a text colour that reads on the page. ─────────────────── */
|
|
119
|
+
const LINK = "border-transparent bg-transparent underline-offset-4 shadow-none hover:bg-transparent hover:underline data-[hovered=true]:bg-transparent data-[hovered=true]:underline";
|
|
120
|
+
const LINK_FLAT = "shadow-none before:hidden active:scale-100 active:underline-offset-5 data-[pressed=true]:scale-100 data-[pressed=true]:underline-offset-5";
|
|
121
|
+
const NEUTRAL_LINK = `${LINK} text-foreground`;
|
|
119
122
|
const NEUTRAL_TRANSPARENT = "transform-gpu border-transparent bg-primary/50 backdrop-blur-md will-change-[filter,background-color] hover:bg-primary/60 data-[hovered=true]:bg-primary/60";
|
|
120
123
|
/* ── Danger tone (was `variant="destructive"`) modifiers ────────────── */
|
|
124
|
+
// Built like neutral soft. The hover pins the fill, since the danger tone
|
|
125
|
+
// darkens its solid fill on hover.
|
|
126
|
+
const DANGER_SOFT = "border-error-border bg-error-soft text-error-foreground hover:bg-error-soft data-[hovered=true]:bg-error-soft";
|
|
121
127
|
const DANGER_OUTLINE = "border-destructive text-destructive hover:bg-destructive/5 data-[hovered=true]:bg-destructive/5";
|
|
122
128
|
const DANGER_GHOST = "text-destructive hover:bg-destructive/10 hover:text-destructive data-[hovered=true]:bg-destructive/10 data-[hovered=true]:text-destructive";
|
|
123
|
-
const
|
|
129
|
+
const DANGER_LINK = `${LINK} text-destructive`;
|
|
130
|
+
// Half the danger fill over a light page is a pale red, so light mode takes
|
|
131
|
+
// dark text; over graphite it is mid-toned, so dark mode takes white, as
|
|
132
|
+
// Pill's status tones do.
|
|
133
|
+
const DANGER_TRANSPARENT = "transform-gpu border-transparent bg-destructive/50 text-foreground backdrop-blur-md will-change-[filter,background-color] hover:bg-destructive/60 data-[hovered=true]:bg-destructive/60 dark:text-white/90";
|
|
124
134
|
/* ── Brand tone (was `variant="brand"`) modifiers ───────────────────── */
|
|
125
135
|
const BRAND_SOLID_GRADIENT = "shadow-button-sheen before:bg-sheen-strong hover:before:bg-sheen-hover active:shadow-none active:before:bg-none disabled:shadow-none disabled:before:bg-none data-disabled:shadow-none data-disabled:before:bg-none data-[hovered=true]:before:bg-sheen-hover data-[pressed=true]:shadow-none data-[pressed=true]:before:bg-none";
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const
|
|
136
|
+
// Built like neutral soft, with Pill's brand soft colours. Dark brand soft is
|
|
137
|
+
// graphite, so the text there takes the brand accent.
|
|
138
|
+
const BRAND_SOFT = "bg-brand-soft text-brand-foreground dark:text-brand-accent";
|
|
139
|
+
// Without a fill, brand yellow is unreadable on the page, so the text takes
|
|
140
|
+
// `brand-strong`; the outline border can stay yellow.
|
|
141
|
+
const BRAND_OUTLINE = "border-brand text-brand-strong hover:bg-brand/5 data-[hovered=true]:bg-brand/5";
|
|
142
|
+
const BRAND_GHOST = "text-brand-strong hover:bg-brand/10 hover:text-brand-strong data-[hovered=true]:bg-brand/10 data-[hovered=true]:text-brand-strong";
|
|
143
|
+
const BRAND_LINK = `${LINK} text-brand-strong`;
|
|
144
|
+
// Over graphite, half the brand fill turns olive and the brand foreground
|
|
145
|
+
// stops reading, so dark mode thickens the fill rather than swapping the text,
|
|
146
|
+
// which keeps the brand and its foreground overridable as a pair.
|
|
147
|
+
const BRAND_TRANSPARENT = "transform-gpu border-transparent bg-brand/50 backdrop-blur-md will-change-[filter,background-color] hover:bg-brand/60 data-[hovered=true]:bg-brand/60 dark:bg-brand/70 dark:hover:bg-brand/80 dark:data-[hovered=true]:bg-brand/80";
|
|
129
148
|
/**
|
|
130
149
|
* buttonVariants - CVA helper for Button styling.
|
|
131
150
|
*
|
|
@@ -191,7 +210,7 @@ const buttonVariants = cva([
|
|
|
191
210
|
},
|
|
192
211
|
{
|
|
193
212
|
asInput: false,
|
|
194
|
-
class: `${NEUTRAL_LINK} ${SOLID_SURFACE} ${
|
|
213
|
+
class: `${NEUTRAL_LINK} ${SOLID_SURFACE} ${LINK_FLAT}`,
|
|
195
214
|
emphasis: "link",
|
|
196
215
|
tone: "neutral",
|
|
197
216
|
},
|
|
@@ -208,6 +227,12 @@ const buttonVariants = cva([
|
|
|
208
227
|
emphasis: "solid",
|
|
209
228
|
tone: "danger",
|
|
210
229
|
},
|
|
230
|
+
{
|
|
231
|
+
asInput: false,
|
|
232
|
+
class: `${DANGER_SOFT} ${SOLID_SURFACE} ${SOLID_SHADOW} ${NEUTRAL_SOFT_GRADIENT}`,
|
|
233
|
+
emphasis: "soft",
|
|
234
|
+
tone: "danger",
|
|
235
|
+
},
|
|
211
236
|
{
|
|
212
237
|
asInput: false,
|
|
213
238
|
class: `${SOLID_SHADOW} ${DANGER_OUTLINE}`,
|
|
@@ -226,6 +251,12 @@ const buttonVariants = cva([
|
|
|
226
251
|
emphasis: "transparent",
|
|
227
252
|
tone: "danger",
|
|
228
253
|
},
|
|
254
|
+
{
|
|
255
|
+
asInput: false,
|
|
256
|
+
class: `${DANGER_LINK} ${LINK_FLAT}`,
|
|
257
|
+
emphasis: "link",
|
|
258
|
+
tone: "danger",
|
|
259
|
+
},
|
|
229
260
|
/* ── Brand tone (base colours from variants.tone) ─────── */
|
|
230
261
|
{
|
|
231
262
|
asInput: false,
|
|
@@ -233,6 +264,12 @@ const buttonVariants = cva([
|
|
|
233
264
|
emphasis: "solid",
|
|
234
265
|
tone: "brand",
|
|
235
266
|
},
|
|
267
|
+
{
|
|
268
|
+
asInput: false,
|
|
269
|
+
class: `${BRAND_SOFT} ${SOLID_SURFACE} ${SOLID_SHADOW} ${NEUTRAL_SOFT_GRADIENT}`,
|
|
270
|
+
emphasis: "soft",
|
|
271
|
+
tone: "brand",
|
|
272
|
+
},
|
|
236
273
|
{
|
|
237
274
|
asInput: false,
|
|
238
275
|
class: `${SOLID_SHADOW} ${BRAND_OUTLINE}`,
|
|
@@ -251,6 +288,12 @@ const buttonVariants = cva([
|
|
|
251
288
|
emphasis: "transparent",
|
|
252
289
|
tone: "brand",
|
|
253
290
|
},
|
|
291
|
+
{
|
|
292
|
+
asInput: false,
|
|
293
|
+
class: `${BRAND_LINK} ${LINK_FLAT}`,
|
|
294
|
+
emphasis: "link",
|
|
295
|
+
tone: "brand",
|
|
296
|
+
},
|
|
254
297
|
// Input chips carry the solid drop-shadow (was `variant="default"`).
|
|
255
298
|
// asInput replaces every tone/emphasis surface (gated `asInput: false`).
|
|
256
299
|
{ asInput: true, class: SOLID_SHADOW },
|
|
@@ -281,10 +324,12 @@ const buttonVariants = cva([
|
|
|
281
324
|
// brand|danger>, emphasis is the full Emphasis plus Button's `link`
|
|
282
325
|
// extension. The `satisfies Record<…>` fails to compile if these keys
|
|
283
326
|
// drift from ButtonTone/ButtonEmphasis (declared below).
|
|
327
|
+
// Danger fades only to /95 on hover: a lighter red under the light
|
|
328
|
+
// foreground and the sheen drops below 4.5:1 at /90.
|
|
284
329
|
tone: {
|
|
285
330
|
neutral: "",
|
|
286
331
|
brand: "border-brand-accent/40 bg-brand text-brand-foreground focus-visible:ring-brand/25 data-[focused=true]:ring-brand/25",
|
|
287
|
-
danger: "border-destructive bg-destructive text-destructive-foreground hover:bg-destructive/
|
|
332
|
+
danger: "border-destructive bg-destructive text-destructive-foreground hover:bg-destructive/95 data-[hovered=true]:bg-destructive/95 focus-visible:ring-red-400/50 data-[focused=true]:ring-red-400/50",
|
|
288
333
|
},
|
|
289
334
|
emphasis: {
|
|
290
335
|
solid: "",
|
|
@@ -40,16 +40,18 @@ const buttonGroupVariants = cva("flex w-fit items-stretch *:focus-visible:relati
|
|
|
40
40
|
dividers: false,
|
|
41
41
|
variant: "segmented",
|
|
42
42
|
},
|
|
43
|
-
//
|
|
43
|
+
// A joined group keeps each child's own border and drops every trailing
|
|
44
|
+
// edge but the last, so neighbours share one line and an outline group
|
|
45
|
+
// reads as one continuous outline. Dividers recolour that shared edge.
|
|
44
46
|
{
|
|
45
|
-
class: "[
|
|
46
|
-
dividers: false,
|
|
47
|
+
class: "[&>*:not(:last-child)]:border-r-0",
|
|
47
48
|
orientation: "horizontal",
|
|
49
|
+
variant: "joined",
|
|
48
50
|
},
|
|
49
51
|
{
|
|
50
|
-
class: "[
|
|
51
|
-
dividers: false,
|
|
52
|
+
class: "[&>*:not(:last-child)]:border-b-0",
|
|
52
53
|
orientation: "vertical",
|
|
54
|
+
variant: "joined",
|
|
53
55
|
},
|
|
54
56
|
// Optional radius overrides (applies to non-separator items)
|
|
55
57
|
{
|
|
@@ -90,7 +92,7 @@ const buttonGroupVariants = cva("flex w-fit items-stretch *:focus-visible:relati
|
|
|
90
92
|
variant: {
|
|
91
93
|
/**
|
|
92
94
|
* Default "joined" behaviour - relies on each child for borders/background,
|
|
93
|
-
* and
|
|
95
|
+
* and collapses the borders and inner radii where neighbours meet.
|
|
94
96
|
*/
|
|
95
97
|
joined: "",
|
|
96
98
|
/**
|
|
@@ -2,16 +2,42 @@
|
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import { cn } from "@instruments/materia-ui/utils/cn";
|
|
4
4
|
import { ChevronLeft, ChevronRight } from "lucide-react";
|
|
5
|
-
import { DayButton, DayPicker } from "react-day-picker";
|
|
5
|
+
import { Day, DayButton, DayPicker } from "react-day-picker";
|
|
6
6
|
import { buttonVariants } from "../button.js";
|
|
7
7
|
import { Icon } from "../icon.js";
|
|
8
8
|
const CalendarChevron = ({ orientation }) => (_jsx(Icon, { icon: orientation === "left" ? ChevronLeft : ChevronRight, size: "xs" }));
|
|
9
9
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* The accent band behind a selected range, painted on the grid cell because
|
|
11
|
+
* only the cells meet edge to edge. Middle days fill their cell, rounded where
|
|
12
|
+
* a week starts or ends; the first and last days fill the half that faces the
|
|
13
|
+
* range, so the band runs into their filled button. A range of one day has no
|
|
14
|
+
* band.
|
|
13
15
|
*/
|
|
14
|
-
const
|
|
16
|
+
const rangeBandClasses = (modifiers) => {
|
|
17
|
+
const start = modifiers.range_start === true;
|
|
18
|
+
const end = modifiers.range_end === true;
|
|
19
|
+
if (modifiers.range_middle === true) {
|
|
20
|
+
return "bg-accent text-accent-foreground first:rounded-l-md last:rounded-r-md";
|
|
21
|
+
}
|
|
22
|
+
if (start && !end) {
|
|
23
|
+
return "bg-linear-to-r from-transparent from-50% to-accent to-50% last:rounded-r-md";
|
|
24
|
+
}
|
|
25
|
+
if (end && !start) {
|
|
26
|
+
return "bg-linear-to-l from-transparent from-50% to-accent to-50% first:rounded-l-md";
|
|
27
|
+
}
|
|
28
|
+
return undefined;
|
|
29
|
+
};
|
|
30
|
+
const CalendarDay = ({ className, modifiers, ...props }) => (_jsx(Day, { className: cn(className, rangeBandClasses(modifiers)), modifiers: modifiers, ...props }));
|
|
31
|
+
/**
|
|
32
|
+
* DayPicker marks state on the grid cell, not the button inside it, so the
|
|
33
|
+
* button carries its own copy to paint the day. A range's middle days are
|
|
34
|
+
* selected too, but sit on the cell's band instead of a fill; today is tinted
|
|
35
|
+
* unless it is filled.
|
|
36
|
+
*/
|
|
37
|
+
const CalendarDayButton = ({ modifiers, ...props }) => {
|
|
38
|
+
const filled = modifiers.selected === true && modifiers.range_middle !== true;
|
|
39
|
+
return (_jsx(DayButton, { "data-selected": filled, "data-today": modifiers.today === true && !filled, modifiers: modifiers, ...props }));
|
|
40
|
+
};
|
|
15
41
|
/**
|
|
16
42
|
* Date picker built on react-day-picker, styled with the shared button variants;
|
|
17
43
|
* supports `single`, `multiple`, and `range` selection via the `mode` prop.
|
|
@@ -26,9 +52,8 @@ const Calendar = ({ className, classNames, showOutsideDays = true, ...props }) =
|
|
|
26
52
|
button_next: cn(buttonVariants({ emphasis: "ghost", tone: "neutral" }), "text-muted-foreground hover:bg-accent hover:text-foreground size-8 p-0"),
|
|
27
53
|
button_previous: cn(buttonVariants({ emphasis: "ghost", tone: "neutral" }), "text-muted-foreground hover:bg-accent hover:text-foreground size-8 p-0"),
|
|
28
54
|
caption_label: "text-paragraph-sm font-medium",
|
|
29
|
-
day:
|
|
30
|
-
|
|
31
|
-
day_button: cn(buttonVariants({ emphasis: "ghost" }), "data-[selected=true]:bg-primary data-[selected=true]:text-primary-foreground data-[selected=true]:hover:bg-primary data-[selected=true]:hover:text-primary-foreground size-8 rounded-md p-0 font-normal"),
|
|
55
|
+
day: "text-paragraph-sm relative w-9 p-0 text-center focus-within:relative focus-within:z-20",
|
|
56
|
+
day_button: cn(buttonVariants({ emphasis: "ghost" }), "data-[today=true]:bg-accent data-[today=true]:text-accent-foreground data-[selected=true]:bg-primary data-[selected=true]:text-primary-foreground data-[selected=true]:hover:bg-primary data-[selected=true]:hover:text-primary-foreground size-8 rounded-md p-0 font-normal"),
|
|
32
57
|
disabled: "text-muted-foreground opacity-50",
|
|
33
58
|
hidden: "invisible",
|
|
34
59
|
month: "flex flex-col gap-4",
|
|
@@ -36,18 +61,19 @@ const Calendar = ({ className, classNames, showOutsideDays = true, ...props }) =
|
|
|
36
61
|
month_grid: "w-full border-collapse space-y-1",
|
|
37
62
|
months: "relative flex flex-col gap-2 sm:flex-row",
|
|
38
63
|
nav: "absolute inset-x-0 top-0 z-10 flex h-9 items-center justify-between px-1",
|
|
39
|
-
outside: "
|
|
40
|
-
range_end: "
|
|
41
|
-
range_middle: "
|
|
42
|
-
range_start: "
|
|
64
|
+
outside: "text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30",
|
|
65
|
+
range_end: "",
|
|
66
|
+
range_middle: "",
|
|
67
|
+
range_start: "",
|
|
43
68
|
selected: "",
|
|
44
|
-
today: "
|
|
69
|
+
today: "",
|
|
45
70
|
week: "mt-2 flex w-full",
|
|
46
71
|
weekday: "w-9 rounded-md text-calendar font-normal text-muted-foreground",
|
|
47
72
|
weekdays: "flex",
|
|
48
73
|
...classNames,
|
|
49
74
|
}, components: {
|
|
50
75
|
Chevron: CalendarChevron,
|
|
76
|
+
Day: CalendarDay,
|
|
51
77
|
DayButton: CalendarDayButton,
|
|
52
78
|
}, showOutsideDays: showOutsideDays, ...props }));
|
|
53
79
|
Calendar.displayName = "Calendar";
|