@passui/react 0.1.45 → 0.1.46

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 (140) hide show
  1. package/README.md +57 -46
  2. package/dist/types/src/components/accordion/accordion.d.ts +1 -1
  3. package/dist/types/src/components/breadcrumb/breadcrumb.d.ts +1 -1
  4. package/dist/types/src/components/inline-disclosure/inline-disclosure.d.ts +1 -1
  5. package/dist/types/src/components/popover/anchored-surface.d.ts +5 -1
  6. package/dist/types/src/components/popover/index.d.ts +41 -0
  7. package/dist/types/src/components/popover/popover-content.d.ts +32 -0
  8. package/dist/types/src/components/popover/popover-root.d.ts +22 -0
  9. package/dist/types/src/components/popover/popover-trigger.d.ts +23 -0
  10. package/dist/types/src/components/stepper/rolling-digit.d.ts +1 -1
  11. package/dist/types/src/components/table/table.d.ts +1 -1
  12. package/dist/types/src/components/view-controller/view-controller.d.ts +2 -2
  13. package/dist/types/src/index.d.ts +1 -1
  14. package/dist/types/src/machines/popover.machine.d.ts +30 -0
  15. package/package.json +307 -307
  16. package/src/appearance/index.ts +2 -2
  17. package/src/appearance/platform.tsx +76 -60
  18. package/src/appearance/theme.tsx +409 -377
  19. package/src/components/accordion/accordion.tsx +58 -49
  20. package/src/components/ambient/ambient.tsx +25 -25
  21. package/src/components/ambient/index.ts +1 -1
  22. package/src/components/aspect-ratio/aspect-ratio.tsx +21 -16
  23. package/src/components/breadcrumb/breadcrumb.tsx +63 -57
  24. package/src/components/button/button-styles.ts +39 -32
  25. package/src/components/button/button.tsx +91 -84
  26. package/src/components/button/icon-button.tsx +27 -25
  27. package/src/components/button/pending-spinner.tsx +8 -8
  28. package/src/components/button/use-pending-indicator.ts +11 -11
  29. package/src/components/chip/chip.tsx +35 -30
  30. package/src/components/code-block/code-block.tsx +40 -39
  31. package/src/components/combo-box/combo-box.tsx +5 -5
  32. package/src/components/command/command.tsx +124 -94
  33. package/src/components/compact-date-picker/calendar-month.ts +75 -53
  34. package/src/components/compact-date-picker/compact-date-picker.tsx +308 -289
  35. package/src/components/compact-time-picker/compact-time-picker.tsx +288 -281
  36. package/src/components/compact-time-picker/time-value.ts +32 -29
  37. package/src/components/content-unavailable-view/content-unavailable-view.tsx +64 -48
  38. package/src/components/content-view/content-view.tsx +168 -159
  39. package/src/components/content-view/index.ts +1 -1
  40. package/src/components/context-menu/conceal-menu-source.ts +25 -25
  41. package/src/components/context-menu/context-menu-host.tsx +439 -402
  42. package/src/components/context-menu/context-menu-store.ts +125 -123
  43. package/src/components/context-menu/menu-navigation.ts +25 -18
  44. package/src/components/context-menu/menu-position.ts +33 -29
  45. package/src/components/context-menu/menu-reveal.ts +32 -32
  46. package/src/components/context-menu/use-context-menu.ts +63 -56
  47. package/src/components/conversation/conversation.tsx +265 -263
  48. package/src/components/conversation/index.ts +1 -1
  49. package/src/components/date-picker/date-picker.tsx +95 -94
  50. package/src/components/date-picker/date-value.ts +20 -10
  51. package/src/components/dialog/dialog-host.tsx +105 -103
  52. package/src/components/dialog/dialog-store.ts +37 -36
  53. package/src/components/dialog/dialog.tsx +192 -179
  54. package/src/components/dialog/index.ts +1 -1
  55. package/src/components/disclosure-group/disclosure-group.tsx +93 -86
  56. package/src/components/divider/divider.tsx +24 -22
  57. package/src/components/inline-disclosure/inline-disclosure.tsx +40 -36
  58. package/src/components/keyboard-shortcut/keyboard-shortcut.tsx +23 -19
  59. package/src/components/labeled-content/labeled-content.tsx +68 -68
  60. package/src/components/locale-switcher/index.ts +1 -1
  61. package/src/components/locale-switcher/locale-switcher.tsx +279 -251
  62. package/src/components/menu/menu.tsx +75 -58
  63. package/src/components/morph/index.ts +2 -2
  64. package/src/components/morph/morph.tsx +227 -201
  65. package/src/components/morph/use-morph-disclosure.ts +19 -19
  66. package/src/components/one-time-code/one-time-code.tsx +216 -200
  67. package/src/components/picker/picker-trigger.ts +16 -13
  68. package/src/components/picker/picker.tsx +371 -345
  69. package/src/components/popover/anchored-surface.tsx +253 -229
  70. package/src/components/popover/index.ts +50 -0
  71. package/src/components/popover/popover-content.tsx +334 -0
  72. package/src/components/popover/popover-fit.ts +58 -53
  73. package/src/components/popover/popover-root.tsx +97 -0
  74. package/src/components/popover/popover-trigger.tsx +71 -0
  75. package/src/components/profile-image/profile-image.tsx +80 -64
  76. package/src/components/progress-view/progress-view.tsx +61 -59
  77. package/src/components/progress-view/use-liquid-progress.ts +38 -38
  78. package/src/components/radio-group/radio-group.tsx +71 -68
  79. package/src/components/search-field/expanding-search-field.tsx +191 -177
  80. package/src/components/search-field/search-field.tsx +59 -50
  81. package/src/components/search-list/search-list.tsx +246 -236
  82. package/src/components/section/section.tsx +58 -58
  83. package/src/components/secure-field/secure-field.tsx +3 -3
  84. package/src/components/segmented-control/segmented-control.tsx +136 -136
  85. package/src/components/sheet/modal-environment.ts +71 -64
  86. package/src/components/sheet/sheet-portal.tsx +58 -53
  87. package/src/components/sheet/sheet.tsx +146 -141
  88. package/src/components/sheet/use-sheet-focus.ts +97 -82
  89. package/src/components/sidebar/sidebar.tsx +78 -78
  90. package/src/components/skeleton/skeleton.tsx +30 -21
  91. package/src/components/slider/slider.tsx +113 -104
  92. package/src/components/slosh-slider/liquid-step.ts +32 -31
  93. package/src/components/slosh-slider/slosh-slider.tsx +265 -228
  94. package/src/components/stat-tile/index.ts +1 -1
  95. package/src/components/stat-tile/stat-tile.tsx +79 -68
  96. package/src/components/status-indicator/status-indicator.tsx +40 -37
  97. package/src/components/stepper/rolling-digit.tsx +82 -68
  98. package/src/components/stepper/stepper.tsx +122 -113
  99. package/src/components/swatch-picker/swatch-picker.tsx +120 -120
  100. package/src/components/tab-bar/tab-bar.tsx +149 -148
  101. package/src/components/tab-view/tab-view.tsx +140 -137
  102. package/src/components/table/table.tsx +117 -98
  103. package/src/components/text-editor/text-editor.tsx +34 -29
  104. package/src/components/text-field/text-field.tsx +182 -180
  105. package/src/components/toast/toast-store.ts +127 -115
  106. package/src/components/toast/toaster.tsx +361 -347
  107. package/src/components/toggle/toggle.tsx +297 -282
  108. package/src/components/tooltip/tooltip-host.tsx +157 -150
  109. package/src/components/view-controller/index.ts +1 -1
  110. package/src/components/view-controller/view-controller.tsx +64 -53
  111. package/src/components/wake-slider/wake-slider.tsx +220 -205
  112. package/src/hooks/scroll/minimize-decision.ts +11 -7
  113. package/src/hooks/use-anchored-disclosure.ts +19 -19
  114. package/src/hooks/use-anchored-placement.ts +29 -25
  115. package/src/hooks/use-dismiss.ts +33 -29
  116. package/src/hooks/use-floating-dismiss.ts +24 -22
  117. package/src/hooks/use-media-query.ts +67 -60
  118. package/src/hooks/use-minimize-on-scroll.ts +69 -60
  119. package/src/hooks/use-visual-viewport.ts +63 -63
  120. package/src/hooks/viewport/types.ts +16 -16
  121. package/src/hooks/viewport/viewport-metrics.ts +16 -10
  122. package/src/icons/icons.tsx +440 -328
  123. package/src/index.ts +91 -91
  124. package/src/lib/anchored-placement.ts +70 -65
  125. package/src/lib/class-names.ts +14 -13
  126. package/src/lib/clipboard.ts +8 -7
  127. package/src/lib/date.ts +61 -60
  128. package/src/lib/floating-layers.ts +39 -24
  129. package/src/lib/haptics.ts +10 -10
  130. package/src/lib/language.ts +9 -9
  131. package/src/lib/range-value.ts +27 -21
  132. package/src/lib/screen-edges.ts +33 -30
  133. package/src/lib/shapes.ts +38 -34
  134. package/src/machines/popover.machine.ts +51 -0
  135. package/src/motion/transitions.ts +46 -43
  136. package/src/styles/code-block.css +69 -43
  137. package/src/styles/motion-controls.css +393 -191
  138. package/src/tokens.ts +1 -1
  139. package/dist/types/src/components/popover/popover.d.ts +0 -21
  140. package/src/components/popover/popover.tsx +0 -334
