@passui/react 0.1.34 → 0.1.36
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/content-unavailable-view/content-unavailable-view.d.ts +4 -1
- package/dist/types/src/components/dialog/dialog.d.ts +4 -1
- package/dist/types/src/components/disclosure-group/disclosure-group.d.ts +4 -1
- package/dist/types/src/components/morph/index.d.ts +2 -0
- package/dist/types/src/components/morph/morph.d.ts +29 -0
- package/dist/types/src/components/morph/use-morph-disclosure.d.ts +13 -0
- package/dist/types/src/components/profile-image/profile-image.d.ts +6 -1
- package/dist/types/src/components/radio-group/radio-group.d.ts +4 -1
- package/dist/types/src/components/sheet/sheet.d.ts +3 -0
- package/dist/types/src/components/swatch-picker/swatch-picker.d.ts +4 -1
- package/dist/types/src/index.d.ts +1 -0
- package/package.json +5 -1
- package/src/components/content-unavailable-view/content-unavailable-view.tsx +28 -3
- package/src/components/dialog/dialog.tsx +48 -5
- package/src/components/disclosure-group/disclosure-group.tsx +31 -9
- package/src/components/morph/index.ts +2 -0
- package/src/components/morph/morph.tsx +210 -0
- package/src/components/morph/use-morph-disclosure.ts +27 -0
- package/src/components/profile-image/profile-image.tsx +28 -6
- package/src/components/radio-group/radio-group.tsx +30 -10
- package/src/components/sheet/sheet.tsx +20 -2
- package/src/components/swatch-picker/swatch-picker.tsx +40 -5
- package/src/index.ts +2 -0
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import type { ReactNode, ComponentProps } from "react";
|
|
2
|
+
export type ContentUnavailableViewSize = "sm" | "md" | "lg";
|
|
2
3
|
export interface ContentUnavailableViewProps extends Omit<ComponentProps<"div">, "title"> {
|
|
3
4
|
title: string;
|
|
4
5
|
description?: string;
|
|
5
6
|
icon?: ReactNode;
|
|
6
7
|
actions?: ReactNode;
|
|
8
|
+
/** Title, description and vertical rhythm. @default "md" */
|
|
9
|
+
size?: ContentUnavailableViewSize;
|
|
7
10
|
}
|
|
8
11
|
/** A focused empty or unavailable state with an optional next action. */
|
|
9
|
-
export declare function ContentUnavailableView({ title, description, icon, actions, className, ...props }: ContentUnavailableViewProps): import("react").JSX.Element;
|
|
12
|
+
export declare function ContentUnavailableView({ title, description, icon, actions, size, className, ...props }: ContentUnavailableViewProps): import("react").JSX.Element;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type FormEvent, type ReactNode } from "react";
|
|
2
2
|
export type DialogPresentation = "drawer" | "center" | "inline";
|
|
3
3
|
export type DialogTone = "default" | "destructive";
|
|
4
|
+
export type DialogSize = "sm" | "md" | "lg";
|
|
4
5
|
export type DialogProps = {
|
|
5
6
|
open: boolean;
|
|
6
7
|
onOpenChange: (open: boolean) => void;
|
|
@@ -10,6 +11,8 @@ export type DialogProps = {
|
|
|
10
11
|
footer?: ReactNode;
|
|
11
12
|
/** `drawer` (responsive sheet), `center` (floating panel), or `inline`. @default "drawer" */
|
|
12
13
|
presentation?: DialogPresentation;
|
|
14
|
+
/** Title, description, body and footer density. @default "md" */
|
|
15
|
+
size?: DialogSize;
|
|
13
16
|
className?: string;
|
|
14
17
|
onSubmit?: (event: FormEvent<HTMLFormElement>) => void;
|
|
15
18
|
};
|
|
@@ -21,7 +24,7 @@ type ActionsProps = {
|
|
|
21
24
|
tone?: DialogTone;
|
|
22
25
|
onConfirm?: () => void;
|
|
23
26
|
};
|
|
24
|
-
export declare function Dialog({ open, onOpenChange, title, description, children, footer, presentation, className, onSubmit, }: DialogProps): import("react").JSX.Element;
|
|
27
|
+
export declare function Dialog({ open, onOpenChange, title, description, children, footer, presentation, size, className, onSubmit, }: DialogProps): import("react").JSX.Element;
|
|
25
28
|
export declare namespace Dialog {
|
|
26
29
|
export { Actions };
|
|
27
30
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ReactNode } from "react";
|
|
2
|
+
export type DisclosureGroupSize = "sm" | "md" | "lg";
|
|
2
3
|
export interface DisclosureGroupProps {
|
|
3
4
|
label: string;
|
|
4
5
|
open: boolean;
|
|
@@ -10,6 +11,8 @@ export interface DisclosureGroupProps {
|
|
|
10
11
|
triggerClassName?: string;
|
|
11
12
|
/** Supporting text beneath the title. */
|
|
12
13
|
description?: string;
|
|
14
|
+
/** Trigger, description and content density. @default "md" */
|
|
15
|
+
size?: DisclosureGroupSize;
|
|
13
16
|
}
|
|
14
17
|
/** A controlled, accessible disclosure row with the shared inline reveal. */
|
|
15
|
-
export declare function DisclosureGroup({ label, open, onOpenChange, children, disabled, className, triggerClassName, description, }: DisclosureGroupProps): import("react").JSX.Element;
|
|
18
|
+
export declare function DisclosureGroup({ label, open, onOpenChange, children, disabled, className, triggerClassName, description, size, }: DisclosureGroupProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type HTMLMotionProps } from "motion/react";
|
|
2
|
+
import { type ReactNode, type RefObject } from "react";
|
|
3
|
+
export interface MorphProps extends Omit<HTMLMotionProps<"div">, "children"> {
|
|
4
|
+
/** Controls whether the overlay is presented. */
|
|
5
|
+
open: boolean;
|
|
6
|
+
/** Called on Escape or outside press. */
|
|
7
|
+
onClose: () => void;
|
|
8
|
+
/** The element whose rect the overlay grows from and collapses back to. */
|
|
9
|
+
sourceRef: RefObject<HTMLElement | null>;
|
|
10
|
+
/** Accessible name (used when there is no labelledBy). */
|
|
11
|
+
label: string;
|
|
12
|
+
/** ID of a visible heading inside the overlay. */
|
|
13
|
+
labelledBy?: string;
|
|
14
|
+
/** Allow tapping the backdrop to close. Escape still calls onClose. @default true */
|
|
15
|
+
dismissOnOutsidePress?: boolean;
|
|
16
|
+
className?: string;
|
|
17
|
+
children?: ReactNode;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* A modal overlay that morphs its glass material from a source element's
|
|
21
|
+
* rect to its own resting rect, then collapses back — Apple's
|
|
22
|
+
* matched-geometry effect. The source is concealed while the overlay is
|
|
23
|
+
* mounted so the material reads as the source itself springing open.
|
|
24
|
+
*
|
|
25
|
+
* Reuses `SheetPortal` (theme inheritance), `useSheetFocus` (focus trap,
|
|
26
|
+
* Escape, modal environment) and the same `menuSurfaceReveal` FLIP
|
|
27
|
+
* primitive that powers anchored `Popover` and `ContextMenu`.
|
|
28
|
+
*/
|
|
29
|
+
export declare function Morph({ open, ...props }: MorphProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Disclosure state that keeps the overlay mounted through its exit animation,
|
|
3
|
+
* mirroring `useAnchoredDisclosure`. Pair with a `useRef<HTMLElement>(null)`
|
|
4
|
+
* for the source.
|
|
5
|
+
*/
|
|
6
|
+
export declare function useMorphDisclosure(disabled?: boolean): {
|
|
7
|
+
open: boolean;
|
|
8
|
+
mounted: boolean;
|
|
9
|
+
show: () => void;
|
|
10
|
+
toggle: () => void;
|
|
11
|
+
close: () => void;
|
|
12
|
+
onExited: () => void;
|
|
13
|
+
};
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { type ComponentProps } from "react";
|
|
2
|
-
export
|
|
2
|
+
export type ProfileImageSize = "sm" | "md" | "lg";
|
|
3
|
+
export interface ProfileImageProps extends ComponentProps<"span"> {
|
|
4
|
+
/** Avatar diameter; fallback initials scale with it. @default "md" */
|
|
5
|
+
size?: ProfileImageSize;
|
|
6
|
+
}
|
|
7
|
+
export declare function ProfileImage({ size, className, ...props }: ProfileImageProps): import("react").JSX.Element;
|
|
3
8
|
export declare function ProfileImageContent({ src, className, onLoad, onError, ...props }: ComponentProps<"img">): import("react").JSX.Element | null;
|
|
4
9
|
export declare function ProfileImageFallback({ className, ...props }: ComponentProps<"span">): import("react").JSX.Element | null;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export type RadioGroupSize = "sm" | "md" | "lg";
|
|
1
2
|
export interface RadioGroupOption<T extends string> {
|
|
2
3
|
value: T;
|
|
3
4
|
label: string;
|
|
@@ -12,6 +13,8 @@ export interface RadioGroupProps<T extends string> {
|
|
|
12
13
|
disabled?: boolean;
|
|
13
14
|
name?: string;
|
|
14
15
|
className?: string;
|
|
16
|
+
/** Legend, row, radio glyph and description density. @default "md" */
|
|
17
|
+
size?: RadioGroupSize;
|
|
15
18
|
}
|
|
16
19
|
/** A labelled choice group using the browser's native radio keyboard behavior. */
|
|
17
|
-
export declare function RadioGroup<T extends string>({ value, onChange, label, options, disabled, name, className, }: RadioGroupProps<T>): import("react").JSX.Element;
|
|
20
|
+
export declare function RadioGroup<T extends string>({ value, onChange, label, options, disabled, name, className, size, }: RadioGroupProps<T>): import("react").JSX.Element;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type ReactNode } from "react";
|
|
2
2
|
export type SheetPlacement = "adaptive" | "center";
|
|
3
|
+
export type SheetSize = "sm" | "md" | "lg";
|
|
3
4
|
export interface SheetProps {
|
|
4
5
|
/** Controls whether the sheet is presented. */
|
|
5
6
|
open: boolean;
|
|
@@ -13,6 +14,8 @@ export interface SheetProps {
|
|
|
13
14
|
labelledBy?: string;
|
|
14
15
|
/** `adaptive` rises from the bottom on phones; `center` always floats. @default "adaptive" */
|
|
15
16
|
placement?: SheetPlacement;
|
|
17
|
+
/** Outer padding and panel max-width. @default "md" */
|
|
18
|
+
size?: SheetSize;
|
|
16
19
|
className?: string;
|
|
17
20
|
children: ReactNode;
|
|
18
21
|
}
|
|
@@ -6,6 +6,7 @@ export interface SwatchPickerOption<T extends string> {
|
|
|
6
6
|
/** Color of the selected checkmark. */
|
|
7
7
|
ink: string;
|
|
8
8
|
}
|
|
9
|
+
export type SwatchPickerSize = "sm" | "md" | "lg";
|
|
9
10
|
export interface SwatchPickerProps<T extends string> {
|
|
10
11
|
value: T;
|
|
11
12
|
options: ReadonlyArray<SwatchPickerOption<T>>;
|
|
@@ -14,6 +15,8 @@ export interface SwatchPickerProps<T extends string> {
|
|
|
14
15
|
label: string;
|
|
15
16
|
/** Custom classes for the swatch container. */
|
|
16
17
|
className?: string;
|
|
18
|
+
/** Swatch diameter, check icon and caption scale. @default "md" */
|
|
19
|
+
size?: SwatchPickerSize;
|
|
17
20
|
}
|
|
18
21
|
/** Colour swatches with a check on the chosen one. */
|
|
19
|
-
export declare function SwatchPicker<T extends string>({ value, options, onChange, label, className, }: SwatchPickerProps<T>): import("react").JSX.Element;
|
|
22
|
+
export declare function SwatchPicker<T extends string>({ value, options, onChange, label, className, size, }: SwatchPickerProps<T>): import("react").JSX.Element;
|
|
@@ -69,3 +69,4 @@ export * from "./components/slosh-slider/slosh-slider";
|
|
|
69
69
|
export * from "./components/wake-slider/wake-slider";
|
|
70
70
|
export * from "./components/one-time-code/one-time-code";
|
|
71
71
|
export * from "./components/aspect-ratio/aspect-ratio";
|
|
72
|
+
export * from "./components/morph";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@passui/react",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.36",
|
|
4
4
|
"description": "Passui: native React controls, design tokens, presentation and appearance.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -62,6 +62,10 @@
|
|
|
62
62
|
"types": "./dist/types/src/components/popover/popover.d.ts",
|
|
63
63
|
"default": "./src/components/popover/popover.tsx"
|
|
64
64
|
},
|
|
65
|
+
"./morph": {
|
|
66
|
+
"types": "./dist/types/src/components/morph/index.d.ts",
|
|
67
|
+
"default": "./src/components/morph/index.ts"
|
|
68
|
+
},
|
|
65
69
|
"./sheet": {
|
|
66
70
|
"types": "./dist/types/src/components/sheet/sheet.d.ts",
|
|
67
71
|
"default": "./src/components/sheet/sheet.tsx"
|
|
@@ -1,11 +1,33 @@
|
|
|
1
1
|
import type { ReactNode, ComponentProps } from "react"
|
|
2
2
|
import { cn } from "../../lib/class-names"
|
|
3
3
|
|
|
4
|
+
export type ContentUnavailableViewSize = "sm" | "md" | "lg"
|
|
5
|
+
|
|
6
|
+
const titleSizes: Record<ContentUnavailableViewSize, string> = {
|
|
7
|
+
sm: "text-subheadline",
|
|
8
|
+
md: "text-headline",
|
|
9
|
+
lg: "text-title3",
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const descriptionSizes: Record<ContentUnavailableViewSize, string> = {
|
|
13
|
+
sm: "text-caption",
|
|
14
|
+
md: "text-footnote",
|
|
15
|
+
lg: "text-subheadline",
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const spacings: Record<ContentUnavailableViewSize, string> = {
|
|
19
|
+
sm: "gap-2 px-6 py-6",
|
|
20
|
+
md: "gap-3 px-6 py-10",
|
|
21
|
+
lg: "gap-4 px-6 py-14",
|
|
22
|
+
}
|
|
23
|
+
|
|
4
24
|
export interface ContentUnavailableViewProps extends Omit<ComponentProps<"div">, "title"> {
|
|
5
25
|
title: string
|
|
6
26
|
description?: string
|
|
7
27
|
icon?: ReactNode
|
|
8
28
|
actions?: ReactNode
|
|
29
|
+
/** Title, description and vertical rhythm. @default "md" */
|
|
30
|
+
size?: ContentUnavailableViewSize
|
|
9
31
|
}
|
|
10
32
|
|
|
11
33
|
/** A focused empty or unavailable state with an optional next action. */
|
|
@@ -14,22 +36,25 @@ export function ContentUnavailableView({
|
|
|
14
36
|
description,
|
|
15
37
|
icon,
|
|
16
38
|
actions,
|
|
39
|
+
size = "md",
|
|
17
40
|
className,
|
|
18
41
|
...props
|
|
19
42
|
}: ContentUnavailableViewProps) {
|
|
20
43
|
return (
|
|
21
44
|
<div
|
|
22
45
|
{...props}
|
|
23
|
-
className={cn("flex flex-col items-center
|
|
46
|
+
className={cn("flex flex-col items-center text-center", spacings[size], className)}
|
|
24
47
|
>
|
|
25
48
|
{icon && (
|
|
26
49
|
<span aria-hidden="true" className="text-label-tertiary">
|
|
27
50
|
{icon}
|
|
28
51
|
</span>
|
|
29
52
|
)}
|
|
30
|
-
<p className="
|
|
53
|
+
<p className={cn(titleSizes[size], "font-semibold text-label")}>{title}</p>
|
|
31
54
|
{description && (
|
|
32
|
-
<p className="max-w-sm text-
|
|
55
|
+
<p className={cn(descriptionSizes[size], "max-w-sm text-label-secondary")}>
|
|
56
|
+
{description}
|
|
57
|
+
</p>
|
|
33
58
|
)}
|
|
34
59
|
{actions && <div className="mt-1 flex flex-wrap justify-center gap-2">{actions}</div>}
|
|
35
60
|
</div>
|
|
@@ -8,6 +8,28 @@ import { createContext, useContext, useId, type FormEvent, type ReactNode } from
|
|
|
8
8
|
const CloseContext = createContext<() => void>(() => {})
|
|
9
9
|
export type DialogPresentation = "drawer" | "center" | "inline"
|
|
10
10
|
export type DialogTone = "default" | "destructive"
|
|
11
|
+
export type DialogSize = "sm" | "md" | "lg"
|
|
12
|
+
|
|
13
|
+
const titleText: Record<DialogSize, string> = {
|
|
14
|
+
sm: "text-subheadline",
|
|
15
|
+
md: "text-title3",
|
|
16
|
+
lg: "text-title2",
|
|
17
|
+
}
|
|
18
|
+
const descriptionText: Record<DialogSize, string> = {
|
|
19
|
+
sm: "text-caption",
|
|
20
|
+
md: "text-footnote",
|
|
21
|
+
lg: "text-subheadline",
|
|
22
|
+
}
|
|
23
|
+
const bodySpacing: Record<DialogSize, string> = {
|
|
24
|
+
sm: "gap-3 p-4",
|
|
25
|
+
md: "gap-4 p-5",
|
|
26
|
+
lg: "gap-5 p-6",
|
|
27
|
+
}
|
|
28
|
+
const footerSpacing: Record<DialogSize, string> = {
|
|
29
|
+
sm: "pt-2",
|
|
30
|
+
md: "pt-3",
|
|
31
|
+
lg: "pt-4",
|
|
32
|
+
}
|
|
11
33
|
|
|
12
34
|
export type DialogProps = {
|
|
13
35
|
open: boolean
|
|
@@ -18,6 +40,8 @@ export type DialogProps = {
|
|
|
18
40
|
footer?: ReactNode
|
|
19
41
|
/** `drawer` (responsive sheet), `center` (floating panel), or `inline`. @default "drawer" */
|
|
20
42
|
presentation?: DialogPresentation
|
|
43
|
+
/** Title, description, body and footer density. @default "md" */
|
|
44
|
+
size?: DialogSize
|
|
21
45
|
className?: string
|
|
22
46
|
onSubmit?: (event: FormEvent<HTMLFormElement>) => void
|
|
23
47
|
}
|
|
@@ -38,6 +62,7 @@ export function Dialog({
|
|
|
38
62
|
children,
|
|
39
63
|
footer,
|
|
40
64
|
presentation = "drawer",
|
|
65
|
+
size = "md",
|
|
41
66
|
className,
|
|
42
67
|
onSubmit,
|
|
43
68
|
}: DialogProps) {
|
|
@@ -47,13 +72,21 @@ export function Dialog({
|
|
|
47
72
|
const content = (
|
|
48
73
|
<>
|
|
49
74
|
{presentation !== "inline" && (
|
|
50
|
-
<h2 id={titleId} className="
|
|
75
|
+
<h2 id={titleId} className={cn(titleText[size], "font-semibold")}>
|
|
51
76
|
{title}
|
|
52
77
|
</h2>
|
|
53
78
|
)}
|
|
54
|
-
{description &&
|
|
79
|
+
{description && (
|
|
80
|
+
<div className={cn(descriptionText[size], "text-label-secondary")}>
|
|
81
|
+
{description}
|
|
82
|
+
</div>
|
|
83
|
+
)}
|
|
55
84
|
{children && <div className="flex flex-col gap-4">{children}</div>}
|
|
56
|
-
{footer &&
|
|
85
|
+
{footer && (
|
|
86
|
+
<div className={cn("flex flex-wrap justify-end gap-2", footerSpacing[size])}>
|
|
87
|
+
{footer}
|
|
88
|
+
</div>
|
|
89
|
+
)}
|
|
57
90
|
</>
|
|
58
91
|
)
|
|
59
92
|
const body = (
|
|
@@ -61,12 +94,22 @@ export function Dialog({
|
|
|
61
94
|
{onSubmit ? (
|
|
62
95
|
<form
|
|
63
96
|
onSubmit={onSubmit}
|
|
64
|
-
className={cn(
|
|
97
|
+
className={cn(
|
|
98
|
+
"flex min-h-0 flex-col overflow-y-auto",
|
|
99
|
+
bodySpacing[size],
|
|
100
|
+
className
|
|
101
|
+
)}
|
|
65
102
|
>
|
|
66
103
|
{content}
|
|
67
104
|
</form>
|
|
68
105
|
) : (
|
|
69
|
-
<div
|
|
106
|
+
<div
|
|
107
|
+
className={cn(
|
|
108
|
+
"flex min-h-0 flex-col overflow-y-auto",
|
|
109
|
+
bodySpacing[size],
|
|
110
|
+
className
|
|
111
|
+
)}
|
|
112
|
+
>
|
|
70
113
|
{content}
|
|
71
114
|
</div>
|
|
72
115
|
)}
|
|
@@ -4,6 +4,29 @@ import { ChevronDownIcon } from "../../icons/icons"
|
|
|
4
4
|
import { cn } from "../../lib/class-names"
|
|
5
5
|
import { InlineDisclosure } from "../inline-disclosure/inline-disclosure"
|
|
6
6
|
|
|
7
|
+
export type DisclosureGroupSize = "sm" | "md" | "lg"
|
|
8
|
+
|
|
9
|
+
const triggerSizes: Record<DisclosureGroupSize, string> = {
|
|
10
|
+
sm: "h-auto min-h-9 w-full rounded-lg px-3 py-2 text-start text-footnote active:scale-100",
|
|
11
|
+
md: "h-auto min-h-11 w-full rounded-xl px-4 py-3 text-start text-subheadline active:scale-100",
|
|
12
|
+
lg: "h-auto min-h-13 w-full rounded-2xl px-5 py-4 text-start text-body active:scale-100",
|
|
13
|
+
}
|
|
14
|
+
const descriptionSizes: Record<DisclosureGroupSize, string> = {
|
|
15
|
+
sm: "text-caption2 leading-caption2 font-normal break-words whitespace-normal text-label-secondary",
|
|
16
|
+
md: "text-footnote leading-footnote font-normal break-words whitespace-normal text-label-secondary",
|
|
17
|
+
lg: "text-subheadline leading-subheadline font-normal break-words whitespace-normal text-label-secondary",
|
|
18
|
+
}
|
|
19
|
+
const contentSizes: Record<DisclosureGroupSize, string> = {
|
|
20
|
+
sm: "min-w-0 px-3 pb-3 text-footnote leading-footnote text-label-secondary [overflow-wrap:anywhere]",
|
|
21
|
+
md: "min-w-0 px-4 pb-4 text-subheadline leading-subheadline text-label-secondary [overflow-wrap:anywhere]",
|
|
22
|
+
lg: "min-w-0 px-5 pb-5 text-body leading-body text-label-secondary [overflow-wrap:anywhere]",
|
|
23
|
+
}
|
|
24
|
+
const chevronSizes: Record<DisclosureGroupSize, number> = {
|
|
25
|
+
sm: 15,
|
|
26
|
+
md: 17,
|
|
27
|
+
lg: 19,
|
|
28
|
+
}
|
|
29
|
+
|
|
7
30
|
export interface DisclosureGroupProps {
|
|
8
31
|
label: string
|
|
9
32
|
open: boolean
|
|
@@ -15,6 +38,8 @@ export interface DisclosureGroupProps {
|
|
|
15
38
|
triggerClassName?: string
|
|
16
39
|
/** Supporting text beneath the title. */
|
|
17
40
|
description?: string
|
|
41
|
+
/** Trigger, description and content density. @default "md" */
|
|
42
|
+
size?: DisclosureGroupSize
|
|
18
43
|
}
|
|
19
44
|
|
|
20
45
|
/** A controlled, accessible disclosure row with the shared inline reveal. */
|
|
@@ -27,6 +52,7 @@ export function DisclosureGroup({
|
|
|
27
52
|
className,
|
|
28
53
|
triggerClassName,
|
|
29
54
|
description,
|
|
55
|
+
size = "md",
|
|
30
56
|
}: DisclosureGroupProps) {
|
|
31
57
|
const id = useId()
|
|
32
58
|
|
|
@@ -43,8 +69,8 @@ export function DisclosureGroup({
|
|
|
43
69
|
disabled={disabled}
|
|
44
70
|
onClick={() => onOpenChange(!open)}
|
|
45
71
|
className={cn(
|
|
46
|
-
|
|
47
|
-
"
|
|
72
|
+
triggerSizes[size],
|
|
73
|
+
"font-normal text-label hover:bg-fill/60 focus-visible:ring-inset focus-visible:ring-offset-0",
|
|
48
74
|
"focus-visible:outline-accent-text disabled:cursor-not-allowed disabled:opacity-40",
|
|
49
75
|
triggerClassName
|
|
50
76
|
)}
|
|
@@ -52,13 +78,11 @@ export function DisclosureGroup({
|
|
|
52
78
|
<span className="flex min-w-0 flex-col gap-0.5">
|
|
53
79
|
<span className="font-medium break-words whitespace-normal">{label}</span>
|
|
54
80
|
{description && (
|
|
55
|
-
<span className=
|
|
56
|
-
{description}
|
|
57
|
-
</span>
|
|
81
|
+
<span className={descriptionSizes[size]}>{description}</span>
|
|
58
82
|
)}
|
|
59
83
|
</span>
|
|
60
84
|
<ChevronDownIcon
|
|
61
|
-
size={
|
|
85
|
+
size={chevronSizes[size]}
|
|
62
86
|
aria-hidden="true"
|
|
63
87
|
className={cn(
|
|
64
88
|
"shrink-0 text-label-tertiary transition-transform duration-normal motion-reduce:transition-none",
|
|
@@ -69,9 +93,7 @@ export function DisclosureGroup({
|
|
|
69
93
|
</h3>
|
|
70
94
|
<section id={id} aria-labelledby={`${id}-trigger`} aria-hidden={!open} inert={!open}>
|
|
71
95
|
<InlineDisclosure open={open}>
|
|
72
|
-
<div className=
|
|
73
|
-
{children}
|
|
74
|
-
</div>
|
|
96
|
+
<div className={contentSizes[size]}>{children}</div>
|
|
75
97
|
</InlineDisclosure>
|
|
76
98
|
</section>
|
|
77
99
|
</div>
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { AnimatePresence, type HTMLMotionProps, motion, useReducedMotion } from "motion/react"
|
|
2
|
+
import { type ReactNode, type RefObject, useLayoutEffect, useRef, useState } from "react"
|
|
3
|
+
import { concealMenuSource } from "../context-menu/conceal-menu-source"
|
|
4
|
+
import { menuSurfaceExit, menuSurfaceReveal, type MenuRect } from "../context-menu/menu-reveal"
|
|
5
|
+
import { useSheetFocus } from "../sheet/use-sheet-focus"
|
|
6
|
+
import { SheetPortal } from "../sheet/sheet-portal"
|
|
7
|
+
import { cn } from "../../lib/class-names"
|
|
8
|
+
import { contentCrossfade, fluidCollapse, fluidOvershoot } from "../../motion/transitions"
|
|
9
|
+
|
|
10
|
+
export interface MorphProps extends Omit<HTMLMotionProps<"div">, "children"> {
|
|
11
|
+
/** Controls whether the overlay is presented. */
|
|
12
|
+
open: boolean
|
|
13
|
+
/** Called on Escape or outside press. */
|
|
14
|
+
onClose: () => void
|
|
15
|
+
/** The element whose rect the overlay grows from and collapses back to. */
|
|
16
|
+
sourceRef: RefObject<HTMLElement | null>
|
|
17
|
+
/** Accessible name (used when there is no labelledBy). */
|
|
18
|
+
label: string
|
|
19
|
+
/** ID of a visible heading inside the overlay. */
|
|
20
|
+
labelledBy?: string
|
|
21
|
+
/** Allow tapping the backdrop to close. Escape still calls onClose. @default true */
|
|
22
|
+
dismissOnOutsidePress?: boolean
|
|
23
|
+
className?: string
|
|
24
|
+
children?: ReactNode
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* A modal overlay that morphs its glass material from a source element's
|
|
29
|
+
* rect to its own resting rect, then collapses back — Apple's
|
|
30
|
+
* matched-geometry effect. The source is concealed while the overlay is
|
|
31
|
+
* mounted so the material reads as the source itself springing open.
|
|
32
|
+
*
|
|
33
|
+
* Reuses `SheetPortal` (theme inheritance), `useSheetFocus` (focus trap,
|
|
34
|
+
* Escape, modal environment) and the same `menuSurfaceReveal` FLIP
|
|
35
|
+
* primitive that powers anchored `Popover` and `ContextMenu`.
|
|
36
|
+
*/
|
|
37
|
+
export function Morph({ open, ...props }: MorphProps) {
|
|
38
|
+
return <AnimatePresence>{open && <OpenMorph {...props} />}</AnimatePresence>
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function OpenMorph({
|
|
42
|
+
onClose,
|
|
43
|
+
sourceRef,
|
|
44
|
+
label,
|
|
45
|
+
labelledBy,
|
|
46
|
+
dismissOnOutsidePress = true,
|
|
47
|
+
className,
|
|
48
|
+
children,
|
|
49
|
+
...props
|
|
50
|
+
}: Omit<MorphProps, "open">) {
|
|
51
|
+
const panel = useRef<HTMLDivElement>(null)
|
|
52
|
+
const reducedMotion = useReducedMotion()
|
|
53
|
+
const [settled, setSettled] = useState(false)
|
|
54
|
+
const [target, setTarget] = useState<{ width: number; height: number; radius: number } | null>(
|
|
55
|
+
null
|
|
56
|
+
)
|
|
57
|
+
const [reveal, setReveal] = useState<ReturnType<typeof menuSurfaceReveal> | null>(null)
|
|
58
|
+
useSheetFocus(panel, onClose)
|
|
59
|
+
|
|
60
|
+
// Measure the panel's resting rect and the source's rect, then compute
|
|
61
|
+
// the reveal (initial material state) from menuSurfaceReveal. Re-measures
|
|
62
|
+
// on resize/scroll so the morph stays aligned if either moves.
|
|
63
|
+
useLayoutEffect(() => {
|
|
64
|
+
const surface = panel.current
|
|
65
|
+
const source = sourceRef.current
|
|
66
|
+
if (!surface) return
|
|
67
|
+
const measure = () => {
|
|
68
|
+
const panelRect = surface.getBoundingClientRect()
|
|
69
|
+
const radius = Number.parseFloat(getComputedStyle(surface).borderTopLeftRadius) || 0
|
|
70
|
+
setTarget((current) =>
|
|
71
|
+
current &&
|
|
72
|
+
current.width === panelRect.width &&
|
|
73
|
+
current.height === panelRect.height &&
|
|
74
|
+
current.radius === radius
|
|
75
|
+
? current
|
|
76
|
+
: { width: panelRect.width, height: panelRect.height, radius }
|
|
77
|
+
)
|
|
78
|
+
const sourceRect = source?.getBoundingClientRect()
|
|
79
|
+
const sourceArg: MenuRect | undefined =
|
|
80
|
+
sourceRect && sourceRect.width > 0 && sourceRect.height > 0
|
|
81
|
+
? {
|
|
82
|
+
left: sourceRect.left,
|
|
83
|
+
top: sourceRect.top,
|
|
84
|
+
width: sourceRect.width,
|
|
85
|
+
height: sourceRect.height,
|
|
86
|
+
}
|
|
87
|
+
: undefined
|
|
88
|
+
setReveal(
|
|
89
|
+
menuSurfaceReveal(sourceArg, {
|
|
90
|
+
left: panelRect.left,
|
|
91
|
+
top: panelRect.top,
|
|
92
|
+
width: panelRect.width,
|
|
93
|
+
height: panelRect.height,
|
|
94
|
+
radius,
|
|
95
|
+
})
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
measure()
|
|
99
|
+
let frame = 0
|
|
100
|
+
const schedule = () => {
|
|
101
|
+
cancelAnimationFrame(frame)
|
|
102
|
+
frame = requestAnimationFrame(measure)
|
|
103
|
+
}
|
|
104
|
+
const observer = new ResizeObserver(schedule)
|
|
105
|
+
observer.observe(surface)
|
|
106
|
+
if (source) observer.observe(source)
|
|
107
|
+
const viewport = window.visualViewport
|
|
108
|
+
viewport?.addEventListener("resize", schedule)
|
|
109
|
+
viewport?.addEventListener("scroll", schedule)
|
|
110
|
+
window.addEventListener("resize", schedule)
|
|
111
|
+
window.addEventListener("scroll", schedule, true)
|
|
112
|
+
return () => {
|
|
113
|
+
observer.disconnect()
|
|
114
|
+
cancelAnimationFrame(frame)
|
|
115
|
+
viewport?.removeEventListener("resize", schedule)
|
|
116
|
+
viewport?.removeEventListener("scroll", schedule)
|
|
117
|
+
window.removeEventListener("resize", schedule)
|
|
118
|
+
window.removeEventListener("scroll", schedule, true)
|
|
119
|
+
}
|
|
120
|
+
}, [sourceRef])
|
|
121
|
+
|
|
122
|
+
// Conceal the source while the morph is mounted; restore on unmount.
|
|
123
|
+
useLayoutEffect(() => {
|
|
124
|
+
const source = sourceRef.current
|
|
125
|
+
if (!source) return
|
|
126
|
+
const restore = concealMenuSource(source, false)
|
|
127
|
+
return () => {
|
|
128
|
+
restore()
|
|
129
|
+
}
|
|
130
|
+
}, [sourceRef])
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<SheetPortal>
|
|
134
|
+
<div className="fixed inset-0 z-[75] flex items-center justify-center p-6">
|
|
135
|
+
<motion.div
|
|
136
|
+
aria-hidden="true"
|
|
137
|
+
className="absolute inset-0 bg-black/30 backdrop-blur-md"
|
|
138
|
+
initial={{ opacity: 0 }}
|
|
139
|
+
animate={{ opacity: 1 }}
|
|
140
|
+
exit={{ opacity: 0 }}
|
|
141
|
+
transition={reducedMotion ? { duration: 0 } : { duration: 0.18 }}
|
|
142
|
+
onClick={dismissOnOutsidePress ? onClose : undefined}
|
|
143
|
+
/>
|
|
144
|
+
<motion.div
|
|
145
|
+
ref={panel}
|
|
146
|
+
role="dialog"
|
|
147
|
+
aria-modal="true"
|
|
148
|
+
aria-label={labelledBy ? undefined : label}
|
|
149
|
+
aria-labelledby={labelledBy}
|
|
150
|
+
tabIndex={-1}
|
|
151
|
+
className={cn(
|
|
152
|
+
"relative isolate flex w-full flex-col rounded-5xl outline-none",
|
|
153
|
+
"max-h-[92%] max-w-[720px]",
|
|
154
|
+
settled && "overflow-hidden",
|
|
155
|
+
className
|
|
156
|
+
)}
|
|
157
|
+
{...props}
|
|
158
|
+
>
|
|
159
|
+
{/* The morphing material — mirrors Popover's data-popover-surface
|
|
160
|
+
pattern. Starts at the source's rect (offset relative to the
|
|
161
|
+
panel) and grows to fill the panel, with the fluid overshoot
|
|
162
|
+
bounce. Border radius interpolates from the source's
|
|
163
|
+
"pillified" radius to the panel's authored radius. */}
|
|
164
|
+
<motion.div
|
|
165
|
+
key={reveal ? "surface" : "measure"}
|
|
166
|
+
aria-hidden="true"
|
|
167
|
+
data-morph-surface=""
|
|
168
|
+
onAnimationComplete={() => {
|
|
169
|
+
if (reveal) setSettled(true)
|
|
170
|
+
}}
|
|
171
|
+
className="glass-menu pointer-events-none absolute top-0 left-0 -z-10 rounded-5xl"
|
|
172
|
+
style={{ width: "100%", height: "100%" }}
|
|
173
|
+
initial={reducedMotion || !reveal ? false : reveal}
|
|
174
|
+
animate={
|
|
175
|
+
target
|
|
176
|
+
? {
|
|
177
|
+
x: 0,
|
|
178
|
+
y: 0,
|
|
179
|
+
width: target.width,
|
|
180
|
+
height: target.height,
|
|
181
|
+
opacity: 1,
|
|
182
|
+
borderRadius: target.radius,
|
|
183
|
+
}
|
|
184
|
+
: undefined
|
|
185
|
+
}
|
|
186
|
+
exit={
|
|
187
|
+
reducedMotion || !reveal
|
|
188
|
+
? { opacity: 0 }
|
|
189
|
+
: { ...menuSurfaceExit(reveal), transition: fluidCollapse }
|
|
190
|
+
}
|
|
191
|
+
transition={
|
|
192
|
+
reducedMotion
|
|
193
|
+
? { duration: 0 }
|
|
194
|
+
: { ...fluidOvershoot, opacity: { duration: 0.18 } }
|
|
195
|
+
}
|
|
196
|
+
/>
|
|
197
|
+
<motion.div
|
|
198
|
+
className="flex min-h-0 flex-1 flex-col overflow-y-auto"
|
|
199
|
+
initial={reducedMotion ? false : contentCrossfade.initial}
|
|
200
|
+
animate={reducedMotion ? undefined : contentCrossfade.animate}
|
|
201
|
+
exit={reducedMotion ? undefined : contentCrossfade.exit}
|
|
202
|
+
transition={reducedMotion ? { duration: 0 } : contentCrossfade.transition}
|
|
203
|
+
>
|
|
204
|
+
{children}
|
|
205
|
+
</motion.div>
|
|
206
|
+
</motion.div>
|
|
207
|
+
</div>
|
|
208
|
+
</SheetPortal>
|
|
209
|
+
)
|
|
210
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from "react"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Disclosure state that keeps the overlay mounted through its exit animation,
|
|
5
|
+
* mirroring `useAnchoredDisclosure`. Pair with a `useRef<HTMLElement>(null)`
|
|
6
|
+
* for the source.
|
|
7
|
+
*/
|
|
8
|
+
export function useMorphDisclosure(disabled = false) {
|
|
9
|
+
const [open, setOpen] = useState(false)
|
|
10
|
+
const [mounted, setMounted] = useState(false)
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
if (disabled) setOpen(false)
|
|
13
|
+
}, [disabled])
|
|
14
|
+
const show = useCallback(() => {
|
|
15
|
+
setMounted(true)
|
|
16
|
+
setOpen(true)
|
|
17
|
+
}, [])
|
|
18
|
+
const toggle = useCallback(() => {
|
|
19
|
+
setMounted(true)
|
|
20
|
+
setOpen((value) => !value)
|
|
21
|
+
}, [])
|
|
22
|
+
const close = useCallback(() => setOpen(false), [])
|
|
23
|
+
const onExited = useCallback(() => {
|
|
24
|
+
if (!open || disabled) setMounted(false)
|
|
25
|
+
}, [open, disabled])
|
|
26
|
+
return { open: open && !disabled, mounted, show, toggle, close, onExited }
|
|
27
|
+
}
|
|
@@ -1,18 +1,40 @@
|
|
|
1
1
|
import { createContext, useContext, useEffect, useState, type ComponentProps } from "react"
|
|
2
2
|
import { cn } from "../../lib/class-names"
|
|
3
3
|
|
|
4
|
+
export type ProfileImageSize = "sm" | "md" | "lg"
|
|
5
|
+
|
|
6
|
+
const diameters: Record<ProfileImageSize, string> = {
|
|
7
|
+
sm: "size-7",
|
|
8
|
+
md: "size-9",
|
|
9
|
+
lg: "size-12",
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const fallbackTextSizes: Record<ProfileImageSize, string> = {
|
|
13
|
+
sm: "text-caption",
|
|
14
|
+
md: "text-footnote",
|
|
15
|
+
lg: "text-subheadline",
|
|
16
|
+
}
|
|
17
|
+
|
|
4
18
|
const ImageContext = createContext<{
|
|
5
19
|
loaded: boolean
|
|
6
20
|
setLoaded: (loaded: boolean) => void
|
|
7
|
-
|
|
8
|
-
|
|
21
|
+
size: ProfileImageSize
|
|
22
|
+
}>({ loaded: false, setLoaded: () => {}, size: "md" })
|
|
23
|
+
|
|
24
|
+
export interface ProfileImageProps extends ComponentProps<"span"> {
|
|
25
|
+
/** Avatar diameter; fallback initials scale with it. @default "md" */
|
|
26
|
+
size?: ProfileImageSize
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function ProfileImage({ size = "md", className, ...props }: ProfileImageProps) {
|
|
9
30
|
const [loaded, setLoaded] = useState(false)
|
|
10
31
|
return (
|
|
11
|
-
<ImageContext value={{ loaded, setLoaded }}>
|
|
32
|
+
<ImageContext value={{ loaded, setLoaded, size }}>
|
|
12
33
|
<span
|
|
13
34
|
{...props}
|
|
14
35
|
className={cn(
|
|
15
|
-
"relative inline-flex
|
|
36
|
+
"relative inline-flex shrink-0 items-center justify-center overflow-hidden rounded-full bg-fill text-label-secondary",
|
|
37
|
+
diameters[size],
|
|
16
38
|
className
|
|
17
39
|
)}
|
|
18
40
|
/>
|
|
@@ -51,8 +73,8 @@ export function ProfileImageContent({
|
|
|
51
73
|
)
|
|
52
74
|
}
|
|
53
75
|
export function ProfileImageFallback({ className, ...props }: ComponentProps<"span">) {
|
|
54
|
-
const { loaded } = useContext(ImageContext)
|
|
76
|
+
const { loaded, size } = useContext(ImageContext)
|
|
55
77
|
return loaded ? null : (
|
|
56
|
-
<span {...props} className={cn("
|
|
78
|
+
<span {...props} className={cn(fallbackTextSizes[size], "font-medium", className)} />
|
|
57
79
|
)
|
|
58
80
|
}
|
|
@@ -1,6 +1,29 @@
|
|
|
1
1
|
import { useId } from "react"
|
|
2
2
|
import { cn } from "../../lib/class-names"
|
|
3
3
|
|
|
4
|
+
export type RadioGroupSize = "sm" | "md" | "lg"
|
|
5
|
+
|
|
6
|
+
const legendSizes: Record<RadioGroupSize, string> = {
|
|
7
|
+
sm: "mb-1.5 text-caption2 font-medium text-label-secondary",
|
|
8
|
+
md: "mb-2 text-footnote font-medium text-label-secondary",
|
|
9
|
+
lg: "mb-2.5 text-subheadline font-medium text-label-secondary",
|
|
10
|
+
}
|
|
11
|
+
const rowSizes: Record<RadioGroupSize, string> = {
|
|
12
|
+
sm: "flex min-h-7 items-start gap-1.5 text-footnote text-label has-disabled:opacity-40 [@media(pointer:coarse)]:min-h-10",
|
|
13
|
+
md: "flex min-h-8 items-start gap-2 text-subheadline text-label has-disabled:opacity-40 [@media(pointer:coarse)]:min-h-11",
|
|
14
|
+
lg: "flex min-h-10 items-start gap-2.5 text-body text-label has-disabled:opacity-40 [@media(pointer:coarse)]:min-h-12",
|
|
15
|
+
}
|
|
16
|
+
const radioSizes: Record<RadioGroupSize, string> = {
|
|
17
|
+
sm: "mt-0.5 size-3.5 shrink-0 accent-accent-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent-text",
|
|
18
|
+
md: "mt-1 size-4 shrink-0 accent-accent-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent-text",
|
|
19
|
+
lg: "mt-1 size-5 shrink-0 accent-accent-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent-text",
|
|
20
|
+
}
|
|
21
|
+
const descriptionSizes: Record<RadioGroupSize, string> = {
|
|
22
|
+
sm: "block text-caption2 text-label-secondary",
|
|
23
|
+
md: "block text-footnote text-label-secondary",
|
|
24
|
+
lg: "block text-subheadline text-label-secondary",
|
|
25
|
+
}
|
|
26
|
+
|
|
4
27
|
export interface RadioGroupOption<T extends string> {
|
|
5
28
|
value: T
|
|
6
29
|
label: string
|
|
@@ -15,6 +38,8 @@ export interface RadioGroupProps<T extends string> {
|
|
|
15
38
|
disabled?: boolean
|
|
16
39
|
name?: string
|
|
17
40
|
className?: string
|
|
41
|
+
/** Legend, row, radio glyph and description density. @default "md" */
|
|
42
|
+
size?: RadioGroupSize
|
|
18
43
|
}
|
|
19
44
|
/** A labelled choice group using the browser's native radio keyboard behavior. */
|
|
20
45
|
export function RadioGroup<T extends string>({
|
|
@@ -25,16 +50,14 @@ export function RadioGroup<T extends string>({
|
|
|
25
50
|
disabled,
|
|
26
51
|
name,
|
|
27
52
|
className,
|
|
53
|
+
size = "md",
|
|
28
54
|
}: RadioGroupProps<T>) {
|
|
29
55
|
const id = useId()
|
|
30
56
|
return (
|
|
31
57
|
<fieldset disabled={disabled} className={cn("flex min-w-0 flex-col gap-2", className)}>
|
|
32
|
-
<legend className=
|
|
58
|
+
<legend className={legendSizes[size]}>{label}</legend>
|
|
33
59
|
{options.map((option, index) => (
|
|
34
|
-
<label
|
|
35
|
-
key={option.value}
|
|
36
|
-
className="flex min-h-8 items-start gap-2 text-subheadline text-label has-disabled:opacity-40 [@media(pointer:coarse)]:min-h-11"
|
|
37
|
-
>
|
|
60
|
+
<label key={option.value} className={rowSizes[size]}>
|
|
38
61
|
<input
|
|
39
62
|
type="radio"
|
|
40
63
|
name={name ?? id}
|
|
@@ -43,15 +66,12 @@ export function RadioGroup<T extends string>({
|
|
|
43
66
|
disabled={option.disabled}
|
|
44
67
|
onChange={() => onChange(option.value)}
|
|
45
68
|
aria-describedby={option.description ? `${id}-${index}` : undefined}
|
|
46
|
-
className=
|
|
69
|
+
className={radioSizes[size]}
|
|
47
70
|
/>
|
|
48
71
|
<span>
|
|
49
72
|
{option.label}
|
|
50
73
|
{option.description && (
|
|
51
|
-
<span
|
|
52
|
-
id={`${id}-${index}`}
|
|
53
|
-
className="block text-footnote text-label-secondary"
|
|
54
|
-
>
|
|
74
|
+
<span id={`${id}-${index}`} className={descriptionSizes[size]}>
|
|
55
75
|
{option.description}
|
|
56
76
|
</span>
|
|
57
77
|
)}
|
|
@@ -7,6 +7,18 @@ import { SheetPortal } from "./sheet-portal"
|
|
|
7
7
|
import { useSheetFocus } from "./use-sheet-focus"
|
|
8
8
|
|
|
9
9
|
export type SheetPlacement = "adaptive" | "center"
|
|
10
|
+
export type SheetSize = "sm" | "md" | "lg"
|
|
11
|
+
|
|
12
|
+
const outerPaddings: Record<SheetSize, string> = {
|
|
13
|
+
sm: "p-4",
|
|
14
|
+
md: "p-6",
|
|
15
|
+
lg: "p-8",
|
|
16
|
+
}
|
|
17
|
+
const panelMaxWidths: Record<SheetSize, string> = {
|
|
18
|
+
sm: "max-w-sm",
|
|
19
|
+
md: "max-w-[560px]",
|
|
20
|
+
lg: "max-w-2xl",
|
|
21
|
+
}
|
|
10
22
|
|
|
11
23
|
export interface SheetProps {
|
|
12
24
|
/** Controls whether the sheet is presented. */
|
|
@@ -21,6 +33,8 @@ export interface SheetProps {
|
|
|
21
33
|
labelledBy?: string
|
|
22
34
|
/** `adaptive` rises from the bottom on phones; `center` always floats. @default "adaptive" */
|
|
23
35
|
placement?: SheetPlacement
|
|
36
|
+
/** Outer padding and panel max-width. @default "md" */
|
|
37
|
+
size?: SheetSize
|
|
24
38
|
className?: string
|
|
25
39
|
children: ReactNode
|
|
26
40
|
}
|
|
@@ -47,6 +61,7 @@ function OpenSheet({
|
|
|
47
61
|
label,
|
|
48
62
|
labelledBy,
|
|
49
63
|
placement = "adaptive",
|
|
64
|
+
size = "md",
|
|
50
65
|
className,
|
|
51
66
|
children,
|
|
52
67
|
}: Omit<SheetProps, "open">) {
|
|
@@ -59,7 +74,9 @@ function OpenSheet({
|
|
|
59
74
|
<div
|
|
60
75
|
className={cn(
|
|
61
76
|
"fixed inset-0 z-[65] flex justify-center",
|
|
62
|
-
placement === "center"
|
|
77
|
+
placement === "center"
|
|
78
|
+
? cn("items-center", outerPaddings[size])
|
|
79
|
+
: cn("items-end sm:items-center", "sm:" + outerPaddings[size])
|
|
63
80
|
)}
|
|
64
81
|
style={
|
|
65
82
|
viewport
|
|
@@ -91,7 +108,8 @@ function OpenSheet({
|
|
|
91
108
|
tabIndex={-1}
|
|
92
109
|
className={cn(
|
|
93
110
|
"glass-menu relative flex w-full flex-col overflow-hidden",
|
|
94
|
-
"max-h-[92%]
|
|
111
|
+
"max-h-[92%]",
|
|
112
|
+
panelMaxWidths[size],
|
|
95
113
|
placement === "center" ? "rounded-sheet" : "rounded-t-sheet sm:rounded-sheet",
|
|
96
114
|
className
|
|
97
115
|
)}
|
|
@@ -14,6 +14,34 @@ export interface SwatchPickerOption<T extends string> {
|
|
|
14
14
|
ink: string
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
export type SwatchPickerSize = "sm" | "md" | "lg"
|
|
18
|
+
|
|
19
|
+
const swatchSizes: Record<SwatchPickerSize, string> = {
|
|
20
|
+
sm: "size-6",
|
|
21
|
+
md: "size-8",
|
|
22
|
+
lg: "size-10",
|
|
23
|
+
}
|
|
24
|
+
const checkSizes: Record<SwatchPickerSize, number> = {
|
|
25
|
+
sm: 13,
|
|
26
|
+
md: 16,
|
|
27
|
+
lg: 19,
|
|
28
|
+
}
|
|
29
|
+
const captionText: Record<SwatchPickerSize, string> = {
|
|
30
|
+
sm: "text-caption2",
|
|
31
|
+
md: "text-caption2",
|
|
32
|
+
lg: "text-caption",
|
|
33
|
+
}
|
|
34
|
+
const fieldsetGaps: Record<SwatchPickerSize, string> = {
|
|
35
|
+
sm: "gap-2",
|
|
36
|
+
md: "gap-3",
|
|
37
|
+
lg: "gap-4",
|
|
38
|
+
}
|
|
39
|
+
const labelGaps: Record<SwatchPickerSize, string> = {
|
|
40
|
+
sm: "gap-1",
|
|
41
|
+
md: "gap-1.5",
|
|
42
|
+
lg: "gap-2",
|
|
43
|
+
}
|
|
44
|
+
|
|
17
45
|
export interface SwatchPickerProps<T extends string> {
|
|
18
46
|
value: T
|
|
19
47
|
options: ReadonlyArray<SwatchPickerOption<T>>
|
|
@@ -22,6 +50,8 @@ export interface SwatchPickerProps<T extends string> {
|
|
|
22
50
|
label: string
|
|
23
51
|
/** Custom classes for the swatch container. */
|
|
24
52
|
className?: string
|
|
53
|
+
/** Swatch diameter, check icon and caption scale. @default "md" */
|
|
54
|
+
size?: SwatchPickerSize
|
|
25
55
|
}
|
|
26
56
|
|
|
27
57
|
/** Colour swatches with a check on the chosen one. */
|
|
@@ -31,17 +61,21 @@ export function SwatchPicker<T extends string>({
|
|
|
31
61
|
onChange,
|
|
32
62
|
label,
|
|
33
63
|
className,
|
|
64
|
+
size = "md",
|
|
34
65
|
}: SwatchPickerProps<T>) {
|
|
35
66
|
const name = useId()
|
|
36
67
|
return (
|
|
37
|
-
<fieldset className={cn("flex flex-wrap
|
|
68
|
+
<fieldset className={cn("flex flex-wrap", fieldsetGaps[size], className)}>
|
|
38
69
|
<legend className="sr-only">{label}</legend>
|
|
39
70
|
{options.map((option) => {
|
|
40
71
|
const selected = option.value === value
|
|
41
72
|
return (
|
|
42
73
|
<label
|
|
43
74
|
key={option.value}
|
|
44
|
-
className=
|
|
75
|
+
className={cn(
|
|
76
|
+
"group flex cursor-pointer flex-col items-center",
|
|
77
|
+
labelGaps[size]
|
|
78
|
+
)}
|
|
45
79
|
>
|
|
46
80
|
<input
|
|
47
81
|
type="radio"
|
|
@@ -56,7 +90,8 @@ export function SwatchPicker<T extends string>({
|
|
|
56
90
|
/>
|
|
57
91
|
<span
|
|
58
92
|
className={cn(
|
|
59
|
-
"flex
|
|
93
|
+
"flex items-center justify-center rounded-full ring-offset-1 ring-offset-elevated transition-[box-shadow,transform] duration-fast group-active:scale-95 peer-focus-visible:ring-2 peer-focus-visible:ring-accent",
|
|
94
|
+
swatchSizes[size],
|
|
60
95
|
selected
|
|
61
96
|
? "ring-[1.5px] ring-label/70"
|
|
62
97
|
: "ring-0 group-hover:ring-[1.5px] group-hover:ring-separator"
|
|
@@ -78,7 +113,7 @@ export function SwatchPicker<T extends string>({
|
|
|
78
113
|
transition={spring.bouncy}
|
|
79
114
|
>
|
|
80
115
|
<CheckIcon
|
|
81
|
-
size={
|
|
116
|
+
size={checkSizes[size]}
|
|
82
117
|
strokeWidth={2.6}
|
|
83
118
|
style={{
|
|
84
119
|
color: option.ink,
|
|
@@ -89,7 +124,7 @@ export function SwatchPicker<T extends string>({
|
|
|
89
124
|
</span>
|
|
90
125
|
<span
|
|
91
126
|
className={cn(
|
|
92
|
-
|
|
127
|
+
captionText[size],
|
|
93
128
|
selected ? "font-semibold text-label" : "text-label-tertiary"
|
|
94
129
|
)}
|
|
95
130
|
>
|