@passui/react 0.1.40 → 0.1.42
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-view/content-view.d.ts +5 -2
- package/dist/types/src/components/context-menu/context-menu-store.d.ts +7 -2
- package/dist/types/src/components/conversation/conversation.d.ts +7 -2
- package/dist/types/src/components/locale-switcher/locale-switcher.d.ts +4 -1
- package/dist/types/src/components/morph/morph.d.ts +3 -0
- package/dist/types/src/components/toast/toast-store.d.ts +5 -0
- package/dist/types/src/components/wake-slider/wake-slider.d.ts +4 -1
- package/package.json +1 -1
- package/src/components/content-view/content-view.tsx +60 -8
- package/src/components/context-menu/context-menu-host.tsx +44 -15
- package/src/components/context-menu/context-menu-store.ts +10 -1
- package/src/components/conversation/conversation.tsx +93 -19
- package/src/components/locale-switcher/locale-switcher.tsx +47 -7
- package/src/components/morph/morph.tsx +24 -2
- package/src/components/toast/toast-store.ts +7 -0
- package/src/components/toast/toaster.tsx +56 -23
- package/src/components/wake-slider/wake-slider.tsx +12 -4
- package/src/styles/motion-controls.css +4 -0
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ComponentPropsWithoutRef, type PropsWithChildren, type ReactNode } from "react";
|
|
2
2
|
export type ContentViewVariant = keyof typeof variants;
|
|
3
3
|
export type ContentViewHeadingLevel = "h1" | "h2" | "h3";
|
|
4
|
+
export type ContentViewSize = "sm" | "md" | "lg";
|
|
4
5
|
type ContentViewProps = ComponentPropsWithoutRef<"section"> & {
|
|
5
6
|
variant?: ContentViewVariant;
|
|
7
|
+
/** Container radius, section gap and text scale. @default "md" */
|
|
8
|
+
size?: ContentViewSize;
|
|
6
9
|
};
|
|
7
10
|
type RowProps = ComponentPropsWithoutRef<"div">;
|
|
8
11
|
type SectionProps = ComponentPropsWithoutRef<"section">;
|
|
@@ -24,7 +27,7 @@ type SubProps = ComponentPropsWithoutRef<"sub">;
|
|
|
24
27
|
declare const variants: {
|
|
25
28
|
readonly elevated: string;
|
|
26
29
|
};
|
|
27
|
-
export declare function ContentView({ variant, className, children, ...props }: ContentViewProps): import("react").JSX.Element;
|
|
30
|
+
export declare function ContentView({ variant, size, className, children, ...props }: ContentViewProps): import("react").JSX.Element;
|
|
28
31
|
export declare namespace ContentView {
|
|
29
32
|
export { Row };
|
|
30
33
|
export { Section };
|
|
@@ -3,6 +3,7 @@ import type { MenuRect } from "./menu-reveal";
|
|
|
3
3
|
export type ContextMenuAlignX = "start" | "end";
|
|
4
4
|
export type ContextMenuAlignY = "top" | "bottom";
|
|
5
5
|
export type ContextMenuEdge = "leading" | "trailing";
|
|
6
|
+
export type ContextMenuSize = "sm" | "md" | "lg";
|
|
6
7
|
/** One row of a context menu, or a divider between groups of rows. */
|
|
7
8
|
export type ContextMenuItem = {
|
|
8
9
|
label: string;
|
|
@@ -23,6 +24,8 @@ export interface OpenMenu {
|
|
|
23
24
|
items: ContextMenuItem[];
|
|
24
25
|
/** Opened by long-press: bigger rows for fingers. */
|
|
25
26
|
touch: boolean;
|
|
27
|
+
/** Surface, item and heading density. @default "md" */
|
|
28
|
+
size: ContextMenuSize;
|
|
26
29
|
/** Which edge of the menu sits at `x`: its left (`start`) or its right (`end`). */
|
|
27
30
|
alignX: ContextMenuAlignX;
|
|
28
31
|
/** Which edge of the menu sits at `y`: its top (`top`) or its bottom (`bottom`). */
|
|
@@ -33,7 +36,7 @@ export interface OpenMenu {
|
|
|
33
36
|
restoreFocus?: boolean;
|
|
34
37
|
}
|
|
35
38
|
/** Opens a menu at a point, such as where a pointer was pressed. */
|
|
36
|
-
export declare function openContextMenu(x: number, y: number, items: ContextMenuItem[], touch?: boolean, anchor?: Element | null): void;
|
|
39
|
+
export declare function openContextMenu(x: number, y: number, items: ContextMenuItem[], touch?: boolean, anchor?: Element | null, size?: ContextMenuSize): void;
|
|
37
40
|
/**
|
|
38
41
|
* Opens a second menu where the last one stood, the way an iOS menu swaps
|
|
39
42
|
* in a submenu: a long list (a note's fonts, its kinds) stays one row in the
|
|
@@ -46,11 +49,13 @@ export declare function openFollowUpMenu(items: ContextMenuItem[]): void;
|
|
|
46
49
|
* trailing follow the text direction, so right-to-left languages mirror.
|
|
47
50
|
* The menu's real size decides where it goes; nothing is guessed.
|
|
48
51
|
*/
|
|
49
|
-
export declare function openMenu(anchor: Element, items: ContextMenuItem[], { edge, above, toolbar, }?: {
|
|
52
|
+
export declare function openMenu(anchor: Element, items: ContextMenuItem[], { edge, above, toolbar, size, }?: {
|
|
50
53
|
edge?: ContextMenuEdge;
|
|
51
54
|
above?: boolean;
|
|
52
55
|
/** Replace this action group with a horizontally expanding menu surface. */
|
|
53
56
|
toolbar?: HTMLElement;
|
|
57
|
+
/** Surface, item and heading density. @default "md" */
|
|
58
|
+
size?: ContextMenuSize;
|
|
54
59
|
}): void;
|
|
55
60
|
export declare function closeContextMenu(restoreFocus?: boolean): void;
|
|
56
61
|
export declare function subscribeContextMenu(listener: () => void): () => void;
|
|
@@ -9,15 +9,18 @@ export type ConversationMessage = {
|
|
|
9
9
|
avatar?: ReactNode;
|
|
10
10
|
createdAt: string;
|
|
11
11
|
};
|
|
12
|
+
export type ConversationSize = "sm" | "md" | "lg";
|
|
12
13
|
/**
|
|
13
14
|
* A thread of plain-text messages like Messages on iPhone: a day line when
|
|
14
15
|
* the date changes, back-to-back messages from one side grouped under one
|
|
15
16
|
* name, theirs on the left with an avatar, yours on the right.
|
|
16
17
|
*/
|
|
17
|
-
declare function Messages({ messages, locale, }: {
|
|
18
|
+
declare function Messages({ messages, locale, size, }: {
|
|
18
19
|
messages: ConversationMessage[];
|
|
19
20
|
/** For day names and times; the reader's own by default. */
|
|
20
21
|
locale?: string;
|
|
22
|
+
/** Bubble, avatar, author and time scale. @default "md" */
|
|
23
|
+
size?: ConversationSize;
|
|
21
24
|
}): import("react").JSX.Element;
|
|
22
25
|
type ComposerProps = {
|
|
23
26
|
onSend: (body: string) => Promise<unknown>;
|
|
@@ -33,9 +36,11 @@ type ComposerProps = {
|
|
|
33
36
|
* (count) => `${count} left`. Without it, just the number shows.
|
|
34
37
|
*/
|
|
35
38
|
remaining?: (count: number) => string;
|
|
39
|
+
/** Surface, textarea, send button and counter scale. @default "md" */
|
|
40
|
+
size?: ConversationSize;
|
|
36
41
|
};
|
|
37
42
|
/** A reply field with a round send button; Ctrl/Cmd+Enter sends, and it clears only once the send worked. */
|
|
38
|
-
declare function Composer({ onSend, pending, maxLength, placeholder, actions, sendLabel, remaining, }: ComposerProps): import("react").JSX.Element;
|
|
43
|
+
declare function Composer({ onSend, pending, maxLength, placeholder, actions, sendLabel, remaining, size, }: ComposerProps): import("react").JSX.Element;
|
|
39
44
|
export declare const Conversation: {
|
|
40
45
|
Messages: typeof Messages;
|
|
41
46
|
Composer: typeof Composer;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export type LocaleSwitcherSize = "sm" | "md" | "lg";
|
|
1
2
|
export type LocaleSwitcherProps<L extends string> = {
|
|
2
3
|
locales: readonly L[];
|
|
3
4
|
locale: L;
|
|
@@ -5,6 +6,8 @@ export type LocaleSwitcherProps<L extends string> = {
|
|
|
5
6
|
/** localStorage key for where the person dragged it. */
|
|
6
7
|
storageKey: string;
|
|
7
8
|
label: string;
|
|
9
|
+
/** Trigger, header, list item and icon scale. @default "md" */
|
|
10
|
+
size?: LocaleSwitcherSize;
|
|
8
11
|
};
|
|
9
12
|
/** A draggable floating language picker that snaps to the nearest corner. */
|
|
10
|
-
export declare function LocaleSwitcher<L extends string>({ locales, locale, onSelect, storageKey, label, }: LocaleSwitcherProps<L>): import("react").JSX.Element;
|
|
13
|
+
export declare function LocaleSwitcher<L extends string>({ locales, locale, onSelect, storageKey, label, size, }: LocaleSwitcherProps<L>): import("react").JSX.Element;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type HTMLMotionProps } from "motion/react";
|
|
2
2
|
import { type ReactNode, type RefObject } from "react";
|
|
3
|
+
export type MorphSize = "sm" | "md" | "lg";
|
|
3
4
|
export interface MorphProps extends Omit<HTMLMotionProps<"div">, "children"> {
|
|
4
5
|
/** Controls whether the overlay is presented. */
|
|
5
6
|
open: boolean;
|
|
@@ -13,6 +14,8 @@ export interface MorphProps extends Omit<HTMLMotionProps<"div">, "children"> {
|
|
|
13
14
|
labelledBy?: string;
|
|
14
15
|
/** Allow tapping the backdrop to close. Escape still calls onClose. @default true */
|
|
15
16
|
dismissOnOutsidePress?: boolean;
|
|
17
|
+
/** Outer padding and panel max-width. @default "md" */
|
|
18
|
+
size?: MorphSize;
|
|
16
19
|
className?: string;
|
|
17
20
|
children?: ReactNode;
|
|
18
21
|
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* a failure. Call `toast` from anywhere; render one `<Toaster />`.
|
|
4
4
|
*/
|
|
5
5
|
export type ToastTone = "neutral" | "success" | "error" | "loading";
|
|
6
|
+
export type ToastSize = "sm" | "md" | "lg";
|
|
6
7
|
export interface ToastAction {
|
|
7
8
|
label: string;
|
|
8
9
|
onClick: () => void;
|
|
@@ -16,11 +17,15 @@ export interface Toast {
|
|
|
16
17
|
/** Milliseconds before it leaves on its own; `null` stays until dismissed. */
|
|
17
18
|
duration: number | null;
|
|
18
19
|
createdAt: number;
|
|
20
|
+
/** Pill size, content padding and title/description text scale. @default "md" */
|
|
21
|
+
size: ToastSize;
|
|
19
22
|
}
|
|
20
23
|
export interface ToastOptions {
|
|
21
24
|
description?: string;
|
|
22
25
|
action?: ToastAction;
|
|
23
26
|
duration?: number | null;
|
|
27
|
+
/** Pill size, content padding and title/description text scale. @default "md" */
|
|
28
|
+
size?: ToastSize;
|
|
24
29
|
}
|
|
25
30
|
/** Changes a toast in place; it morphs to its new state. */
|
|
26
31
|
declare function update(id: number, tone: ToastTone, title: string, options?: ToastOptions): number;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export type WakeSliderSize = "sm" | "md" | "lg";
|
|
1
2
|
export interface WakeSliderProps {
|
|
2
3
|
label: string;
|
|
3
4
|
value?: number;
|
|
@@ -8,5 +9,7 @@ export interface WakeSliderProps {
|
|
|
8
9
|
disabled?: boolean;
|
|
9
10
|
className?: string;
|
|
10
11
|
formatValue?: (value: number) => string;
|
|
12
|
+
/** Bar height at rest and value text scale. @default "md" */
|
|
13
|
+
size?: WakeSliderSize;
|
|
11
14
|
}
|
|
12
|
-
export declare function WakeSlider({ label, value: controlledValue, defaultValue, onChange, disabled, className, formatValue, bars, wake, }: WakeSliderProps): import("react").JSX.Element;
|
|
15
|
+
export declare function WakeSlider({ label, value: controlledValue, defaultValue, onChange, disabled, className, formatValue, size, bars, wake, }: WakeSliderProps): import("react").JSX.Element;
|
package/package.json
CHANGED
|
@@ -1,11 +1,49 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
useContext,
|
|
4
|
+
type ComponentPropsWithoutRef,
|
|
5
|
+
type PropsWithChildren,
|
|
6
|
+
type ReactNode,
|
|
7
|
+
} from "react"
|
|
2
8
|
import { cn } from "../../lib/class-names"
|
|
3
9
|
|
|
4
10
|
export type ContentViewVariant = keyof typeof variants
|
|
5
11
|
export type ContentViewHeadingLevel = "h1" | "h2" | "h3"
|
|
12
|
+
export type ContentViewSize = "sm" | "md" | "lg"
|
|
13
|
+
|
|
14
|
+
const containerRadius: Record<ContentViewSize, string> = {
|
|
15
|
+
sm: "rounded-xl",
|
|
16
|
+
md: "rounded-2xl",
|
|
17
|
+
lg: "rounded-3xl",
|
|
18
|
+
}
|
|
19
|
+
const sectionGaps: Record<ContentViewSize, string> = {
|
|
20
|
+
sm: "gap-2",
|
|
21
|
+
md: "gap-3",
|
|
22
|
+
lg: "gap-4",
|
|
23
|
+
}
|
|
24
|
+
const spanText: Record<ContentViewSize, string> = {
|
|
25
|
+
sm: "text-footnote tracking-tighter font-light",
|
|
26
|
+
md: "text-callout tracking-tighter font-light",
|
|
27
|
+
lg: "text-subheadline tracking-tighter font-light",
|
|
28
|
+
}
|
|
29
|
+
const subText: Record<ContentViewSize, string> = {
|
|
30
|
+
sm: "text-footnote text-label",
|
|
31
|
+
md: "text-subheadline text-label",
|
|
32
|
+
lg: "text-body text-label",
|
|
33
|
+
}
|
|
34
|
+
const headerActionGaps: Record<ContentViewSize, string> = {
|
|
35
|
+
sm: "gap-2",
|
|
36
|
+
md: "gap-3",
|
|
37
|
+
lg: "gap-4",
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const ContentViewSizeContext = createContext<ContentViewSize>("md")
|
|
41
|
+
const useContentViewSize = () => useContext(ContentViewSizeContext)
|
|
6
42
|
|
|
7
43
|
type ContentViewProps = ComponentPropsWithoutRef<"section"> & {
|
|
8
44
|
variant?: ContentViewVariant
|
|
45
|
+
/** Container radius, section gap and text scale. @default "md" */
|
|
46
|
+
size?: ContentViewSize
|
|
9
47
|
}
|
|
10
48
|
|
|
11
49
|
type RowProps = ComponentPropsWithoutRef<"div">
|
|
@@ -38,14 +76,24 @@ const variants = {
|
|
|
38
76
|
|
|
39
77
|
export function ContentView({
|
|
40
78
|
variant = "elevated",
|
|
79
|
+
size = "md",
|
|
41
80
|
className,
|
|
42
81
|
children,
|
|
43
82
|
...props
|
|
44
83
|
}: ContentViewProps) {
|
|
45
84
|
return (
|
|
46
|
-
<
|
|
47
|
-
|
|
48
|
-
|
|
85
|
+
<ContentViewSizeContext value={size}>
|
|
86
|
+
<section
|
|
87
|
+
className={cn(
|
|
88
|
+
variants[variant],
|
|
89
|
+
variant === "elevated" && containerRadius[size],
|
|
90
|
+
className
|
|
91
|
+
)}
|
|
92
|
+
{...props}
|
|
93
|
+
>
|
|
94
|
+
{children}
|
|
95
|
+
</section>
|
|
96
|
+
</ContentViewSizeContext>
|
|
49
97
|
)
|
|
50
98
|
}
|
|
51
99
|
|
|
@@ -58,8 +106,9 @@ function Row({ className, children, ...props }: RowProps) {
|
|
|
58
106
|
}
|
|
59
107
|
|
|
60
108
|
function Section({ className, children, ...props }: SectionProps) {
|
|
109
|
+
const size = useContentViewSize()
|
|
61
110
|
return (
|
|
62
|
-
<section className={cn("flex flex-col
|
|
111
|
+
<section className={cn("flex flex-col", sectionGaps[size], className)} {...props}>
|
|
63
112
|
{children}
|
|
64
113
|
</section>
|
|
65
114
|
)
|
|
@@ -83,10 +132,11 @@ function Header({
|
|
|
83
132
|
</Tag>
|
|
84
133
|
)
|
|
85
134
|
|
|
135
|
+
const size = useContentViewSize()
|
|
86
136
|
return (
|
|
87
137
|
<div className={className}>
|
|
88
138
|
{action ? (
|
|
89
|
-
<div className={cn("flex items-center
|
|
139
|
+
<div className={cn("flex items-center", headerActionGaps[size], actionClassName)}>
|
|
90
140
|
{title}
|
|
91
141
|
{action}
|
|
92
142
|
</div>
|
|
@@ -128,16 +178,18 @@ function P({ className, children, ...props }: PProps) {
|
|
|
128
178
|
}
|
|
129
179
|
|
|
130
180
|
function Span({ className, children, ...props }: SpanProps) {
|
|
181
|
+
const size = useContentViewSize()
|
|
131
182
|
return (
|
|
132
|
-
<span className={cn(
|
|
183
|
+
<span className={cn(spanText[size], className)} {...props}>
|
|
133
184
|
{children}
|
|
134
185
|
</span>
|
|
135
186
|
)
|
|
136
187
|
}
|
|
137
188
|
|
|
138
189
|
function Sub({ className, children, ...props }: SubProps) {
|
|
190
|
+
const size = useContentViewSize()
|
|
139
191
|
return (
|
|
140
|
-
<sub className={cn(
|
|
192
|
+
<sub className={cn(subText[size], className)} {...props}>
|
|
141
193
|
{children}
|
|
142
194
|
</sub>
|
|
143
195
|
)
|
|
@@ -13,9 +13,45 @@ import { menuSurfaceExit, menuSurfaceReveal } from "./menu-reveal"
|
|
|
13
13
|
import {
|
|
14
14
|
closeContextMenu,
|
|
15
15
|
getContextMenu,
|
|
16
|
-
type
|
|
16
|
+
type ContextMenuItem,
|
|
17
|
+
type ContextMenuSize,
|
|
18
|
+
openContextMenu,
|
|
19
|
+
openFollowUpMenu,
|
|
20
|
+
openMenu,
|
|
17
21
|
subscribeContextMenu,
|
|
18
22
|
} from "./context-menu-store"
|
|
23
|
+
import type { OpenMenu } from "./context-menu-store"
|
|
24
|
+
|
|
25
|
+
const surfaceSizes: Record<ContextMenuSize, string> = {
|
|
26
|
+
sm: "min-w-44 max-w-[min(240px,calc(100vw-24px))] rounded-2xl p-1",
|
|
27
|
+
md: "min-w-50 max-w-[min(280px,calc(100vw-24px))] rounded-3xl p-1.5",
|
|
28
|
+
lg: "min-w-56 max-w-[min(320px,calc(100vw-24px))] rounded-3xl p-2",
|
|
29
|
+
}
|
|
30
|
+
const itemSizes: Record<ContextMenuSize, string> = {
|
|
31
|
+
sm: "flex items-center gap-2 rounded-md px-2 py-1 text-start text-caption transition-colors disabled:opacity-40",
|
|
32
|
+
md: "flex items-center gap-2.5 rounded-lg px-2.5 text-start text-subheadline transition-colors disabled:opacity-40",
|
|
33
|
+
lg: "flex items-center gap-3 rounded-lg px-3 py-2 text-start text-body transition-colors disabled:opacity-40",
|
|
34
|
+
}
|
|
35
|
+
const headingSizes: Record<ContextMenuSize, string> = {
|
|
36
|
+
sm: "px-2 pt-1 pb-0.5 text-caption2 font-semibold text-label-tertiary",
|
|
37
|
+
md: "px-2.5 pt-1.5 pb-0.5 text-caption2 font-semibold text-label-tertiary",
|
|
38
|
+
lg: "px-3 pt-2 pb-1 text-caption font-semibold text-label-tertiary",
|
|
39
|
+
}
|
|
40
|
+
const dividerSizes: Record<ContextMenuSize, string> = {
|
|
41
|
+
sm: "mx-1.5 my-0.5 h-px bg-separator/80",
|
|
42
|
+
md: "mx-2 my-1 h-px bg-separator/80",
|
|
43
|
+
lg: "mx-2.5 my-1.5 h-px bg-separator/80",
|
|
44
|
+
}
|
|
45
|
+
const iconSlotSizes: Record<ContextMenuSize, string> = {
|
|
46
|
+
sm: "flex w-3.5 shrink-0 justify-center text-label-secondary",
|
|
47
|
+
md: "flex w-4 shrink-0 justify-center text-label-secondary",
|
|
48
|
+
lg: "flex w-5 shrink-0 justify-center text-label-secondary",
|
|
49
|
+
}
|
|
50
|
+
const checkIconSizes: Record<ContextMenuSize, number> = {
|
|
51
|
+
sm: 13,
|
|
52
|
+
md: 15,
|
|
53
|
+
lg: 17,
|
|
54
|
+
}
|
|
19
55
|
|
|
20
56
|
/** Renders the open context menu. Mount once, near the app root. */
|
|
21
57
|
export function ContextMenuHost() {
|
|
@@ -251,7 +287,8 @@ function Menu({ menu }: { menu: OpenMenu }) {
|
|
|
251
287
|
}}
|
|
252
288
|
{...{ [FLOATING_LAYER]: "" }}
|
|
253
289
|
className={cn(
|
|
254
|
-
"fixed z-75 isolate flex
|
|
290
|
+
"fixed z-75 isolate flex flex-col outline-none",
|
|
291
|
+
surfaceSizes[menu.size],
|
|
255
292
|
menu.touch && "min-w-60",
|
|
256
293
|
!present && "pointer-events-none"
|
|
257
294
|
)}
|
|
@@ -327,19 +364,11 @@ function Menu({ menu }: { menu: OpenMenu }) {
|
|
|
327
364
|
>
|
|
328
365
|
{menu.items.map((item, index) => {
|
|
329
366
|
if (item === "divider") {
|
|
330
|
-
return
|
|
331
|
-
<div
|
|
332
|
-
key={`divider-${index}`}
|
|
333
|
-
className="mx-2 my-1 h-px bg-separator/80"
|
|
334
|
-
/>
|
|
335
|
-
)
|
|
367
|
+
return <div key={`divider-${index}`} className={dividerSizes[menu.size]} />
|
|
336
368
|
}
|
|
337
369
|
if ("heading" in item) {
|
|
338
370
|
return (
|
|
339
|
-
<span
|
|
340
|
-
key={`heading-${index}`}
|
|
341
|
-
className="px-2.5 pt-1.5 pb-0.5 text-caption2 font-semibold text-label-tertiary"
|
|
342
|
-
>
|
|
371
|
+
<span key={`heading-${index}`} className={headingSizes[menu.size]}>
|
|
343
372
|
{item.heading}
|
|
344
373
|
</span>
|
|
345
374
|
)
|
|
@@ -360,7 +389,7 @@ function Menu({ menu }: { menu: OpenMenu }) {
|
|
|
360
389
|
item.onSelect()
|
|
361
390
|
}}
|
|
362
391
|
className={cn(
|
|
363
|
-
|
|
392
|
+
itemSizes[menu.size],
|
|
364
393
|
menu.touch ? "py-2.5" : "py-1.5",
|
|
365
394
|
item.destructive ? "text-danger" : "text-label",
|
|
366
395
|
focused === current &&
|
|
@@ -368,10 +397,10 @@ function Menu({ menu }: { menu: OpenMenu }) {
|
|
|
368
397
|
(item.destructive ? "bg-danger/10" : "bg-fill")
|
|
369
398
|
)}
|
|
370
399
|
>
|
|
371
|
-
<span className=
|
|
400
|
+
<span className={iconSlotSizes[menu.size]}>
|
|
372
401
|
{item.checked ? (
|
|
373
402
|
<CheckIcon
|
|
374
|
-
size={
|
|
403
|
+
size={checkIconSizes[menu.size]}
|
|
375
404
|
strokeWidth={2.4}
|
|
376
405
|
className="text-accent-text"
|
|
377
406
|
/>
|
|
@@ -4,6 +4,7 @@ import type { MenuRect } from "./menu-reveal"
|
|
|
4
4
|
export type ContextMenuAlignX = "start" | "end"
|
|
5
5
|
export type ContextMenuAlignY = "top" | "bottom"
|
|
6
6
|
export type ContextMenuEdge = "leading" | "trailing"
|
|
7
|
+
export type ContextMenuSize = "sm" | "md" | "lg"
|
|
7
8
|
|
|
8
9
|
/** One row of a context menu, or a divider between groups of rows. */
|
|
9
10
|
export type ContextMenuItem =
|
|
@@ -27,6 +28,8 @@ export interface OpenMenu {
|
|
|
27
28
|
items: ContextMenuItem[]
|
|
28
29
|
/** Opened by long-press: bigger rows for fingers. */
|
|
29
30
|
touch: boolean
|
|
31
|
+
/** Surface, item and heading density. @default "md" */
|
|
32
|
+
size: ContextMenuSize
|
|
30
33
|
/** Which edge of the menu sits at `x`: its left (`start`) or its right (`end`). */
|
|
31
34
|
alignX: ContextMenuAlignX
|
|
32
35
|
/** Which edge of the menu sits at `y`: its top (`top`) or its bottom (`bottom`). */
|
|
@@ -52,7 +55,8 @@ export function openContextMenu(
|
|
|
52
55
|
y: number,
|
|
53
56
|
items: ContextMenuItem[],
|
|
54
57
|
touch = false,
|
|
55
|
-
anchor: Element | null = typeof document === "undefined" ? null : document.activeElement
|
|
58
|
+
anchor: Element | null = typeof document === "undefined" ? null : document.activeElement,
|
|
59
|
+
size: ContextMenuSize = "md"
|
|
56
60
|
) {
|
|
57
61
|
menu = {
|
|
58
62
|
id: nextId++,
|
|
@@ -61,6 +65,7 @@ export function openContextMenu(
|
|
|
61
65
|
y,
|
|
62
66
|
items,
|
|
63
67
|
touch,
|
|
68
|
+
size,
|
|
64
69
|
alignX: "start",
|
|
65
70
|
alignY: "top",
|
|
66
71
|
}
|
|
@@ -93,11 +98,14 @@ export function openMenu(
|
|
|
93
98
|
edge = "leading",
|
|
94
99
|
above = false,
|
|
95
100
|
toolbar,
|
|
101
|
+
size = "md",
|
|
96
102
|
}: {
|
|
97
103
|
edge?: ContextMenuEdge
|
|
98
104
|
above?: boolean
|
|
99
105
|
/** Replace this action group with a horizontally expanding menu surface. */
|
|
100
106
|
toolbar?: HTMLElement
|
|
107
|
+
/** Surface, item and heading density. @default "md" */
|
|
108
|
+
size?: ContextMenuSize
|
|
101
109
|
} = {}
|
|
102
110
|
) {
|
|
103
111
|
const rect = anchor.getBoundingClientRect()
|
|
@@ -112,6 +120,7 @@ export function openMenu(
|
|
|
112
120
|
y: toolbar ? rect.top : above ? rect.bottom : rect.top,
|
|
113
121
|
items,
|
|
114
122
|
touch: false,
|
|
123
|
+
size,
|
|
115
124
|
alignX: right ? "end" : "start",
|
|
116
125
|
alignY: above ? "bottom" : "top",
|
|
117
126
|
toolbar,
|
|
@@ -16,12 +16,73 @@ export type ConversationMessage = {
|
|
|
16
16
|
createdAt: string
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
export type ConversationSize = "sm" | "md" | "lg"
|
|
20
|
+
|
|
21
|
+
const avatarSizes: Record<ConversationSize, string> = {
|
|
22
|
+
sm: "size-6",
|
|
23
|
+
md: "size-7",
|
|
24
|
+
lg: "size-8",
|
|
25
|
+
}
|
|
26
|
+
const bubbleClasses: Record<ConversationSize, string> = {
|
|
27
|
+
sm: "rounded-3xl px-3 py-1.5 text-footnote leading-snug",
|
|
28
|
+
md: "rounded-4xl px-3.5 py-2 text-subheadline leading-snug",
|
|
29
|
+
lg: "rounded-5xl px-4 py-2.5 text-body leading-snug",
|
|
30
|
+
}
|
|
31
|
+
const bubbleTails: Record<ConversationSize, string> = {
|
|
32
|
+
sm: "rounded-br-sm",
|
|
33
|
+
md: "rounded-br-md",
|
|
34
|
+
lg: "rounded-br-lg",
|
|
35
|
+
}
|
|
36
|
+
const bubbleTailsOther: Record<ConversationSize, string> = {
|
|
37
|
+
sm: "rounded-bl-sm",
|
|
38
|
+
md: "rounded-bl-md",
|
|
39
|
+
lg: "rounded-bl-lg",
|
|
40
|
+
}
|
|
41
|
+
const authorText: Record<ConversationSize, string> = {
|
|
42
|
+
sm: "px-3 text-caption2 text-label-secondary",
|
|
43
|
+
md: "px-3 text-caption2 text-label-secondary",
|
|
44
|
+
lg: "px-3.5 text-caption text-label-secondary",
|
|
45
|
+
}
|
|
46
|
+
const timeText: Record<ConversationSize, string> = {
|
|
47
|
+
sm: "px-3 text-caption2 text-label-secondary tabular-nums",
|
|
48
|
+
md: "px-3 text-caption2 text-label-secondary tabular-nums",
|
|
49
|
+
lg: "px-3.5 text-caption text-label-secondary tabular-nums",
|
|
50
|
+
}
|
|
51
|
+
const dayMargins: Record<ConversationSize, string> = {
|
|
52
|
+
sm: "my-3",
|
|
53
|
+
md: "my-4",
|
|
54
|
+
lg: "my-5",
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Composer size records
|
|
58
|
+
const composerSurfaces: Record<ConversationSize, string> = {
|
|
59
|
+
sm: "flex items-end gap-2 rounded-4xl border border-separator bg-elevated py-1 pr-1 pl-3 transition-colors focus-within:border-accent-text/60",
|
|
60
|
+
md: "flex items-end gap-2 rounded-5xl border border-separator bg-elevated py-1.5 pr-1.5 pl-4 transition-colors focus-within:border-accent-text/60",
|
|
61
|
+
lg: "flex items-end gap-2 rounded-5xl border border-separator bg-elevated py-2 pr-2 pl-5 transition-colors focus-within:border-accent-text/60",
|
|
62
|
+
}
|
|
63
|
+
const textareaClasses: Record<ConversationSize, string> = {
|
|
64
|
+
sm: "field-sizing-content max-h-48 min-h-8 flex-1 resize-none bg-transparent py-1 text-footnote text-label outline-none placeholder:text-label-secondary",
|
|
65
|
+
md: "field-sizing-content max-h-48 min-h-9 flex-1 resize-none bg-transparent py-1.5 text-subheadline text-label outline-none placeholder:text-label-secondary",
|
|
66
|
+
lg: "field-sizing-content max-h-48 min-h-10 flex-1 resize-none bg-transparent py-2 text-body text-label outline-none placeholder:text-label-secondary",
|
|
67
|
+
}
|
|
68
|
+
const sendButtonClasses: Record<ConversationSize, string> = {
|
|
69
|
+
sm: "size-7 shrink-0 rounded-full",
|
|
70
|
+
md: "size-8 shrink-0 rounded-full",
|
|
71
|
+
lg: "size-9 shrink-0 rounded-full",
|
|
72
|
+
}
|
|
73
|
+
const sendIconSizes: Record<ConversationSize, number> = {
|
|
74
|
+
sm: 16,
|
|
75
|
+
md: 18,
|
|
76
|
+
lg: 20,
|
|
77
|
+
}
|
|
78
|
+
const counterText: Record<ConversationSize, string> = {
|
|
79
|
+
sm: "text-caption2 text-label-secondary tabular-nums",
|
|
80
|
+
md: "text-caption2 text-label-secondary tabular-nums",
|
|
81
|
+
lg: "text-caption text-label-secondary tabular-nums",
|
|
82
|
+
}
|
|
83
|
+
|
|
19
84
|
const GROUP_GAP_MS = 5 * 60 * 1000
|
|
20
85
|
|
|
21
|
-
/**
|
|
22
|
-
* The day line: today and yesterday in the reader's own words, from
|
|
23
|
-
* Intl rather than built-in English, else the full date.
|
|
24
|
-
*/
|
|
25
86
|
function dayLabel(iso: string, locale?: string) {
|
|
26
87
|
const date = new Date(iso)
|
|
27
88
|
const today = new Date()
|
|
@@ -59,10 +120,13 @@ const clock = (iso: string, locale?: string) =>
|
|
|
59
120
|
function Messages({
|
|
60
121
|
messages,
|
|
61
122
|
locale,
|
|
123
|
+
size = "md",
|
|
62
124
|
}: {
|
|
63
125
|
messages: ConversationMessage[]
|
|
64
126
|
/** For day names and times; the reader's own by default. */
|
|
65
127
|
locale?: string
|
|
128
|
+
/** Bubble, avatar, author and time scale. @default "md" */
|
|
129
|
+
size?: ConversationSize
|
|
66
130
|
}) {
|
|
67
131
|
return (
|
|
68
132
|
<ol data-not-typeset className="flex flex-col">
|
|
@@ -87,10 +151,12 @@ function Messages({
|
|
|
87
151
|
return (
|
|
88
152
|
<li key={message.id} className="flex flex-col">
|
|
89
153
|
{newDay && (
|
|
90
|
-
// the reader's own day and clock, which a server can't know
|
|
91
154
|
<span
|
|
92
155
|
suppressHydrationWarning
|
|
93
|
-
className=
|
|
156
|
+
className={cn(
|
|
157
|
+
dayMargins[size],
|
|
158
|
+
"text-center text-caption2 font-medium text-label-secondary"
|
|
159
|
+
)}
|
|
94
160
|
>
|
|
95
161
|
{dayLabel(message.createdAt, locale)}
|
|
96
162
|
</span>
|
|
@@ -103,7 +169,12 @@ function Messages({
|
|
|
103
169
|
)}
|
|
104
170
|
>
|
|
105
171
|
{!message.mine && (
|
|
106
|
-
<span
|
|
172
|
+
<span
|
|
173
|
+
className={cn(
|
|
174
|
+
"flex shrink-0 items-center justify-center",
|
|
175
|
+
avatarSizes[size]
|
|
176
|
+
)}
|
|
177
|
+
>
|
|
107
178
|
{endsGroup && message.avatar}
|
|
108
179
|
</span>
|
|
109
180
|
)}
|
|
@@ -114,21 +185,21 @@ function Messages({
|
|
|
114
185
|
)}
|
|
115
186
|
>
|
|
116
187
|
{startsGroup && message.author && (
|
|
117
|
-
<span className=
|
|
118
|
-
{message.author}
|
|
119
|
-
</span>
|
|
188
|
+
<span className={authorText[size]}>{message.author}</span>
|
|
120
189
|
)}
|
|
121
|
-
{/* text only: a message can never become markup */}
|
|
122
190
|
<p
|
|
123
191
|
suppressHydrationWarning
|
|
124
192
|
title={formatTime(message.createdAt)}
|
|
125
193
|
className={cn(
|
|
126
|
-
|
|
194
|
+
bubbleClasses[size],
|
|
195
|
+
"whitespace-pre-wrap wrap-break-word",
|
|
127
196
|
message.mine
|
|
128
197
|
? "bg-inverse text-on-inverse"
|
|
129
198
|
: "bg-fill text-label",
|
|
130
199
|
endsGroup &&
|
|
131
|
-
(message.mine
|
|
200
|
+
(message.mine
|
|
201
|
+
? bubbleTails[size]
|
|
202
|
+
: bubbleTailsOther[size])
|
|
132
203
|
)}
|
|
133
204
|
>
|
|
134
205
|
{message.body}
|
|
@@ -137,7 +208,7 @@ function Messages({
|
|
|
137
208
|
<time
|
|
138
209
|
suppressHydrationWarning
|
|
139
210
|
dateTime={message.createdAt}
|
|
140
|
-
className=
|
|
211
|
+
className={timeText[size]}
|
|
141
212
|
>
|
|
142
213
|
{clock(message.createdAt, locale)}
|
|
143
214
|
</time>
|
|
@@ -165,6 +236,8 @@ type ComposerProps = {
|
|
|
165
236
|
* (count) => `${count} left`. Without it, just the number shows.
|
|
166
237
|
*/
|
|
167
238
|
remaining?: (count: number) => string
|
|
239
|
+
/** Surface, textarea, send button and counter scale. @default "md" */
|
|
240
|
+
size?: ConversationSize
|
|
168
241
|
}
|
|
169
242
|
|
|
170
243
|
/** A reply field with a round send button; Ctrl/Cmd+Enter sends, and it clears only once the send worked. */
|
|
@@ -176,6 +249,7 @@ function Composer({
|
|
|
176
249
|
actions,
|
|
177
250
|
sendLabel,
|
|
178
251
|
remaining,
|
|
252
|
+
size = "md",
|
|
179
253
|
}: ComposerProps) {
|
|
180
254
|
const [body, setBody] = useState("")
|
|
181
255
|
const ready = body.trim().length > 0 && !pending
|
|
@@ -194,7 +268,7 @@ function Composer({
|
|
|
194
268
|
void send()
|
|
195
269
|
}}
|
|
196
270
|
>
|
|
197
|
-
<div className=
|
|
271
|
+
<div className={composerSurfaces[size]}>
|
|
198
272
|
<textarea
|
|
199
273
|
value={body}
|
|
200
274
|
onChange={(event) => setBody(event.target.value)}
|
|
@@ -207,21 +281,21 @@ function Composer({
|
|
|
207
281
|
maxLength={maxLength}
|
|
208
282
|
placeholder={placeholder}
|
|
209
283
|
rows={1}
|
|
210
|
-
className=
|
|
284
|
+
className={textareaClasses[size]}
|
|
211
285
|
/>
|
|
212
286
|
<IconButton
|
|
213
287
|
type="submit"
|
|
214
288
|
size="icon-xs"
|
|
215
289
|
disabled={!ready}
|
|
216
290
|
label={sendLabel}
|
|
217
|
-
className=
|
|
291
|
+
className={sendButtonClasses[size]}
|
|
218
292
|
>
|
|
219
|
-
<SendIcon size={
|
|
293
|
+
<SendIcon size={sendIconSizes[size]} strokeWidth={2.5} />
|
|
220
294
|
</IconButton>
|
|
221
295
|
</div>
|
|
222
296
|
<div className="flex min-h-6 items-center gap-2 px-1">
|
|
223
297
|
{actions}
|
|
224
|
-
<span className="ml-auto
|
|
298
|
+
<span className={cn("ml-auto", counterText[size])}>
|
|
225
299
|
{body.length > maxLength * 0.8 &&
|
|
226
300
|
(remaining?.(maxLength - body.length) ?? maxLength - body.length)}
|
|
227
301
|
</span>
|
|
@@ -6,6 +6,39 @@ import { useDismiss } from "../../hooks/use-dismiss"
|
|
|
6
6
|
import { CheckIcon, GlobeIcon } from "../../icons/icons"
|
|
7
7
|
import { Popover } from "../popover/popover"
|
|
8
8
|
|
|
9
|
+
export type LocaleSwitcherSize = "sm" | "md" | "lg"
|
|
10
|
+
|
|
11
|
+
const triggerSizes: Record<LocaleSwitcherSize, string> = {
|
|
12
|
+
sm: "glass-menu flex h-9 min-w-16 touch-none cursor-grab items-center justify-center gap-1.5 rounded-full px-3 text-caption2 font-semibold text-label active:cursor-grabbing",
|
|
13
|
+
md: "glass-menu flex h-12 min-w-20 touch-none cursor-grab items-center justify-center gap-2 rounded-full px-4 text-caption font-semibold text-label active:cursor-grabbing",
|
|
14
|
+
lg: "glass-menu flex h-14 min-w-24 touch-none cursor-grab items-center justify-center gap-2.5 rounded-full px-5 text-subheadline font-semibold text-label active:cursor-grabbing",
|
|
15
|
+
}
|
|
16
|
+
const iconSizes: Record<LocaleSwitcherSize, string> = {
|
|
17
|
+
sm: "size-4",
|
|
18
|
+
md: "size-5",
|
|
19
|
+
lg: "size-6",
|
|
20
|
+
}
|
|
21
|
+
const popoverWidths: Record<LocaleSwitcherSize, string> = {
|
|
22
|
+
sm: "w-44",
|
|
23
|
+
md: "w-52",
|
|
24
|
+
lg: "w-60",
|
|
25
|
+
}
|
|
26
|
+
const headerSizes: Record<LocaleSwitcherSize, string> = {
|
|
27
|
+
sm: "flex min-h-9 w-full items-center justify-center gap-1.5 rounded-4xl bg-fill/50 text-caption2 font-semibold text-label",
|
|
28
|
+
md: "flex min-h-12 w-full items-center justify-center gap-2 rounded-4xl bg-fill/50 text-caption font-semibold text-label",
|
|
29
|
+
lg: "flex min-h-14 w-full items-center justify-center gap-2.5 rounded-4xl bg-fill/50 text-subheadline font-semibold text-label",
|
|
30
|
+
}
|
|
31
|
+
const itemSizes: Record<LocaleSwitcherSize, string> = {
|
|
32
|
+
sm: "flex min-h-9 items-center justify-between gap-2 rounded-4xl px-3 py-2 text-left text-caption text-label outline-none",
|
|
33
|
+
md: "flex min-h-11 items-center justify-between gap-3 rounded-4xl px-3.5 py-2.5 text-left text-footnote text-label outline-none",
|
|
34
|
+
lg: "flex min-h-13 items-center justify-between gap-3 rounded-4xl px-4 py-3 text-left text-subheadline text-label outline-none",
|
|
35
|
+
}
|
|
36
|
+
const checkIconSizes: Record<LocaleSwitcherSize, string> = {
|
|
37
|
+
sm: "size-3.5 shrink-0",
|
|
38
|
+
md: "size-4 shrink-0",
|
|
39
|
+
lg: "size-5 shrink-0",
|
|
40
|
+
}
|
|
41
|
+
|
|
9
42
|
type Side = "left" | "right"
|
|
10
43
|
type Edge = "top" | "bottom"
|
|
11
44
|
|
|
@@ -58,6 +91,8 @@ export type LocaleSwitcherProps<L extends string> = {
|
|
|
58
91
|
/** localStorage key for where the person dragged it. */
|
|
59
92
|
storageKey: string
|
|
60
93
|
label: string
|
|
94
|
+
/** Trigger, header, list item and icon scale. @default "md" */
|
|
95
|
+
size?: LocaleSwitcherSize
|
|
61
96
|
}
|
|
62
97
|
|
|
63
98
|
/** A draggable floating language picker that snaps to the nearest corner. */
|
|
@@ -67,6 +102,7 @@ export function LocaleSwitcher<L extends string>({
|
|
|
67
102
|
onSelect,
|
|
68
103
|
storageKey,
|
|
69
104
|
label,
|
|
105
|
+
size = "md",
|
|
70
106
|
}: LocaleSwitcherProps<L>) {
|
|
71
107
|
const containerRef = useRef<HTMLDivElement>(null)
|
|
72
108
|
const triggerRef = useRef<HTMLButtonElement>(null)
|
|
@@ -177,9 +213,9 @@ export function LocaleSwitcher<L extends string>({
|
|
|
177
213
|
setOpen(true)
|
|
178
214
|
}
|
|
179
215
|
}}
|
|
180
|
-
className=
|
|
216
|
+
className={triggerSizes[size]}
|
|
181
217
|
>
|
|
182
|
-
<GlobeIcon aria-hidden="true" className=
|
|
218
|
+
<GlobeIcon aria-hidden="true" className={iconSizes[size]} />
|
|
183
219
|
{locale.toUpperCase()}
|
|
184
220
|
</button>
|
|
185
221
|
<Popover
|
|
@@ -189,7 +225,8 @@ export function LocaleSwitcher<L extends string>({
|
|
|
189
225
|
role="menu"
|
|
190
226
|
aria-label={label}
|
|
191
227
|
className={cn(
|
|
192
|
-
"top-0 flex
|
|
228
|
+
"top-0 flex max-w-[calc(100vw-2.5rem)] gap-2 p-2",
|
|
229
|
+
popoverWidths[size],
|
|
193
230
|
anchor.side === "right" ? "right-0" : "left-0",
|
|
194
231
|
growsUp ? "flex-col-reverse" : "flex-col"
|
|
195
232
|
)}
|
|
@@ -201,9 +238,9 @@ export function LocaleSwitcher<L extends string>({
|
|
|
201
238
|
setOpen(false)
|
|
202
239
|
triggerRef.current?.focus()
|
|
203
240
|
}}
|
|
204
|
-
className=
|
|
241
|
+
className={headerSizes[size]}
|
|
205
242
|
>
|
|
206
|
-
<GlobeIcon aria-hidden="true" className=
|
|
243
|
+
<GlobeIcon aria-hidden="true" className={iconSizes[size]} />
|
|
207
244
|
{locale.toUpperCase()}
|
|
208
245
|
</button>
|
|
209
246
|
<div className="flex flex-col gap-1">
|
|
@@ -224,14 +261,17 @@ export function LocaleSwitcher<L extends string>({
|
|
|
224
261
|
}
|
|
225
262
|
}}
|
|
226
263
|
className={cn(
|
|
227
|
-
|
|
264
|
+
itemSizes[size],
|
|
228
265
|
"hover:bg-fill/65 focus-visible:bg-fill/65 transition-colors",
|
|
229
266
|
active && "bg-accent/12 font-medium"
|
|
230
267
|
)}
|
|
231
268
|
>
|
|
232
269
|
{item.label}
|
|
233
270
|
{active && (
|
|
234
|
-
<CheckIcon
|
|
271
|
+
<CheckIcon
|
|
272
|
+
aria-hidden="true"
|
|
273
|
+
className={checkIconSizes[size]}
|
|
274
|
+
/>
|
|
235
275
|
)}
|
|
236
276
|
</button>
|
|
237
277
|
)
|
|
@@ -7,6 +7,19 @@ import { SheetPortal } from "../sheet/sheet-portal"
|
|
|
7
7
|
import { cn } from "../../lib/class-names"
|
|
8
8
|
import { contentCrossfade, fluidCollapse, fluidOvershoot } from "../../motion/transitions"
|
|
9
9
|
|
|
10
|
+
export type MorphSize = "sm" | "md" | "lg"
|
|
11
|
+
|
|
12
|
+
const outerPaddings: Record<MorphSize, string> = {
|
|
13
|
+
sm: "p-4",
|
|
14
|
+
md: "p-6",
|
|
15
|
+
lg: "p-8",
|
|
16
|
+
}
|
|
17
|
+
const panelMaxWidths: Record<MorphSize, string> = {
|
|
18
|
+
sm: "max-w-sm",
|
|
19
|
+
md: "max-w-[720px]",
|
|
20
|
+
lg: "max-w-3xl",
|
|
21
|
+
}
|
|
22
|
+
|
|
10
23
|
export interface MorphProps extends Omit<HTMLMotionProps<"div">, "children"> {
|
|
11
24
|
/** Controls whether the overlay is presented. */
|
|
12
25
|
open: boolean
|
|
@@ -20,6 +33,8 @@ export interface MorphProps extends Omit<HTMLMotionProps<"div">, "children"> {
|
|
|
20
33
|
labelledBy?: string
|
|
21
34
|
/** Allow tapping the backdrop to close. Escape still calls onClose. @default true */
|
|
22
35
|
dismissOnOutsidePress?: boolean
|
|
36
|
+
/** Outer padding and panel max-width. @default "md" */
|
|
37
|
+
size?: MorphSize
|
|
23
38
|
className?: string
|
|
24
39
|
children?: ReactNode
|
|
25
40
|
}
|
|
@@ -44,6 +59,7 @@ function OpenMorph({
|
|
|
44
59
|
label,
|
|
45
60
|
labelledBy,
|
|
46
61
|
dismissOnOutsidePress = true,
|
|
62
|
+
size = "md",
|
|
47
63
|
className,
|
|
48
64
|
children,
|
|
49
65
|
...props
|
|
@@ -131,7 +147,12 @@ function OpenMorph({
|
|
|
131
147
|
|
|
132
148
|
return (
|
|
133
149
|
<SheetPortal>
|
|
134
|
-
<div
|
|
150
|
+
<div
|
|
151
|
+
className={cn(
|
|
152
|
+
"fixed inset-0 z-[75] flex items-center justify-center",
|
|
153
|
+
outerPaddings[size]
|
|
154
|
+
)}
|
|
155
|
+
>
|
|
135
156
|
<motion.div
|
|
136
157
|
aria-hidden="true"
|
|
137
158
|
className="absolute inset-0 bg-black/30 backdrop-blur-md"
|
|
@@ -150,7 +171,8 @@ function OpenMorph({
|
|
|
150
171
|
tabIndex={-1}
|
|
151
172
|
className={cn(
|
|
152
173
|
"relative isolate flex w-full flex-col rounded-5xl outline-none",
|
|
153
|
-
"max-h-[92%]
|
|
174
|
+
"max-h-[92%]",
|
|
175
|
+
panelMaxWidths[size],
|
|
154
176
|
settled && "overflow-hidden",
|
|
155
177
|
className
|
|
156
178
|
)}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* a failure. Call `toast` from anywhere; render one `<Toaster />`.
|
|
4
4
|
*/
|
|
5
5
|
export type ToastTone = "neutral" | "success" | "error" | "loading"
|
|
6
|
+
export type ToastSize = "sm" | "md" | "lg"
|
|
6
7
|
|
|
7
8
|
export interface ToastAction {
|
|
8
9
|
label: string
|
|
@@ -18,12 +19,16 @@ export interface Toast {
|
|
|
18
19
|
/** Milliseconds before it leaves on its own; `null` stays until dismissed. */
|
|
19
20
|
duration: number | null
|
|
20
21
|
createdAt: number
|
|
22
|
+
/** Pill size, content padding and title/description text scale. @default "md" */
|
|
23
|
+
size: ToastSize
|
|
21
24
|
}
|
|
22
25
|
|
|
23
26
|
export interface ToastOptions {
|
|
24
27
|
description?: string
|
|
25
28
|
action?: ToastAction
|
|
26
29
|
duration?: number | null
|
|
30
|
+
/** Pill size, content padding and title/description text scale. @default "md" */
|
|
31
|
+
size?: ToastSize
|
|
27
32
|
}
|
|
28
33
|
|
|
29
34
|
const DEFAULT_DURATION = 4200
|
|
@@ -54,6 +59,7 @@ function show(tone: ToastTone, title: string, options: ToastOptions = {}): numbe
|
|
|
54
59
|
action: options.action,
|
|
55
60
|
duration,
|
|
56
61
|
createdAt: Date.now(),
|
|
62
|
+
size: options.size ?? "md",
|
|
57
63
|
},
|
|
58
64
|
...toasts,
|
|
59
65
|
].slice(0, MAX_VISIBLE + 2)
|
|
@@ -79,6 +85,7 @@ function update(id: number, tone: ToastTone, title: string, options: ToastOption
|
|
|
79
85
|
? null
|
|
80
86
|
: DEFAULT_DURATION,
|
|
81
87
|
createdAt: Date.now(),
|
|
88
|
+
size: options.size ?? t.size,
|
|
82
89
|
}
|
|
83
90
|
: t
|
|
84
91
|
)
|
|
@@ -7,12 +7,39 @@ import {
|
|
|
7
7
|
getToasts,
|
|
8
8
|
subscribeToasts,
|
|
9
9
|
type Toast,
|
|
10
|
+
type ToastSize,
|
|
10
11
|
type ToastTone,
|
|
11
12
|
toast as toastApi,
|
|
12
13
|
toastLimits,
|
|
13
14
|
} from "./toast-store"
|
|
14
15
|
|
|
15
|
-
const PILL = 40
|
|
16
|
+
const PILL: Record<ToastSize, number> = { sm: 32, md: 40, lg: 48 }
|
|
17
|
+
|
|
18
|
+
const contentClasses: Record<ToastSize, string> = {
|
|
19
|
+
sm: "absolute top-0 left-0 flex w-max max-w-[min(420px,calc(100vw-24px))] items-start gap-2 py-2 pr-3 pl-2 text-on-inverse",
|
|
20
|
+
md: "absolute top-0 left-0 flex w-max max-w-[min(420px,calc(100vw-24px))] items-start gap-2.5 py-[10px] pr-4 pl-[10px] text-on-inverse",
|
|
21
|
+
lg: "absolute top-0 left-0 flex w-max max-w-[min(420px,calc(100vw-24px))] items-start gap-3 py-3 pr-5 pl-3 text-on-inverse",
|
|
22
|
+
}
|
|
23
|
+
const titleClasses: Record<ToastSize, string> = {
|
|
24
|
+
sm: "text-footnote leading-4 font-semibold",
|
|
25
|
+
md: "text-subheadline leading-5 font-semibold",
|
|
26
|
+
lg: "text-body leading-6 font-semibold",
|
|
27
|
+
}
|
|
28
|
+
const descriptionClasses: Record<ToastSize, string> = {
|
|
29
|
+
sm: "text-caption2 leading-snug text-on-inverse/70",
|
|
30
|
+
md: "text-footnote leading-snug text-on-inverse/70",
|
|
31
|
+
lg: "text-subheadline leading-snug text-on-inverse/70",
|
|
32
|
+
}
|
|
33
|
+
const actionClasses: Record<ToastSize, string> = {
|
|
34
|
+
sm: "ml-1 shrink-0 self-center rounded-full bg-on-inverse/12 px-2.5 py-0.5 text-caption2 font-semibold text-on-inverse transition-colors hover:bg-on-inverse/20",
|
|
35
|
+
md: "ml-1 shrink-0 self-center rounded-full bg-on-inverse/12 px-3 py-1 text-footnote font-semibold text-on-inverse transition-colors hover:bg-on-inverse/20",
|
|
36
|
+
lg: "ml-1 shrink-0 self-center rounded-full bg-on-inverse/12 px-3.5 py-1.5 text-subheadline font-semibold text-on-inverse transition-colors hover:bg-on-inverse/20",
|
|
37
|
+
}
|
|
38
|
+
const toneIconClasses: Record<ToastSize, string> = {
|
|
39
|
+
sm: "flex size-4 shrink-0 items-center justify-center rounded-full",
|
|
40
|
+
md: "flex size-5 shrink-0 items-center justify-center rounded-full",
|
|
41
|
+
lg: "flex size-6 shrink-0 items-center justify-center rounded-full",
|
|
42
|
+
}
|
|
16
43
|
const EMPTY: Toast[] = []
|
|
17
44
|
/** A little loose, so the body overshoots and settles like a drop of honey. */
|
|
18
45
|
const BODY_SPRING = {
|
|
@@ -110,10 +137,11 @@ function ToastItem({
|
|
|
110
137
|
filterId: string
|
|
111
138
|
reducedMotion: boolean
|
|
112
139
|
}) {
|
|
140
|
+
const pill = PILL[toast.size]
|
|
113
141
|
const content = useRef<HTMLDivElement>(null)
|
|
114
142
|
const [size, setSize] = useState({
|
|
115
|
-
width:
|
|
116
|
-
height:
|
|
143
|
+
width: pill,
|
|
144
|
+
height: pill,
|
|
117
145
|
})
|
|
118
146
|
const [open, setOpen] = useState(false)
|
|
119
147
|
|
|
@@ -143,8 +171,8 @@ function ToastItem({
|
|
|
143
171
|
return () => clearTimeout(timer)
|
|
144
172
|
}, [toast.id, toast.duration, paused])
|
|
145
173
|
|
|
146
|
-
const width = open ? size.width :
|
|
147
|
-
const height = open ? size.height :
|
|
174
|
+
const width = open ? size.width : pill
|
|
175
|
+
const height = open ? size.height : pill
|
|
148
176
|
// Older toasts tuck in behind the newest, and fan out on hover.
|
|
149
177
|
const stacked = !expanded && index > 0
|
|
150
178
|
|
|
@@ -156,7 +184,7 @@ function ToastItem({
|
|
|
156
184
|
initial={reducedMotion ? false : { opacity: 0, y: -24, scale: 0.6 }}
|
|
157
185
|
animate={{
|
|
158
186
|
opacity: stacked ? 1 - index * 0.25 : 1,
|
|
159
|
-
y: stacked ? index * 9 : index * (
|
|
187
|
+
y: stacked ? index * 9 : index * (pill + 14) + index * 6,
|
|
160
188
|
scale: stacked ? 1 - index * 0.05 : 1,
|
|
161
189
|
}}
|
|
162
190
|
exit={
|
|
@@ -180,7 +208,7 @@ function ToastItem({
|
|
|
180
208
|
<motion.div
|
|
181
209
|
role="status"
|
|
182
210
|
className="pointer-events-auto relative"
|
|
183
|
-
initial={{ width:
|
|
211
|
+
initial={{ width: pill, height: pill }}
|
|
184
212
|
animate={{ width, height }}
|
|
185
213
|
transition={reducedMotion ? { duration: 0 } : BODY_SPRING}
|
|
186
214
|
>
|
|
@@ -194,21 +222,21 @@ function ToastItem({
|
|
|
194
222
|
>
|
|
195
223
|
<motion.span
|
|
196
224
|
className="absolute top-3 left-3 rounded-full bg-inverse shadow-xl"
|
|
197
|
-
style={{ width:
|
|
225
|
+
style={{ width: pill, height: pill }}
|
|
198
226
|
animate={{ scale: open ? 1 : 1.06 }}
|
|
199
227
|
transition={reducedMotion ? { duration: 0 } : spring.bouncy}
|
|
200
228
|
/>
|
|
201
229
|
<motion.span
|
|
202
230
|
className="absolute top-3 left-3 bg-inverse"
|
|
203
231
|
initial={{
|
|
204
|
-
width:
|
|
205
|
-
height:
|
|
206
|
-
borderRadius:
|
|
232
|
+
width: pill,
|
|
233
|
+
height: pill,
|
|
234
|
+
borderRadius: pill / 2,
|
|
207
235
|
}}
|
|
208
236
|
animate={{
|
|
209
237
|
width,
|
|
210
238
|
height,
|
|
211
|
-
borderRadius: Math.min(
|
|
239
|
+
borderRadius: Math.min(pill / 2, height / 2, 22),
|
|
212
240
|
}}
|
|
213
241
|
transition={reducedMotion ? { duration: 0 } : BODY_SPRING}
|
|
214
242
|
/>
|
|
@@ -217,8 +245,8 @@ function ToastItem({
|
|
|
217
245
|
{/* Content, crisp above the shapes. */}
|
|
218
246
|
<motion.div
|
|
219
247
|
ref={content}
|
|
220
|
-
className=
|
|
221
|
-
style={{ minHeight:
|
|
248
|
+
className={contentClasses[toast.size]}
|
|
249
|
+
style={{ minHeight: pill }}
|
|
222
250
|
animate={{ opacity: open ? 1 : 0 }}
|
|
223
251
|
transition={
|
|
224
252
|
reducedMotion
|
|
@@ -229,12 +257,12 @@ function ToastItem({
|
|
|
229
257
|
}
|
|
230
258
|
}
|
|
231
259
|
>
|
|
232
|
-
<ToneIcon tone={toast.tone} reducedMotion={reducedMotion} />
|
|
260
|
+
<ToneIcon tone={toast.tone} size={toast.size} reducedMotion={reducedMotion} />
|
|
233
261
|
<div className="flex min-w-0 flex-col py-px">
|
|
234
262
|
<AnimatePresence mode="popLayout" initial={false}>
|
|
235
263
|
<motion.span
|
|
236
264
|
key={toast.title}
|
|
237
|
-
className=
|
|
265
|
+
className={titleClasses[toast.size]}
|
|
238
266
|
initial={
|
|
239
267
|
reducedMotion
|
|
240
268
|
? false
|
|
@@ -264,7 +292,7 @@ function ToastItem({
|
|
|
264
292
|
</motion.span>
|
|
265
293
|
</AnimatePresence>
|
|
266
294
|
{toast.description && (
|
|
267
|
-
<span className=
|
|
295
|
+
<span className={descriptionClasses[toast.size]}>
|
|
268
296
|
{toast.description}
|
|
269
297
|
</span>
|
|
270
298
|
)}
|
|
@@ -276,7 +304,7 @@ function ToastItem({
|
|
|
276
304
|
toast.action?.onClick()
|
|
277
305
|
toastApi.dismiss(toast.id)
|
|
278
306
|
}}
|
|
279
|
-
className=
|
|
307
|
+
className={actionClasses[toast.size]}
|
|
280
308
|
>
|
|
281
309
|
{toast.action.label}
|
|
282
310
|
</button>
|
|
@@ -294,15 +322,20 @@ const toneColors: Record<ToastTone, string> = {
|
|
|
294
322
|
loading: "bg-on-inverse/15 text-on-inverse",
|
|
295
323
|
}
|
|
296
324
|
|
|
297
|
-
function ToneIcon({
|
|
325
|
+
function ToneIcon({
|
|
326
|
+
tone,
|
|
327
|
+
size,
|
|
328
|
+
reducedMotion,
|
|
329
|
+
}: {
|
|
330
|
+
tone: ToastTone
|
|
331
|
+
size: ToastSize
|
|
332
|
+
reducedMotion: boolean
|
|
333
|
+
}) {
|
|
298
334
|
return (
|
|
299
335
|
<AnimatePresence mode="popLayout" initial={false}>
|
|
300
336
|
<motion.span
|
|
301
337
|
key={tone}
|
|
302
|
-
className={cn(
|
|
303
|
-
"flex size-5 shrink-0 items-center justify-center rounded-full",
|
|
304
|
-
toneColors[tone]
|
|
305
|
-
)}
|
|
338
|
+
className={cn(toneIconClasses[size], toneColors[tone])}
|
|
306
339
|
initial={
|
|
307
340
|
reducedMotion
|
|
308
341
|
? false
|
|
@@ -24,7 +24,9 @@ import { cn } from "../../lib/class-names"
|
|
|
24
24
|
changes, which is what the reading and the colours need, and
|
|
25
25
|
the loop runs only while some bar still has energy. */
|
|
26
26
|
|
|
27
|
-
const H = 20 // a bar at rest, px
|
|
27
|
+
const H: Record<WakeSliderSize, number> = { sm: 16, md: 20, lg: 26 } // a bar at rest, px
|
|
28
|
+
|
|
29
|
+
export type WakeSliderSize = "sm" | "md" | "lg"
|
|
28
30
|
|
|
29
31
|
export interface WakeSliderProps {
|
|
30
32
|
label: string
|
|
@@ -36,6 +38,8 @@ export interface WakeSliderProps {
|
|
|
36
38
|
disabled?: boolean
|
|
37
39
|
className?: string
|
|
38
40
|
formatValue?: (value: number) => string
|
|
41
|
+
/** Bar height at rest and value text scale. @default "md" */
|
|
42
|
+
size?: WakeSliderSize
|
|
39
43
|
}
|
|
40
44
|
|
|
41
45
|
export function WakeSlider({
|
|
@@ -46,6 +50,7 @@ export function WakeSlider({
|
|
|
46
50
|
disabled = false,
|
|
47
51
|
className,
|
|
48
52
|
formatValue,
|
|
53
|
+
size = "md",
|
|
49
54
|
/* how many bars make the track */
|
|
50
55
|
bars = 36,
|
|
51
56
|
/* how high the wake rises, 0..100 */
|
|
@@ -144,7 +149,10 @@ export function WakeSlider({
|
|
|
144
149
|
const lit = Math.round((value / 100) * (n - 1))
|
|
145
150
|
|
|
146
151
|
return (
|
|
147
|
-
<div
|
|
152
|
+
<div
|
|
153
|
+
className={cn("passui-wake-slider not-typeset", disabled && "opacity-40", className)}
|
|
154
|
+
data-size={size}
|
|
155
|
+
>
|
|
148
156
|
<div
|
|
149
157
|
ref={track}
|
|
150
158
|
className="passui-wake-track"
|
|
@@ -156,7 +164,7 @@ export function WakeSlider({
|
|
|
156
164
|
aria-valuemin={0}
|
|
157
165
|
aria-valuemax={100}
|
|
158
166
|
aria-valuenow={value}
|
|
159
|
-
style={{ height: H * 2.6, "--wake-bars": n } as CSSProperties}
|
|
167
|
+
style={{ height: H[size] * 2.6, "--wake-bars": n } as CSSProperties}
|
|
160
168
|
onPointerDown={(e) => {
|
|
161
169
|
if (disabled || e.button !== 0) return
|
|
162
170
|
e.currentTarget.focus()
|
|
@@ -225,7 +233,7 @@ export function WakeSlider({
|
|
|
225
233
|
aria-hidden="true"
|
|
226
234
|
className="passui-wake-bar"
|
|
227
235
|
data-on={(value > 0 && i <= lit) || undefined}
|
|
228
|
-
style={{ height: H }}
|
|
236
|
+
style={{ height: H[size] }}
|
|
229
237
|
/>
|
|
230
238
|
))}
|
|
231
239
|
</div>
|
|
@@ -189,6 +189,10 @@
|
|
|
189
189
|
font-variant-numeric: tabular-nums;
|
|
190
190
|
color: var(--color-label-secondary);
|
|
191
191
|
}
|
|
192
|
+
.passui-wake-slider[data-size="sm"] { gap: 0.75rem; }
|
|
193
|
+
.passui-wake-slider[data-size="sm"] .passui-wake-value { font-size: var(--text-caption); }
|
|
194
|
+
.passui-wake-slider[data-size="lg"] { gap: 1.25rem; }
|
|
195
|
+
.passui-wake-slider[data-size="lg"] .passui-wake-value { font-size: var(--text-subheadline); }
|
|
192
196
|
@media (prefers-reduced-motion: reduce) {
|
|
193
197
|
.passui-wake-bar, .passui-search-pill { transition: none; }
|
|
194
198
|
.passui-search-pill:active { scale: 1; }
|