@@ -1,76 +1,78 @@
1
- import type { ReactNode } from "react"
2
- import type { MenuRect } from "./menu-reveal"
1
+ import type { ReactNode } from "react";
2
+ import type { MenuRect } from "./menu-reveal";
3
3
 
4
- export type ContextMenuAlignX = "start" | "end"
5
- export type ContextMenuAlignY = "top" | "bottom"
6
- export type ContextMenuEdge = "leading" | "trailing"
7
- export type ContextMenuSize = "sm" | "md" | "lg"
4
+ export type ContextMenuAlignX = "start" | "end";
5
+ export type ContextMenuAlignY = "top" | "bottom";
6
+ export type ContextMenuEdge = "leading" | "trailing";
7
+ export type ContextMenuSize = "sm" | "md" | "lg";
8
8
 
9
9
  /** One row of a context menu, or a divider between groups of rows. */
10
10
  export type ContextMenuItem =
11
- | {
12
- label: string
13
- icon?: ReactNode
14
- onSelect: () => void
15
- destructive?: boolean
16
- disabled?: boolean
17
- /** Marks the current choice, e.g. a note's kind. */
18
- checked?: boolean
19
- }
20
- | { heading: string }
21
- | "divider"
11
+ | {
12
+ label: string;
13
+ icon?: ReactNode;
14
+ onSelect: () => void;
15
+ destructive?: boolean;
16
+ disabled?: boolean;
17
+ /** Marks the current choice, e.g. a note's kind. */
18
+ checked?: boolean;
19
+ }
20
+ | { heading: string }
21
+ | "divider";
22
22
 
