@fuzdev/fuz_ui 0.169.0

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 (323) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +93 -0
  3. package/dist/Alert.svelte +108 -0
  4. package/dist/Alert.svelte.d.ts +16 -0
  5. package/dist/Alert.svelte.d.ts.map +1 -0
  6. package/dist/ApiDeclarationList.svelte +35 -0
  7. package/dist/ApiDeclarationList.svelte.d.ts +9 -0
  8. package/dist/ApiDeclarationList.svelte.d.ts.map +1 -0
  9. package/dist/ApiIndex.svelte +65 -0
  10. package/dist/ApiIndex.svelte.d.ts +23 -0
  11. package/dist/ApiIndex.svelte.d.ts.map +1 -0
  12. package/dist/ApiModule.svelte +124 -0
  13. package/dist/ApiModule.svelte.d.ts +22 -0
  14. package/dist/ApiModule.svelte.d.ts.map +1 -0
  15. package/dist/Breadcrumb.svelte +83 -0
  16. package/dist/Breadcrumb.svelte.d.ts +23 -0
  17. package/dist/Breadcrumb.svelte.d.ts.map +1 -0
  18. package/dist/Card.svelte +157 -0
  19. package/dist/Card.svelte.d.ts +13 -0
  20. package/dist/Card.svelte.d.ts.map +1 -0
  21. package/dist/ColorSchemeInput.svelte +65 -0
  22. package/dist/ColorSchemeInput.svelte.d.ts +11 -0
  23. package/dist/ColorSchemeInput.svelte.d.ts.map +1 -0
  24. package/dist/Contextmenu.svelte +30 -0
  25. package/dist/Contextmenu.svelte.d.ts +32 -0
  26. package/dist/Contextmenu.svelte.d.ts.map +1 -0
  27. package/dist/ContextmenuEntry.svelte +74 -0
  28. package/dist/ContextmenuEntry.svelte.d.ts +12 -0
  29. package/dist/ContextmenuEntry.svelte.d.ts.map +1 -0
  30. package/dist/ContextmenuLinkEntry.svelte +112 -0
  31. package/dist/ContextmenuLinkEntry.svelte.d.ts +12 -0
  32. package/dist/ContextmenuLinkEntry.svelte.d.ts.map +1 -0
  33. package/dist/ContextmenuRoot.svelte +372 -0
  34. package/dist/ContextmenuRoot.svelte.d.ts +71 -0
  35. package/dist/ContextmenuRoot.svelte.d.ts.map +1 -0
  36. package/dist/ContextmenuRootForSafariCompatibility.svelte +541 -0
  37. package/dist/ContextmenuRootForSafariCompatibility.svelte.d.ts +79 -0
  38. package/dist/ContextmenuRootForSafariCompatibility.svelte.d.ts.map +1 -0
  39. package/dist/ContextmenuSeparator.svelte +16 -0
  40. package/dist/ContextmenuSeparator.svelte.d.ts +4 -0
  41. package/dist/ContextmenuSeparator.svelte.d.ts.map +1 -0
  42. package/dist/ContextmenuSubmenu.svelte +116 -0
  43. package/dist/ContextmenuSubmenu.svelte.d.ts +10 -0
  44. package/dist/ContextmenuSubmenu.svelte.d.ts.map +1 -0
  45. package/dist/ContextmenuTextEntry.svelte +21 -0
  46. package/dist/ContextmenuTextEntry.svelte.d.ts +10 -0
  47. package/dist/ContextmenuTextEntry.svelte.d.ts.map +1 -0
  48. package/dist/CopyToClipboard.svelte +81 -0
  49. package/dist/CopyToClipboard.svelte.d.ts +18 -0
  50. package/dist/CopyToClipboard.svelte.d.ts.map +1 -0
  51. package/dist/DeclarationDetail.svelte +340 -0
  52. package/dist/DeclarationDetail.svelte.d.ts +8 -0
  53. package/dist/DeclarationDetail.svelte.d.ts.map +1 -0
  54. package/dist/DeclarationLink.svelte +50 -0
  55. package/dist/DeclarationLink.svelte.d.ts +8 -0
  56. package/dist/DeclarationLink.svelte.d.ts.map +1 -0
  57. package/dist/Details.svelte +51 -0
  58. package/dist/Details.svelte.d.ts +20 -0
  59. package/dist/Details.svelte.d.ts.map +1 -0
  60. package/dist/Dialog.svelte +217 -0
  61. package/dist/Dialog.svelte.d.ts +30 -0
  62. package/dist/Dialog.svelte.d.ts.map +1 -0
  63. package/dist/Dialogs.svelte +28 -0
  64. package/dist/Dialogs.svelte.d.ts +11 -0
  65. package/dist/Dialogs.svelte.d.ts.map +1 -0
  66. package/dist/Docs.svelte +179 -0
  67. package/dist/Docs.svelte.d.ts +13 -0
  68. package/dist/Docs.svelte.d.ts.map +1 -0
  69. package/dist/DocsContent.svelte +40 -0
  70. package/dist/DocsContent.svelte.d.ts +14 -0
  71. package/dist/DocsContent.svelte.d.ts.map +1 -0
  72. package/dist/DocsFooter.svelte +64 -0
  73. package/dist/DocsFooter.svelte.d.ts +15 -0
  74. package/dist/DocsFooter.svelte.d.ts.map +1 -0
  75. package/dist/DocsLink.svelte +41 -0
  76. package/dist/DocsLink.svelte.d.ts +12 -0
  77. package/dist/DocsLink.svelte.d.ts.map +1 -0
  78. package/dist/DocsList.svelte +44 -0
  79. package/dist/DocsList.svelte.d.ts +11 -0
  80. package/dist/DocsList.svelte.d.ts.map +1 -0
  81. package/dist/DocsMenu.svelte +55 -0
  82. package/dist/DocsMenu.svelte.d.ts +11 -0
  83. package/dist/DocsMenu.svelte.d.ts.map +1 -0
  84. package/dist/DocsMenuHeader.svelte +15 -0
  85. package/dist/DocsMenuHeader.svelte.d.ts +9 -0
  86. package/dist/DocsMenuHeader.svelte.d.ts.map +1 -0
  87. package/dist/DocsModulesList.svelte +32 -0
  88. package/dist/DocsModulesList.svelte.d.ts +7 -0
  89. package/dist/DocsModulesList.svelte.d.ts.map +1 -0
  90. package/dist/DocsPageLinks.svelte +61 -0
  91. package/dist/DocsPageLinks.svelte.d.ts +8 -0
  92. package/dist/DocsPageLinks.svelte.d.ts.map +1 -0
  93. package/dist/DocsPrimaryNav.svelte +93 -0
  94. package/dist/DocsPrimaryNav.svelte.d.ts +11 -0
  95. package/dist/DocsPrimaryNav.svelte.d.ts.map +1 -0
  96. package/dist/DocsSearch.svelte +48 -0
  97. package/dist/DocsSearch.svelte.d.ts +11 -0
  98. package/dist/DocsSearch.svelte.d.ts.map +1 -0
  99. package/dist/DocsSecondaryNav.svelte +63 -0
  100. package/dist/DocsSecondaryNav.svelte.d.ts +9 -0
  101. package/dist/DocsSecondaryNav.svelte.d.ts.map +1 -0
  102. package/dist/DocsTertiaryNav.svelte +118 -0
  103. package/dist/DocsTertiaryNav.svelte.d.ts +10 -0
  104. package/dist/DocsTertiaryNav.svelte.d.ts.map +1 -0
  105. package/dist/EcosystemLinks.svelte +53 -0
  106. package/dist/EcosystemLinks.svelte.d.ts +7 -0
  107. package/dist/EcosystemLinks.svelte.d.ts.map +1 -0
  108. package/dist/EcosystemLinksPanel.svelte +22 -0
  109. package/dist/EcosystemLinksPanel.svelte.d.ts +8 -0
  110. package/dist/EcosystemLinksPanel.svelte.d.ts.map +1 -0
  111. package/dist/GithubLink.svelte +75 -0
  112. package/dist/GithubLink.svelte.d.ts +14 -0
  113. package/dist/GithubLink.svelte.d.ts.map +1 -0
  114. package/dist/Glyph.svelte +28 -0
  115. package/dist/Glyph.svelte.d.ts +9 -0
  116. package/dist/Glyph.svelte.d.ts.map +1 -0
  117. package/dist/Hashlink.svelte +41 -0
  118. package/dist/Hashlink.svelte.d.ts +8 -0
  119. package/dist/Hashlink.svelte.d.ts.map +1 -0
  120. package/dist/HiddenPersonalLinks.svelte +6 -0
  121. package/dist/HiddenPersonalLinks.svelte.d.ts +27 -0
  122. package/dist/HiddenPersonalLinks.svelte.d.ts.map +1 -0
  123. package/dist/HueInput.svelte +127 -0
  124. package/dist/HueInput.svelte.d.ts +11 -0
  125. package/dist/HueInput.svelte.d.ts.map +1 -0
  126. package/dist/ImgOrSvg.svelte +58 -0
  127. package/dist/ImgOrSvg.svelte.d.ts +25 -0
  128. package/dist/ImgOrSvg.svelte.d.ts.map +1 -0
  129. package/dist/LibraryDetail.svelte +297 -0
  130. package/dist/LibraryDetail.svelte.d.ts +15 -0
  131. package/dist/LibraryDetail.svelte.d.ts.map +1 -0
  132. package/dist/LibrarySummary.svelte +151 -0
  133. package/dist/LibrarySummary.svelte.d.ts +16 -0
  134. package/dist/LibrarySummary.svelte.d.ts.map +1 -0
  135. package/dist/MdnLink.svelte +40 -0
  136. package/dist/MdnLink.svelte.d.ts +8 -0
  137. package/dist/MdnLink.svelte.d.ts.map +1 -0
  138. package/dist/Mdz.svelte +30 -0
  139. package/dist/Mdz.svelte.d.ts +10 -0
  140. package/dist/Mdz.svelte.d.ts.map +1 -0
  141. package/dist/MdzNodeView.svelte +93 -0
  142. package/dist/MdzNodeView.svelte.d.ts +9 -0
  143. package/dist/MdzNodeView.svelte.d.ts.map +1 -0
  144. package/dist/ModuleLink.svelte +48 -0
  145. package/dist/ModuleLink.svelte.d.ts +8 -0
  146. package/dist/ModuleLink.svelte.d.ts.map +1 -0
  147. package/dist/PasteFromClipboard.svelte +35 -0
  148. package/dist/PasteFromClipboard.svelte.d.ts +9 -0
  149. package/dist/PasteFromClipboard.svelte.d.ts.map +1 -0
  150. package/dist/PendingAnimation.svelte +62 -0
  151. package/dist/PendingAnimation.svelte.d.ts +13 -0
  152. package/dist/PendingAnimation.svelte.d.ts.map +1 -0
  153. package/dist/PendingButton.svelte +75 -0
  154. package/dist/PendingButton.svelte.d.ts +17 -0
  155. package/dist/PendingButton.svelte.d.ts.map +1 -0
  156. package/dist/ProjectLinks.svelte +54 -0
  157. package/dist/ProjectLinks.svelte.d.ts +19 -0
  158. package/dist/ProjectLinks.svelte.d.ts.map +1 -0
  159. package/dist/Redirect.svelte +44 -0
  160. package/dist/Redirect.svelte.d.ts +23 -0
  161. package/dist/Redirect.svelte.d.ts.map +1 -0
  162. package/dist/Spiders.svelte +57 -0
  163. package/dist/Spiders.svelte.d.ts +9 -0
  164. package/dist/Spiders.svelte.d.ts.map +1 -0
  165. package/dist/Svg.svelte +99 -0
  166. package/dist/Svg.svelte.d.ts +54 -0
  167. package/dist/Svg.svelte.d.ts.map +1 -0
  168. package/dist/Teleport.svelte +48 -0
  169. package/dist/Teleport.svelte.d.ts +15 -0
  170. package/dist/Teleport.svelte.d.ts.map +1 -0
  171. package/dist/ThemeInput.svelte +75 -0
  172. package/dist/ThemeInput.svelte.d.ts +15 -0
  173. package/dist/ThemeInput.svelte.d.ts.map +1 -0
  174. package/dist/Themed.svelte +101 -0
  175. package/dist/Themed.svelte.d.ts +24 -0
  176. package/dist/Themed.svelte.d.ts.map +1 -0
  177. package/dist/TomeContent.svelte +67 -0
  178. package/dist/TomeContent.svelte.d.ts +12 -0
  179. package/dist/TomeContent.svelte.d.ts.map +1 -0
  180. package/dist/TomeHeader.svelte +56 -0
  181. package/dist/TomeHeader.svelte.d.ts +4 -0
  182. package/dist/TomeHeader.svelte.d.ts.map +1 -0
  183. package/dist/TomeLink.svelte +29 -0
  184. package/dist/TomeLink.svelte.d.ts +10 -0
  185. package/dist/TomeLink.svelte.d.ts.map +1 -0
  186. package/dist/TomeSection.svelte +65 -0
  187. package/dist/TomeSection.svelte.d.ts +24 -0
  188. package/dist/TomeSection.svelte.d.ts.map +1 -0
  189. package/dist/TomeSectionHeader.svelte +90 -0
  190. package/dist/TomeSectionHeader.svelte.d.ts +13 -0
  191. package/dist/TomeSectionHeader.svelte.d.ts.map +1 -0
  192. package/dist/TypeLink.svelte +19 -0
  193. package/dist/TypeLink.svelte.d.ts +7 -0
  194. package/dist/TypeLink.svelte.d.ts.map +1 -0
  195. package/dist/alert.d.ts +7 -0
  196. package/dist/alert.d.ts.map +1 -0
  197. package/dist/alert.js +6 -0
  198. package/dist/api_search.svelte.d.ts +16 -0
  199. package/dist/api_search.svelte.d.ts.map +1 -0
  200. package/dist/api_search.svelte.js +61 -0
  201. package/dist/constants.d.ts +2 -0
  202. package/dist/constants.d.ts.map +1 -0
  203. package/dist/constants.js +3 -0
  204. package/dist/context_helpers.d.ts +17 -0
  205. package/dist/context_helpers.d.ts.map +1 -0
  206. package/dist/context_helpers.js +19 -0
  207. package/dist/contextmenu_helpers.d.ts +16 -0
  208. package/dist/contextmenu_helpers.d.ts.map +1 -0
  209. package/dist/contextmenu_helpers.js +39 -0
  210. package/dist/contextmenu_state.svelte.d.ts +152 -0
  211. package/dist/contextmenu_state.svelte.d.ts.map +1 -0
  212. package/dist/contextmenu_state.svelte.js +424 -0
  213. package/dist/csp.d.ts +160 -0
  214. package/dist/csp.d.ts.map +1 -0
  215. package/dist/csp.js +354 -0
  216. package/dist/csp_of_ryanatkn.d.ts +6 -0
  217. package/dist/csp_of_ryanatkn.d.ts.map +1 -0
  218. package/dist/csp_of_ryanatkn.js +14 -0
  219. package/dist/declaration.svelte.d.ts +84 -0
  220. package/dist/declaration.svelte.d.ts.map +1 -0
  221. package/dist/declaration.svelte.js +66 -0
  222. package/dist/declaration_contextmenu.d.ts +4 -0
  223. package/dist/declaration_contextmenu.d.ts.map +1 -0
  224. package/dist/declaration_contextmenu.js +14 -0
  225. package/dist/dialog.d.ts +24 -0
  226. package/dist/dialog.d.ts.map +1 -0
  227. package/dist/dialog.js +12 -0
  228. package/dist/dimensions.svelte.d.ts +5 -0
  229. package/dist/dimensions.svelte.d.ts.map +1 -0
  230. package/dist/dimensions.svelte.js +4 -0
  231. package/dist/docs_helpers.svelte.d.ts +48 -0
  232. package/dist/docs_helpers.svelte.d.ts.map +1 -0
  233. package/dist/docs_helpers.svelte.js +99 -0
  234. package/dist/helpers.d.ts +2 -0
  235. package/dist/helpers.d.ts.map +1 -0
  236. package/dist/helpers.js +16 -0
  237. package/dist/intersect.svelte.d.ts +47 -0
  238. package/dist/intersect.svelte.d.ts.map +1 -0
  239. package/dist/intersect.svelte.js +92 -0
  240. package/dist/library.svelte.d.ts +197 -0
  241. package/dist/library.svelte.d.ts.map +1 -0
  242. package/dist/library.svelte.js +130 -0
  243. package/dist/library_gen.d.ts +34 -0
  244. package/dist/library_gen.d.ts.map +1 -0
  245. package/dist/library_gen.js +123 -0
  246. package/dist/library_gen_helpers.d.ts +85 -0
  247. package/dist/library_gen_helpers.d.ts.map +1 -0
  248. package/dist/library_gen_helpers.js +188 -0
  249. package/dist/library_helpers.d.ts +54 -0
  250. package/dist/library_helpers.d.ts.map +1 -0
  251. package/dist/library_helpers.js +102 -0
  252. package/dist/logos.d.ts +134 -0
  253. package/dist/logos.d.ts.map +1 -0
  254. package/dist/logos.js +281 -0
  255. package/dist/mdz.d.ts +106 -0
  256. package/dist/mdz.d.ts.map +1 -0
  257. package/dist/mdz.js +1481 -0
  258. package/dist/mdz_components.d.ts +37 -0
  259. package/dist/mdz_components.d.ts.map +1 -0
  260. package/dist/mdz_components.js +12 -0
  261. package/dist/module.svelte.d.ts +47 -0
  262. package/dist/module.svelte.d.ts.map +1 -0
  263. package/dist/module.svelte.js +56 -0
  264. package/dist/module_contextmenu.d.ts +4 -0
  265. package/dist/module_contextmenu.d.ts.map +1 -0
  266. package/dist/module_contextmenu.js +14 -0
  267. package/dist/module_helpers.d.ts +69 -0
  268. package/dist/module_helpers.d.ts.map +1 -0
  269. package/dist/module_helpers.js +87 -0
  270. package/dist/rune_helpers.svelte.d.ts +6 -0
  271. package/dist/rune_helpers.svelte.d.ts.map +1 -0
  272. package/dist/rune_helpers.svelte.js +10 -0
  273. package/dist/storage.d.ts +13 -0
  274. package/dist/storage.d.ts.map +1 -0
  275. package/dist/storage.js +43 -0
  276. package/dist/svelte_helpers.d.ts +37 -0
  277. package/dist/svelte_helpers.d.ts.map +1 -0
  278. package/dist/svelte_helpers.js +245 -0
  279. package/dist/themer.svelte.d.ts +24 -0
  280. package/dist/themer.svelte.d.ts.map +1 -0
  281. package/dist/themer.svelte.js +43 -0
  282. package/dist/tome.d.ts +80 -0
  283. package/dist/tome.d.ts.map +1 -0
  284. package/dist/tome.js +27 -0
  285. package/dist/ts_helpers.d.ts +110 -0
  286. package/dist/ts_helpers.d.ts.map +1 -0
  287. package/dist/ts_helpers.js +533 -0
  288. package/dist/tsdoc_helpers.d.ts +98 -0
  289. package/dist/tsdoc_helpers.d.ts.map +1 -0
  290. package/dist/tsdoc_helpers.js +221 -0
  291. package/package.json +128 -0
  292. package/src/lib/alert.ts +14 -0
  293. package/src/lib/api_search.svelte.ts +85 -0
  294. package/src/lib/constants.ts +3 -0
  295. package/src/lib/context_helpers.ts +47 -0
  296. package/src/lib/contextmenu_helpers.ts +63 -0
  297. package/src/lib/contextmenu_state.svelte.ts +515 -0
  298. package/src/lib/csp.ts +576 -0
  299. package/src/lib/csp_of_ryanatkn.ts +16 -0
  300. package/src/lib/declaration.svelte.ts +102 -0
  301. package/src/lib/declaration_contextmenu.ts +22 -0
  302. package/src/lib/dialog.ts +35 -0
  303. package/src/lib/dimensions.svelte.ts +4 -0
  304. package/src/lib/docs_helpers.svelte.ts +149 -0
  305. package/src/lib/helpers.ts +10 -0
  306. package/src/lib/intersect.svelte.ts +152 -0
  307. package/src/lib/library.svelte.ts +162 -0
  308. package/src/lib/library_gen.ts +160 -0
  309. package/src/lib/library_gen_helpers.ts +262 -0
  310. package/src/lib/library_helpers.ts +123 -0
  311. package/src/lib/logos.ts +302 -0
  312. package/src/lib/mdz.ts +1819 -0
  313. package/src/lib/mdz_components.ts +34 -0
  314. package/src/lib/module.svelte.ts +78 -0
  315. package/src/lib/module_contextmenu.ts +20 -0
  316. package/src/lib/module_helpers.ts +113 -0
  317. package/src/lib/rune_helpers.svelte.ts +10 -0
  318. package/src/lib/storage.ts +48 -0
  319. package/src/lib/svelte_helpers.ts +303 -0
  320. package/src/lib/themer.svelte.ts +68 -0
  321. package/src/lib/tome.ts +38 -0
  322. package/src/lib/ts_helpers.ts +662 -0
  323. package/src/lib/tsdoc_helpers.ts +259 -0
