@blotch/dolan 22.2.2 → 22.2.4

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 (259) hide show
  1. package/dist/commands/add.d.ts +6 -2
  2. package/dist/commands/add.js +41 -11
  3. package/dist/commands/add.js.map +1 -1
  4. package/dist/commands/add.test.js +162 -9
  5. package/dist/commands/add.test.js.map +1 -1
  6. package/dist/commands/diff.d.ts +2 -1
  7. package/dist/commands/diff.js +38 -25
  8. package/dist/commands/diff.js.map +1 -1
  9. package/dist/commands/diff.test.js +72 -11
  10. package/dist/commands/diff.test.js.map +1 -1
  11. package/dist/commands/doctor.js +228 -34
  12. package/dist/commands/doctor.js.map +1 -1
  13. package/dist/commands/doctor.test.js +367 -10
  14. package/dist/commands/doctor.test.js.map +1 -1
  15. package/dist/commands/eject.js +7 -0
  16. package/dist/commands/eject.js.map +1 -1
  17. package/dist/commands/eject.test.js +39 -0
  18. package/dist/commands/eject.test.js.map +1 -1
  19. package/dist/commands/info.js +3 -2
  20. package/dist/commands/info.js.map +1 -1
  21. package/dist/commands/init.d.ts +37 -1
  22. package/dist/commands/init.js +169 -40
  23. package/dist/commands/init.js.map +1 -1
  24. package/dist/commands/init.test.js +392 -27
  25. package/dist/commands/init.test.js.map +1 -1
  26. package/dist/commands/install-packages.d.ts +6 -0
  27. package/dist/commands/install-packages.js +20 -0
  28. package/dist/commands/install-packages.js.map +1 -0
  29. package/dist/commands/install-packages.test.d.ts +1 -0
  30. package/dist/commands/install-packages.test.js +55 -0
  31. package/dist/commands/install-packages.test.js.map +1 -0
  32. package/dist/commands/list.js +21 -14
  33. package/dist/commands/list.js.map +1 -1
  34. package/dist/commands/list.test.js +62 -5
  35. package/dist/commands/list.test.js.map +1 -1
  36. package/dist/commands/outdated.js +11 -10
  37. package/dist/commands/outdated.js.map +1 -1
  38. package/dist/commands/outdated.test.js +20 -5
  39. package/dist/commands/outdated.test.js.map +1 -1
  40. package/dist/commands/remove.d.ts +4 -0
  41. package/dist/commands/remove.js +17 -4
  42. package/dist/commands/remove.js.map +1 -1
  43. package/dist/commands/remove.test.js +53 -2
  44. package/dist/commands/remove.test.js.map +1 -1
  45. package/dist/commands/theme.js +1 -1
  46. package/dist/commands/theme.js.map +1 -1
  47. package/dist/commands/update.d.ts +11 -0
  48. package/dist/commands/update.js +174 -58
  49. package/dist/commands/update.js.map +1 -1
  50. package/dist/commands/update.test.js +587 -33
  51. package/dist/commands/update.test.js.map +1 -1
  52. package/dist/index.js +7 -0
  53. package/dist/index.js.map +1 -1
  54. package/dist/lockfile.d.ts +7 -0
  55. package/dist/lockfile.js +36 -0
  56. package/dist/lockfile.js.map +1 -1
  57. package/dist/lockfile.test.d.ts +1 -0
  58. package/dist/lockfile.test.js +25 -0
  59. package/dist/lockfile.test.js.map +1 -0
  60. package/dist/registry.d.ts +14 -0
  61. package/dist/registry.js +78 -24
  62. package/dist/registry.js.map +1 -1
  63. package/dist/registry.test.js +119 -38
  64. package/dist/registry.test.js.map +1 -1
  65. package/dist/test-utils.d.ts +5 -0
  66. package/dist/test-utils.js +6 -1
  67. package/dist/test-utils.js.map +1 -1
  68. package/dist/testing/fire-lynx-event.d.ts +14 -0
  69. package/dist/testing/fire-lynx-event.js +19 -0
  70. package/dist/testing/fire-lynx-event.js.map +1 -0
  71. package/dist/testing/fire-lynx-event.test.d.ts +1 -0
  72. package/dist/testing/fire-lynx-event.test.js +44 -0
  73. package/dist/testing/fire-lynx-event.test.js.map +1 -0
  74. package/dist/testing/lynx-main-thread.d.ts +22 -0
  75. package/dist/testing/lynx-main-thread.js +11 -0
  76. package/dist/testing/lynx-main-thread.js.map +1 -0
  77. package/dist/testing/lynx-main-thread.test.d.ts +1 -0
  78. package/dist/testing/lynx-main-thread.test.js +24 -0
  79. package/dist/testing/lynx-main-thread.test.js.map +1 -0
  80. package/dist/testing/set-input-signal.d.ts +14 -0
  81. package/dist/testing/set-input-signal.js +21 -0
  82. package/dist/testing/set-input-signal.js.map +1 -0
  83. package/dist/testing/set-input-signal.test.d.ts +1 -0
  84. package/dist/testing/set-input-signal.test.js +20 -0
  85. package/dist/testing/set-input-signal.test.js.map +1 -0
  86. package/dist/ui/components/accordion/accordion.test.ts +158 -144
  87. package/dist/ui/components/accordion/accordion.ts +19 -14
  88. package/dist/ui/components/action-sheet/action-sheet.test.ts +283 -151
  89. package/dist/ui/components/action-sheet/action-sheet.ts +77 -43
  90. package/dist/ui/components/alert/alert.test.ts +67 -19
  91. package/dist/ui/components/alert/alert.ts +14 -16
  92. package/dist/ui/components/alert-dialog/alert-dialog.test.ts +208 -94
  93. package/dist/ui/components/alert-dialog/alert-dialog.ts +75 -38
  94. package/dist/ui/components/area-chart/area-chart.test.ts +134 -119
  95. package/dist/ui/components/area-chart/area-chart.ts +100 -315
  96. package/dist/ui/components/area-chart/index.ts +2 -3
  97. package/dist/ui/components/avatar/avatar.test.ts +89 -45
  98. package/dist/ui/components/badge/badge.test.ts +57 -23
  99. package/dist/ui/components/badge/badge.ts +12 -14
  100. package/dist/ui/components/bar-chart/bar-chart.test.ts +172 -45
  101. package/dist/ui/components/bar-chart/bar-chart.ts +140 -294
  102. package/dist/ui/components/bar-chart/index.ts +3 -3
  103. package/dist/ui/components/bottom-sheet/bottom-sheet.test.ts +386 -128
  104. package/dist/ui/components/bottom-sheet/bottom-sheet.ts +170 -53
  105. package/dist/ui/components/button/button.test.ts +611 -2
  106. package/dist/ui/components/button/button.ts +434 -49
  107. package/dist/ui/components/button/index.ts +2 -0
  108. package/dist/ui/components/button-group/button-group.test.ts +484 -2
  109. package/dist/ui/components/button-group/button-group.ts +380 -68
  110. package/dist/ui/components/button-group/index.ts +3 -0
  111. package/dist/ui/components/candlestick-chart/candlestick-chart.test.ts +183 -58
  112. package/dist/ui/components/candlestick-chart/candlestick-chart.ts +150 -323
  113. package/dist/ui/components/candlestick-chart/index.ts +3 -4
  114. package/dist/ui/components/card/card.test.ts +82 -62
  115. package/dist/ui/components/chart/chart-focus.test.ts +132 -0
  116. package/dist/ui/components/chart/chart-focus.ts +115 -0
  117. package/dist/ui/components/chart/chart-presets.test.ts +335 -0
  118. package/dist/ui/components/chart/chart-presets.ts +278 -0
  119. package/dist/ui/components/chart/chart-scene.test.ts +589 -0
  120. package/dist/ui/components/chart/chart-scene.ts +483 -0
  121. package/dist/ui/components/chart/chart-theme.test.ts +85 -0
  122. package/dist/ui/components/chart/chart-theme.ts +210 -0
  123. package/dist/ui/components/chart/chart-zoom.test.ts +159 -0
  124. package/dist/ui/components/chart/chart-zoom.ts +162 -0
  125. package/dist/ui/components/chart/chart.test.ts +1017 -0
  126. package/dist/ui/components/chart/chart.ts +934 -0
  127. package/dist/ui/components/chart/index.ts +24 -0
  128. package/dist/ui/components/checkbox/checkbox.test.ts +124 -42
  129. package/dist/ui/components/checkbox/checkbox.ts +23 -23
  130. package/dist/ui/components/collapsible/collapsible.test.ts +158 -81
  131. package/dist/ui/components/collapsible/collapsible.ts +19 -14
  132. package/dist/ui/components/context-menu/context-menu.test.ts +224 -0
  133. package/dist/ui/components/context-menu/context-menu.ts +110 -0
  134. package/dist/ui/components/context-menu/index.ts +1 -0
  135. package/dist/ui/components/dialog/dialog.test.ts +213 -108
  136. package/dist/ui/components/dialog/dialog.ts +72 -35
  137. package/dist/ui/components/dock/dock-scroll.test.ts +130 -0
  138. package/dist/ui/components/dock/dock-scroll.ts +133 -0
  139. package/dist/ui/components/dock/dock.test.ts +749 -0
  140. package/dist/ui/components/dock/dock.ts +960 -0
  141. package/dist/ui/components/dock/index.ts +2 -0
  142. package/dist/ui/components/empty-state/empty-state.test.ts +28 -39
  143. package/dist/ui/components/glass/concentric-radius.test.ts +13 -0
  144. package/dist/ui/components/glass/concentric-radius.ts +17 -0
  145. package/dist/ui/components/glass/glass-design.test.ts +78 -0
  146. package/dist/ui/components/glass/glass-design.ts +119 -0
  147. package/dist/ui/components/glass/glass.test.ts +338 -0
  148. package/dist/ui/components/glass/glass.ts +360 -0
  149. package/dist/ui/components/glass/index.ts +18 -0
  150. package/dist/ui/components/icon/icon-theme.test.ts +169 -0
  151. package/dist/ui/components/icon/icon-theme.ts +227 -0
  152. package/dist/ui/components/icon/icon.test.ts +85 -25
  153. package/dist/ui/components/icon/icon.ts +38 -21
  154. package/dist/ui/components/input/input.test.ts +178 -101
  155. package/dist/ui/components/input/input.ts +15 -12
  156. package/dist/ui/components/label/label.test.ts +11 -24
  157. package/dist/ui/components/line-chart/index.ts +2 -3
  158. package/dist/ui/components/line-chart/line-chart.test.ts +149 -56
  159. package/dist/ui/components/line-chart/line-chart.ts +83 -241
  160. package/dist/ui/components/list/list.test.ts +119 -21
  161. package/dist/ui/components/list/list.ts +21 -15
  162. package/dist/ui/components/menu/index.ts +25 -0
  163. package/dist/ui/components/menu/menu-placement.test.ts +153 -0
  164. package/dist/ui/components/menu/menu-placement.ts +159 -0
  165. package/dist/ui/components/menu/menu-popup.test.ts +632 -0
  166. package/dist/ui/components/menu/menu-popup.ts +544 -0
  167. package/dist/ui/components/menu/menu.test.ts +429 -0
  168. package/dist/ui/components/menu/menu.ts +415 -0
  169. package/dist/ui/components/nav-drawer/nav-drawer.test.ts +435 -191
  170. package/dist/ui/components/nav-drawer/nav-drawer.ts +127 -56
  171. package/dist/ui/components/pagination/pagination.test.ts +177 -65
  172. package/dist/ui/components/pagination/pagination.ts +55 -8
  173. package/dist/ui/components/progress/progress.test.ts +102 -46
  174. package/dist/ui/components/progress/progress.ts +52 -54
  175. package/dist/ui/components/radio-group/radio-group.test.ts +128 -83
  176. package/dist/ui/components/radio-group/radio-group.ts +24 -24
  177. package/dist/ui/components/scatter-chart/index.ts +2 -3
  178. package/dist/ui/components/scatter-chart/scatter-chart.test.ts +131 -26
  179. package/dist/ui/components/scatter-chart/scatter-chart.ts +104 -250
  180. package/dist/ui/components/scroll-area/scroll-area.test.ts +95 -24
  181. package/dist/ui/components/scroll-area/scroll-area.ts +8 -8
  182. package/dist/ui/components/select/select.test.ts +138 -78
  183. package/dist/ui/components/select/select.ts +17 -11
  184. package/dist/ui/components/separator/separator.test.ts +18 -17
  185. package/dist/ui/components/sheet/sheet.test.ts +64 -11
  186. package/dist/ui/components/sheet/sheet.ts +9 -1
  187. package/dist/ui/components/skeleton/skeleton.test.ts +24 -7
  188. package/dist/ui/components/skeleton/skeleton.ts +10 -16
  189. package/dist/ui/components/spinner/spinner.test.ts +67 -22
  190. package/dist/ui/components/spinner/spinner.ts +48 -41
  191. package/dist/ui/components/switch/switch.test.ts +116 -46
  192. package/dist/ui/components/switch/switch.ts +8 -13
  193. package/dist/ui/components/tab-bar/index.ts +1 -0
  194. package/dist/ui/components/tab-bar/tab-bar.test.ts +280 -0
  195. package/dist/ui/components/tab-bar/tab-bar.ts +230 -0
  196. package/dist/ui/components/tabs/tabs.test.ts +215 -157
  197. package/dist/ui/components/tabs/tabs.ts +110 -118
  198. package/dist/ui/components/textarea/textarea.test.ts +206 -100
  199. package/dist/ui/components/textarea/textarea.ts +14 -12
  200. package/dist/ui/components/toast/toast.test.ts +436 -315
  201. package/dist/ui/components/toast/toast.ts +62 -55
  202. package/dist/ui/components/toggle/toggle.test.ts +247 -140
  203. package/dist/ui/theme/chart-colors.test.ts +35 -0
  204. package/dist/ui/theme/dark.css +46 -2
  205. package/dist/ui/theme/default.css +63 -7
  206. package/dist/ui/theme/icon-colors.test.ts +35 -0
  207. package/dist/ui/theme/rgb-channels.test.ts +84 -0
  208. package/dist/ui/theme/tailwind-build.test.ts +114 -0
  209. package/dist/ui/theme/tailwind-css.test.ts +83 -0
  210. package/dist/ui/theme/tailwind-plugin.test.ts +42 -2
  211. package/dist/ui/theme/tailwind-plugin.ts +98 -25
  212. package/dist/ui/theme/tailwind.css +113 -0
  213. package/dist/ui/utils/animate.test.ts +13 -1
  214. package/dist/utils/analyze.d.ts +14 -0
  215. package/dist/utils/analyze.js +15 -0
  216. package/dist/utils/analyze.js.map +1 -1
  217. package/dist/utils/analyze.test.js +14 -1
  218. package/dist/utils/analyze.test.js.map +1 -1
  219. package/dist/utils/installed.d.ts +29 -0
  220. package/dist/utils/installed.js +43 -0
  221. package/dist/utils/installed.js.map +1 -0
  222. package/dist/utils/installed.test.d.ts +1 -0
  223. package/dist/utils/installed.test.js +57 -0
  224. package/dist/utils/installed.test.js.map +1 -0
  225. package/dist/utils/interactive.d.ts +9 -0
  226. package/dist/utils/interactive.js +10 -0
  227. package/dist/utils/interactive.js.map +1 -0
  228. package/dist/utils/interactive.test.d.ts +1 -0
  229. package/dist/utils/interactive.test.js +24 -0
  230. package/dist/utils/interactive.test.js.map +1 -0
  231. package/dist/utils/packages.d.ts +36 -0
  232. package/dist/utils/packages.js +70 -0
  233. package/dist/utils/packages.js.map +1 -0
  234. package/dist/utils/packages.test.d.ts +1 -0
  235. package/dist/utils/packages.test.js +89 -0
  236. package/dist/utils/packages.test.js.map +1 -0
  237. package/dist/utils/resolve-paths.d.ts +17 -1
  238. package/dist/utils/resolve-paths.js +21 -3
  239. package/dist/utils/resolve-paths.js.map +1 -1
  240. package/dist/utils/resolve-paths.test.js +38 -2
  241. package/dist/utils/resolve-paths.test.js.map +1 -1
  242. package/dist/utils/tailwind.d.ts +25 -0
  243. package/dist/utils/tailwind.js +51 -0
  244. package/dist/utils/tailwind.js.map +1 -0
  245. package/dist/utils/tailwind.test.d.ts +1 -0
  246. package/dist/utils/tailwind.test.js +71 -0
  247. package/dist/utils/tailwind.test.js.map +1 -0
  248. package/package.json +13 -1
  249. package/dist/ui/components/area-chart/area-chart-render.test.ts +0 -121
  250. package/dist/ui/components/bar-chart/bar-chart-render.test.ts +0 -174
  251. package/dist/ui/components/button/button-render.test.ts +0 -113
  252. package/dist/ui/components/button-group/button-group-render.test.ts +0 -146
  253. package/dist/ui/components/candlestick-chart/candlestick-chart-render.test.ts +0 -174
  254. package/dist/ui/components/cartesian-chart/cartesian-chart-render.test.ts +0 -188
  255. package/dist/ui/components/cartesian-chart/cartesian-chart.test.ts +0 -525
  256. package/dist/ui/components/cartesian-chart/cartesian-chart.ts +0 -1140
  257. package/dist/ui/components/cartesian-chart/index.ts +0 -18
  258. package/dist/ui/components/line-chart/line-chart-render.test.ts +0 -121
  259. package/dist/ui/components/scatter-chart/scatter-chart-render.test.ts +0 -157