23
23
  export interface OpenMenu {
24
- id: number
25
- anchor: Element | null
26
- x: number
27
- y: number
28
- items: ContextMenuItem[]
29
- /** Opened by long-press: bigger rows for fingers. */
30
- touch: boolean
31
- /** Surface, item and heading density. @default "md" */
32
- size: ContextMenuSize
33
- /** Which edge of the menu sits at `x`: its left (`start`) or its right (`end`). */
34
- alignX: ContextMenuAlignX
35
- /** Which edge of the menu sits at `y`: its top (`top`) or its bottom (`bottom`). */
36
- alignY: ContextMenuAlignY
37
- source?: MenuRect
38
- toolbar?: HTMLElement
39
- toolbarRect?: MenuRect
40
- restoreFocus?: boolean
24
+ id: number;
25
+ anchor: Element | null;
26
+ x: number;
27
+ y: number;
28
+ items: ContextMenuItem[];
29
+ /** Opened by long-press: bigger rows for fingers. */
30
+ touch: boolean;
31
+ /** Surface, item and heading density. @default "md" */
32
+ size: ContextMenuSize;
33
+ /** Which edge of the menu sits at `x`: its left (`start`) or its right (`end`). */
34
+ alignX: ContextMenuAlignX;
35
+ /** Which edge of the menu sits at `y`: its top (`top`) or its bottom (`bottom`). */
36
+ alignY: ContextMenuAlignY;
37
+ source?: MenuRect;
38
+ toolbar?: HTMLElement;
39
+ toolbarRect?: MenuRect;
40
+ restoreFocus?: boolean;
41
41
  }