@@ -0,0 +1,515 @@
1
+ import {onDestroy, type Snippet} from 'svelte';
2
+ import type {Result} from '@fuzdev/fuz_util/result.js';
3
+ import {is_promise} from '@fuzdev/fuz_util/async.js';
4
+ import {BROWSER} from 'esm-env';
5
+ import type {SvelteHTMLElements} from 'svelte/elements';
6
+ import {EMPTY_OBJECT} from '@fuzdev/fuz_util/object.js';
7
+ import type {Attachment} from 'svelte/attachments';
8
+
9
+ import {Dimensions} from './dimensions.svelte.js';
10
+ import {create_context} from './context_helpers.js';
11
+ import {url_to_root_relative} from './library_helpers.js';
12
+
13
+ export type ContextmenuParams =
14
+ | Snippet
15
+ // TODO maybe this should be generic?
16
+ | {snippet: 'link'; props: {href: string; icon?: string}}
17
+ | {snippet: 'text'; props: {content: string; icon: string; run: ContextmenuRun}}
18
+ | {snippet: 'separator'; props: SvelteHTMLElements['li']};
19
+
20
+ export type ContextmenuActivateResult =
21
+ | void
22
+ | undefined // eslint-disable-line @typescript-eslint/no-redundant-type-constituents
23
+ | Result<{close?: boolean}, {message?: string}>;
24
+
25
+ export type ItemState = SubmenuState | EntryState;
26
+
27
+ export class EntryState {
28
+ readonly is_menu = false; // TODO rename to `type`?
29
+ readonly menu: SubmenuState | RootMenuState;
30
+
31
+ readonly run: () => ContextmenuRun;
32
+ readonly disabled: () => boolean;
33
+
34
+ selected: boolean = $state(false);
35
+ pending: boolean = $state(false);
36
+ error_message: string | null = $state(null);
37
+ promise: Promise<ContextmenuActivateResult> | null = $state.raw(null);
38
+
39
+ constructor(
40
+ menu: SubmenuState | RootMenuState,
41
+ run: () => ContextmenuRun,
42
+ disabled: () => boolean = () => false,
43
+ ) {
44
+ this.menu = menu;
45
+ this.run = run;
46
+ this.disabled = disabled;
47
+ }
48
+ }
49
+
50
+ export class SubmenuState {
51
+ readonly is_menu = true;
52
+ readonly menu: SubmenuState | RootMenuState;
53
+ readonly depth: number;
54
+
55
+ selected: boolean = $state(false);
56
+ items: ReadonlyArray<ItemState> = $state.raw([]);
57
+
58
+ constructor(menu: SubmenuState | RootMenuState, depth: number) {
59
+ this.menu = menu;
60
+ this.depth = depth;
61
+ }
62
+ }
63
+
64
+ export class RootMenuState {
65
+ readonly is_menu = true;
66
+ readonly menu = null;
67
+ readonly depth = 1;
68
+
69
+ items: ReadonlyArray<ItemState> = $state.raw([]);
70
+ }
71
+
72
+ export type ContextmenuRun = () => ContextmenuActivateResult | Promise<ContextmenuActivateResult>;
73
+
74
+ export interface ContextmenuStateOptions {
75
+ layout?: Dimensions; // TODO consider making this a prop on `ContextmenuRoot`, and being assigned here
76
+ }
77
+
78
+ /**
79
+ * Creates a `contextmenu` store.
80
+ * See usage with `ContextmenuRoot.svelte` and `Contextmenu.svelte`.
81
+ *
82
+ * @see https://developer.mozilla.org/en-US/docs/Web/API/Element/contextmenu_event
83
+ */
84
+ export class ContextmenuState {
85
+ layout: Dimensions; // TODO $state?
86
+ /**
87
+ * If an initial layout is provided, control is deferred externally.
88
+ * Otherwise the layout syncs to the page dimensions.
89
+ */
90
+ readonly has_custom_layout: boolean;
91
+
92
+ // State for external consumers.
93
+ opened: boolean = $state(false);
94
+ x: number = $state(0);
95
+ y: number = $state(0);
96
+ params: ReadonlyArray<ContextmenuParams> = $state.raw([]);
97
+ error: string | undefined = $state();
98
+
99
+ // These arrays use immutable updates (reassignment, not mutation).
100
+ // If you need reactivity, use `$contextmenu` in a reactive statement to react to all changes, and
101
+ // then access the immutable `contextmenu.root_menu` and `contextmenu.selections`.
102
+ // See `ContextmenuEntry.svelte` and `ContextmenuSubmenu.svelte` for reactive usage examples.
103
+ readonly root_menu: RootMenuState = new RootMenuState();
104
+ selections: ReadonlyArray<ItemState> = $state.raw([]);
105
+
106
+ can_collapse = $derived(this.selections.length > 1);
107
+
108
+ can_expand = $derived.by(() => {
109
+ const selected = this.selections.at(-1);
110
+ return !!selected?.is_menu && selected.items.length > 0;
111
+ });
112
+
113
+ can_select_next = $derived.by(() => {
114
+ const menu = this.selections.at(-1)?.menu ?? this.root_menu;
115
+ return menu.items.length > 1;
116
+ });
117
+
118
+ can_select_previous = $derived.by(() => {
119
+ const menu = this.selections.at(-1)?.menu ?? this.root_menu;
120
+ return menu.items.length > 1;
121
+ });
122
+
123
+ can_activate = $derived.by(() => {
124
+ const selected = this.selections.at(-1);
125
+ if (!selected) return false;
126
+ if (selected.is_menu) return selected.items.length > 0;
127
+ return !selected.disabled();
128
+ });
129
+
130
+ constructor(options: ContextmenuStateOptions = EMPTY_OBJECT) {
131
+ const {layout} = options;
132
+
133
+ this.has_custom_layout = !!layout;
134
+ this.layout = layout ?? new Dimensions();
135
+ }
136
+
137
+ open(params: Array<ContextmenuParams>, x: number, y: number): void {
138
+ this.selections = [];
139
+ this.opened = true;
140
+ this.x = x;
141
+ this.y = y;
142
+ this.params = params;
143
+ }
144
+
145
+ close(): void {
146
+ if (!this.opened) return;
147
+ this.reset_items(this.root_menu.items);
148
+ this.opened = false;
149
+ }
150
+
151
+ reset_items(items: ReadonlyArray<ItemState>): void {
152
+ for (const item of items) {
153
+ if (item.is_menu) {
154
+ this.reset_items(item.items);
155
+ } else {
156
+ item.promise = null;
157
+ item.pending = false;
158
+ item.error_message = null;
159
+ }
160
+ }
161
+ }
162
+
163
+ activate(item: ItemState): boolean | Promise<ContextmenuActivateResult> {
164
+ if (item.is_menu) {
165
+ this.expand_selected();
166
+ } else {
167
+ if (item.disabled()) return false;
168
+ let returned;
169
+ try {
170
+ returned = item.run()();
171
+ } catch (error) {
172
+ const message = typeof error?.message === 'string' ? error.message : undefined;
173
+ item.error_message = message ?? 'unknown error';
174
+ this.error = message;
175
+ }
176
+ if (is_promise(returned)) {
177
+ item.pending = true;
178
+ item.error_message = null;
179
+ const promise = (item.promise = returned
180
+ .then(
181
+ (result) => {
182
+ if (promise !== item.promise) return;
183
+ if (typeof result?.ok === 'boolean') {
184
+ if (result.ok) {
185
+ if (result.close !== false) {
186
+ this.close();
187
+ }
188
+ } else {
189
+ const message = typeof result.message === 'string' ? result.message : undefined;
190
+ item.error_message = message ?? 'unknown error';
191
+ this.error = message;
192
+ }
193
+ } else {
194
+ // void or undefined - default behavior is to close
195
+ this.close();
196
+ }
197
+ return result;
198
+ },
199
+ (err) => {
200
+ if (promise !== item.promise) return;
201
+ const message = typeof err?.message === 'string' ? err.message : undefined;
202
+ item.error_message = message ?? 'unknown error';
203
+ this.error = message;
204
+ },
205
+ )
206
+ .finally(() => {
207
+ if (promise !== item.promise) return;
208
+ item.pending = false;
209
+ item.promise = null;
210
+ }));
211
+ return item.promise; // async path
212
+ }
213
+ // synchronous path
214
+ if (typeof returned?.ok === 'boolean') {
215
+ if (returned.ok) {
216
+ if (returned.close !== false) {
217
+ this.close();
218
+ }
219
+ } else {
220
+ const message = typeof returned.message === 'string' ? returned.message : undefined;
221
+ item.error_message = message ?? 'unknown error';
222
+ this.error = message;
223
+ }
224
+ } else {
225
+ // void or undefined - default behavior is to close
226
+ this.close();
227
+ }
228
+ }
229
+ return true;
230
+ }
231
+
232
+ activate_selected(): void | boolean | Promise<ContextmenuActivateResult> {
233
+ const selected = this.selections.at(-1);
234
+ if (selected) {
235
+ return this.activate(selected);
236
+ }
237
+ this.select_first();
238
+ }
239
+
240
+ // Instead of diffing, this does the simple thing and
241
+ // deselects everything and then re-creates the list of selections.
242
+ // Could be improved but it's fine because we're using mutation and the N is very small,
243
+ // and it allows us to have a single code path for the various selection methods.
244
+ /**
245
+ * Activates the selected entry, or if none, selects the first.
246
+ */
247
+ // TODO implement focus management per APG: call .focus() on the selected item's DOM element (requires storing element refs in EntryState/SubmenuState)
248
+ select(item: ItemState): void {
249
+ if (this.selections.at(-1) === item) return;
250
+ for (const s of this.selections) s.selected = false;
251
+ const new_selections: Array<ItemState> = [];
252
+ let i: ItemState | RootMenuState = item;
253
+ do {
254
+ i.selected = true;
255
+ new_selections.unshift(i);
256
+ } while ((i = i.menu) && i.menu); // eslint-disable-line @typescript-eslint/no-unnecessary-condition
257
+ this.selections = new_selections;
258
+ }
259
+
260
+ collapse_selected(): void {
261
+ if (!this.can_collapse) return;
262
+ const deselected = this.selections.at(-1)!;
263
+ deselected.selected = false;
264
+ this.selections = this.selections.slice(0, -1);
265
+ }
266
+
267
+ expand_selected(): void {
268
+ if (!this.can_expand) return;
269
+ const parent = this.selections.at(-1);
270
+ if (!parent?.is_menu || !parent.items.length) return;
271
+ const selected = parent.items[0]!;
272
+ selected.selected = true;
273
+ this.selections = [...this.selections, selected];
274
+ }
275
+
276
+ select_next(): void {
277
+ if (!this.selections.length) {
278
+ this.select_first();
279
+ return;
280
+ }
281
+ const item = this.selections.at(-1)!;
282
+ const index = item.menu.items.indexOf(item);
283
+ this.select(item.menu.items[index === item.menu.items.length - 1 ? 0 : index + 1]!);
284
+ }
285
+
286
+ select_previous(): void {
287
+ if (!this.selections.length) {
288
+ this.select_last();
289
+ return;
290
+ }
291
+ const item = this.selections.at(-1)!;
292
+ const index = item.menu.items.indexOf(item);
293
+ this.select(item.menu.items[index === 0 ? item.menu.items.length - 1 : index - 1]!);
294
+ }
295
+
296
+ select_first(): void {
297
+ const menu = this.selections.at(-1)?.menu ?? this.root_menu;
298
+ if (!menu.items.length) return;
299
+ this.select(menu.items[0]!);
300
+ }
301
+
302
+ select_last(): void {
303
+ const {items} = this.selections.at(-1)?.menu ?? this.root_menu;
304
+ this.select(items.at(-1)!);
305
+ }
306
+
307
+ /**
308
+ * Used by `ContextmenuEntry` and custom entry components
309
+ * @initializes
310
+ */
311
+ add_entry(run: () => ContextmenuRun, disabled: () => boolean = () => false): EntryState {
312
+ const menu = contextmenu_submenu_context.get_maybe() ?? this.root_menu;
313
+ const entry = new EntryState(menu, run, disabled);
314
+ menu.items = [...menu.items, entry];
315
+ // TODO messy, runs more than needed
316
+ onDestroy(() => {
317
+ menu.items = [];
318
+ });
319
+ return entry;
320
+ }
321
+
322
+ /**
323
+ * @initializes
324
+ */
325
+ add_submenu(): SubmenuState {
326
+ const menu = contextmenu_submenu_context.get_maybe() ?? this.root_menu;
327
+ const submenu = new SubmenuState(menu, menu.depth + 1);
328
+ menu.items = [...menu.items, submenu];
329
+ contextmenu_submenu_context.set(submenu);
330
+ // TODO messy, runs more than needed
331
+ onDestroy(() => {
332
+ menu.items = [];
333
+ });
334
+ return submenu;
335
+ }
336
+ }
337
+
338
+ // The dataset key must not have capital letters or dashes or it'll differ between JS and DOM:
339
+ // https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/dataset
340
+ const CONTEXTMENU_DATASET_KEY = 'contextmenu';
341
+ const CONTEXTMENU_DOM_QUERY = `a,[data-${CONTEXTMENU_DATASET_KEY}]`;
342
+ const contextmenu_cache: Map<string, ContextmenuParams | Array<ContextmenuParams>> = new Map();
343
+ let cache_key_counter = 0;
344
+
345
+ /**
346
+ * Creates an attachment that sets up contextmenu behavior on an element.
347
+ * @param params Contextmenu parameters or nullish to disable
348
+ */
349
+ export const contextmenu_attachment =
350
+ <T extends ContextmenuParams, U extends T | Array<T>>(
351
+ params: U | null | undefined,
352
+ ): Attachment<HTMLElement | SVGElement> =>
353
+ (el): undefined | (() => void) => {
354
+ // TODO could clean up the dataset attr, maybe use a weakmap?
355
+ if (params == null) return;
356
+
357
+ // Only create key once per element, reuse on updates
358
+ let key = el.dataset[CONTEXTMENU_DATASET_KEY];
359
+ if (!key) {
360
+ key = cache_key_counter++ + '';
361
+ el.dataset[CONTEXTMENU_DATASET_KEY] = key;
362
+ }
363
+
364
+ contextmenu_cache.set(key, params);
365
+
366
+ return () => {
367
+ contextmenu_cache.delete(key);
368
+ };
369
+ };
370
+
371
+ const CONTEXTMENU_OPEN_VIBRATE_DURATION = 17;
372
+
373
+ export interface ContextmenuOpenOptions {
374
+ link_enabled?: boolean;
375
+ text_enabled?: boolean;
376
+ separator_enabled?: boolean;
377
+ vibrate?: boolean;
378
+ }
379
+
380
+ /**
381
+ * Opens the contextmenu, if appropriate,
382
+ * querying the menu items from the DOM starting at the event target.
383
+ * @param target the leaf element from which to open the contextmenu
384
+ * @param x the page X coordinate at which to open the contextmenu, typically the mouse `pageX`
385
+ * @param y the page Y coordinate at which to open the contextmenu, typically the mouse `pageY`
386
+ * @param contextmenu the contextmenu store
387
+ * @param options optional configuration for filtering entries and haptic feedback
388
+ * @returns a boolean indicating if the menu was opened or not
389
+ */
390
+ export const contextmenu_open = (
391
+ target: HTMLElement | SVGElement,
392
+ x: number,
393
+ y: number,
394
+ contextmenu: ContextmenuState,
395
+ options?: ContextmenuOpenOptions,
396
+ ): boolean => {
397
+ const {
398
+ link_enabled = true,
399
+ text_enabled = true,
400
+ separator_enabled = true,
401
+ vibrate = true,
402
+ } = options ?? EMPTY_OBJECT;
403
+
404
+ const params = contextmenu_query_params(target)?.filter(
405
+ (p) =>
406
+ typeof p === 'function' ||
407
+ ((p.snippet !== 'link' || link_enabled) &&
408
+ (p.snippet !== 'text' || text_enabled) &&
409
+ (p.snippet !== 'separator' || separator_enabled)),
410
+ );
411
+
412
+ // No-op if empty
413
+ if (!params?.length) return false;
414
+
415
+ contextmenu.open(params, x, y);
416
+
417
+ // `navigator.vibrate()` works with `ContextmenuRoot` but gets blocked by some browsers
418
+ // when used with `ContextmenuRootForSafariCompatibility` because its longpress
419
+ // workaround triggers from a timeout rather than a direct user interaction.
420
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
421
+ if (BROWSER && vibrate && navigator.vibrate) {
422
+ navigator.vibrate(CONTEXTMENU_OPEN_VIBRATE_DURATION);
423
+ }
424
+
425
+ return true;
426
+ };
427
+
428
+ const contextmenu_query_params = (
429
+ target: HTMLElement | SVGElement,
430
+ ): null | Array<ContextmenuParams> => {
431
+ let params: null | Array<ContextmenuParams> = null;
432
+ // crawl DOM for contextmenu entries
433
+ let el: HTMLElement | SVGElement | null | undefined = target;
434
+ let cache_key: string, cached: ContextmenuParams | Array<ContextmenuParams> | undefined;
435
+ while ((el = el?.closest(CONTEXTMENU_DOM_QUERY))) {
436
+ if ((cache_key = el.dataset[CONTEXTMENU_DATASET_KEY]!)) {
437
+ params ??= [];
438
+ cached = contextmenu_cache.get(cache_key);
439
+ if (cached === undefined) {
440
+ continue;
441
+ }
442
+ // preserve bubbling order
443
+ if (Array.isArray(cached)) {
444
+ (params ??= []).push(...cached);
445
+ } else {
446
+ (params ??= []).push(cached);
447
+ }
448
+ }
449
+ if (el.tagName === 'A') {
450
+ (params ??= []).push({
451
+ snippet: 'link',
452
+ // anchor elements have the full url, but we want the slash-prefixed/absolute/root-relative version
453
+ props: {href: url_to_root_relative((el as HTMLAnchorElement).href)},
454
+ });
455
+ }
456
+ el = el.parentElement;
457
+ }
458
+ // add "copy text" entry if anything is selected and we have any other entries
459
+ if (params) {
460
+ const text = window.getSelection()?.toString();
461
+ if (text) {
462
+ params.unshift({
463
+ snippet: 'text',
464
+ props: {
465
+ content: 'copy text',
466
+ icon: '📋',
467
+ run: async () => {
468
+ await navigator.clipboard.writeText(text);
469
+ },
470
+ },
471
+ });
472
+ }
473
+ }
474
+ return params;
475
+ };
476
+
477
+ export const contextmenu_context = create_context<ContextmenuState>();
478
+
479
+ export const contextmenu_submenu_context = create_context<SubmenuState>();
480
+
481
+ export const contextmenu_dimensions_context = create_context(() => new Dimensions());
482
+
483
+ // Global registry of non-scoped contextmenu roots (only used in DEV)
484
+ const non_scoped_roots: Set<symbol> = new Set();
485
+
486
+ /**
487
+ * Registers a contextmenu root and warns if multiple non-scoped roots are detected.
488
+ * Only active in development mode. Automatically handles cleanup on unmount.
489
+ *
490
+ * @param get_scoped Getter function that returns the current scoped value
491
+ */
492
+ export const contextmenu_check_global_root = (get_scoped: () => boolean): void => {
493
+ $effect(() => {
494
+ const id = Symbol('contextmenu_root');
495
+
496
+ if (!get_scoped()) {
497
+ // Register as global (non-scoped)
498
+ non_scoped_roots.add(id);
499
+
500
+ if (non_scoped_roots.size > 1) {
501
+ // eslint-disable-next-line no-console
502
+ console.error(
503
+ `Detected multiple non-scoped contextmenu roots (${non_scoped_roots.size} mounted). ` +
504
+ 'Only one global contextmenu root should be active at a time. ' +
505
+ 'Are you missing a `scoped` attribute?',
506
+ );
507
+ }
508
+ }
509
+
510
+ // Cleanup: unregister when scoped changes or component unmounts
511
+ return () => {
512
+ non_scoped_roots.delete(id);
513
+ };
514
+ });
515
+ };