@passui/react 0.1.39 → 0.1.41

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.
Files changed (27) hide show
  1. package/dist/types/src/components/code-block/code-block.d.ts +5 -2
  2. package/dist/types/src/components/content-view/content-view.d.ts +5 -2
  3. package/dist/types/src/components/context-menu/context-menu-store.d.ts +7 -2
  4. package/dist/types/src/components/conversation/conversation.d.ts +7 -2
  5. package/dist/types/src/components/one-time-code/one-time-code.d.ts +4 -1
  6. package/dist/types/src/components/popover/popover.d.ts +3 -0
  7. package/dist/types/src/components/search-list/search-list.d.ts +4 -1
  8. package/dist/types/src/components/sidebar/sidebar.d.ts +8 -3
  9. package/dist/types/src/components/slosh-slider/slosh-slider.d.ts +4 -1
  10. package/dist/types/src/components/toast/toast-store.d.ts +5 -0
  11. package/dist/types/src/components/tooltip/tooltip-host.d.ts +1 -0
  12. package/package.json +1 -1
  13. package/src/components/code-block/code-block.tsx +7 -1
  14. package/src/components/content-view/content-view.tsx +60 -8
  15. package/src/components/context-menu/context-menu-host.tsx +44 -15
  16. package/src/components/context-menu/context-menu-store.ts +10 -1
  17. package/src/components/conversation/conversation.tsx +93 -19
  18. package/src/components/one-time-code/one-time-code.tsx +22 -2
  19. package/src/components/popover/popover.tsx +15 -2
  20. package/src/components/search-list/search-list.tsx +27 -9
  21. package/src/components/sidebar/sidebar.tsx +38 -11
  22. package/src/components/slosh-slider/slosh-slider.tsx +6 -0
  23. package/src/components/toast/toast-store.ts +7 -0
  24. package/src/components/toast/toaster.tsx +56 -23
  25. package/src/components/tooltip/tooltip-host.tsx +14 -1
  26. package/src/styles/code-block.css +12 -0
  27. package/src/styles/motion-controls.css +8 -0
@@ -1,10 +1,13 @@
1
1
  import { type ComponentProps, type ReactNode } from "react";