42
42
 
43
- let menu: OpenMenu | null = null
43
+ let menu: OpenMenu | null = null;
44
44
  /** The menu shown most recently, kept after it closes so a follow-up can stand in its place. */
45
- let last: OpenMenu | null = null
46
- let nextId = 1
47
- const listeners = new Set<() => void>()
45
+ let last: OpenMenu | null = null;
46
+ let nextId = 1;
47
+ const listeners = new Set<() => void>();
48
48
  const emit = () => {
49
- for (const listener of listeners) listener()
50
- }
49
+ for (const listener of listeners) listener();
50
+ };
51
51
 
52
52
  /** Opens a menu at a point, such as where a pointer was pressed. */
53
53
  export function openContextMenu(
54
- x: number,
55
- y: number,
56
- items: ContextMenuItem[],
57
- touch = false,
58
- anchor: Element | null = typeof document === "undefined" ? null : document.activeElement,
59
- size: ContextMenuSize = "md"
54
+ x: number,
55
+ y: number,
56
+ items: ContextMenuItem[],
57
+ touch = false,
58
+ anchor: Element | null = typeof document === "undefined"
59
+ ? null
60
+ : document.activeElement,
61
+ size: ContextMenuSize = "md",
60
62
  ) {
61
- menu = {
62
- id: nextId++,
63
- anchor,
64
- x,
65
- y,
66
- items,
67
- touch,
68
- size,
69
- alignX: "start",
70
- alignY: "top",
71
- }
72
- last = menu
73
- emit()
63
+ menu = {
64
+ id: nextId++,
65
+ anchor,
66
+ x,
67
+ y,
68
+ items,
69
+ touch,
70
+ size,
71
+ alignX: "start",
72
+ alignY: "top",
73
+ };
74
+ last = menu;
75
+ emit();
74
76
  }
75
77
 
76
78
  /**
@@ -79,10 +81,10 @@ export function openContextMenu(
79
81
  * first menu instead of making it tall.
80
82
  */
81
83
  export function openFollowUpMenu(items: ContextMenuItem[]) {
82
- if (!last) return
83
- menu = { ...last, id: nextId++, items, source: undefined }
84
- last = menu
85
- emit()
84
+ if (!last) return;
85
+ menu = { ...last, id: nextId++, items, source: undefined };
86
+ last = menu;
87
+ emit();
86
88
  }
87
89
 
88
90
  /**
@@ -92,74 +94,74 @@ export function openFollowUpMenu(items: ContextMenuItem[]) {
92
94
  * The menu's real size decides where it goes; nothing is guessed.
93
95
  */
