@passui/react 0.1.27 → 0.1.29

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.
@@ -1,7 +1,10 @@
1
1
  import type { HTMLAttributes } from "react";
2
2
  export type ChipTone = "neutral" | "accent" | "public" | "family" | "friends";
3
+ export type ChipSize = "xs" | "sm" | "md";
3
4
  export interface ChipProps extends HTMLAttributes<HTMLSpanElement> {
4
5
  /** `neutral`, `accent`, `public`, `family`, or `friends`. @default "neutral" */
5
6
  tone?: ChipTone;
7
+ /** `xs`, `sm`, or `md`. @default "sm" */
8
+ size?: ChipSize;
6
9
  }
7
- export declare function Chip({ tone, className, ...props }: ChipProps): import("react").JSX.Element;
10
+ export declare function Chip({ tone, size, className, ...props }: ChipProps): import("react").JSX.Element;
@@ -1,4 +1,5 @@
1
1
  import type { HTMLAttributes, ReactNode } from "react";
2
+ export type LabeledContentSize = "sm" | "md" | "lg";
2
3
  export interface LabeledContentProps extends HTMLAttributes<HTMLDivElement> {
3
4
  /** The visible name of the row. */
4
5
  label: string;
@@ -10,10 +11,12 @@ export interface LabeledContentProps extends HTMLAttributes<HTMLDivElement> {
10
11
  stacked?: boolean;
11
12
  /** Places a wide control below its label on phones. @default false */
12
13
  wide?: boolean;
14
+ /** Row padding density. @default "md" */
15
+ size?: LabeledContentSize;
13
16
  }
14
17
  /**
15
18
  * A row: a label, an optional description, and its control at the trailing
16
19
  * end, as SwiftUI's `LabeledContent`. Wide controls move under the label
17
20
  * when there is no room beside it, rather than squeezing.
18
21
  */
19
- export declare function LabeledContent({ label, description, children, stacked, wide, className, ...props }: LabeledContentProps): import("react").JSX.Element;
22
+ export declare function LabeledContent({ label, description, children, stacked, wide, size, className, ...props }: LabeledContentProps): 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
- /** Anchored panels settle toward their source without shrinking their text. */
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: 1;
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: 1;
62
+ readonly scale: 0.95;
38
63
  readonly y: number;
39
64
  };
40
65
  readonly transition: {
41
- readonly damping: 26;
42
- readonly stiffness: 240;
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: 1;
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: 1;
88
+ readonly scale: 0.95;
65
89
  readonly y: number;
66
90
  };
