@tanishqkancharla/maui 0.0.60 → 0.0.62
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/dist/components/ActivityIndicator.d.ts +13 -0
- package/dist/components/ActivityIndicator.js +56 -0
- package/dist/components/Button.js +2 -6
- package/dist/components/Checkbox.js +3 -2
- package/dist/components/LoadingScreen.d.ts +1 -1
- package/dist/components/LoadingScreen.js +3 -3
- package/dist/components/Radio.js +3 -2
- package/dist/components/Switch.js +2 -1
- package/dist/maui.d.ts +1 -0
- package/dist/maui.js +1 -0
- package/dist/tokens/shadow.d.ts +3 -0
- package/dist/tokens/shadow.js +5 -0
- package/package.json +1 -1
- package/skills/maui/SKILL.md +1 -0
- package/skills/maui/references/components/activity-indicator.md +16 -0
- package/skills/maui/references/components/loading-screen.md +4 -4
- package/skills/maui/references/components/thinking.md +2 -0
- package/src/apps/JsxEditor/catalog.ts +14 -2
- package/src/components/ActivityIndicator.tsx +112 -0
- package/src/components/Button.tsx +2 -7
- package/src/components/Checkbox.tsx +6 -1
- package/src/components/LoadingScreen.tsx +4 -5
- package/src/components/Radio.tsx +6 -1
- package/src/components/Switch.tsx +5 -0
- package/src/maui.ts +5 -0
- package/src/pages/ActivityIndicatorPage.tsx +81 -0
- package/src/pages/LoadingScreenPage.tsx +2 -2
- package/src/pages/Maui.tsx +29 -12
- package/src/tokens/shadow.ts +6 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type ActivityIndicatorVariant = "primary" | "accent" | "muted";
|
|
2
|
+
export type ActivityIndicatorProps = {
|
|
3
|
+
size?: string;
|
|
4
|
+
variant?: ActivityIndicatorVariant;
|
|
5
|
+
className?: string;
|
|
6
|
+
"aria-label"?: string;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* iOS-style spinner: eight rounded spokes with a fading tail. The wheel
|
|
10
|
+
* rotates in discrete 45° steps, like UIActivityIndicatorView, so the bright
|
|
11
|
+
* spoke appears to hop clockwise.
|
|
12
|
+
*/
|
|
13
|
+
export declare function ActivityIndicator({ size, variant, className, "aria-label": ariaLabel, }: ActivityIndicatorProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef } from "react";
|
|
3
|
+
import { style, useStyles } from "purse-styles";
|
|
4
|
+
import { colors } from "../tokens/colors";
|
|
5
|
+
import { cls } from "../utils/cls";
|
|
6
|
+
// Geometry is in a 24×24 viewBox. Spokes end this far from the center.
|
|
7
|
+
const VIEWBOX_CENTER = 12;
|
|
8
|
+
const SPOKE_OUTER_RADIUS = 9.5;
|
|
9
|
+
const SPOKE_COUNT = 8;
|
|
10
|
+
const SPOKE_LENGTH = 2.75;
|
|
11
|
+
const SPOKE_WIDTH = 2.5;
|
|
12
|
+
const MIN_SPOKE_OPACITY = 0.15;
|
|
13
|
+
const ROTATION_MS = 800;
|
|
14
|
+
const SPOKE_TOP = VIEWBOX_CENTER - SPOKE_OUTER_RADIUS;
|
|
15
|
+
// The top spoke leads; spokes counterclockwise from it fade toward the tail.
|
|
16
|
+
const spokes = Array.from({ length: SPOKE_COUNT }, (_, index) => {
|
|
17
|
+
const distanceBehindHead = (SPOKE_COUNT - index) % SPOKE_COUNT;
|
|
18
|
+
return {
|
|
19
|
+
angle: (index * 360) / SPOKE_COUNT,
|
|
20
|
+
opacity: 1 - (distanceBehindHead * (1 - MIN_SPOKE_OPACITY)) / (SPOKE_COUNT - 1),
|
|
21
|
+
};
|
|
22
|
+
});
|
|
23
|
+
/**
|
|
24
|
+
* iOS-style spinner: eight rounded spokes with a fading tail. The wheel
|
|
25
|
+
* rotates in discrete 45° steps, like UIActivityIndicatorView, so the bright
|
|
26
|
+
* spoke appears to hop clockwise.
|
|
27
|
+
*/
|
|
28
|
+
export function ActivityIndicator({ size = "1em", variant = "accent", className, "aria-label": ariaLabel = "Loading", }) {
|
|
29
|
+
const svgRef = useRef(null);
|
|
30
|
+
const rootClassName = useStyles(rootClass, variantStyles[variant]);
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
const animation = svgRef.current?.animate([{ transform: "rotate(0deg)" }, { transform: "rotate(360deg)" }], {
|
|
33
|
+
duration: ROTATION_MS,
|
|
34
|
+
iterations: Infinity,
|
|
35
|
+
easing: `steps(${SPOKE_COUNT})`,
|
|
36
|
+
});
|
|
37
|
+
return () => animation?.cancel();
|
|
38
|
+
}, []);
|
|
39
|
+
return (_jsx("span", { role: "status", "aria-label": ariaLabel, className: cls(rootClassName, className), style: { width: size, height: size }, children: _jsx("svg", { ref: svgRef, "aria-hidden": "true", focusable: "false", viewBox: "0 0 24 24", width: "100%", height: "100%", children: spokes.map(({ angle, opacity }) => (_jsx("line", { x1: VIEWBOX_CENTER, y1: SPOKE_TOP, x2: VIEWBOX_CENTER, y2: SPOKE_TOP + SPOKE_LENGTH, stroke: "currentColor", strokeWidth: SPOKE_WIDTH, strokeLinecap: "round", opacity: opacity, transform: `rotate(${angle} ${VIEWBOX_CENTER} ${VIEWBOX_CENTER})` }, angle))) }) }));
|
|
40
|
+
}
|
|
41
|
+
const variantStyles = {
|
|
42
|
+
primary: style({
|
|
43
|
+
color: "currentColor",
|
|
44
|
+
}),
|
|
45
|
+
accent: style({
|
|
46
|
+
color: colors.accent[10],
|
|
47
|
+
}),
|
|
48
|
+
muted: style({
|
|
49
|
+
color: colors.gray[11],
|
|
50
|
+
}),
|
|
51
|
+
};
|
|
52
|
+
const rootClass = style({
|
|
53
|
+
display: "inline-flex",
|
|
54
|
+
verticalAlign: "-0.125em",
|
|
55
|
+
flexShrink: 0,
|
|
56
|
+
});
|
|
@@ -9,7 +9,7 @@ import { backgroundColor, surfaceMixPercent, surfaceWash, } from "../tokens/back
|
|
|
9
9
|
import { colors } from "../tokens/colors";
|
|
10
10
|
import { focusRing } from "../tokens/focusRing";
|
|
11
11
|
import { radius } from "../tokens/radius";
|
|
12
|
-
import { shadowVars } from "../tokens/shadow";
|
|
12
|
+
import { shadowVars, tintedControl } from "../tokens/shadow";
|
|
13
13
|
import { controlSize, iconSizeValues } from "../tokens/sizing";
|
|
14
14
|
import { spacing } from "../tokens/spacing";
|
|
15
15
|
import { text } from "../tokens/text";
|
|
@@ -146,10 +146,6 @@ function onSolidText(color) {
|
|
|
146
146
|
function fillShade(color, lightnessOffset, alpha = 1) {
|
|
147
147
|
return `oklch(from ${color} calc(l - ${lightnessOffset}) c h / ${alpha})`;
|
|
148
148
|
}
|
|
149
|
-
/** Dark-only top highlight, a fill-tinted lift (half strength in dark mode), and an edge painted beneath the lift: the fill's alpha step 6 in light mode, a near-black tint of the fill in dark mode. */
|
|
150
|
-
function primaryButtonShadow(color, lightEdge) {
|
|
151
|
-
return `inset 0 .5px light-dark(transparent, rgb(255 255 255 / 0.15)), 0 1px 2px light-dark(${fillShade(color, 0, 0.35)}, ${fillShade(color, 0, 0.18)}), 0 3px 8px -3px light-dark(${fillShade(color, 0, 0.6)}, ${fillShade(color, 0, 0.3)}), 0 0 0 .5px light-dark(${lightEdge}, oklch(from ${color} 0.18 calc(c * 0.5) h))`;
|
|
152
|
-
}
|
|
153
149
|
/**
|
|
154
150
|
* Diagonal from a slightly darker, more saturated, warmer-rotated fill to the
|
|
155
151
|
* fill. The offsets are the OKLCH delta between Campsite's #FB432C → #FF591E.
|
|
@@ -160,7 +156,7 @@ function primaryButtonGradient(color) {
|
|
|
160
156
|
/** Hover lightens the top of the gradient instead of replacing the fill. */
|
|
161
157
|
const primaryHoverSheen = `linear-gradient(to bottom, rgb(255 255 255 / 0.12), rgb(255 255 255 / 0))`;
|
|
162
158
|
const primaryClass = memoize((fill, textColor, lightEdge) => {
|
|
163
|
-
const primaryShadow =
|
|
159
|
+
const primaryShadow = tintedControl(fill, lightEdge);
|
|
164
160
|
const gradient = primaryButtonGradient(fill);
|
|
165
161
|
return style(buttonBaseClass, focusRing("&:focus-visible", primaryShadow), {
|
|
166
162
|
color: textColor,
|
|
@@ -27,11 +27,11 @@ const check = style(motion.standard("opacity", "transform"), {
|
|
|
27
27
|
height: checkboxSize.checkmark,
|
|
28
28
|
paddingTop: "0.5px",
|
|
29
29
|
paddingLeft: "0.5px",
|
|
30
|
-
fill:
|
|
30
|
+
fill: "white",
|
|
31
31
|
opacity: 0,
|
|
32
32
|
transform: "scale(0)",
|
|
33
33
|
});
|
|
34
|
-
const root = style(flex({ alignItems: "center", gap: 3 }), focusRing(`&[data-focus-visible] ${box}`, shadowVars.control), {
|
|
34
|
+
const root = style(flex({ alignItems: "center", gap: 3 }), focusRing(`&[data-focus-visible] ${box}`, shadowVars.control), focusRing(`&[data-selected][data-focus-visible] ${box}`, shadowVars.selectedControl), {
|
|
35
35
|
position: "relative",
|
|
36
36
|
width: "fit-content",
|
|
37
37
|
minHeight: controlSize.minTarget,
|
|
@@ -41,6 +41,7 @@ const root = style(flex({ alignItems: "center", gap: 3 }), focusRing(`&[data-foc
|
|
|
41
41
|
},
|
|
42
42
|
[`&[data-selected] ${box}`]: {
|
|
43
43
|
backgroundColor: colors.accent[9],
|
|
44
|
+
boxShadow: shadowVars.selectedControl,
|
|
44
45
|
},
|
|
45
46
|
[`&[data-selected][data-hovered] ${box}`]: {
|
|
46
47
|
backgroundColor: surfaceWash(colors.gray[12], surfaceMixPercent.hover, colors.accent[9]),
|
|
@@ -4,7 +4,7 @@ export type LoadingScreenProps = {
|
|
|
4
4
|
};
|
|
5
5
|
/**
|
|
6
6
|
* Full-size loading state. A `progressLabel` on the first render fades the
|
|
7
|
-
*
|
|
7
|
+
* activity indicator and label in together. Without a label, the indicator
|
|
8
8
|
* waits 2s before fading in; a label that arrives later animates in and
|
|
9
9
|
* shifts the indicator so the pair stays centered.
|
|
10
10
|
*/
|
|
@@ -7,9 +7,9 @@ import { radius } from "../tokens/radius";
|
|
|
7
7
|
import { spacing } from "../tokens/spacing";
|
|
8
8
|
import { text } from "../tokens/text";
|
|
9
9
|
import { cls } from "../utils/cls";
|
|
10
|
+
import { ActivityIndicator } from "./ActivityIndicator";
|
|
10
11
|
import { Crossfade } from "./Crossfade";
|
|
11
12
|
import { Text } from "./Text";
|
|
12
|
-
import { Thinking } from "./Thinking";
|
|
13
13
|
const INDICATOR_DELAY_MS = 2000;
|
|
14
14
|
function resolvedLabel(progressLabel) {
|
|
15
15
|
const trimmed = progressLabel?.trim();
|
|
@@ -17,7 +17,7 @@ function resolvedLabel(progressLabel) {
|
|
|
17
17
|
}
|
|
18
18
|
/**
|
|
19
19
|
* Full-size loading state. A `progressLabel` on the first render fades the
|
|
20
|
-
*
|
|
20
|
+
* activity indicator and label in together. Without a label, the indicator
|
|
21
21
|
* waits 2s before fading in; a label that arrives later animates in and
|
|
22
22
|
* shifts the indicator so the pair stays centered.
|
|
23
23
|
*/
|
|
@@ -53,7 +53,7 @@ export function LoadingScreen({ progressLabel, className, }) {
|
|
|
53
53
|
}, INDICATOR_DELAY_MS);
|
|
54
54
|
return () => window.clearTimeout(timeoutId);
|
|
55
55
|
}, [hadLabelAtStart, label, showIndicator]);
|
|
56
|
-
return (_jsx("div", { className: cls(rootClassName, className), role: "status", "aria-live": "polite", "aria-busy": "true", children: _jsx(AnimatePresence, { children: showIndicator ? (_jsxs(motion.div, { className: stackClassName, initial: { opacity: 0 }, animate: { opacity: 1 }, transition: transition, children: [_jsx(
|
|
56
|
+
return (_jsx("div", { className: cls(rootClassName, className), role: "status", "aria-live": "polite", "aria-busy": "true", children: _jsx(AnimatePresence, { children: showIndicator ? (_jsxs(motion.div, { className: stackClassName, initial: { opacity: 0 }, animate: { opacity: 1 }, transition: transition, children: [_jsx(ActivityIndicator, { size: "1.25em", "aria-label": label ?? "Loading" }), _jsx(AnimatePresence, { initial: false, children: showLabel && label ? (_jsx(motion.div, { className: labelSlotClassName, initial: reduceMotion
|
|
57
57
|
? { opacity: 0 }
|
|
58
58
|
: { opacity: 0, height: 0 }, animate: reduceMotion
|
|
59
59
|
? { opacity: 1 }
|
package/dist/components/Radio.js
CHANGED
|
@@ -22,10 +22,10 @@ const dot = style(radius.circle, motion.standard("transform"), {
|
|
|
22
22
|
left: "4px",
|
|
23
23
|
width: "6px",
|
|
24
24
|
height: "6px",
|
|
25
|
-
backgroundColor:
|
|
25
|
+
backgroundColor: "white",
|
|
26
26
|
transform: "scale(0)",
|
|
27
27
|
});
|
|
28
|
-
const root = style(flex({ alignItems: "center", gap: 3 }), focusRing(`&[data-focus-visible] ${circle}`, shadowVars.control), {
|
|
28
|
+
const root = style(flex({ alignItems: "center", gap: 3 }), focusRing(`&[data-focus-visible] ${circle}`, shadowVars.control), focusRing(`&[data-selected][data-focus-visible] ${circle}`, shadowVars.selectedControl), {
|
|
29
29
|
position: "relative",
|
|
30
30
|
width: "fit-content",
|
|
31
31
|
[`&[data-hovered] ${circle}`]: {
|
|
@@ -33,6 +33,7 @@ const root = style(flex({ alignItems: "center", gap: 3 }), focusRing(`&[data-foc
|
|
|
33
33
|
},
|
|
34
34
|
[`&[data-selected] ${circle}`]: {
|
|
35
35
|
backgroundColor: colors.accent[9],
|
|
36
|
+
boxShadow: shadowVars.selectedControl,
|
|
36
37
|
},
|
|
37
38
|
[`&[data-selected][data-hovered] ${circle}`]: {
|
|
38
39
|
backgroundColor: surfaceWash(colors.gray[12], surfaceMixPercent.hover, colors.accent[9]),
|
|
@@ -42,7 +42,7 @@ const thumb = style(radius.circle, motion.standard("transform"), {
|
|
|
42
42
|
backgroundColor: "#ffffff",
|
|
43
43
|
boxShadow: "0 1px 2px rgba(0, 0, 0, 0.16)",
|
|
44
44
|
});
|
|
45
|
-
const root = style(flex({ alignItems: "center", gap: 3 }), focusRing(`&[data-focus-visible] ${track}`, shadowVars.control), {
|
|
45
|
+
const root = style(flex({ alignItems: "center", gap: 3 }), focusRing(`&[data-focus-visible] ${track}`, shadowVars.control), focusRing(`&[data-selected][data-focus-visible] ${track}`, shadowVars.selectedControl), {
|
|
46
46
|
position: "relative",
|
|
47
47
|
width: "fit-content",
|
|
48
48
|
minHeight: controlSize.minTarget,
|
|
@@ -51,6 +51,7 @@ const root = style(flex({ alignItems: "center", gap: 3 }), focusRing(`&[data-foc
|
|
|
51
51
|
},
|
|
52
52
|
[`&[data-selected] ${track}`]: {
|
|
53
53
|
backgroundColor: colors.accent[9],
|
|
54
|
+
boxShadow: shadowVars.selectedControl,
|
|
54
55
|
},
|
|
55
56
|
[`&[data-selected][data-hovered] ${track}`]: {
|
|
56
57
|
backgroundColor: switchOnHover,
|
package/dist/maui.d.ts
CHANGED
|
@@ -35,6 +35,7 @@ export { Overlay } from "./components/Overlay";
|
|
|
35
35
|
export { Drawer, drawerStyles, type DrawerProps, type DrawerSide, } from "./components/Drawer";
|
|
36
36
|
export { Table, TableHeader, TableHead, TableBody, TableFooter, TableRow, TableCell, TableCaption, tableStyles, type TableAlign, type TableProps, type TableHeaderProps, type TableHeadProps, type TableBodyProps, type TableFooterProps, type TableRowProps, type TableCellProps, } from "./components/Table";
|
|
37
37
|
export { Prose, useProseSize, proseContainerStyle, proseMaxWidth } from "./components/Prose";
|
|
38
|
+
export { ActivityIndicator, type ActivityIndicatorProps, type ActivityIndicatorVariant, } from "./components/ActivityIndicator";
|
|
38
39
|
export { Thinking, type ThinkingProps, type ThinkingVariant, } from "./components/Thinking";
|
|
39
40
|
export { Crossfade, type CrossfadeProps, type CrossfadeDirection, } from "./components/Crossfade";
|
|
40
41
|
export { LoadingScreen, type LoadingScreenProps, } from "./components/LoadingScreen";
|
package/dist/maui.js
CHANGED
|
@@ -38,6 +38,7 @@ export { Overlay } from "./components/Overlay";
|
|
|
38
38
|
export { Drawer, drawerStyles, } from "./components/Drawer";
|
|
39
39
|
export { Table, TableHeader, TableHead, TableBody, TableFooter, TableRow, TableCell, TableCaption, tableStyles, } from "./components/Table";
|
|
40
40
|
export { Prose, useProseSize, proseContainerStyle, proseMaxWidth } from "./components/Prose";
|
|
41
|
+
export { ActivityIndicator, } from "./components/ActivityIndicator";
|
|
41
42
|
export { Thinking, } from "./components/Thinking";
|
|
42
43
|
export { Crossfade, } from "./components/Crossfade";
|
|
43
44
|
export { LoadingScreen, } from "./components/LoadingScreen";
|
package/dist/tokens/shadow.d.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
/** Dark-only top highlight, fill-tinted lift, and a 0.5px edge beneath the lift. */
|
|
2
|
+
export declare function tintedControl(color: string, lightEdge: string): string;
|
|
1
3
|
export declare const shadowVars: import("purse-styles").VariableGroup<{
|
|
2
4
|
readonly control: {
|
|
3
5
|
readonly default: `0 .5px 1px #00000018, 0 0 4px -1px #0001, 0 0 0 .5px ${string}`;
|
|
4
6
|
readonly ":root[data-theme=\"dark\"]": "inset 0 .5px rgb(255 255 255 / 0.09), 0 1px .5px #00000018, 0 0 3px -1px #0005, 0 0 0 .5px rgb(0 0 0 / 0.8)";
|
|
5
7
|
};
|
|
8
|
+
readonly selectedControl: string;
|
|
6
9
|
readonly subtle: `rgba(var(--${string}), 0.08) 0px 0px 0px 1px, rgba(0, 0, 0, var(--${string})) 0px 1px 1px -0.5px, rgba(0, 0, 0, var(--${string})) 0px 3px 3px -1.5px`;
|
|
7
10
|
readonly medium: `rgba(var(--${string}), 0.08) 0px 0px 0px 1px, rgba(0, 0, 0, var(--${string})) 0px 1px 1px -0.5px, rgba(0, 0, 0, var(--${string})) 0px 3px 3px -1.5px, rgba(0, 0, 0, var(--${string})) 0px 6px 6px -3px`;
|
|
8
11
|
readonly strong: `rgba(var(--${string}), 0.08) 0px 0px 0px 1px, rgba(0, 0, 0, var(--${string})) 0px 1px 1px -0.5px, rgba(0, 0, 0, var(--${string})) 0px 3px 3px -1.5px, rgba(0, 0, 0, var(--${string})) 0px 6px 6px -3px, rgba(0, 0, 0, calc(var(--${string}) * 0.67)) 0px 12px 12px -6px, rgba(0, 0, 0, calc(var(--${string}) * 0.67)) 0px 24px 24px -12px`;
|
package/dist/tokens/shadow.js
CHANGED
|
@@ -13,6 +13,10 @@ const shadowParams = defineVars({
|
|
|
13
13
|
});
|
|
14
14
|
const rgb = shadowParams.foregroundRgb;
|
|
15
15
|
const blur = shadowParams.blurOpacity;
|
|
16
|
+
/** Dark-only top highlight, fill-tinted lift, and a 0.5px edge beneath the lift. */
|
|
17
|
+
export function tintedControl(color, lightEdge) {
|
|
18
|
+
return `inset 0 .5px light-dark(transparent, rgb(255 255 255 / 0.15)), 0 1px 2px light-dark(oklch(from ${color} l c h / 0.35), oklch(from ${color} l c h / 0.18)), 0 3px 8px -3px light-dark(oklch(from ${color} l c h / 0.6), oklch(from ${color} l c h / 0.3)), 0 0 0 .5px light-dark(${lightEdge}, oklch(from ${color} 0.18 calc(c * 0.5) h))`;
|
|
19
|
+
}
|
|
16
20
|
export const shadowVars = defineVars({
|
|
17
21
|
control: {
|
|
18
22
|
// 0.5px edge listed last, so it paints beneath the drop: light reads as a
|
|
@@ -20,6 +24,7 @@ export const shadowVars = defineVars({
|
|
|
20
24
|
default: `0 .5px 1px #00000018, 0 0 4px -1px #0001, 0 0 0 .5px ${colors.grayAlpha[6]}`,
|
|
21
25
|
[DARK_THEME]: `inset 0 .5px rgb(255 255 255 / 0.09), 0 1px .5px #00000018, 0 0 3px -1px #0005, 0 0 0 .5px rgb(0 0 0 / 0.8)`,
|
|
22
26
|
},
|
|
27
|
+
selectedControl: tintedControl(colors.accent[9], colors.accentAlpha[6]),
|
|
23
28
|
subtle: `rgba(${rgb}, 0.08) 0px 0px 0px 1px, rgba(0, 0, 0, ${blur}) 0px 1px 1px -0.5px, rgba(0, 0, 0, ${blur}) 0px 3px 3px -1.5px`,
|
|
24
29
|
medium: `rgba(${rgb}, 0.08) 0px 0px 0px 1px, rgba(0, 0, 0, ${blur}) 0px 1px 1px -0.5px, rgba(0, 0, 0, ${blur}) 0px 3px 3px -1.5px, rgba(0, 0, 0, ${blur}) 0px 6px 6px -3px`,
|
|
25
30
|
strong: `rgba(${rgb}, 0.08) 0px 0px 0px 1px, rgba(0, 0, 0, ${blur}) 0px 1px 1px -0.5px, rgba(0, 0, 0, ${blur}) 0px 3px 3px -1.5px, rgba(0, 0, 0, ${blur}) 0px 6px 6px -3px, rgba(0, 0, 0, calc(${blur} * 0.67)) 0px 12px 12px -6px, rgba(0, 0, 0, calc(${blur} * 0.67)) 0px 24px 24px -12px`,
|
package/package.json
CHANGED
package/skills/maui/SKILL.md
CHANGED
|
@@ -30,6 +30,7 @@ Use Maui components. When one can't do what you need, build from the matching `r
|
|
|
30
30
|
| Prose | [prose.md](references/components/prose.md) (`Prose`, `H1`–`H4`, `P`, lists, `Label`, `Link`) |
|
|
31
31
|
| Editor / MarkdownEditor | [editor.md](references/components/editor.md) (optional TipTap peers; separate imports) |
|
|
32
32
|
| Thinking | [thinking.md](references/components/thinking.md) |
|
|
33
|
+
| Activity indicator | [activity-indicator.md](references/components/activity-indicator.md) |
|
|
33
34
|
| Crossfade | [crossfade.md](references/components/crossfade.md) |
|
|
34
35
|
| Loading screen | [loading-screen.md](references/components/loading-screen.md) |
|
|
35
36
|
| Text | [text.md](references/components/text.md) |
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Activity indicator
|
|
2
|
+
|
|
3
|
+
Gallery: `/components/activity-indicator`. Import from `"maui"`.
|
|
4
|
+
|
|
5
|
+
iOS-style spinner: eight spokes with a fading tail that rotate in 45° steps. `size` is a CSS length (default `1em`). `variant`: `"accent"` (default) | `"primary"` (currentColor) | `"muted"`. `aria-label` defaults to `"Loading"`.
|
|
6
|
+
|
|
7
|
+
On an accent fill, such as a primary button, pass `variant="primary"` so the spinner takes the button text color.
|
|
8
|
+
|
|
9
|
+
Use it for loading: fetching, saving, syncing, and other indeterminate waits. When waiting on LLM inference, use [Thinking](thinking.md) instead. The `Spinner` icon is a static glyph, not this component.
|
|
10
|
+
|
|
11
|
+
```tsx
|
|
12
|
+
<Button variant="primary">
|
|
13
|
+
<ActivityIndicator variant="primary" aria-label="Saving" />
|
|
14
|
+
Saving
|
|
15
|
+
</Button>
|
|
16
|
+
```
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Loading screen
|
|
2
2
|
|
|
3
|
-
Gallery: `/components/loading-screen`. Import from `"maui"`. Uses [
|
|
3
|
+
Gallery: `/components/loading-screen`. Import from `"maui"`. Uses [Activity indicator](activity-indicator.md) and [Crossfade](crossfade.md).
|
|
4
4
|
|
|
5
|
-
Fills available width and height (`flex: 1`, `width/height: 100%`). Centered `
|
|
5
|
+
Fills available width and height (`flex: 1`, `width/height: 100%`). Centered `ActivityIndicator` (accent, `1.25em`), with `spacing.value(12)` (24px at the default scale) above the label.
|
|
6
6
|
|
|
7
7
|
```tsx
|
|
8
8
|
<LoadingScreen />
|
|
@@ -11,7 +11,7 @@ Fills available width and height (`flex: 1`, `width/height: 100%`). Centered `Th
|
|
|
11
11
|
|
|
12
12
|
| First paint | Later `progressLabel` |
|
|
13
13
|
| --- | --- |
|
|
14
|
-
| Label present |
|
|
15
|
-
| No label |
|
|
14
|
+
| Label present | Indicator + label fade in together. Later labels Crossfade `up`. Label punctuation is preserved; nothing is appended |
|
|
15
|
+
| No label | The indicator waits **2s**, then fades in. A label before 2s fades both in immediately. A label after 2s animates in and shifts the indicator so the pair stays centered |
|
|
16
16
|
|
|
17
17
|
`role="status"` `aria-busy` `aria-live="polite"`.
|
|
@@ -4,6 +4,8 @@ Gallery: `/components/thinking`. Import from `"maui"`.
|
|
|
4
4
|
|
|
5
5
|
3×3 Game of Life, text-sized. `size` is a CSS length (default `1em`). `variant`: `"primary"` (currentColor) | `"accent"` | `"muted"`.
|
|
6
6
|
|
|
7
|
+
Use Thinking only while waiting on LLM inference, such as a model drafting a reply or an agent working. For ordinary loading (fetching, saving, syncing), use [Activity indicator](activity-indicator.md).
|
|
8
|
+
|
|
7
9
|
```tsx
|
|
8
10
|
<Flex row alignItems="center" gap={4}>
|
|
9
11
|
<Thinking size="0.75em" variant="muted" />
|
|
@@ -42,6 +42,7 @@ import {
|
|
|
42
42
|
import { Text } from "../../components/Text"
|
|
43
43
|
import { TabBar } from "../../components/TabBar"
|
|
44
44
|
import { Thinking } from "../../components/Thinking"
|
|
45
|
+
import { ActivityIndicator } from "../../components/ActivityIndicator"
|
|
45
46
|
import { Crossfade } from "../../components/Crossfade"
|
|
46
47
|
import { LoadingScreen } from "../../components/LoadingScreen"
|
|
47
48
|
import { Tooltip } from "../../components/Tooltip"
|
|
@@ -529,6 +530,16 @@ export const catalog: CatalogComponent[] = [
|
|
|
529
530
|
{ name: "className" },
|
|
530
531
|
],
|
|
531
532
|
},
|
|
533
|
+
{
|
|
534
|
+
name: "ActivityIndicator",
|
|
535
|
+
info: "iOS-style spinner. Eight spokes with a fading tail rotate in 45° steps. Accent by default.",
|
|
536
|
+
attributes: [
|
|
537
|
+
{ name: "size", info: "CSS length, default 1em" },
|
|
538
|
+
{ name: "variant", values: ["accent", "primary", "muted"] },
|
|
539
|
+
{ name: "aria-label", info: "Default Loading" },
|
|
540
|
+
{ name: "className" },
|
|
541
|
+
],
|
|
542
|
+
},
|
|
532
543
|
{
|
|
533
544
|
name: "Crossfade",
|
|
534
545
|
info: "When contentKey changes, fades previous children out and the next in. Omit direction for an in-place opacity fade. Pass up, down, left, or right to slide. contentKey is required.",
|
|
@@ -544,11 +555,11 @@ export const catalog: CatalogComponent[] = [
|
|
|
544
555
|
},
|
|
545
556
|
{
|
|
546
557
|
name: "LoadingScreen",
|
|
547
|
-
info: "Full-size loading state. Optional progressLabel is shown in accent, medium weight, with trailing ....
|
|
558
|
+
info: "Full-size loading state. Optional progressLabel is shown in accent, medium weight, with trailing .... Accent ActivityIndicator. Delays 2s when no label at start; label changes Crossfade up.",
|
|
548
559
|
attributes: [
|
|
549
560
|
{
|
|
550
561
|
name: "progressLabel",
|
|
551
|
-
info: "Optional status under the
|
|
562
|
+
info: "Optional status under the activity indicator",
|
|
552
563
|
},
|
|
553
564
|
{ name: "className" },
|
|
554
565
|
],
|
|
@@ -600,6 +611,7 @@ export const previewScope: Record<string, unknown> = {
|
|
|
600
611
|
Text,
|
|
601
612
|
TabBar,
|
|
602
613
|
Thinking,
|
|
614
|
+
ActivityIndicator,
|
|
603
615
|
Crossfade,
|
|
604
616
|
LoadingScreen,
|
|
605
617
|
Tooltip,
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react"
|
|
2
|
+
import { style, useStyles } from "purse-styles"
|
|
3
|
+
import { colors } from "../tokens/colors"
|
|
4
|
+
import { cls } from "../utils/cls"
|
|
5
|
+
|
|
6
|
+
// Geometry is in a 24×24 viewBox. Spokes end this far from the center.
|
|
7
|
+
const VIEWBOX_CENTER = 12
|
|
8
|
+
const SPOKE_OUTER_RADIUS = 9.5
|
|
9
|
+
const SPOKE_COUNT = 8
|
|
10
|
+
const SPOKE_LENGTH = 2.75
|
|
11
|
+
const SPOKE_WIDTH = 2.5
|
|
12
|
+
const MIN_SPOKE_OPACITY = 0.15
|
|
13
|
+
const ROTATION_MS = 800
|
|
14
|
+
|
|
15
|
+
const SPOKE_TOP = VIEWBOX_CENTER - SPOKE_OUTER_RADIUS
|
|
16
|
+
|
|
17
|
+
// The top spoke leads; spokes counterclockwise from it fade toward the tail.
|
|
18
|
+
const spokes = Array.from({ length: SPOKE_COUNT }, (_, index) => {
|
|
19
|
+
const distanceBehindHead = (SPOKE_COUNT - index) % SPOKE_COUNT
|
|
20
|
+
return {
|
|
21
|
+
angle: (index * 360) / SPOKE_COUNT,
|
|
22
|
+
opacity:
|
|
23
|
+
1 - (distanceBehindHead * (1 - MIN_SPOKE_OPACITY)) / (SPOKE_COUNT - 1),
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
export type ActivityIndicatorVariant = "primary" | "accent" | "muted"
|
|
28
|
+
|
|
29
|
+
export type ActivityIndicatorProps = {
|
|
30
|
+
size?: string
|
|
31
|
+
variant?: ActivityIndicatorVariant
|
|
32
|
+
className?: string
|
|
33
|
+
"aria-label"?: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* iOS-style spinner: eight rounded spokes with a fading tail. The wheel
|
|
38
|
+
* rotates in discrete 45° steps, like UIActivityIndicatorView, so the bright
|
|
39
|
+
* spoke appears to hop clockwise.
|
|
40
|
+
*/
|
|
41
|
+
export function ActivityIndicator({
|
|
42
|
+
size = "1em",
|
|
43
|
+
variant = "accent",
|
|
44
|
+
className,
|
|
45
|
+
"aria-label": ariaLabel = "Loading",
|
|
46
|
+
}: ActivityIndicatorProps) {
|
|
47
|
+
const svgRef = useRef<SVGSVGElement>(null)
|
|
48
|
+
const rootClassName = useStyles(rootClass, variantStyles[variant])
|
|
49
|
+
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
const animation = svgRef.current?.animate(
|
|
52
|
+
[{ transform: "rotate(0deg)" }, { transform: "rotate(360deg)" }],
|
|
53
|
+
{
|
|
54
|
+
duration: ROTATION_MS,
|
|
55
|
+
iterations: Infinity,
|
|
56
|
+
easing: `steps(${SPOKE_COUNT})`,
|
|
57
|
+
},
|
|
58
|
+
)
|
|
59
|
+
return () => animation?.cancel()
|
|
60
|
+
}, [])
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<span
|
|
64
|
+
role="status"
|
|
65
|
+
aria-label={ariaLabel}
|
|
66
|
+
className={cls(rootClassName, className)}
|
|
67
|
+
style={{ width: size, height: size }}
|
|
68
|
+
>
|
|
69
|
+
<svg
|
|
70
|
+
ref={svgRef}
|
|
71
|
+
aria-hidden="true"
|
|
72
|
+
focusable="false"
|
|
73
|
+
viewBox="0 0 24 24"
|
|
74
|
+
width="100%"
|
|
75
|
+
height="100%"
|
|
76
|
+
>
|
|
77
|
+
{spokes.map(({ angle, opacity }) => (
|
|
78
|
+
<line
|
|
79
|
+
key={angle}
|
|
80
|
+
x1={VIEWBOX_CENTER}
|
|
81
|
+
y1={SPOKE_TOP}
|
|
82
|
+
x2={VIEWBOX_CENTER}
|
|
83
|
+
y2={SPOKE_TOP + SPOKE_LENGTH}
|
|
84
|
+
stroke="currentColor"
|
|
85
|
+
strokeWidth={SPOKE_WIDTH}
|
|
86
|
+
strokeLinecap="round"
|
|
87
|
+
opacity={opacity}
|
|
88
|
+
transform={`rotate(${angle} ${VIEWBOX_CENTER} ${VIEWBOX_CENTER})`}
|
|
89
|
+
/>
|
|
90
|
+
))}
|
|
91
|
+
</svg>
|
|
92
|
+
</span>
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const variantStyles = {
|
|
97
|
+
primary: style({
|
|
98
|
+
color: "currentColor",
|
|
99
|
+
}),
|
|
100
|
+
accent: style({
|
|
101
|
+
color: colors.accent[10],
|
|
102
|
+
}),
|
|
103
|
+
muted: style({
|
|
104
|
+
color: colors.gray[11],
|
|
105
|
+
}),
|
|
106
|
+
} as const
|
|
107
|
+
|
|
108
|
+
const rootClass = style({
|
|
109
|
+
display: "inline-flex",
|
|
110
|
+
verticalAlign: "-0.125em",
|
|
111
|
+
flexShrink: 0,
|
|
112
|
+
})
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
import { colors, type ColorName, type ColorScale } from "../tokens/colors"
|
|
16
16
|
import { focusRing } from "../tokens/focusRing"
|
|
17
17
|
import { radius } from "../tokens/radius"
|
|
18
|
-
import { shadowVars } from "../tokens/shadow"
|
|
18
|
+
import { shadowVars, tintedControl } from "../tokens/shadow"
|
|
19
19
|
import { controlSize, iconSizeValues } from "../tokens/sizing"
|
|
20
20
|
import { spacing } from "../tokens/spacing"
|
|
21
21
|
import { text } from "../tokens/text"
|
|
@@ -193,11 +193,6 @@ function fillShade(color: string, lightnessOffset: number, alpha = 1) {
|
|
|
193
193
|
return `oklch(from ${color} calc(l - ${lightnessOffset}) c h / ${alpha})`
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
-
/** Dark-only top highlight, a fill-tinted lift (half strength in dark mode), and an edge painted beneath the lift: the fill's alpha step 6 in light mode, a near-black tint of the fill in dark mode. */
|
|
197
|
-
function primaryButtonShadow(color: string, lightEdge: string) {
|
|
198
|
-
return `inset 0 .5px light-dark(transparent, rgb(255 255 255 / 0.15)), 0 1px 2px light-dark(${fillShade(color, 0, 0.35)}, ${fillShade(color, 0, 0.18)}), 0 3px 8px -3px light-dark(${fillShade(color, 0, 0.6)}, ${fillShade(color, 0, 0.3)}), 0 0 0 .5px light-dark(${lightEdge}, oklch(from ${color} 0.18 calc(c * 0.5) h))`
|
|
199
|
-
}
|
|
200
|
-
|
|
201
196
|
/**
|
|
202
197
|
* Diagonal from a slightly darker, more saturated, warmer-rotated fill to the
|
|
203
198
|
* fill. The offsets are the OKLCH delta between Campsite's #FB432C → #FF591E.
|
|
@@ -211,7 +206,7 @@ const primaryHoverSheen = `linear-gradient(to bottom, rgb(255 255 255 / 0.12), r
|
|
|
211
206
|
|
|
212
207
|
const primaryClass = memoize(
|
|
213
208
|
(fill: string, textColor: string, lightEdge: string) => {
|
|
214
|
-
const primaryShadow =
|
|
209
|
+
const primaryShadow = tintedControl(fill, lightEdge)
|
|
215
210
|
const gradient = primaryButtonGradient(fill)
|
|
216
211
|
return style(buttonBaseClass, focusRing("&:focus-visible", primaryShadow), {
|
|
217
212
|
color: textColor,
|
|
@@ -44,7 +44,7 @@ const check = style(motion.standard("opacity", "transform"), {
|
|
|
44
44
|
height: checkboxSize.checkmark,
|
|
45
45
|
paddingTop: "0.5px",
|
|
46
46
|
paddingLeft: "0.5px",
|
|
47
|
-
fill:
|
|
47
|
+
fill: "white",
|
|
48
48
|
opacity: 0,
|
|
49
49
|
transform: "scale(0)",
|
|
50
50
|
})
|
|
@@ -52,6 +52,10 @@ const check = style(motion.standard("opacity", "transform"), {
|
|
|
52
52
|
const root = style(
|
|
53
53
|
flex({ alignItems: "center", gap: 3 }),
|
|
54
54
|
focusRing(`&[data-focus-visible] ${box}`, shadowVars.control),
|
|
55
|
+
focusRing(
|
|
56
|
+
`&[data-selected][data-focus-visible] ${box}`,
|
|
57
|
+
shadowVars.selectedControl,
|
|
58
|
+
),
|
|
55
59
|
{
|
|
56
60
|
position: "relative",
|
|
57
61
|
width: "fit-content",
|
|
@@ -62,6 +66,7 @@ const root = style(
|
|
|
62
66
|
},
|
|
63
67
|
[`&[data-selected] ${box}`]: {
|
|
64
68
|
backgroundColor: colors.accent[9],
|
|
69
|
+
boxShadow: shadowVars.selectedControl,
|
|
65
70
|
},
|
|
66
71
|
[`&[data-selected][data-hovered] ${box}`]: {
|
|
67
72
|
backgroundColor: surfaceWash(
|
|
@@ -10,9 +10,9 @@ import { radius } from "../tokens/radius"
|
|
|
10
10
|
import { spacing } from "../tokens/spacing"
|
|
11
11
|
import { text } from "../tokens/text"
|
|
12
12
|
import { cls } from "../utils/cls"
|
|
13
|
+
import { ActivityIndicator } from "./ActivityIndicator"
|
|
13
14
|
import { Crossfade } from "./Crossfade"
|
|
14
15
|
import { Text } from "./Text"
|
|
15
|
-
import { Thinking } from "./Thinking"
|
|
16
16
|
|
|
17
17
|
const INDICATOR_DELAY_MS = 2000
|
|
18
18
|
|
|
@@ -28,7 +28,7 @@ function resolvedLabel(progressLabel: string | undefined) {
|
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
30
|
* Full-size loading state. A `progressLabel` on the first render fades the
|
|
31
|
-
*
|
|
31
|
+
* activity indicator and label in together. Without a label, the indicator
|
|
32
32
|
* waits 2s before fading in; a label that arrives later animates in and
|
|
33
33
|
* shifts the indicator so the pair stays centered.
|
|
34
34
|
*/
|
|
@@ -87,9 +87,8 @@ export function LoadingScreen({
|
|
|
87
87
|
animate={{ opacity: 1 }}
|
|
88
88
|
transition={transition}
|
|
89
89
|
>
|
|
90
|
-
<
|
|
91
|
-
|
|
92
|
-
size="0.8em"
|
|
90
|
+
<ActivityIndicator
|
|
91
|
+
size="1.25em"
|
|
93
92
|
aria-label={label ?? "Loading"}
|
|
94
93
|
/>
|
|
95
94
|
<AnimatePresence initial={false}>
|
package/src/components/Radio.tsx
CHANGED
|
@@ -40,13 +40,17 @@ const dot = style(radius.circle, motion.standard("transform"), {
|
|
|
40
40
|
left: "4px",
|
|
41
41
|
width: "6px",
|
|
42
42
|
height: "6px",
|
|
43
|
-
backgroundColor:
|
|
43
|
+
backgroundColor: "white",
|
|
44
44
|
transform: "scale(0)",
|
|
45
45
|
})
|
|
46
46
|
|
|
47
47
|
const root = style(
|
|
48
48
|
flex({ alignItems: "center", gap: 3 }),
|
|
49
49
|
focusRing(`&[data-focus-visible] ${circle}`, shadowVars.control),
|
|
50
|
+
focusRing(
|
|
51
|
+
`&[data-selected][data-focus-visible] ${circle}`,
|
|
52
|
+
shadowVars.selectedControl,
|
|
53
|
+
),
|
|
50
54
|
{
|
|
51
55
|
position: "relative",
|
|
52
56
|
width: "fit-content",
|
|
@@ -55,6 +59,7 @@ const root = style(
|
|
|
55
59
|
},
|
|
56
60
|
[`&[data-selected] ${circle}`]: {
|
|
57
61
|
backgroundColor: colors.accent[9],
|
|
62
|
+
boxShadow: shadowVars.selectedControl,
|
|
58
63
|
},
|
|
59
64
|
[`&[data-selected][data-hovered] ${circle}`]: {
|
|
60
65
|
backgroundColor: surfaceWash(
|
|
@@ -58,6 +58,10 @@ const thumb = style(radius.circle, motion.standard("transform"), {
|
|
|
58
58
|
const root = style(
|
|
59
59
|
flex({ alignItems: "center", gap: 3 }),
|
|
60
60
|
focusRing(`&[data-focus-visible] ${track}`, shadowVars.control),
|
|
61
|
+
focusRing(
|
|
62
|
+
`&[data-selected][data-focus-visible] ${track}`,
|
|
63
|
+
shadowVars.selectedControl,
|
|
64
|
+
),
|
|
61
65
|
{
|
|
62
66
|
position: "relative",
|
|
63
67
|
width: "fit-content",
|
|
@@ -67,6 +71,7 @@ const root = style(
|
|
|
67
71
|
},
|
|
68
72
|
[`&[data-selected] ${track}`]: {
|
|
69
73
|
backgroundColor: colors.accent[9],
|
|
74
|
+
boxShadow: shadowVars.selectedControl,
|
|
70
75
|
},
|
|
71
76
|
[`&[data-selected][data-hovered] ${track}`]: {
|
|
72
77
|
backgroundColor: switchOnHover,
|
package/src/maui.ts
CHANGED
|
@@ -139,6 +139,11 @@ export {
|
|
|
139
139
|
type TableCellProps,
|
|
140
140
|
} from "./components/Table"
|
|
141
141
|
export { Prose, useProseSize, proseContainerStyle, proseMaxWidth } from "./components/Prose"
|
|
142
|
+
export {
|
|
143
|
+
ActivityIndicator,
|
|
144
|
+
type ActivityIndicatorProps,
|
|
145
|
+
type ActivityIndicatorVariant,
|
|
146
|
+
} from "./components/ActivityIndicator"
|
|
142
147
|
export {
|
|
143
148
|
Thinking,
|
|
144
149
|
type ThinkingProps,
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { style, useStyles } from "purse-styles"
|
|
2
|
+
import { Panel } from "./Panel"
|
|
3
|
+
import { ActivityIndicator } from "../components/ActivityIndicator"
|
|
4
|
+
import { Button } from "../components/Button"
|
|
5
|
+
import { Prose } from "../components/Prose"
|
|
6
|
+
import { H2, H3, P } from "../components/Typography"
|
|
7
|
+
import { flex } from "../tokens/layout"
|
|
8
|
+
import { text } from "../tokens/text"
|
|
9
|
+
|
|
10
|
+
export function ActivityIndicatorPage() {
|
|
11
|
+
const pageClassName = useStyles(pageClass)
|
|
12
|
+
const rowClassName = useStyles(rowClass)
|
|
13
|
+
const columnClassName = useStyles(columnClass)
|
|
14
|
+
const inlineExampleClassName = useStyles(inlineExampleClass)
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<Prose className={pageClassName}>
|
|
18
|
+
<H2>Activity indicator</H2>
|
|
19
|
+
<P>
|
|
20
|
+
An iOS-style spinner. Eight rounded spokes fade from the leading spoke
|
|
21
|
+
to the tail, and the wheel rotates in 45° steps so the bright spoke hops
|
|
22
|
+
clockwise. It is text-sized and accent-colored by default. Use it for
|
|
23
|
+
loading; use Thinking when waiting on LLM inference.
|
|
24
|
+
</P>
|
|
25
|
+
|
|
26
|
+
<H3>Variants</H3>
|
|
27
|
+
<Panel className={columnClassName}>
|
|
28
|
+
<p className={inlineExampleClassName}>
|
|
29
|
+
<ActivityIndicator variant="accent" aria-label="Loading, accent" />
|
|
30
|
+
Loading messages
|
|
31
|
+
</p>
|
|
32
|
+
<p className={inlineExampleClassName}>
|
|
33
|
+
<ActivityIndicator variant="primary" aria-label="Loading, primary" />
|
|
34
|
+
Syncing calendar
|
|
35
|
+
</p>
|
|
36
|
+
<p className={inlineExampleClassName}>
|
|
37
|
+
<ActivityIndicator variant="muted" aria-label="Loading, muted" />
|
|
38
|
+
Fetching attachments
|
|
39
|
+
</p>
|
|
40
|
+
</Panel>
|
|
41
|
+
|
|
42
|
+
<H3>Sizes</H3>
|
|
43
|
+
<Panel className={rowClassName}>
|
|
44
|
+
<ActivityIndicator size="1em" aria-label="Loading, small" />
|
|
45
|
+
<ActivityIndicator size="20px" aria-label="Loading, medium" />
|
|
46
|
+
<ActivityIndicator size="37px" aria-label="Loading, large" />
|
|
47
|
+
</Panel>
|
|
48
|
+
|
|
49
|
+
<H3>In a button</H3>
|
|
50
|
+
<Panel className={rowClassName}>
|
|
51
|
+
<Button variant="primary">
|
|
52
|
+
<ActivityIndicator variant="primary" aria-label="Saving" />
|
|
53
|
+
Saving
|
|
54
|
+
</Button>
|
|
55
|
+
<Button>
|
|
56
|
+
<ActivityIndicator aria-label="Refreshing" />
|
|
57
|
+
Refreshing
|
|
58
|
+
</Button>
|
|
59
|
+
</Panel>
|
|
60
|
+
</Prose>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const pageClass = style({
|
|
65
|
+
maxWidth: "1200px",
|
|
66
|
+
paddingBottom: "32px",
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
const rowClass = style(flex({ alignItems: "center", gap: 6 }))
|
|
70
|
+
|
|
71
|
+
const columnClass = style(flex({ direction: "column", gap: 6 }))
|
|
72
|
+
|
|
73
|
+
const inlineExampleClass = style(
|
|
74
|
+
text({ size: "md", fontWeight: 400, color: "highContrast" }),
|
|
75
|
+
{
|
|
76
|
+
display: "flex",
|
|
77
|
+
alignItems: "center",
|
|
78
|
+
gap: "6px",
|
|
79
|
+
margin: 0,
|
|
80
|
+
},
|
|
81
|
+
)
|
|
@@ -98,7 +98,7 @@ export function LoadingScreenPage() {
|
|
|
98
98
|
Fills whatever width and height it is given, with{" "}
|
|
99
99
|
<Code>radius.lg</Code> on the surface. Pass{" "}
|
|
100
100
|
<Code>progressLabel</Code> to show a status under a small, accent
|
|
101
|
-
|
|
101
|
+
activity indicator. The label is accent, medium weight, and keeps the
|
|
102
102
|
punctuation you provide. Label changes Crossfade up. With no label at
|
|
103
103
|
mount, the indicator waits 2s before fading in.
|
|
104
104
|
</P>
|
|
@@ -136,7 +136,7 @@ export function LoadingScreenPage() {
|
|
|
136
136
|
|
|
137
137
|
<H3>No label — wait 2 seconds</H3>
|
|
138
138
|
<P>
|
|
139
|
-
Mounts empty. After 2 seconds the
|
|
139
|
+
Mounts empty. After 2 seconds the activity indicator fades in,
|
|
140
140
|
centered, with no label.
|
|
141
141
|
</P>
|
|
142
142
|
<Flex column gap={4}>
|
package/src/pages/Maui.tsx
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type React from "react"
|
|
2
2
|
import { Suspense, useEffect, useState } from "react"
|
|
3
|
-
import { useFilter } from "react-aria"
|
|
4
3
|
import {
|
|
5
4
|
Link as WouterLink,
|
|
6
5
|
Redirect,
|
|
@@ -13,6 +12,7 @@ import {
|
|
|
13
12
|
import { defineVars, style, useStyles } from "purse-styles"
|
|
14
13
|
import { Button } from "../components/Button"
|
|
15
14
|
import { Drawer } from "../components/Drawer"
|
|
15
|
+
import { FuzzyString } from "../components/FuzzyString"
|
|
16
16
|
import { SearchField } from "../components/Input"
|
|
17
17
|
import { Select, SelectItem } from "../components/Select"
|
|
18
18
|
import { H3, Label } from "../components/Typography"
|
|
@@ -26,8 +26,10 @@ import { text } from "../tokens/text"
|
|
|
26
26
|
import { type ThemePreference, useTheme } from "../theme/ThemeContext"
|
|
27
27
|
import { isScalePreference, useScale } from "../theme/ScaleContext"
|
|
28
28
|
import { LARGE_SCALE } from "../theme/dataScale"
|
|
29
|
+
import { fuzzyMatch } from "../utils/fuzzyMatch"
|
|
29
30
|
import { galleryCompactMedia } from "./galleryCompact"
|
|
30
31
|
import { ReadmePage } from "./ReadmePage"
|
|
32
|
+
import { ActivityIndicatorPage } from "./ActivityIndicatorPage"
|
|
31
33
|
import { AvatarPage } from "./AvatarPage"
|
|
32
34
|
import { BadgePage } from "./BadgePage"
|
|
33
35
|
import { BackgroundColorTokenPage } from "./BackgroundColorTokenPage"
|
|
@@ -152,6 +154,11 @@ const navigation: NavEntry[] = [
|
|
|
152
154
|
},
|
|
153
155
|
{ label: "Editor", path: "/components/editor", page: EditorPage },
|
|
154
156
|
{ label: "Thinking", path: "/components/thinking", page: ThinkingPage },
|
|
157
|
+
{
|
|
158
|
+
label: "Activity indicator",
|
|
159
|
+
path: "/components/activity-indicator",
|
|
160
|
+
page: ActivityIndicatorPage,
|
|
161
|
+
},
|
|
155
162
|
{
|
|
156
163
|
label: "Crossfade",
|
|
157
164
|
path: "/components/crossfade",
|
|
@@ -363,19 +370,18 @@ function MauiNavigation() {
|
|
|
363
370
|
const emptyClassName = useStyles(navEmptyClass)
|
|
364
371
|
const { preference, setPreference } = useTheme()
|
|
365
372
|
const { preference: scale, setPreference: setScale } = useScale()
|
|
366
|
-
const { contains } = useFilter({ sensitivity: "base" })
|
|
367
373
|
const normalizedQuery = query.trim()
|
|
374
|
+
const matches = (label: string) =>
|
|
375
|
+
fuzzyMatch(normalizedQuery, label) !== undefined
|
|
368
376
|
const filteredNavigation = normalizedQuery
|
|
369
377
|
? navigation.flatMap<NavEntry>((entry) => {
|
|
370
378
|
if (!isNavGroup(entry)) {
|
|
371
|
-
return
|
|
379
|
+
return matches(entry.label) ? [entry] : []
|
|
372
380
|
}
|
|
373
381
|
|
|
374
|
-
const children =
|
|
382
|
+
const children = matches(entry.label)
|
|
375
383
|
? entry.children
|
|
376
|
-
: entry.children.filter((item) =>
|
|
377
|
-
contains(item.label, normalizedQuery),
|
|
378
|
-
)
|
|
384
|
+
: entry.children.filter((item) => matches(item.label))
|
|
379
385
|
return children.length > 0 ? [{ ...entry, children }] : []
|
|
380
386
|
})
|
|
381
387
|
: navigation
|
|
@@ -459,15 +465,21 @@ function MauiNavigation() {
|
|
|
459
465
|
{filteredNavigation.map((entry) =>
|
|
460
466
|
isNavGroup(entry) ? (
|
|
461
467
|
<li className={groupClassName} key={entry.label}>
|
|
462
|
-
<Label>
|
|
468
|
+
<Label>
|
|
469
|
+
<HighlightedLabel label={entry.label} query={normalizedQuery} />
|
|
470
|
+
</Label>
|
|
463
471
|
<ul className={childrenClassName}>
|
|
464
472
|
{entry.children.map((item) => (
|
|
465
|
-
<NavLink
|
|
473
|
+
<NavLink
|
|
474
|
+
key={item.path}
|
|
475
|
+
item={item}
|
|
476
|
+
query={normalizedQuery}
|
|
477
|
+
/>
|
|
466
478
|
))}
|
|
467
479
|
</ul>
|
|
468
480
|
</li>
|
|
469
481
|
) : (
|
|
470
|
-
<NavLink key={entry.path} item={entry} />
|
|
482
|
+
<NavLink key={entry.path} item={entry} query={normalizedQuery} />
|
|
471
483
|
),
|
|
472
484
|
)}
|
|
473
485
|
{filteredNavigation.length === 0 ? (
|
|
@@ -480,7 +492,12 @@ function MauiNavigation() {
|
|
|
480
492
|
)
|
|
481
493
|
}
|
|
482
494
|
|
|
483
|
-
function
|
|
495
|
+
function HighlightedLabel(props: { label: string; query: string }) {
|
|
496
|
+
const match = props.query ? fuzzyMatch(props.query, props.label) : undefined
|
|
497
|
+
return match ? <FuzzyString match={match} /> : props.label
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function NavLink(props: { item: NavItem; query: string }) {
|
|
484
501
|
const className = useStyles(navLinkClass)
|
|
485
502
|
const [selected] = useRoute(props.item.path)
|
|
486
503
|
|
|
@@ -491,7 +508,7 @@ function NavLink(props: { item: NavItem }) {
|
|
|
491
508
|
href={props.item.path}
|
|
492
509
|
aria-current={selected ? "page" : undefined}
|
|
493
510
|
>
|
|
494
|
-
{props.item.label}
|
|
511
|
+
<HighlightedLabel label={props.item.label} query={props.query} />
|
|
495
512
|
</WouterLink>
|
|
496
513
|
</li>
|
|
497
514
|
)
|
package/src/tokens/shadow.ts
CHANGED
|
@@ -16,6 +16,11 @@ const shadowParams = defineVars({
|
|
|
16
16
|
const rgb = shadowParams.foregroundRgb
|
|
17
17
|
const blur = shadowParams.blurOpacity
|
|
18
18
|
|
|
19
|
+
/** Dark-only top highlight, fill-tinted lift, and a 0.5px edge beneath the lift. */
|
|
20
|
+
export function tintedControl(color: string, lightEdge: string) {
|
|
21
|
+
return `inset 0 .5px light-dark(transparent, rgb(255 255 255 / 0.15)), 0 1px 2px light-dark(oklch(from ${color} l c h / 0.35), oklch(from ${color} l c h / 0.18)), 0 3px 8px -3px light-dark(oklch(from ${color} l c h / 0.6), oklch(from ${color} l c h / 0.3)), 0 0 0 .5px light-dark(${lightEdge}, oklch(from ${color} 0.18 calc(c * 0.5) h))`
|
|
22
|
+
}
|
|
23
|
+
|
|
19
24
|
export const shadowVars = defineVars({
|
|
20
25
|
control: {
|
|
21
26
|
// 0.5px edge listed last, so it paints beneath the drop: light reads as a
|
|
@@ -23,6 +28,7 @@ export const shadowVars = defineVars({
|
|
|
23
28
|
default: `0 .5px 1px #00000018, 0 0 4px -1px #0001, 0 0 0 .5px ${colors.grayAlpha[6]}`,
|
|
24
29
|
[DARK_THEME]: `inset 0 .5px rgb(255 255 255 / 0.09), 0 1px .5px #00000018, 0 0 3px -1px #0005, 0 0 0 .5px rgb(0 0 0 / 0.8)`,
|
|
25
30
|
},
|
|
31
|
+
selectedControl: tintedControl(colors.accent[9], colors.accentAlpha[6]),
|
|
26
32
|
subtle: `rgba(${rgb}, 0.08) 0px 0px 0px 1px, rgba(0, 0, 0, ${blur}) 0px 1px 1px -0.5px, rgba(0, 0, 0, ${blur}) 0px 3px 3px -1.5px`,
|
|
27
33
|
medium: `rgba(${rgb}, 0.08) 0px 0px 0px 1px, rgba(0, 0, 0, ${blur}) 0px 1px 1px -0.5px, rgba(0, 0, 0, ${blur}) 0px 3px 3px -1.5px, rgba(0, 0, 0, ${blur}) 0px 6px 6px -3px`,
|
|
28
34
|
strong: `rgba(${rgb}, 0.08) 0px 0px 0px 1px, rgba(0, 0, 0, ${blur}) 0px 1px 1px -0.5px, rgba(0, 0, 0, ${blur}) 0px 3px 3px -1.5px, rgba(0, 0, 0, ${blur}) 0px 6px 6px -3px, rgba(0, 0, 0, calc(${blur} * 0.67)) 0px 12px 12px -6px, rgba(0, 0, 0, calc(${blur} * 0.67)) 0px 24px 24px -12px`,
|