94
96
  export function openMenu(
95
- anchor: Element,
96
- items: ContextMenuItem[],
97
- {
98
- edge = "leading",
99
- above = false,
100
- toolbar,
101
- size = "md",
102
- }: {
103
- edge?: ContextMenuEdge
104
- above?: boolean
105
- /** Replace this action group with a horizontally expanding menu surface. */
106
- toolbar?: HTMLElement
107
- /** Surface, item and heading density. @default "md" */
108
- size?: ContextMenuSize
109
- } = {}
97
+ anchor: Element,
98
+ items: ContextMenuItem[],
99
+ {
100
+ edge = "leading",
101
+ above = false,
102
+ toolbar,
103
+ size = "md",
104
+ }: {
105
+ edge?: ContextMenuEdge;
106
+ above?: boolean;
107
+ /** Replace this action group with a horizontally expanding menu surface. */
108
+ toolbar?: HTMLElement;
109
+ /** Surface, item and heading density. @default "md" */
110
+ size?: ContextMenuSize;
111
+ } = {},
110
112
  ) {
111
- const rect = anchor.getBoundingClientRect()
112
- const toolbarRect = toolbar?.getBoundingClientRect()
113
- const rtl = getComputedStyle(anchor).direction === "rtl"
114
- // Trailing is the right edge in left-to-right text, the left edge in right-to-left.
115
- const right = (edge === "trailing") !== rtl
116
- menu = {
117
- id: nextId++,
118
- anchor,
119
- x: right ? (toolbarRect ?? rect).right : (toolbarRect ?? rect).left,
120
- y: toolbar ? rect.top : above ? rect.bottom : rect.top,
121
- items,
122
- touch: false,
123
- size,
124
- alignX: right ? "end" : "start",
125
- alignY: above ? "bottom" : "top",
126
- toolbar,
127
- toolbarRect,
128
- source: {
129
- left: rect.left,
130
- top: rect.top,
131
- width: rect.width,
132
- height: rect.height,
133
- },
134
- }
135
- last = menu
136
- emit()
113
+ const rect = anchor.getBoundingClientRect();
114
+ const toolbarRect = toolbar?.getBoundingClientRect();
115
+ const rtl = getComputedStyle(anchor).direction === "rtl";
116
+ // Trailing is the right edge in left-to-right text, the left edge in right-to-left.
117
+ const right = (edge === "trailing") !== rtl;
118
+ menu = {
119
+ id: nextId++,
120
+ anchor,
121
+ x: right ? (toolbarRect ?? rect).right : (toolbarRect ?? rect).left,
122
+ y: toolbar ? rect.top : above ? rect.bottom : rect.top,
123
+ items,
124
+ touch: false,
125
+ size,
126
+ alignX: right ? "end" : "start",
127
+ alignY: above ? "bottom" : "top",
128
+ toolbar,
129
+ toolbarRect,
130
+ source: {
131
+ left: rect.left,
132
+ top: rect.top,
133
+ width: rect.width,
134
+ height: rect.height,
135
+ },
136
+ };
137
+ last = menu;
138
+ emit();
137
139
  }
138
140
 
139
141
  export function closeContextMenu(restoreFocus = true) {
140
- if (!menu) return
141
- const anchor = menu.anchor
142
- const toolbar = menu.toolbar
143
- menu.restoreFocus = restoreFocus
144
- menu = null
145
- if (
146
- restoreFocus &&
147
- !toolbar &&
148
- typeof HTMLElement !== "undefined" &&
149
- anchor instanceof HTMLElement &&
150
- anchor.isConnected
151
- )
152
- anchor.focus()
153
- emit()
142
+ if (!menu) return;
143
+ const anchor = menu.anchor;
144
+ const toolbar = menu.toolbar;
145
+ menu.restoreFocus = restoreFocus;
146
+ menu = null;
147
+ if (
148
+ restoreFocus &&
149
+ !toolbar &&
150
+ typeof HTMLElement !== "undefined" &&
151
+ anchor instanceof HTMLElement &&
152
+ anchor.isConnected
153
+ )
154
+ anchor.focus();
155
+ emit();
154
156
  }
155
157
 
156
158
  export function subscribeContextMenu(listener: () => void) {
157
- listeners.add(listener)
158
- return () => {
159
- listeners.delete(listener)
160
- }
159
+ listeners.add(listener);
160
+ return () => {
161
+ listeners.delete(listener);
162
+ };
161
163
  }
162
164
 
163
165
  export function getContextMenu(): OpenMenu | null {
164
- return menu
166
+ return menu;
165
167
  }
@@ -1,29 +1,36 @@
1
- export type MenuNavigationKey = "ArrowDown" | "ArrowUp" | "Home" | "End"
1
+ export type MenuNavigationKey = "ArrowDown" | "ArrowUp" | "Home" | "End";
2
2
 
3
3
  export function isMenuNavigationKey(key: string): key is MenuNavigationKey {
4
- return key === "ArrowDown" || key === "ArrowUp" || key === "Home" || key === "End"
4
+ return (
5
+ key === "ArrowDown" || key === "ArrowUp" || key === "Home" || key === "End"
6
+ );
5
7
  }
6
8
 
7
9
  /** Index of the next enabled action in a menu, with wrapping arrow navigation. */