2
- export interface CodeBlockProps extends Omit<ComponentProps<"div">, "children"> {
2
+ export type CodeBlockSize = "sm" | "md" | "lg";
3
+ export interface CodeBlockProps extends Omit<ComponentProps<"div">, "children" | "size"> {
3
4
  code: string;
4
5
  /** Supply a highlighter's rendered pre/code element; source remains the fallback. */
5
6
  children?: ReactNode;
6
7
  /** Show a nonselectable line gutter. @default true */
7
8
  lineNumbers?: boolean;
9
+ /** Code font size, padding and line gutter scale. @default "md" */
10
+ size?: CodeBlockSize;
8
11
  }
9
12
  /** Code presentation without shipping a highlighter or formatter to consumers. */
10
- export declare function CodeBlock({ code, children, lineNumbers, className, ...props }: CodeBlockProps): import("react").JSX.Element;
13
+ export declare function CodeBlock({ code, children, lineNumbers, size, className, ...props }: CodeBlockProps): import("react").JSX.Element;
@@ -1,8 +1,11 @@
1
- import type { ComponentPropsWithoutRef, PropsWithChildren, ReactNode } from "react";
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,6 +1,7 @@
1
1
  import { type InputHTMLAttributes, type Ref } from "react";
2
2
  export type OneTimeCodeStatus = "idle" | "verifying" | "success" | "error";
3
3
  export type OneTimeCodeFormat = "numeric" | "alphanumeric";
4
+ export type OneTimeCodeSize = "sm" | "md" | "lg";
4
5
  export interface OneTimeCodeProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "value" | "defaultValue" | "onChange" | "type" | "maxLength" | "size"> {
5
6
  label: string;
6
7
  value: string;
@@ -16,8 +17,10 @@ export interface OneTimeCodeProps extends Omit<InputHTMLAttributes<HTMLInputElem
16
17
  status?: OneTimeCodeStatus;
17
18
  /** Localized verification feedback announced without reading decorative cells. */
18
19
  statusLabel?: string;
20
+ /** Cell height, digit text scale and check icon size. @default "md" */
21
+ size?: OneTimeCodeSize;
19
22
  }
20
23
  /** Normalize common numeric keyboards and optional ASCII letters in one native field. */
21
24
  export declare function normalizeCode(value: string, length: number, format?: OneTimeCodeFormat): string;
22
25
  /** Six cells, one field: paste, autofill, selection and Backspace stay native. */
23
- export declare function OneTimeCode({ label, value, onChange, onComplete, length, format, ref, status, statusLabel, className, disabled, id: suppliedId, ...props }: OneTimeCodeProps): import("react").JSX.Element;
26
+ export declare function OneTimeCode({ label, value, onChange, onComplete, length, format, ref, status, statusLabel, size, className, disabled, id: suppliedId, ...props }: OneTimeCodeProps): import("react").JSX.Element;
@@ -1,6 +1,7 @@
1
1
  import { type HTMLMotionProps } from "motion/react";
2
2
  import { type ReactNode, type RefObject } from "react";
3
3
  export type PopoverOrigin = "top-left" | "top-right";
4
+ export type PopoverSize = "sm" | "md";
4
5
  export interface PopoverProps extends Omit<HTMLMotionProps<"div">, "children"> {
5
6
  open: boolean;
6
7
  children?: ReactNode;
@@ -8,6 +9,8 @@ export interface PopoverProps extends Omit<HTMLMotionProps<"div">, "children"> {
8
9
  origin?: PopoverOrigin;
9
10
  /** Trigger whose shape the popover uses during its opening and closing motion. */
10
11
  anchorRef?: RefObject<HTMLElement | null>;
12
+ /** Surface corner radius. @default "md" */
13
+ size?: PopoverSize;
11
14
  }
12
15
  /**
13
16
  * A glass surface that springs open from its anchor. Position it with
@@ -1,7 +1,10 @@
1
1
  import { type ComponentProps } from "react";
2
- export declare function SearchList({ loop, open, onKeyDown, className, ...props }: ComponentProps<"div"> & {
2
+ export type SearchListSize = "sm" | "md" | "lg";
3
+ export declare function SearchList({ loop, open, size, onKeyDown, className, ...props }: ComponentProps<"div"> & {
3
4
  loop?: boolean;
4
5
  open?: boolean;
6
+ /** Field, item and heading density. @default "md" */
7
+ size?: SearchListSize;
5
8
  }): import("react").JSX.Element;