@@ -0,0 +1,153 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ type MenuPlacementOptions,
4
+ type MenuRect,
5
+ menuStartFrame,
6
+ placeMenu,
7
+ } from './menu-placement';
8
+
9
+ /** A phone's screen inside its safe area, in points. */
10
+ const AREA = { width: 402, height: 778 };
11
+ const MENU = { width: 250, height: 200 };
12
+
13
+ const rect = (
14
+ left: number,
15
+ top: number,
16
+ width: number,
17
+ height: number,
18
+ ): MenuRect => ({ left, top, width, height });
19
+
20
+ const place = (options: Partial<MenuPlacementOptions> & { anchor: MenuRect }) =>
21
+ placeMenu({ area: AREA, menu: MENU, mode: 'beside', ...options });
22
+
23
+ describe('placeMenu', () => {
24
+ it('opens below an anchor with room below, lined up with its left edge', () => {
25
+ const placement = place({ anchor: rect(16, 100, 370, 96) });
26
+
27
+ expect(placement).toEqual({
28
+ frame: rect(16, 204, 250, 200),
29
+ anchor: rect(16, 100, 370, 96),
30
+ vertical: 'top',
31
+ horizontal: 'left',
32
+ });
33
+ });
34
+
35
+ it('lines up with the right edge of an anchor in the right half', () => {
36
+ const placement = place({ anchor: rect(340, 10, 44, 44) });
37
+
38
+ expect(placement.horizontal).toBe('right');
39
+ expect(placement.frame.left).toBe(384 - 250);
40
+ });
41
+
42
+ it('keeps the menu inside the area horizontally', () => {
43
+ expect(place({ anchor: rect(380, 10, 60, 44) }).frame.left).toBe(
44
+ 402 - 8 - 250,
45
+ );
46
+ expect(place({ anchor: rect(-40, 10, 44, 44) }).frame.left).toBe(8);
47
+ });
48
+
49
+ it('opens above an anchor near the bottom', () => {
50
+ const placement = place({ anchor: rect(16, 700, 100, 44) });
51
+
52
+ expect(placement.vertical).toBe('bottom');
53
+ expect(placement.frame.top).toBe(700 - 8 - 200);
54
+ });
55
+
56
+ it('moves a lifted anchor up when the menu fits on neither side', () => {
57
+ const anchor = rect(16, 150, 370, 450);
58
+ const placement = place({ anchor, movableAnchor: true });
59
+
60
+ // The anchor moves up until the menu fits below it, inside the margin.
61
+ const anchorTop = 778 - 8 - 200 - 8 - 450;
62
+ expect(placement.anchor).toEqual({ ...anchor, top: anchorTop });
63
+ expect(placement.frame.top).toBe(anchorTop + 450 + 8);
64
+ expect(placement.vertical).toBe('top');
65
+ });
66
+
67
+ it('covers an anchor it can neither open beside nor move', () => {
68
+ const anchor = rect(16, 100, 370, 600);
69
+
70
+ // Too tall to move out of the way, or not movable: the menu sits at the
71
+ // bottom of the area, over the anchor.
72
+ for (const movableAnchor of [true, false]) {
73
+ const placement = place({ anchor, movableAnchor });
74
+ expect(placement.anchor).toEqual(anchor);
75
+ expect(placement.frame.top).toBe(778 - 8 - 200);
76
+ }
77
+ });
78
+
79
+ it('shrinks a menu taller or wider than the area to fit', () => {
80
+ const placement = place({
81
+ anchor: rect(16, 100, 44, 44),
82
+ menu: { width: 500, height: 1000 },
83
+ });
84
+
85
+ expect(placement.frame).toEqual(rect(8, 8, 402 - 16, 778 - 16));
86
+ });
87
+
88
+ it('grows a pull-down menu over its anchor, from its top edge', () => {
89
+ const placement = place({ anchor: rect(16, 100, 80, 44), mode: 'grow' });
90
+
91
+ expect(placement.frame).toEqual(rect(16, 100, 250, 200));
92
+ expect(placement.vertical).toBe('top');
93
+ });
94
+
95
+ it('grows a pull-down menu from the bottom edge of an anchor near the bottom', () => {
96
+ const placement = place({ anchor: rect(16, 720, 80, 44), mode: 'grow' });
97
+
98
+ expect(placement.frame.top).toBe(764 - 200);
99
+ expect(placement.vertical).toBe('bottom');
100
+ });
101
+
102
+ it('takes its own gap and margin', () => {
103
+ const placement = place({
104
+ anchor: rect(0, 100, 80, 40),
105
+ gap: 0,
106
+ margin: 0,
107
+ });
108
+
109
+ expect(placement.frame).toEqual(rect(0, 140, 250, 200));
110
+ });
111
+ });
112
+
113
+ describe('menuStartFrame', () => {
114
+ const below = placeMenu({
115
+ anchor: rect(16, 100, 80, 44),
116
+ area: AREA,
117
+ menu: MENU,
118
+ mode: 'beside',
119
+ });
120
+ const aboveRight = placeMenu({
121
+ anchor: rect(300, 700, 80, 44),
122
+ area: AREA,
123
+ menu: MENU,
124
+ mode: 'beside',
125
+ });
126
+
127
+ it('starts a pull-down menu as its anchor', () => {
128
+ expect(menuStartFrame(below, 'anchor')).toEqual(below.anchor);
129
+ });
130
+
131
+ it('starts a context menu small, in the corner nearest its anchor', () => {
132
+ expect(menuStartFrame(below, 'corner')).toEqual(
133
+ rect(16, 152, 250 * 0.3, 200 * 0.3),
134
+ );
135
+ const { frame } = aboveRight;
136
+ expect(menuStartFrame(aboveRight, 'corner')).toEqual(
137
+ rect(
138
+ frame.left + frame.width - 250 * 0.3,
139
+ frame.top + frame.height - 200 * 0.3,
140
+ 250 * 0.3,
141
+ 200 * 0.3,
142
+ ),
143
+ );
144
+ });
145
+
146
+ it('starts a Material menu as a sliver along the edge nearest its anchor', () => {
147
+ expect(menuStartFrame(below, 'edge')).toEqual(rect(16, 152, 250, 0));
148
+ const { frame } = aboveRight;
149
+ expect(menuStartFrame(aboveRight, 'edge')).toEqual(
150
+ rect(frame.left, frame.top + frame.height, 250, 0),
151
+ );
152
+ });
153
+ });
@@ -0,0 +1,159 @@
1
+ /** A box on screen, in px. */
2
+ export type MenuRect = {
3
+ readonly left: number;
4
+ readonly top: number;
5
+ readonly width: number;
6
+ readonly height: number;
7
+ };
8
+
9
+ /** A size, in px. */
10
+ export type MenuSize = { readonly width: number; readonly height: number };
11
+
12
+ /**
13
+ * How a menu sits against the element that opened it.
14
+ *
15
+ * - `'grow'`: over it, from its top edge (or its bottom edge, near the bottom
16
+ * of the screen), as Apple's pull-down menus grow out of their button.
17
+ * - `'beside'`: below it, or above it near the bottom of the screen, a gap
18
+ * away. Context menus open beside the pressed element, and Material menus
19
+ * beside their anchor.
20
+ */
21
+ export type MenuPlacementMode = 'grow' | 'beside';
22
+
23
+ export type MenuPlacementOptions = {
24
+ /** The element that opened the menu, in the area's coordinates. */
25
+ readonly anchor: MenuRect;
26
+ /** The area the menu has to stay in: the screen inside its safe area. */
27
+ readonly area: MenuSize;
28
+ /** The menu's size at rest. */
29
+ readonly menu: MenuSize;
30
+ readonly mode: MenuPlacementMode;
31
+ /**
32
+ * Whether the anchor may move to make room, as the lifted element of a
33
+ * context menu does when the menu fits neither below nor above it.
34
+ */
35
+ readonly movableAnchor?: boolean;
36
+ /** The space between the menu and a `'beside'` anchor. */
37
+ readonly gap?: number;
38
+ /** The space the menu keeps from the area's edges. */
39
+ readonly margin?: number;
40
+ };
41
+
42
+ export type MenuPlacement = {
43
+ /** The menu's box at rest. */
44
+ readonly frame: MenuRect;
45
+ /**
46
+ * Where the anchor sits while the menu is open: where it was, unless
47
+ * `movableAnchor` let it move to make room.
48
+ */
49
+ readonly anchor: MenuRect;
50
+ /**
51
+ * The menu edge nearest the anchor, which the menu grows from and its
52
+ * content is pinned to: it hangs from its `'top'` below or over the anchor,
53
+ * and stands on its `'bottom'` above it.
54
+ */
55
+ readonly vertical: 'top' | 'bottom';
56
+ /**
57
+ * The anchor edge the menu lines up with: its `'right'` edge for an anchor
58
+ * entirely in the right half of the area, such as a button in a corner,
59
+ * so the menu opens toward the middle of the screen, and its `'left'` edge
60
+ * otherwise, which includes a row as wide as the screen.
61
+ */
62
+ readonly horizontal: 'left' | 'right';
63
+ };
64
+
65
+ const clamp = (value: number, min: number, max: number): number =>
66
+ Math.min(Math.max(value, min), Math.max(min, max));
67
+
68
+ /**
69
+ * Where a menu goes on screen, beside or over the element that opened it,
70
+ * kept inside the area (the screen within its safe area): below the element,
71
+ * or above it when there's no room below, lined up with whichever of its
72
+ * edges is nearer the middle of the screen.
73
+ */
74
+ export const placeMenu = ({
75
+ anchor,
76
+ area,
77
+ menu,
78
+ mode,
79
+ movableAnchor = false,
80
+ gap = 8,
81
+ margin = 8,
82
+ }: MenuPlacementOptions): MenuPlacement => {
83
+ const width = clamp(menu.width, 0, area.width - 2 * margin);
84
+ const height = clamp(menu.height, 0, area.height - 2 * margin);
85
+ const horizontal = anchor.left < area.width / 2 ? 'left' : 'right';
86
+ const left = clamp(
87
+ horizontal === 'left' ? anchor.left : anchor.left + anchor.width - width,
88
+ margin,
89
+ area.width - margin - width,
90
+ );
91
+ const bottomLimit = area.height - margin;
92
+ const anchorBottom = anchor.top + anchor.height;
93
+
94
+ const place = (
95
+ top: number,
96
+ vertical: 'top' | 'bottom',
97
+ anchorTop = anchor.top,
98
+ ): MenuPlacement => ({
99
+ frame: {
100
+ left,
101
+ top: clamp(top, margin, bottomLimit - height),
102
+ width,
103
+ height,
104
+ },
105
+ anchor: { ...anchor, top: anchorTop },
106
+ vertical,
107
+ horizontal,
108
+ });
109
+
110
+ if (mode === 'grow') {
111
+ // Over the anchor, from its top edge, or else from its bottom edge.
112
+ return anchor.top + height <= bottomLimit
113
+ ? place(anchor.top, 'top')
114
+ : place(anchorBottom - height, 'bottom');
115
+ }
116
+
117
+ if (anchorBottom + gap + height <= bottomLimit) {
118
+ return place(anchorBottom + gap, 'top');
119
+ }
120
+ if (anchor.top - gap - height >= margin) {
121
+ return place(anchor.top - gap - height, 'bottom');
122
+ }
123
+ // Room on neither side. A lifted element moves up until the menu fits
124
+ // below it, as iOS moves a context menu's preview; if even that can't
125
+ // make room, or the anchor can't move, the menu covers the anchor.
126
+ const anchorTop = bottomLimit - height - gap - anchor.height;
127
+ if (movableAnchor && anchorTop >= margin) {
128
+ return place(anchorTop + anchor.height + gap, 'top', anchorTop);
129
+ }
130
+ return place(bottomLimit - height, 'top');
131
+ };
132
+
133
+ /**
134
+ * Where a menu starts growing from, and shrinks back to, as it opens and
135
+ * closes.
136
+ *
137
+ * - From the anchor itself for a `'grow'` menu: one shape whose frame grows,
138
+ * which Liquid Glass follows frame by frame (its glass can't morph one
139
+ * shape into another).
140
+ * - From a small box in the corner nearest the anchor for a context menu.
141
+ * - From a sliver along the edge nearest the anchor for Material, whose
142
+ * menus unfold from their anchor.
143
+ */
144
+ export const menuStartFrame = (
145
+ placement: MenuPlacement,
146
+ from: 'anchor' | 'corner' | 'edge',
147
+ ): MenuRect => {
148
+ const { frame, anchor, vertical, horizontal } = placement;
149
+ if (from === 'anchor') return anchor;
150
+ const scale = from === 'corner' ? 0.3 : 1;
151
+ const width = frame.width * scale;
152
+ const height = from === 'corner' ? frame.height * scale : 0;
153
+ return {
154
+ left: horizontal === 'left' ? frame.left : frame.left + frame.width - width,
155
+ top: vertical === 'top' ? frame.top : frame.top + frame.height - height,
156
+ width,
157
+ height,
158
+ };
159
+ };