67
91
  readonly transition: {
68
- readonly damping: 26;
69
- readonly stiffness: 240;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@passui/react",
3
- "version": "0.1.27",
3
+ "version": "0.1.29",
4
4
  "description": "Passui: native React controls, design tokens, presentation and appearance.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -2,6 +2,7 @@ import type { HTMLAttributes } from "react"
2
2
  import { cn } from "../../lib/class-names"
3
3
 
4
4
  export type ChipTone = "neutral" | "accent" | "public" | "family" | "friends"
5
+ export type ChipSize = "xs" | "sm" | "md"
5
6
 
6
7
  const tones: Record<ChipTone, string> = {
7
8
  neutral: "bg-fill text-label-secondary",
@@ -11,17 +12,26 @@ const tones: Record<ChipTone, string> = {
11
12
  friends: "bg-[#ede8ff] text-[#5a3fc0] dark:bg-[#26203f] dark:text-[#c3b5ff]",
12
13
  }
13
14
 
15
+ const sizes: Record<ChipSize, string> = {
16
+ xs: "px-1 py-0 text-caption2",
17
+ sm: "px-1.5 py-0.5 text-caption2",
18
+ md: "px-2 py-0.5 text-caption",
19
+ }
20
+
14
21
  export interface ChipProps extends HTMLAttributes<HTMLSpanElement> {
15
22
  /** `neutral`, `accent`, `public`, `family`, or `friends`. @default "neutral" */
16
23
  tone?: ChipTone
24
+ /** `xs`, `sm`, or `md`. @default "sm" */
25
+ size?: ChipSize
17
26
  }
18
27
 
19
- export function Chip({ tone = "neutral", className, ...props }: ChipProps) {
28
+ export function Chip({ tone = "neutral", size = "sm", className, ...props }: ChipProps) {
20
29
  return (
21
30
  <span
22
31
  className={cn(
23
- "inline-flex items-center gap-1 rounded-full px-1.75 py-0.5 text-caption2 font-semibold",
32
+ "inline-flex items-center gap-1 rounded-full font-semibold",
24
33
  tones[tone],
34
+ sizes[size],
25
35
  className
26
36
  )}
27
37
  {...props}
@@ -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.smooth}
96
+ transition={reducedMotion ? { duration: 0 } : spring.bouncy}
94
97
  className="overflow-hidden"
98
+ style={{ transformOrigin: "top center" }}
95
99
  >
96
- {body}
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>
@@ -1,6 +1,14 @@
1
1
  import type { HTMLAttributes, ReactNode } from "react"
2
2
  import { cn } from "../../lib/class-names"
3
3
 
4
+ export type LabeledContentSize = "sm" | "md" | "lg"
5
+
6
+ const padding: Record<LabeledContentSize, string> = {
7
+ sm: "px-3 py-2",
8
+ md: "px-4 py-3",
9
+ lg: "px-5 py-4",
10
+ }
11
+
4
12
  export interface LabeledContentProps extends HTMLAttributes<HTMLDivElement> {
5
13
  /** The visible name of the row. */
6
14
  label: string
@@ -12,6 +20,8 @@ export interface LabeledContentProps extends HTMLAttributes<HTMLDivElement> {
12
20
  stacked?: boolean
13
21
  /** Places a wide control below its label on phones. @default false */
14
22
  wide?: boolean
23
+ /** Row padding density. @default "md" */
24
+ size?: LabeledContentSize
15
25
  }
16
26
 
17
27
  /**
@@ -25,13 +35,15 @@ export function LabeledContent({
25
35
  children,
26
36
  stacked,
27
37
  wide,
38
+ size = "md",
28
39
  className,
29
40
  ...props
30
41
  }: LabeledContentProps) {
31
42
  return (
32
43
  <div
33
44
  className={cn(
34
- "flex flex-wrap gap-x-4 gap-y-2 px-4 py-3",
45
+ "flex flex-wrap gap-x-4 gap-y-2",
46
+ padding[size],
35
47
  stacked
36
48
  ? "flex-col items-stretch"
37
49
  : cn(
@@ -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 { floatingPanelMotion, spring } from "../../motion/transitions"
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
- : { ...spring.smooth, opacity: { duration: 0.16 } }
296
+ : { ...fluidOvershoot, opacity: { duration: 0.16 } }
287
297
  }
288
298
  />
289
299
  )}
290
- {children}
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
  }
@@ -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
- initial={reducedMotion ? { opacity: 0 } : { y: 40, opacity: 0 }}
99
- animate={{ y: 0, opacity: 1 }}
100
- exit={reducedMotion ? { opacity: 0 } : { y: 30, opacity: 0 }}
101
- transition={reducedMotion ? { duration: 0 } : spring.smooth}
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
- {children}
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
  )
@@ -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
- /** Anchored panels settle toward their source without shrinking their text. */
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: 1, y: direction * 6 },
44
+ initial: { opacity: 0, scale: 0.92, y: direction * 6 },
18
45
  animate: { opacity: 1, scale: 1, y: 0 },
19
- exit: { opacity: 0, scale: 1, y: direction * 4 },
46
+ exit: { opacity: 0, scale: 0.95, y: direction * 4 },
20
47
  transition: {
21
- ...spring.smooth,
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 },