6
9
  export declare function SearchListField({ value, onValueChange, className, ...props }: Omit<ComponentProps<"input">, "value" | "onChange"> & {
7
10
  value?: string;
@@ -12,8 +12,13 @@ export interface SidebarItemContentProps {
12
12
  active?: boolean;
13
13
  children: ReactNode;
14
14
  }
15
- export declare function SidebarSection({ title, children }: SidebarSectionProps): import("react").JSX.Element;
15
+ export declare function SidebarSection({ title, children, size, }: SidebarSectionProps & {
16
+ size?: SidebarSize;
17
+ }): import("react").JSX.Element;
16
18
  /** Classes for a sidebar row; apply to the app's router link. */
17
19
  export type SidebarDensity = "regular" | "compact";
18
- export declare function sidebarItemClass(active?: boolean, density?: SidebarDensity): string;
19
- export declare function SidebarItemContent({ icon, count, active, children }: SidebarItemContentProps): import("react").JSX.Element;
20
+ export type SidebarSize = "sm" | "md" | "lg";
21
+ export declare function sidebarItemClass(active?: boolean, density?: SidebarDensity, size?: SidebarSize): string;
22
+ export declare function SidebarItemContent({ icon, count, active, size, children, }: SidebarItemContentProps & {
23
+ size?: SidebarSize;
24
+ }): import("react").JSX.Element;
@@ -1,3 +1,4 @@
1
+ export type SloshSliderSize = "sm" | "md" | "lg";
1
2
  export interface SloshSliderProps {
2
3
  label: string;
3
4
  value?: number;
@@ -11,8 +12,10 @@ export interface SloshSliderProps {
11
12
  tilt?: number;
12
13
  /** Track corner, from square to a pill, 0–20. @default 20 */
13
14
  corner?: number;
15
+ /** Track height and knob scale. @default "md" */
16
+ size?: SloshSliderSize;
14
17
  disabled?: boolean;
15
18
  className?: string;
16
19
  formatValue?: (value: number) => string;
17
20
  }
18
- export declare function SloshSlider({ label, value, defaultValue, onChange, viscosity, momentum, tilt, corner, disabled, className, formatValue, }: SloshSliderProps): import("react").JSX.Element;
21
+ export declare function SloshSlider({ label, value, defaultValue, onChange, viscosity, momentum, tilt, corner, size, disabled, className, formatValue, }: SloshSliderProps): import("react").JSX.Element;
@@ -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 TooltipSize = "sm" | "md" | "lg";
1
2
  /**
2
3
  * Tooltips for any element with `data-tooltip`, on devices that can hover.
3
4
  * One listener for the whole app: a short delay before the first, then
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@passui/react",
3
- "version": "0.1.39",
3
+ "version": "0.1.41",
4
4
  "description": "Passui: native React controls, design tokens, presentation and appearance.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1,12 +1,16 @@
1
1
  import { Fragment, type ComponentProps, type ReactNode } from "react"
2
2
  import { cn } from "../../lib/class-names"
3
3
 
4
- export interface CodeBlockProps extends Omit<ComponentProps<"div">, "children"> {
4
+ export type CodeBlockSize = "sm" | "md" | "lg"
5
+
6
+ export interface CodeBlockProps extends Omit<ComponentProps<"div">, "children" | "size"> {
5
7
  code: string
6
8
  /** Supply a highlighter's rendered pre/code element; source remains the fallback. */
7
9
  children?: ReactNode
8
10
  /** Show a nonselectable line gutter. @default true */
9
11
  lineNumbers?: boolean
12
+ /** Code font size, padding and line gutter scale. @default "md" */
13
+ size?: CodeBlockSize
10
14
  }
11
15
 
12
16
  /** Code presentation without shipping a highlighter or formatter to consumers. */
@@ -14,6 +18,7 @@ export function CodeBlock({
14
18
  code,
15
19
  children,
16
20
  lineNumbers = true,
21
+ size = "md",
17
22
  className,
18
23
  ...props
19
24
  }: CodeBlockProps) {
@@ -23,6 +28,7 @@ export function CodeBlock({
23
28
  {...props}
24
29
  className={cn("passui-code-block", className)}
25
30
  data-line-numbers={lineNumbers || undefined}
31
+ data-size={size}
26
32
  >
27
33
  {children ?? (
28
34
  <pre>
@@ -1,11 +1,49 @@
1
- import type { ComponentPropsWithoutRef, PropsWithChildren, ReactNode } from "react"
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
- <section className={cn(variants[variant], className)} {...props}>
47
- {children}
48
- </section>
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 gap-3", className)} {...props}>
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 gap-3", actionClassName)}>
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("text-callout tracking-tighter font-light", className)} {...props}>
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("text-subheadline text-label", className)} {...props}>
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 OpenMenu,
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 min-w-50 max-w-[min(280px,calc(100vw-24px))] flex-col rounded-3xl p-1.5 outline-none",
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
- "flex items-center gap-2.5 rounded-lg px-2.5 text-start text-subheadline transition-colors disabled:opacity-40",
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="flex w-4 shrink-0 justify-center text-label-secondary">
400
+ <span className={iconSlotSizes[menu.size]}>
372
401
  {item.checked ? (
373
402
  <CheckIcon
374
- size={15}
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,