8
- export function nextMenuIndex(key: MenuNavigationKey, current: number, count: number): number {
9
- if (count <= 0) return -1
10
- if (key === "Home") return 0
11
- if (key === "End") return count - 1
12
- if (key === "ArrowDown") return (current + 1) % count
13
- return current < 0 ? count - 1 : (current - 1 + count) % count
10
+ export function nextMenuIndex(
11
+ key: MenuNavigationKey,
12
+ current: number,
13
+ count: number,
14
+ ): number {
15
+ if (count <= 0) return -1;
16
+ if (key === "Home") return 0;
17
+ if (key === "End") return count - 1;
18
+ if (key === "ArrowDown") return (current + 1) % count;
19
+ return current < 0 ? count - 1 : (current - 1 + count) % count;
14
20
  }
15
21
 
16
22
  /** Finds a matching label after the current action, wrapping at the end. */
17
23
  export function findMenuTypeaheadIndex(
18
- labels: ReadonlyArray<string>,
19
- query: string,
20
- current: number
24
+ labels: ReadonlyArray<string>,
25
+ query: string,
26
+ current: number,
21
27
  ): number {
22
- if (!labels.length || !query.trim()) return -1
23
- const needle = query.toLocaleLowerCase()
24
- for (let offset = 1; offset <= labels.length; offset++) {
25
- const index = (current + offset + labels.length) % labels.length
26
- if (labels[index]?.trim().toLocaleLowerCase().startsWith(needle)) return index
27
- }
28
- return -1
28
+ if (!labels.length || !query.trim()) return -1;
29
+ const needle = query.toLocaleLowerCase();
30
+ for (let offset = 1; offset <= labels.length; offset++) {
31
+ const index = (current + offset + labels.length) % labels.length;
32
+ if (labels[index]?.trim().toLocaleLowerCase().startsWith(needle))
33
+ return index;
34
+ }
35
+ return -1;
29
36
  }
@@ -1,34 +1,38 @@
1
- import type { OpenMenu } from "./context-menu-store"
2
- import type { MenuRect } from "./menu-reveal"
1
+ import type { OpenMenu } from "./context-menu-store";
2
+ import type { MenuRect } from "./menu-reveal";
3
3
 
4
- type MenuAnchor = Pick<OpenMenu, "x" | "y" | "alignX" | "alignY">
5
- type MenuSize = Pick<MenuRect, "width" | "height">
4
+ type MenuAnchor = Pick<OpenMenu, "x" | "y" | "alignX" | "alignY">;
5
+ type MenuSize = Pick<MenuRect, "width" | "height">;
6
6
  type MenuBounds = {
7
- left: number
8
- top: number
9
- right: number
10
- bottom: number
11
- }
7
+ left: number;
8
+ top: number;
9
+ right: number;
10
+ bottom: number;
11
+ };
12
12
 
13
13
  /** Fit within the current visible viewport, including a keyboard or Safari pan. */
