@passui/react 0.1.26 → 0.1.28
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/types/src/components/segmented-control/segmented-control.d.ts +3 -1
- package/dist/types/src/components/stat-tile/stat-tile.d.ts +3 -1
- package/dist/types/src/components/swatch-picker/swatch-picker.d.ts +3 -1
- package/dist/types/src/components/text-field/text-field.d.ts +7 -2
- package/dist/types/src/components/toggle/toggle.d.ts +3 -1
- package/dist/types/src/motion/transitions.d.ts +86 -13
- package/package.json +1 -1
- package/src/components/dialog/dialog.tsx +15 -3
- package/src/components/popover/popover.tsx +19 -3
- package/src/components/segmented-control/segmented-control.tsx +4 -1
- package/src/components/sheet/sheet.tsx +41 -6
- package/src/components/stat-tile/stat-tile.tsx +17 -5
- package/src/components/swatch-picker/swatch-picker.tsx +4 -1
- package/src/components/text-field/text-field.tsx +58 -4
- package/src/components/toggle/toggle.tsx +18 -4
- package/src/motion/transitions.ts +63 -5
|
@@ -10,6 +10,8 @@ export interface SegmentedControlProps<T extends string> {
|
|
|
10
10
|
onChange: (value: NoInfer<T>) => void;
|
|
11
11
|
/** Accessible name for the group and its compact picker. */
|
|
12
12
|
label: string;
|
|
13
|
+
/** Custom classes for the control container. */
|
|
14
|
+
className?: string;
|
|
13
15
|
}
|
|
14
16
|
/**
|
|
15
17
|
* Mutually exclusive choices with a sliding selection, as on Apple
|
|
@@ -17,4 +19,4 @@ export interface SegmentedControlProps<T extends string> {
|
|
|
17
19
|
* translation, a narrow pane) it becomes a pop-up button instead of
|
|
18
20
|
* squeezing or spilling its text.
|
|
19
21
|
*/
|
|
20
|
-
export declare function SegmentedControl<T extends string>({ value, options, onChange, label, }: SegmentedControlProps<T>): import("react").JSX.Element;
|
|
22
|
+
export declare function SegmentedControl<T extends string>({ value, options, onChange, label, className, }: SegmentedControlProps<T>): import("react").JSX.Element;
|
|
@@ -10,6 +10,8 @@ export interface StatTileProps {
|
|
|
10
10
|
/** Makes the tile a toggle, e.g. a filter. */
|
|
11
11
|
onClick?: () => void;
|
|
12
12
|
active?: boolean;
|
|
13
|
+
/** Custom classes for the tile container. */
|
|
14
|
+
className?: string;
|
|
13
15
|
}
|
|
14
16
|
/** One number with its label, optionally a toggle. */
|
|
15
|
-
export declare function StatTile({ label, value, caption, tone, onClick, active }: StatTileProps): import("react").JSX.Element;
|
|
17
|
+
export declare function StatTile({ label, value, caption, tone, onClick, active, className, }: StatTileProps): import("react").JSX.Element;
|
|
@@ -12,6 +12,8 @@ export interface SwatchPickerProps<T extends string> {
|
|
|
12
12
|
onChange: (value: T) => void;
|
|
13
13
|
/** Accessible name for the group. */
|
|
14
14
|
label: string;
|
|
15
|
+
/** Custom classes for the swatch container. */
|
|
16
|
+
className?: string;
|
|
15
17
|
}
|
|
16
18
|
/** Colour swatches with a check on the chosen one. */
|
|
17
|
-
export declare function SwatchPicker<T extends string>({ value, options, onChange, label, }: SwatchPickerProps<T>): import("react").JSX.Element;
|
|
19
|
+
export declare function SwatchPicker<T extends string>({ value, options, onChange, label, className, }: SwatchPickerProps<T>): import("react").JSX.Element;
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { type ComponentProps, type ReactNode } from "react";
|
|
2
|
+
/** Shared text control classes for components that reuse TextField's surface. */
|
|
2
3
|
export declare const textControlClasses: string;
|
|
3
|
-
|
|
4
|
+
/** Size scale shared across TextField, SecureField, and form fields. */
|
|
5
|
+
export type TextFieldSize = "xs" | "sm" | "md" | "lg";
|
|
6
|
+
export type TextFieldProps = Omit<ComponentProps<"input">, "size"> & {
|
|
4
7
|
label: string;
|
|
5
8
|
description?: ReactNode;
|
|
6
9
|
errors?: string[];
|
|
@@ -16,5 +19,7 @@ export type TextFieldProps = ComponentProps<"input"> & {
|
|
|
16
19
|
};
|
|
17
20
|
/** Float the label into the outline on focus or when filled. @default "above" */
|
|
18
21
|
labelPlacement?: "above" | "floating";
|
|
22
|
+
/** Field height and text scale. @default "md" */
|
|
23
|
+
size?: TextFieldSize;
|
|
19
24
|
};
|
|
20
|
-
export declare function TextField({ label, description, errors, icon, trailingIcon, action, revealLabels, labelPlacement, placeholder, type, id: suppliedId, className, ...props }: TextFieldProps): import("react").JSX.Element;
|
|
25
|
+
export declare function TextField({ label, description, errors, icon, trailingIcon, action, revealLabels, labelPlacement, size, placeholder, type, id: suppliedId, className, ...props }: TextFieldProps): import("react").JSX.Element;
|
|
@@ -13,5 +13,7 @@ export interface ToggleProps {
|
|
|
13
13
|
mixed?: boolean;
|
|
14
14
|
/** Keeps the accessible name while hiding the visible label. @default false */
|
|
15
15
|
labelHidden?: boolean;
|
|
16
|
+
/** Custom classes for the toggle container. */
|
|
17
|
+
className?: string;
|
|
16
18
|
}
|
|
17
|
-
export declare function Toggle({ checked, onChange, label, disabled, style, mixed, labelHidden, }: ToggleProps): import("react").JSX.Element;
|
|
19
|
+
export declare function Toggle({ checked, onChange, label, disabled, style, mixed, labelHidden, className, }: ToggleProps): import("react").JSX.Element;
|
|
@@ -19,12 +19,37 @@ export declare const spring: {
|
|
|
19
19
|
readonly type: "spring";
|
|
20
20
|
};
|
|
21
21
|
};
|
|
22
|
+
/**
|
|
23
|
+
* Apple's fluid overshoot: cubic-bezier(0.34, 1.56, 0.1, 1).
|
|
24
|
+
* Replaces traditional ease profiles with an authentic fluid bounce
|
|
25
|
+
* that overshoots and settles, matching Apple's custom interface physics.
|
|
26
|
+
* Used for zoom/matched-geometry transitions where the surface emerges
|
|
27
|
+
* from the trigger and snaps into place with a physical bounce.
|
|
28
|
+
*/
|
|
29
|
+
export declare const fluidOvershoot: {
|
|
30
|
+
readonly duration: 0.4;
|
|
31
|
+
readonly ease: [number, number, number, number];
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Exit motion: content collapses back toward its source.
|
|
35
|
+
* The surface shrinks toward the trigger's position while fading,
|
|
36
|
+
* like a water drop absorbing back into its origin.
|
|
37
|
+
*/
|
|
38
|
+
export declare const fluidCollapse: {
|
|
39
|
+
readonly duration: 0.28;
|
|
40
|
+
readonly ease: [number, number, number, number];
|
|
41
|
+
};
|
|
22
42
|
export type FloatingPanelSide = "above" | "below";
|
|
23
|
-
/**
|
|
43
|
+
/**
|
|
44
|
+
* Anchored panels settle toward their source without shrinking their text.
|
|
45
|
+
* Now uses origin-based scaling: the panel grows from the trigger's
|
|
46
|
+
* position (transform-origin set by the caller) with a scale + opacity
|
|
47
|
+
* crossfade, rather than a plain slide.
|
|
48
|
+
*/
|
|
24
49
|
export declare function floatingPanelMotion(side?: FloatingPanelSide): {
|
|
25
50
|
readonly initial: {
|
|
26
51
|
readonly opacity: 0;
|
|
27
|
-
readonly scale:
|
|
52
|
+
readonly scale: 0.92;
|
|
28
53
|
readonly y: number;
|
|
29
54
|
};
|
|
30
55
|
readonly animate: {
|
|
@@ -34,16 +59,15 @@ export declare function floatingPanelMotion(side?: FloatingPanelSide): {
|
|
|
34
59
|
};
|
|
35
60
|
readonly exit: {
|
|
36
61
|
readonly opacity: 0;
|
|
37
|
-
readonly scale:
|
|
62
|
+
readonly scale: 0.95;
|
|
38
63
|
readonly y: number;
|
|
39
64
|
};
|
|
40
65
|
readonly transition: {
|
|
41
|
-
readonly
|
|
42
|
-
readonly
|
|
43
|
-
readonly mass: 1;
|
|
44
|
-
readonly type: "spring";
|
|
66
|
+
readonly duration: 0.4;
|
|
67
|
+
readonly ease: [number, number, number, number];
|
|
45
68
|
readonly opacity: {
|
|
46
69
|
readonly duration: 0.16;
|
|
70
|
+
readonly ease: [number, number, number, number];
|
|
47
71
|
};
|
|
48
72
|
};
|
|
49
73
|
};
|
|
@@ -51,7 +75,7 @@ export declare function floatingPanelMotion(side?: FloatingPanelSide): {
|
|
|
51
75
|
export declare const popoverMotion: {
|
|
52
76
|
readonly initial: {
|
|
53
77
|
readonly opacity: 0;
|
|
54
|
-
readonly scale:
|
|
78
|
+
readonly scale: 0.92;
|
|
55
79
|
readonly y: number;
|
|
56
80
|
};
|
|
57
81
|
readonly animate: {
|
|
@@ -61,19 +85,68 @@ export declare const popoverMotion: {
|
|
|
61
85
|
};
|
|
62
86
|
readonly exit: {
|
|
63
87
|
readonly opacity: 0;
|
|
64
|
-
readonly scale:
|
|
88
|
+
readonly scale: 0.95;
|
|
65
89
|
readonly y: number;
|
|
66
90
|
};
|
|
67
91
|
readonly transition: {
|
|
68
|
-
readonly
|
|
69
|
-
readonly
|
|
70
|
-
readonly mass: 1;
|
|
71
|
-
readonly type: "spring";
|
|
92
|
+
readonly duration: 0.4;
|
|
93
|
+
readonly ease: [number, number, number, number];
|
|
72
94
|
readonly opacity: {
|
|
73
95
|
readonly duration: 0.16;
|
|
96
|
+
readonly ease: [number, number, number, number];
|
|
74
97
|
};
|
|
75
98
|
};
|
|
76
99
|
};
|
|
100
|
+
/**
|
|
101
|
+
* Zoom/matched-geometry transition for dialogs and center-placed sheets.
|
|
102
|
+
* The surface starts small (scale 0.88) and invisible, then grows to
|
|
103
|
+
* full size with the fluid overshoot bounce. Content elements inside
|
|
104
|
+
* crossfade in slightly delayed so they phase into visibility
|
|
105
|
+
* asynchronously — not a harsh switch.
|
|
106
|
+
*/
|
|
107
|
+
export declare const zoomReveal: {
|
|
108
|
+
readonly initial: {
|
|
109
|
+
readonly opacity: 0;
|
|
110
|
+
readonly scale: 0.88;
|
|
111
|
+
};
|
|
112
|
+
readonly animate: {
|
|
113
|
+
readonly opacity: 1;
|
|
114
|
+
readonly scale: 1;
|
|
115
|
+
};
|
|
116
|
+
readonly exit: {
|
|
117
|
+
readonly opacity: 0;
|
|
118
|
+
readonly scale: 0.92;
|
|
119
|
+
};
|
|
120
|
+
readonly transition: {
|
|
121
|
+
readonly duration: 0.4;
|
|
122
|
+
readonly ease: [number, number, number, number];
|
|
123
|
+
};
|
|
124
|
+
};
|
|
125
|
+
/**
|
|
126
|
+
* Content crossfade for elements inside an overlay.
|
|
127
|
+
* Rather than a harsh switch, sub-elements scale and phase into
|
|
128
|
+
* visibility asynchronously. Use for children of dialogs, sheets,
|
|
129
|
+
* and popovers — the container arrives first, then content fades in.
|
|
130
|
+
*/
|
|
131
|
+
export declare const contentCrossfade: {
|
|
132
|
+
readonly initial: {
|
|
133
|
+
readonly opacity: 0;
|
|
134
|
+
readonly scale: 0.96;
|
|
135
|
+
};
|
|
136
|
+
readonly animate: {
|
|
137
|
+
readonly opacity: 1;
|
|
138
|
+
readonly scale: 1;
|
|
139
|
+
};
|
|
140
|
+
readonly exit: {
|
|
141
|
+
readonly opacity: 0;
|
|
142
|
+
readonly scale: 0.98;
|
|
143
|
+
};
|
|
144
|
+
readonly transition: {
|
|
145
|
+
readonly duration: 0.24;
|
|
146
|
+
readonly ease: [number, number, number, number];
|
|
147
|
+
readonly delay: 0.04;
|
|
148
|
+
};
|
|
149
|
+
};
|
|
77
150
|
/** Floating bars that rise from the bottom edge. */
|
|
78
151
|
export declare const riseMotion: {
|
|
79
152
|
readonly initial: {
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@ import { Sheet } from "../sheet/sheet"
|
|
|
2
2
|
import { AnimatePresence, motion, useReducedMotion } from "motion/react"
|
|
3
3
|
import { Button } from "../button/button"
|
|
4
4
|
import { cn } from "../../lib/class-names"
|
|
5
|
-
import { spring } from "../../motion/transitions"
|
|
5
|
+
import { contentCrossfade, spring, zoomReveal } from "../../motion/transitions"
|
|
6
6
|
import { createContext, useContext, useId, type FormEvent, type ReactNode } from "react"
|
|
7
7
|
|
|
8
8
|
const CloseContext = createContext<() => void>(() => {})
|
|
@@ -81,19 +81,31 @@ export function Dialog({
|
|
|
81
81
|
initial={{
|
|
82
82
|
height: reducedMotion ? "auto" : 0,
|
|
83
83
|
opacity: 0,
|
|
84
|
+
scale: reducedMotion ? 1 : 0.97,
|
|
84
85
|
}}
|
|
85
86
|
animate={{
|
|
86
87
|
height: "auto",
|
|
87
88
|
opacity: 1,
|
|
89
|
+
scale: 1,
|
|
88
90
|
}}
|
|
89
91
|
exit={{
|
|
90
92
|
height: reducedMotion ? "auto" : 0,
|
|
91
93
|
opacity: 0,
|
|
94
|
+
scale: reducedMotion ? 1 : 0.98,
|
|
92
95
|
}}
|
|
93
|
-
transition={reducedMotion ? { duration: 0 } : spring.
|
|
96
|
+
transition={reducedMotion ? { duration: 0 } : spring.bouncy}
|
|
94
97
|
className="overflow-hidden"
|
|
98
|
+
style={{ transformOrigin: "top center" }}
|
|
95
99
|
>
|
|
96
|
-
|
|
100
|
+
<motion.div
|
|
101
|
+
initial={reducedMotion ? false : contentCrossfade.initial}
|
|
102
|
+
animate={reducedMotion ? undefined : contentCrossfade.animate}
|
|
103
|
+
transition={
|
|
104
|
+
reducedMotion ? { duration: 0 } : contentCrossfade.transition
|
|
105
|
+
}
|
|
106
|
+
>
|
|
107
|
+
{body}
|
|
108
|
+
</motion.div>
|
|
97
109
|
</motion.div>
|
|
98
110
|
)}
|
|
99
111
|
</AnimatePresence>
|
|
@@ -10,7 +10,12 @@ import { concealMenuSource } from "../context-menu/conceal-menu-source"
|
|
|
10
10
|
import { menuSurfaceExit, menuSurfaceReveal } from "../context-menu/menu-reveal"
|
|
11
11
|
import { cn } from "../../lib/class-names"
|
|
12
12
|
import { screenEdges } from "../../lib/screen-edges"
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
contentCrossfade,
|
|
15
|
+
floatingPanelMotion,
|
|
16
|
+
fluidOvershoot,
|
|
17
|
+
spring,
|
|
18
|
+
} from "../../motion/transitions"
|
|
14
19
|
import { fitPopover, type PopoverFit } from "./popover-fit"
|
|
15
20
|
|
|
16
21
|
export type PopoverOrigin = "top-left" | "top-right"
|
|
@@ -56,6 +61,11 @@ function OpenPopover({
|
|
|
56
61
|
const [reveal, setReveal] = useState<ReturnType<typeof menuSurfaceReveal> | null>(null)
|
|
57
62
|
const popoverMotion = floatingPanelMotion(fit.above ? "above" : "below")
|
|
58
63
|
|
|
64
|
+
// For anchored popovers, the surface morphs from the trigger via
|
|
65
|
+
// menuSurfaceReveal. For non-anchored popovers, we use scale + the
|
|
66
|
+
// fluid overshoot spring so the panel grows from the origin corner
|
|
67
|
+
// with a physical bounce — Apple's matched-geometry feel.
|
|
68
|
+
|
|
59
69
|
useLayoutEffect(() => {
|
|
60
70
|
const refit = () => {
|
|
61
71
|
const element = ref.current
|
|
@@ -283,11 +293,17 @@ function OpenPopover({
|
|
|
283
293
|
transition={
|
|
284
294
|
reducedMotion
|
|
285
295
|
? { duration: 0 }
|
|
286
|
-
: { ...
|
|
296
|
+
: { ...fluidOvershoot, opacity: { duration: 0.16 } }
|
|
287
297
|
}
|
|
288
298
|
/>
|
|
289
299
|
)}
|
|
290
|
-
|
|
300
|
+
<motion.div
|
|
301
|
+
initial={reducedMotion ? false : anchorRef ? { opacity: 0 } : undefined}
|
|
302
|
+
animate={reducedMotion ? undefined : anchorRef ? { opacity: 1 } : undefined}
|
|
303
|
+
transition={reducedMotion ? { duration: 0 } : { duration: 0.16, delay: 0.06 }}
|
|
304
|
+
>
|
|
305
|
+
{children}
|
|
306
|
+
</motion.div>
|
|
291
307
|
</motion.div>
|
|
292
308
|
)
|
|
293
309
|
}
|
|
@@ -14,6 +14,8 @@ export interface SegmentedControlProps<T extends string> {
|
|
|
14
14
|
onChange: (value: NoInfer<T>) => void
|
|
15
15
|
/** Accessible name for the group and its compact picker. */
|
|
16
16
|
label: string
|
|
17
|
+
/** Custom classes for the control container. */
|
|
18
|
+
className?: string
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
/**
|
|
@@ -27,6 +29,7 @@ export function SegmentedControl<T extends string>({
|
|
|
27
29
|
options,
|
|
28
30
|
onChange,
|
|
29
31
|
label,
|
|
32
|
+
className,
|
|
30
33
|
}: SegmentedControlProps<T>) {
|
|
31
34
|
const id = useId()
|
|
32
35
|
const reducedMotion = useReducedMotion()
|
|
@@ -47,7 +50,7 @@ export function SegmentedControl<T extends string>({
|
|
|
47
50
|
}, [])
|
|
48
51
|
|
|
49
52
|
return (
|
|
50
|
-
<div ref={frame} className="relative w-full min-w-0">
|
|
53
|
+
<div ref={frame} className={cn("relative w-full min-w-0", className)}>
|
|
51
54
|
{/* The control at its natural size, to know whether it fits. */}
|
|
52
55
|
<div
|
|
53
56
|
ref={measure}
|
|
@@ -2,7 +2,7 @@ import { AnimatePresence, motion, useReducedMotion } from "motion/react"
|
|
|
2
2
|
import { type ReactNode, useRef } from "react"
|
|
3
3
|
import { useVisualViewport } from "../../hooks/use-visual-viewport"
|
|
4
4
|
import { cn } from "../../lib/class-names"
|
|
5
|
-
import { spring } from "../../motion/transitions"
|
|
5
|
+
import { contentCrossfade, spring, zoomReveal } from "../../motion/transitions"
|
|
6
6
|
import { SheetPortal } from "./sheet-portal"
|
|
7
7
|
import { useSheetFocus } from "./use-sheet-focus"
|
|
8
8
|
|
|
@@ -95,12 +95,47 @@ function OpenSheet({
|
|
|
95
95
|
placement === "center" ? "rounded-sheet" : "rounded-t-sheet sm:rounded-sheet",
|
|
96
96
|
className
|
|
97
97
|
)}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
98
|
+
style={{
|
|
99
|
+
transformOrigin: placement === "center" ? "center center" : "bottom center",
|
|
100
|
+
}}
|
|
101
|
+
initial={
|
|
102
|
+
reducedMotion
|
|
103
|
+
? { opacity: 0 }
|
|
104
|
+
: placement === "center"
|
|
105
|
+
? zoomReveal.initial
|
|
106
|
+
: { y: 40, opacity: 0, scale: 0.96 }
|
|
107
|
+
}
|
|
108
|
+
animate={
|
|
109
|
+
reducedMotion
|
|
110
|
+
? { opacity: 1 }
|
|
111
|
+
: placement === "center"
|
|
112
|
+
? zoomReveal.animate
|
|
113
|
+
: { y: 0, opacity: 1, scale: 1 }
|
|
114
|
+
}
|
|
115
|
+
exit={
|
|
116
|
+
reducedMotion
|
|
117
|
+
? { opacity: 0 }
|
|
118
|
+
: placement === "center"
|
|
119
|
+
? zoomReveal.exit
|
|
120
|
+
: { y: 30, opacity: 0, scale: 0.98 }
|
|
121
|
+
}
|
|
122
|
+
transition={
|
|
123
|
+
reducedMotion
|
|
124
|
+
? { duration: 0 }
|
|
125
|
+
: placement === "center"
|
|
126
|
+
? zoomReveal.transition
|
|
127
|
+
: { ...spring.bouncy, opacity: { duration: 0.18 } }
|
|
128
|
+
}
|
|
102
129
|
>
|
|
103
|
-
|
|
130
|
+
<motion.div
|
|
131
|
+
className="flex min-h-0 flex-1 flex-col overflow-y-auto"
|
|
132
|
+
initial={reducedMotion ? false : contentCrossfade.initial}
|
|
133
|
+
animate={reducedMotion ? undefined : contentCrossfade.animate}
|
|
134
|
+
exit={reducedMotion ? undefined : contentCrossfade.exit}
|
|
135
|
+
transition={reducedMotion ? { duration: 0 } : contentCrossfade.transition}
|
|
136
|
+
>
|
|
137
|
+
{children}
|
|
138
|
+
</motion.div>
|
|
104
139
|
</motion.div>
|
|
105
140
|
</div>
|
|
106
141
|
)
|
|
@@ -13,10 +13,20 @@ export interface StatTileProps {
|
|
|
13
13
|
/** Makes the tile a toggle, e.g. a filter. */
|
|
14
14
|
onClick?: () => void
|
|
15
15
|
active?: boolean
|
|
16
|
+
/** Custom classes for the tile container. */
|
|
17
|
+
className?: string
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
/** One number with its label, optionally a toggle. */
|
|
19
|
-
export function StatTile({
|
|
21
|
+
export function StatTile({
|
|
22
|
+
label,
|
|
23
|
+
value,
|
|
24
|
+
caption,
|
|
25
|
+
tone,
|
|
26
|
+
onClick,
|
|
27
|
+
active = false,
|
|
28
|
+
className,
|
|
29
|
+
}: StatTileProps) {
|
|
20
30
|
const body = (
|
|
21
31
|
<>
|
|
22
32
|
<span className="text-label-secondary">{label}</span>
|
|
@@ -26,17 +36,19 @@ export function StatTile({ label, value, caption, tone, onClick, active = false
|
|
|
26
36
|
{caption && <span className="text-label-secondary">{caption}</span>}
|
|
27
37
|
</>
|
|
28
38
|
)
|
|
29
|
-
const
|
|
30
|
-
"flex min-w-0 flex-col gap-2 rounded-2xl border border-separator/35 bg-elevated p-4 text-left shadow-xs"
|
|
39
|
+
const tileClass = cn(
|
|
40
|
+
"flex min-w-0 flex-col gap-2 rounded-2xl border border-separator/35 bg-elevated p-4 text-left shadow-xs",
|
|
41
|
+
className
|
|
42
|
+
)
|
|
31
43
|
|
|
32
|
-
if (!onClick) return <div className={
|
|
44
|
+
if (!onClick) return <div className={tileClass}>{body}</div>
|
|
33
45
|
return (
|
|
34
46
|
<button
|
|
35
47
|
type="button"
|
|
36
48
|
aria-pressed={active}
|
|
37
49
|
onClick={onClick}
|
|
38
50
|
className={cn(
|
|
39
|
-
|
|
51
|
+
tileClass,
|
|
40
52
|
"ring-1 ring-transparent transition-colors hover:bg-fill/60",
|
|
41
53
|
active && "bg-fill/70 ring-label/15"
|
|
42
54
|
)}
|
|
@@ -20,6 +20,8 @@ export interface SwatchPickerProps<T extends string> {
|
|
|
20
20
|
onChange: (value: T) => void
|
|
21
21
|
/** Accessible name for the group. */
|
|
22
22
|
label: string
|
|
23
|
+
/** Custom classes for the swatch container. */
|
|
24
|
+
className?: string
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
/** Colour swatches with a check on the chosen one. */
|
|
@@ -28,10 +30,11 @@ export function SwatchPicker<T extends string>({
|
|
|
28
30
|
options,
|
|
29
31
|
onChange,
|
|
30
32
|
label,
|
|
33
|
+
className,
|
|
31
34
|
}: SwatchPickerProps<T>) {
|
|
32
35
|
const name = useId()
|
|
33
36
|
return (
|
|
34
|
-
<fieldset className="flex flex-wrap gap-3">
|
|
37
|
+
<fieldset className={cn("flex flex-wrap gap-3", className)}>
|
|
35
38
|
<legend className="sr-only">{label}</legend>
|
|
36
39
|
{options.map((option) => {
|
|
37
40
|
const selected = option.value === value
|
|
@@ -2,6 +2,7 @@ import { type ComponentProps, type ReactNode, useId, useState } from "react"
|
|
|
2
2
|
import { Button } from "../button/button"
|
|
3
3
|
import { cn } from "../../lib/class-names"
|
|
4
4
|
|
|
5
|
+
/** Shared text control classes for components that reuse TextField's surface. */
|
|
5
6
|
export const textControlClasses = cn(
|
|
6
7
|
"w-full min-w-0 rounded-xl border border-separator bg-elevated",
|
|
7
8
|
"px-3 py-2 text-body text-label placeholder:text-label-tertiary",
|
|
@@ -11,7 +12,55 @@ export const textControlClasses = cn(
|
|
|
11
12
|
|
|
12
13
|
const fieldIconClasses = "flex shrink-0 items-center text-label-secondary"
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
/** Size scale shared across TextField, SecureField, and form fields. */
|
|
16
|
+
export type TextFieldSize = "xs" | "sm" | "md" | "lg"
|
|
17
|
+
|
|
18
|
+
interface FieldSize {
|
|
19
|
+
height: string
|
|
20
|
+
floatingHeight: string
|
|
21
|
+
padding: string
|
|
22
|
+
text: string
|
|
23
|
+
radius: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function fieldSize(size: TextFieldSize): FieldSize {
|
|
27
|
+
switch (size) {
|
|
28
|
+
case "xs":
|
|
29
|
+
return {
|
|
30
|
+
height: "h-8",
|
|
31
|
+
floatingHeight: "h-9",
|
|
32
|
+
padding: "px-2.5",
|
|
33
|
+
text: "text-caption",
|
|
34
|
+
radius: "rounded-lg",
|
|
35
|
+
}
|
|
36
|
+
case "sm":
|
|
37
|
+
return {
|
|
38
|
+
height: "h-9",
|
|
39
|
+
floatingHeight: "h-10",
|
|
40
|
+
padding: "px-3",
|
|
41
|
+
text: "text-subheadline",
|
|
42
|
+
radius: "rounded-xl",
|
|
43
|
+
}
|
|
44
|
+
case "lg":
|
|
45
|
+
return {
|
|
46
|
+
height: "h-12",
|
|
47
|
+
floatingHeight: "h-14",
|
|
48
|
+
padding: "px-4",
|
|
49
|
+
text: "text-body",
|
|
50
|
+
radius: "rounded-xl",
|
|
51
|
+
}
|
|
52
|
+
default:
|
|
53
|
+
return {
|
|
54
|
+
height: "h-11",
|
|
55
|
+
floatingHeight: "h-12",
|
|
56
|
+
padding: "px-3",
|
|
57
|
+
text: "text-body",
|
|
58
|
+
radius: "rounded-xl",
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export type TextFieldProps = Omit<ComponentProps<"input">, "size"> & {
|
|
15
64
|
label: string
|
|
16
65
|
description?: ReactNode
|
|
17
66
|
errors?: string[]
|
|
@@ -24,6 +73,8 @@ export type TextFieldProps = ComponentProps<"input"> & {
|
|
|
24
73
|
revealLabels?: { show: string; hide: string }
|
|
25
74
|
/** Float the label into the outline on focus or when filled. @default "above" */
|
|
26
75
|
labelPlacement?: "above" | "floating"
|
|
76
|
+
/** Field height and text scale. @default "md" */
|
|
77
|
+
size?: TextFieldSize
|
|
27
78
|
}
|
|
28
79
|
|
|
29
80
|
export function TextField({
|
|
@@ -35,6 +86,7 @@ export function TextField({
|
|
|
35
86
|
action,
|
|
36
87
|
revealLabels,
|
|
37
88
|
labelPlacement = "above",
|
|
89
|
+
size = "md",
|
|
38
90
|
placeholder,
|
|
39
91
|
type = "text",
|
|
40
92
|
id: suppliedId,
|
|
@@ -46,6 +98,7 @@ export function TextField({
|
|
|
46
98
|
const [revealed, setRevealed] = useState(false)
|
|
47
99
|
const messages = Array.from(new Set(errors))
|
|
48
100
|
const reveal = type === "password" && revealLabels
|
|
101
|
+
const dims = fieldSize(size)
|
|
49
102
|
return (
|
|
50
103
|
<div className="not-typeset flex flex-col gap-2">
|
|
51
104
|
{labelPlacement === "above" && (
|
|
@@ -55,7 +108,7 @@ export function TextField({
|
|
|
55
108
|
)}
|
|
56
109
|
<div
|
|
57
110
|
className={cn(
|
|
58
|
-
|
|
111
|
+
`relative flex items-center gap-2 ${dims.padding} ${dims.radius} bg-elevated`,
|
|
59
112
|
labelPlacement === "floating"
|
|
60
113
|
? "passui-floating-field"
|
|
61
114
|
: "border border-separator focus-within:ring-1 focus-within:ring-accent/40 has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-accent-text/70"
|
|
@@ -99,8 +152,9 @@ export function TextField({
|
|
|
99
152
|
.join(" ") || undefined
|
|
100
153
|
}
|
|
101
154
|
className={cn(
|
|
102
|
-
|
|
103
|
-
|
|
155
|
+
labelPlacement === "floating" ? dims.floatingHeight : dims.height,
|
|
156
|
+
"w-full min-w-0 bg-transparent text-label outline-none",
|
|
157
|
+
dims.text,
|
|
104
158
|
"placeholder:text-label-tertiary disabled:opacity-40",
|
|
105
159
|
className
|
|
106
160
|
)}
|
|
@@ -28,6 +28,8 @@ export interface ToggleProps {
|
|
|
28
28
|
mixed?: boolean
|
|
29
29
|
/** Keeps the accessible name while hiding the visible label. @default false */
|
|
30
30
|
labelHidden?: boolean
|
|
31
|
+
/** Custom classes for the toggle container. */
|
|
32
|
+
className?: string
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
export function Toggle({
|
|
@@ -38,6 +40,7 @@ export function Toggle({
|
|
|
38
40
|
style = "switch",
|
|
39
41
|
mixed = false,
|
|
40
42
|
labelHidden = false,
|
|
43
|
+
className,
|
|
41
44
|
}: ToggleProps) {
|
|
42
45
|
const input = useRef<HTMLInputElement>(null)
|
|
43
46
|
useEffect(() => {
|
|
@@ -48,7 +51,8 @@ export function Toggle({
|
|
|
48
51
|
<label
|
|
49
52
|
className={cn(
|
|
50
53
|
"flex cursor-pointer items-center gap-3 text-subheadline",
|
|
51
|
-
disabled && "cursor-not-allowed opacity-40"
|
|
54
|
+
disabled && "cursor-not-allowed opacity-40",
|
|
55
|
+
className
|
|
52
56
|
)}
|
|
53
57
|
>
|
|
54
58
|
<span className="relative inline-flex size-5 shrink-0">
|
|
@@ -82,7 +86,15 @@ export function Toggle({
|
|
|
82
86
|
</label>
|
|
83
87
|
)
|
|
84
88
|
}
|
|
85
|
-
return
|
|
89
|
+
return (
|
|
90
|
+
<SwitchToggle
|
|
91
|
+
checked={checked}
|
|
92
|
+
onChange={onChange}
|
|
93
|
+
label={label}
|
|
94
|
+
disabled={disabled}
|
|
95
|
+
className={className}
|
|
96
|
+
/>
|
|
97
|
+
)
|
|
86
98
|
}
|
|
87
99
|
|
|
88
100
|
function SwitchToggle({
|
|
@@ -90,7 +102,8 @@ function SwitchToggle({
|
|
|
90
102
|
onChange,
|
|
91
103
|
label,
|
|
92
104
|
disabled,
|
|
93
|
-
|
|
105
|
+
className,
|
|
106
|
+
}: Pick<ToggleProps, "checked" | "onChange" | "label" | "disabled" | "className">) {
|
|
94
107
|
const reducedMotion = useReducedMotion()
|
|
95
108
|
const id = `toggle-goo-${useId().replace(/:/g, "")}`
|
|
96
109
|
const x = useMotionValue(checked ? 22 : 2)
|
|
@@ -163,7 +176,8 @@ function SwitchToggle({
|
|
|
163
176
|
className={cn(
|
|
164
177
|
"relative h-[30px] w-[50px] shrink-0 touch-pan-y rounded-full transition-colors duration-normal motion-reduce:transition-none",
|
|
165
178
|
"shadow-[inset_0_1px_2px_rgb(0_0_0/0.10)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/70 focus-visible:ring-offset-1 focus-visible:ring-offset-background disabled:opacity-45",
|
|
166
|
-
checked ? "bg-accent" : "bg-separator/80"
|
|
179
|
+
checked ? "bg-accent" : "bg-separator/80",
|
|
180
|
+
className
|
|
167
181
|
)}
|
|
168
182
|
>
|
|
169
183
|
<svg aria-hidden="true" width="0" height="0" className="absolute">
|
|
@@ -8,18 +8,45 @@ export const spring = {
|
|
|
8
8
|
bouncy: { type: "spring", ...springs.bouncy },
|
|
9
9
|
} as const satisfies Record<keyof typeof springs, Transition>
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Apple's fluid overshoot: cubic-bezier(0.34, 1.56, 0.1, 1).
|
|
13
|
+
* Replaces traditional ease profiles with an authentic fluid bounce
|
|
14
|
+
* that overshoots and settles, matching Apple's custom interface physics.
|
|
15
|
+
* Used for zoom/matched-geometry transitions where the surface emerges
|
|
16
|
+
* from the trigger and snaps into place with a physical bounce.
|
|
17
|
+
*/
|
|
18
|
+
export const fluidOvershoot = {
|
|
19
|
+
duration: 0.4,
|
|
20
|
+
ease: [0.34, 1.56, 0.1, 1] as [number, number, number, number],
|
|
21
|
+
} as const
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Exit motion: content collapses back toward its source.
|
|
25
|
+
* The surface shrinks toward the trigger's position while fading,
|
|
26
|
+
* like a water drop absorbing back into its origin.
|
|
27
|
+
*/
|
|
28
|
+
export const fluidCollapse = {
|
|
29
|
+
duration: 0.28,
|
|
30
|
+
ease: [0.4, 0, 0.6, 1] as [number, number, number, number],
|
|
31
|
+
} as const
|
|
32
|
+
|
|
11
33
|
export type FloatingPanelSide = "above" | "below"
|
|
12
34
|
|
|
13
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* Anchored panels settle toward their source without shrinking their text.
|
|
37
|
+
* Now uses origin-based scaling: the panel grows from the trigger's
|
|
38
|
+
* position (transform-origin set by the caller) with a scale + opacity
|
|
39
|
+
* crossfade, rather than a plain slide.
|
|
40
|
+
*/
|
|
14
41
|
export function floatingPanelMotion(side: FloatingPanelSide = "below") {
|
|
15
42
|
const direction = side === "above" ? 1 : -1
|
|
16
43
|
return {
|
|
17
|
-
initial: { opacity: 0, scale:
|
|
44
|
+
initial: { opacity: 0, scale: 0.92, y: direction * 6 },
|
|
18
45
|
animate: { opacity: 1, scale: 1, y: 0 },
|
|
19
|
-
exit: { opacity: 0, scale:
|
|
46
|
+
exit: { opacity: 0, scale: 0.95, y: direction * 4 },
|
|
20
47
|
transition: {
|
|
21
|
-
...
|
|
22
|
-
opacity: { duration: 0.16 },
|
|
48
|
+
...fluidOvershoot,
|
|
49
|
+
opacity: { duration: 0.16, ease: [0.4, 0, 0.2, 1] as [number, number, number, number] },
|
|
23
50
|
},
|
|
24
51
|
} as const
|
|
25
52
|
}
|
|
@@ -27,6 +54,37 @@ export function floatingPanelMotion(side: FloatingPanelSide = "below") {
|
|
|
27
54
|
/** Default below-anchor presentation, retained for existing consumers. */
|
|
28
55
|
export const popoverMotion = floatingPanelMotion()
|
|
29
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Zoom/matched-geometry transition for dialogs and center-placed sheets.
|
|
59
|
+
* The surface starts small (scale 0.88) and invisible, then grows to
|
|
60
|
+
* full size with the fluid overshoot bounce. Content elements inside
|
|
61
|
+
* crossfade in slightly delayed so they phase into visibility
|
|
62
|
+
* asynchronously — not a harsh switch.
|
|
63
|
+
*/
|
|
64
|
+
export const zoomReveal = {
|
|
65
|
+
initial: { opacity: 0, scale: 0.88 },
|
|
66
|
+
animate: { opacity: 1, scale: 1 },
|
|
67
|
+
exit: { opacity: 0, scale: 0.92 },
|
|
68
|
+
transition: fluidOvershoot,
|
|
69
|
+
} as const
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Content crossfade for elements inside an overlay.
|
|
73
|
+
* Rather than a harsh switch, sub-elements scale and phase into
|
|
74
|
+
* visibility asynchronously. Use for children of dialogs, sheets,
|
|
75
|
+
* and popovers — the container arrives first, then content fades in.
|
|
76
|
+
*/
|
|
77
|
+
export const contentCrossfade = {
|
|
78
|
+
initial: { opacity: 0, scale: 0.96 },
|
|
79
|
+
animate: { opacity: 1, scale: 1 },
|
|
80
|
+
exit: { opacity: 0, scale: 0.98 },
|
|
81
|
+
transition: {
|
|
82
|
+
duration: 0.24,
|
|
83
|
+
ease: [0.4, 0, 0.2, 1] as [number, number, number, number],
|
|
84
|
+
delay: 0.04,
|
|
85
|
+
},
|
|
86
|
+
} as const
|
|
87
|
+
|
|
30
88
|
/** Floating bars that rise from the bottom edge. */
|
|
31
89
|
export const riseMotion = {
|
|
32
90
|
initial: { opacity: 0, y: 32 },
|