14
- export function menuPosition(menu: MenuAnchor, size: MenuSize, bounds: MenuBounds) {
15
- const { width, height } = size
16
- const { left, top, right, bottom } = bounds
17
- const fitsAfterX = menu.x + width <= right
18
- const fitsBeforeX = menu.x - width >= left
19
- const endX = menu.alignX === "end" ? fitsBeforeX || !fitsAfterX : !fitsAfterX
20
- const fitsBelow = menu.y + height <= bottom
21
- const fitsAbove = menu.y - height >= top
22
- const endY = menu.alignY === "bottom" ? fitsAbove || !fitsBelow : !fitsBelow
23
- const clamp = (value: number, min: number, max: number) =>
24
- Math.min(Math.max(value, min), Math.max(min, max))
25
- return {
26
- left: clamp(endX ? menu.x - width : menu.x, left, right - width),
27
- top: clamp(endY ? menu.y - height : menu.y, top, bottom - height),
28
- originX: endX ? 1 : 0,
29
- originY: endY ? 1 : 0,
30
- maxHeight: Math.max(0, bottom - top),
31
- width,
32
- height: Math.min(height, Math.max(0, bottom - top)),
33
- }
14
+ export function menuPosition(
15
+ menu: MenuAnchor,
16
+ size: MenuSize,
17
+ bounds: MenuBounds,
18
+ ) {
19
+ const { width, height } = size;
20
+ const { left, top, right, bottom } = bounds;
21
+ const fitsAfterX = menu.x + width <= right;
22
+ const fitsBeforeX = menu.x - width >= left;
23
+ const endX = menu.alignX === "end" ? fitsBeforeX || !fitsAfterX : !fitsAfterX;
24
+ const fitsBelow = menu.y + height <= bottom;
25
+ const fitsAbove = menu.y - height >= top;
26
+ const endY = menu.alignY === "bottom" ? fitsAbove || !fitsBelow : !fitsBelow;
27
+ const clamp = (value: number, min: number, max: number) =>
28
+ Math.min(Math.max(value, min), Math.max(min, max));
29
+ return {
30
+ left: clamp(endX ? menu.x - width : menu.x, left, right - width),
31
+ top: clamp(endY ? menu.y - height : menu.y, top, bottom - height),
32
+ originX: endX ? 1 : 0,
33
+ originY: endY ? 1 : 0,
34
+ maxHeight: Math.max(0, bottom - top),
35
+ width,
36
+ height: Math.min(height, Math.max(0, bottom - top)),
37
+ };
34
38
  }
@@ -1,42 +1,42 @@
1
1
  export type MenuRect = {
2
- left: number
3
- top: number
4
- width: number
5
- height: number
6
- }
2
+ left: number;
3
+ top: number;
4
+ width: number;
5
+ height: number;
6
+ };
7
7
 
8
8
  /** Return toward the source while handing its material back to the real trigger. */
9
9
  export function menuSurfaceExit(source: ReturnType<typeof menuSurfaceReveal>) {
10
- return { ...source, opacity: 0 }
10
+ return { ...source, opacity: 0 };
11
11
  }
12
12
 
13
13
  /** Animate the material's bounds independently so menu text never stretches. */
14
14
  export function menuSurfaceReveal(
15
- source: MenuRect | undefined,
16
- target: MenuRect & { radius?: number }
15
+ source: MenuRect | undefined,
16
+ target: MenuRect & { radius?: number },
17
17
  ) {
18
- if (
19
- !source ||
20
- source.width <= 0 ||
21
- source.height <= 0 ||
22
- target.width <= 0 ||
23
- target.height <= 0
24
- ) {
25
- return {
26
- x: 0,
27
- y: -4,
28
- width: target.width,
29
- height: target.height,
30
- opacity: 0,
31
- borderRadius: target.radius ?? 0,
32
- }
33
- }
34
- return {
35
- x: source.left - target.left,
36
- y: source.top - target.top,
37
- width: source.width,
38
- height: source.height,
39
- opacity: 1,
40
- borderRadius: Math.min(source.width, source.height) / 2,
41
- }
18
+ if (
19
+ !source ||
20
+ source.width <= 0 ||
21
+ source.height <= 0 ||
22
+ target.width <= 0 ||
23
+ target.height <= 0
24
+ ) {
25
+ return {
26
+ x: 0,
27
+ y: -4,
28
+ width: target.width,
29
+ height: target.height,
30
+ opacity: 0,
31
+ borderRadius: target.radius ?? 0,
32
+ };
33
+ }
34
+ return {
35
+ x: source.left - target.left,
36
+ y: source.top - target.top,
37
+ width: source.width,
38
+ height: source.height,
39
+ opacity: 1,
40
+ borderRadius: Math.min(source.width, source.height) / 2,
41
+ };
42
42
  }
@@ -1,66 +1,73 @@
1
- import { type MouseEvent, type PointerEvent, useRef } from "react"
2
- import { haptic } from "../../lib/haptics"
3
- import { type ContextMenuItem, openContextMenu } from "./context-menu-store"
1
+ import { type MouseEvent, type PointerEvent, useRef } from "react";
2
+ import { haptic } from "../../lib/haptics";
3
+ import { type ContextMenuItem, openContextMenu } from "./context-menu-store";
4
4
 
5
- const LONG_PRESS_MS = 480
6
- const MOVE_TOLERANCE = 10
5
+ const LONG_PRESS_MS = 480;
6
+ const MOVE_TOLERANCE = 10;
7
7
 
8
8
  /**
9
9
  * Right-click on desktop, long-press on touch screens: both open the same
10
10
  * menu of actions. Spread the returned handlers onto the element.
11
11
  */
12
12
  export function useContextMenu(items: () => ContextMenuItem[]) {
13
- const press = useRef<{
14
- x: number
15
- y: number
16
- timer: ReturnType<typeof setTimeout>
17
- } | null>(null)
18
- const suppressClick = useRef(false)
13
+ const press = useRef<{
14
+ x: number;
15
+ y: number;
16
+ timer: ReturnType<typeof setTimeout>;
17
+ } | null>(null);
18
+ const suppressClick = useRef(false);
19
19
 
20
- const cancel = () => {
21
- if (press.current) clearTimeout(press.current.timer)
22
- press.current = null
23
- }
20
+ const cancel = () => {
21
+ if (press.current) clearTimeout(press.current.timer);
22
+ press.current = null;
23
+ };
24
24
 
25
- return {
26
- onContextMenu(event: MouseEvent) {
27
- event.preventDefault()
28
- cancel()
29
- openContextMenu(event.clientX, event.clientY, items(), false, event.currentTarget)
30
- },
31
- onPointerDown(event: PointerEvent) {
32
- if (event.pointerType !== "touch") return
33
- const { clientX: x, clientY: y } = event
34
- const anchor = event.currentTarget
35
- cancel()
36
- press.current = {
37
- x,
38
- y,
39
- timer: setTimeout(() => {
40
- press.current = null
41
- suppressClick.current = true
42
- haptic("medium")
43
- openContextMenu(x, y, items(), true, anchor)
44
- }, LONG_PRESS_MS),
45
- }
46
- },
47
- onPointerMove(event: PointerEvent) {
48
- const start = press.current
49
- if (
50
- start &&
51
- Math.hypot(event.clientX - start.x, event.clientY - start.y) > MOVE_TOLERANCE
52
- ) {
53
- cancel()
54
- }
55
- },
56
- onPointerUp: cancel,
57
- onPointerCancel: cancel,
58
- // The tap that ends a long-press shouldn't also open the row.
59
- onClickCapture(event: MouseEvent) {
60
- if (!suppressClick.current) return
61
- suppressClick.current = false
62
- event.preventDefault()
63
- event.stopPropagation()
64
- },
65
- }
25
+ return {
26
+ onContextMenu(event: MouseEvent) {
27
+ event.preventDefault();
28
+ cancel();
29
+ openContextMenu(
30
+ event.clientX,
31
+ event.clientY,
32
+ items(),
33
+ false,
34
+ event.currentTarget,
35
+ );
36
+ },
37
+ onPointerDown(event: PointerEvent) {
38
+ if (event.pointerType !== "touch") return;
39
+ const { clientX: x, clientY: y } = event;
40
+ const anchor = event.currentTarget;
41
+ cancel();
42
+ press.current = {
43
+ x,
44
+ y,
45
+ timer: setTimeout(() => {
46
+ press.current = null;
47
+ suppressClick.current = true;
48
+ haptic("medium");
49
+ openContextMenu(x, y, items(), true, anchor);
50
+ }, LONG_PRESS_MS),
51
+ };
52
+ },
53
+ onPointerMove(event: PointerEvent) {
54
+ const start = press.current;
55
+ if (
56
+ start &&
57
+ Math.hypot(event.clientX - start.x, event.clientY - start.y) >
58
+ MOVE_TOLERANCE
59
+ ) {
60
+ cancel();
61
+ }
62
+ },
63
+ onPointerUp: cancel,
64
+ onPointerCancel: cancel,
65
+ // The tap that ends a long-press shouldn't also open the row.
66
+ onClickCapture(event: MouseEvent) {
67
+ if (!suppressClick.current) return;
68
+ suppressClick.current = false;
69
+ event.preventDefault();
70
+ event.stopPropagation();
71
+ },
72
+ };
66
73
  }