@fuzdev/fuz_ui 0.202.0 → 0.203.1

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 (71) hide show
  1. package/dist/ApiIndex.svelte +6 -3
  2. package/dist/ApiIndex.svelte.d.ts +1 -1
  3. package/dist/ApiIndex.svelte.d.ts.map +1 -1
  4. package/dist/ApiModule.svelte +72 -21
  5. package/dist/ApiModule.svelte.d.ts +1 -1
  6. package/dist/ApiModule.svelte.d.ts.map +1 -1
  7. package/dist/ContextmenuEntry.svelte +15 -16
  8. package/dist/ContextmenuEntry.svelte.d.ts +3 -2
  9. package/dist/ContextmenuEntry.svelte.d.ts.map +1 -1
  10. package/dist/ContextmenuIcon.svelte +30 -0
  11. package/dist/ContextmenuIcon.svelte.d.ts +9 -0
  12. package/dist/ContextmenuIcon.svelte.d.ts.map +1 -0
  13. package/dist/ContextmenuLinkEntry.svelte +23 -12
  14. package/dist/ContextmenuLinkEntry.svelte.d.ts +2 -1
  15. package/dist/ContextmenuLinkEntry.svelte.d.ts.map +1 -1
  16. package/dist/ContextmenuMenu.svelte +207 -0
  17. package/dist/ContextmenuMenu.svelte.d.ts +41 -0
  18. package/dist/ContextmenuMenu.svelte.d.ts.map +1 -0
  19. package/dist/ContextmenuRoot.svelte +58 -260
  20. package/dist/ContextmenuRoot.svelte.d.ts +2 -60
  21. package/dist/ContextmenuRoot.svelte.d.ts.map +1 -1
  22. package/dist/ContextmenuRootForSafariCompatibility.svelte +72 -283
  23. package/dist/ContextmenuRootForSafariCompatibility.svelte.d.ts +2 -58
  24. package/dist/ContextmenuRootForSafariCompatibility.svelte.d.ts.map +1 -1
  25. package/dist/ContextmenuSeparator.svelte +2 -1
  26. package/dist/ContextmenuSeparator.svelte.d.ts.map +1 -1
  27. package/dist/ContextmenuSubmenu.svelte +31 -25
  28. package/dist/ContextmenuSubmenu.svelte.d.ts +3 -2
  29. package/dist/ContextmenuSubmenu.svelte.d.ts.map +1 -1
  30. package/dist/ContextmenuTextEntry.svelte +6 -4
  31. package/dist/ContextmenuTextEntry.svelte.d.ts +3 -1
  32. package/dist/ContextmenuTextEntry.svelte.d.ts.map +1 -1
  33. package/dist/DeclarationLink.svelte +15 -2
  34. package/dist/DeclarationLink.svelte.d.ts +7 -0
  35. package/dist/DeclarationLink.svelte.d.ts.map +1 -1
  36. package/dist/DocsLink.svelte +3 -1
  37. package/dist/DocsLink.svelte.d.ts.map +1 -1
  38. package/dist/DocsTertiaryNav.svelte +2 -1
  39. package/dist/DocsTertiaryNav.svelte.d.ts.map +1 -1
  40. package/dist/LibraryDetail.svelte +9 -3
  41. package/dist/LibraryDetail.svelte.d.ts +1 -1
  42. package/dist/LibraryDetail.svelte.d.ts.map +1 -1
  43. package/dist/ModuleLink.svelte +2 -1
  44. package/dist/ModuleLink.svelte.d.ts.map +1 -1
  45. package/dist/TypeLink.svelte +2 -1
  46. package/dist/TypeLink.svelte.d.ts.map +1 -1
  47. package/dist/contextmenu_helpers.d.ts +324 -1
  48. package/dist/contextmenu_helpers.d.ts.map +1 -1
  49. package/dist/contextmenu_helpers.js +403 -2
  50. package/dist/contextmenu_state.svelte.d.ts +61 -15
  51. package/dist/contextmenu_state.svelte.d.ts.map +1 -1
  52. package/dist/contextmenu_state.svelte.js +206 -119
  53. package/dist/declaration.svelte.d.ts +1 -0
  54. package/dist/declaration.svelte.d.ts.map +1 -1
  55. package/dist/declaration.svelte.js +2 -1
  56. package/dist/icons.d.ts +15 -3
  57. package/dist/icons.d.ts.map +1 -1
  58. package/dist/icons.js +17 -3
  59. package/dist/library.svelte.d.ts +30 -3
  60. package/dist/library.svelte.d.ts.map +1 -1
  61. package/dist/library.svelte.js +38 -0
  62. package/dist/module.svelte.d.ts +62 -1
  63. package/dist/module.svelte.d.ts.map +1 -1
  64. package/dist/module.svelte.js +71 -1
  65. package/package.json +2 -2
  66. package/src/lib/contextmenu_helpers.ts +546 -3
  67. package/src/lib/contextmenu_state.svelte.ts +219 -119
  68. package/src/lib/declaration.svelte.ts +2 -1
  69. package/src/lib/icons.ts +19 -3
  70. package/src/lib/library.svelte.ts +43 -1
  71. package/src/lib/module.svelte.ts +97 -2
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import {library_context, type Library} from './library.svelte.js';
2
+ import {set_library_context_with_fallback, type Library} from './library.svelte.js';
3
3
  import {tome_get_by_slug, type Tome} from './tome.js';
4
4
  import TomeContent from './TomeContent.svelte';
5
5
  import TomeLink from './TomeLink.svelte';
@@ -9,13 +9,13 @@
9
9
  import {create_api_search} from './api_search.svelte.js';
10
10
 
11
11
  const {
12
- library = library_context.get(),
12
+ library: library_prop,
13
13
  tome = tome_get_by_slug('api'),
14
14
  minimal = false,
15
15
  }: {
16
16
  /**
17
17
  * The library instance to render API docs for.
18
- * Defaults to getting from library_context.
18
+ * Defaults to getting from `library_context`.
19
19
  */
20
20
  library?: Library;
21
21
  /**
@@ -30,6 +30,9 @@
30
30
  minimal?: boolean;
31
31
  } = $props();
32
32
 
33
+ const get_library = set_library_context_with_fallback(() => library_prop, 'ApiIndex');
34
+ const library = $derived(get_library());
35
+
33
36
  const search = $derived(create_api_search(library));
34
37
  </script>
35
38
 
@@ -3,7 +3,7 @@ import { type Tome } from './tome.js';
3
3
  type $$ComponentProps = {
4
4
  /**
5
5
  * The library instance to render API docs for.
6
- * Defaults to getting from library_context.
6
+ * Defaults to getting from `library_context`.
7
7
  */
8
8
  library?: Library;
9
9
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"ApiIndex.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/ApiIndex.svelte"],"names":[],"mappings":"AAGA,OAAO,EAAkB,KAAK,OAAO,EAAC,MAAM,qBAAqB,CAAC;AAClE,OAAO,EAAmB,KAAK,IAAI,EAAC,MAAM,WAAW,CAAC;AAQrD,KAAK,gBAAgB,GAAI;IACxB;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;OAGG;IACH,IAAI,CAAC,EAAE,IAAI,CAAC;IACZ;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;CAClB,CAAC;AAgDH,QAAA,MAAM,QAAQ,sDAAwC,CAAC;AACvD,KAAK,QAAQ,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC;AAC5C,eAAe,QAAQ,CAAC"}
1
+ {"version":3,"file":"ApiIndex.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/ApiIndex.svelte"],"names":[],"mappings":"AAGA,OAAO,EAAoC,KAAK,OAAO,EAAC,MAAM,qBAAqB,CAAC;AACpF,OAAO,EAAmB,KAAK,IAAI,EAAC,MAAM,WAAW,CAAC;AAQrD,KAAK,gBAAgB,GAAI;IACxB;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;OAGG;IACH,IAAI,CAAC,EAAE,IAAI,CAAC;IACZ;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;CAClB,CAAC;AAmDH,QAAA,MAAM,QAAQ,sDAAwC,CAAC;AACvD,KAAK,QAAQ,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC;AAC5C,eAAe,QAAQ,CAAC"}
@@ -1,18 +1,19 @@
1
1
  <script lang="ts">
2
- import {library_context, type Library} from './library.svelte.js';
2
+ import {set_library_context_with_fallback, type Library} from './library.svelte.js';
3
3
  import {tome_get_by_slug, type Tome} from './tome.js';
4
4
  import TomeContent from './TomeContent.svelte';
5
5
  import TomeSection from './TomeSection.svelte';
6
6
  import TomeSectionHeader from './TomeSectionHeader.svelte';
7
7
  import DocsSearch from './DocsSearch.svelte';
8
8
  import ModuleLink from './ModuleLink.svelte';
9
+ import DeclarationLink from './DeclarationLink.svelte';
9
10
  import Mdz from './Mdz.svelte';
10
11
  import ApiDeclarationList from './ApiDeclarationList.svelte';
11
12
  import {create_module_declaration_search} from './api_search.svelte.js';
12
13
 
13
14
  const {
14
15
  module_path: module_path_param,
15
- library = library_context.get(),
16
+ library: library_prop,
16
17
  tome = tome_get_by_slug('api'),
17
18
  }: {
18
19
  /**
@@ -21,7 +22,7 @@
21
22
  module_path: string | Array<string>;
22
23
  /**
23
24
  * The library instance to render API docs for.
24
- * Defaults to getting from library_context.
25
+ * Defaults to getting from `library_context`.
25
26
  */
26
27
  library?: Library;
27
28
  /**
@@ -31,6 +32,9 @@
31
32
  tome?: Tome;
32
33
  } = $props();
33
34
 
35
+ const get_library = set_library_context_with_fallback(() => library_prop, 'ApiModule');
36
+ const library = $derived(get_library());
37
+
34
38
  // normalize module_path to string (could be array from [...module_path] route param)
35
39
  const module_path = $derived(
36
40
  Array.isArray(module_path_param) ? module_path_param.join('/') : module_path_param,
@@ -89,30 +93,77 @@
89
93
  <Mdz content={module.module_comment} />
90
94
  </section>
91
95
  {/if}
96
+ {#if source_url}
97
+ <section>
98
+ <!-- eslint-disable-next-line svelte/no-navigation-without-resolve -->
99
+ <a href={source_url} class="chip" target="_blank" rel="noopener">view source</a>
100
+ </section>
101
+ {/if}
92
102
  <!-- Declarations section -->
93
- <TomeSection>
94
- <TomeSectionHeader text="Declarations" />
103
+ {#if module.has_declarations}
104
+ <TomeSection>
105
+ <TomeSectionHeader text="Declarations" />
95
106
 
96
- <section>
97
107
  {#if search.all.length > 1}
98
- <DocsSearch
99
- placeholder="search declarations in this module..."
100
- declaration_count={search.all.length}
101
- filtered_declaration_count={search.query.trim() ? search.filtered.length : undefined}
102
- bind:search_query={search.query}
103
- />
108
+ <section>
109
+ <DocsSearch
110
+ placeholder="search declarations in this module..."
111
+ declaration_count={search.all.length}
112
+ filtered_declaration_count={search.query.trim() ? search.filtered.length : undefined}
113
+ bind:search_query={search.query}
114
+ />
115
+ </section>
104
116
  {/if}
105
117
 
106
- {#if source_url}
107
- <p>
108
- <!-- eslint-disable-next-line svelte/no-navigation-without-resolve -->
109
- <a href={source_url} class="chip" target="_blank" rel="noopener">view source</a>
110
- </p>
111
- {/if}
112
- </section>
118
+ <ApiDeclarationList declarations={search.filtered} search_query={search.query} />
119
+ </TomeSection>
120
+ {/if}
113
121
 
114
- <ApiDeclarationList declarations={search.filtered} search_query={search.query} />
115
- </TomeSection>
122
+ <!-- Re-exports section -->
123
+ {#if module.has_any_re_exports}
124
+ <TomeSection>
125
+ <TomeSectionHeader text="Re-exports" />
126
+ <ul class="unstyled">
127
+ {#each module.star_exports as star_path (star_path)}
128
+ <li class="mb_lg">
129
+ <p class="mb_sm">everything from <ModuleLink module_path={star_path} /></p>
130
+ </li>
131
+ {/each}
132
+ {#each module.external_star_exports as specifier (specifier)}
133
+ <li class="mb_lg">
134
+ <p class="mb_sm">everything from <code>{specifier}</code></p>
135
+ </li>
136
+ {/each}
137
+ {#each module.re_exports_by_module as [from_path, entries] (from_path)}
138
+ <li class="mb_lg">
139
+ <p class="mb_sm">from <ModuleLink module_path={from_path} /></p>
140
+ <div class="row gap_md flex-wrap:wrap">
141
+ {#each entries as entry (entry.name)}
142
+ <DeclarationLink name={entry.name} module_path={from_path}>
143
+ {#if entry.typeOnly}<small>type</small>
144
+ {/if}{entry.name}
145
+ </DeclarationLink>
146
+ {/each}
147
+ </div>
148
+ </li>
149
+ {/each}
150
+ {#each module.external_re_exports_by_specifier as [specifier, entries] (specifier)}
151
+ <li class="mb_lg">
152
+ <p class="mb_sm">from <code>{specifier}</code></p>
153
+ <div class="row gap_md flex-wrap:wrap">
154
+ {#each entries as entry (entry.name)}
155
+ <span class="chip">
156
+ {#if entry.typeOnly}<small>type</small>
157
+ {/if}{#if entry.originalName}<small>{entry.originalName} as</small>
158
+ {/if}{entry.name}
159
+ </span>
160
+ {/each}
161
+ </div>
162
+ </li>
163
+ {/each}
164
+ </ul>
165
+ </TomeSection>
166
+ {/if}
116
167
 
117
168
  <!-- Depends on section -->
118
169
  {#if module.has_dependencies}
@@ -7,7 +7,7 @@ type $$ComponentProps = {
7
7
  module_path: string | Array<string>;
8
8
  /**
9
9
  * The library instance to render API docs for.
10
- * Defaults to getting from library_context.
10
+ * Defaults to getting from `library_context`.
11
11
  */
12
12
  library?: Library;
13
13
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"ApiModule.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/ApiModule.svelte"],"names":[],"mappings":"AAGA,OAAO,EAAkB,KAAK,OAAO,EAAC,MAAM,qBAAqB,CAAC;AAClE,OAAO,EAAmB,KAAK,IAAI,EAAC,MAAM,WAAW,CAAC;AAUrD,KAAK,gBAAgB,GAAI;IACxB;;OAEG;IACH,WAAW,EAAE,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC;IACpC;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;OAGG;IACH,IAAI,CAAC,EAAE,IAAI,CAAC;CACZ,CAAC;AAiIH,QAAA,MAAM,SAAS,sDAAwC,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}
1
+ {"version":3,"file":"ApiModule.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/ApiModule.svelte"],"names":[],"mappings":"AAGA,OAAO,EAAoC,KAAK,OAAO,EAAC,MAAM,qBAAqB,CAAC;AACpF,OAAO,EAAmB,KAAK,IAAI,EAAC,MAAM,WAAW,CAAC;AAWrD,KAAK,gBAAgB,GAAI;IACxB;;OAEG;IACH,WAAW,EAAE,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC;IACpC;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;OAGG;IACH,IAAI,CAAC,EAAE,IAAI,CAAC;CACZ,CAAC;AAoLH,QAAA,MAAM,SAAS,sDAAwC,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}
@@ -1,9 +1,11 @@
1
1
  <script lang="ts">
2
- import type {Snippet} from 'svelte';
2
+ import {onDestroy, type Snippet} from 'svelte';
3
3
  import {swallow} from '@fuzdev/fuz_util/dom.js';
4
4
 
5
5
  import PendingAnimation from './PendingAnimation.svelte';
6
6
  import {contextmenu_context, type ContextmenuRun} from './contextmenu_state.svelte.js';
7
+ import type {SvgData} from './svg.js';
8
+ import ContextmenuIcon from './ContextmenuIcon.svelte';
7
9
 
8
10
  const {
9
11
  run,
@@ -12,7 +14,7 @@
12
14
  disabled: disabled_prop = false,
13
15
  }: {
14
16
  run: ContextmenuRun;
15
- icon?: string | Snippet;
17
+ icon?: SvgData | string | Snippet;
16
18
  children: Snippet;
17
19
  disabled?: boolean;
18
20
  } = $props();
@@ -28,6 +30,11 @@
28
30
 
29
31
  const {selected, pending, error_message} = $derived(entry);
30
32
  const disabled = $derived(entry.disabled());
33
+
34
+ let activate_timeout: NodeJS.Timeout | null = null;
35
+ onDestroy(() => {
36
+ if (activate_timeout !== null) clearTimeout(activate_timeout);
37
+ });
31
38
  </script>
32
39
 
33
40
  <!-- disabling the a11y warning because a parent element handles keyboard events -->
@@ -37,7 +44,6 @@
37
44
  class:selected
38
45
  class:disabled
39
46
  role="menuitem"
40
- aria-label="contextmenu entry"
41
47
  aria-disabled={disabled}
42
48
  tabindex="-1"
43
49
  title={error_message ? `Error: ${error_message}` : undefined}
@@ -46,7 +52,11 @@
46
52
  : () => {
47
53
  // This timeout lets event handlers react to the current DOM
48
54
  // before the item's changes are applied.
49
- setTimeout(() => contextmenu.activate(entry));
55
+ if (activate_timeout !== null) clearTimeout(activate_timeout);
56
+ activate_timeout = setTimeout(() => {
57
+ activate_timeout = null;
58
+ void contextmenu.activate(entry);
59
+ });
50
60
  }}
51
61
  onmouseenter={disabled
52
62
  ? undefined
@@ -57,20 +67,9 @@
57
67
  >
58
68
  <div class="content">
59
69
  <div class="icon">
60
- {#if typeof icon === 'string'}
61
- {icon}
62
- {:else if icon}
63
- {@render icon()}
64
- {/if}
70
+ <ContextmenuIcon {icon} />
65
71
  </div>
66
72
  <div class="title">{@render children()}</div>
67
73
  {#if pending}<PendingAnimation />{:else if error_message}⚠️{/if}
68
74
  </div>
69
75
  </li>
70
-
71
- <style>
72
- /* TODO hacky, needed because the base `.menuitem` added z-index */
73
- .menuitem {
74
- z-index: unset;
75
- }
76
- </style>
@@ -1,8 +1,9 @@
1
- import type { Snippet } from 'svelte';
1
+ import { type Snippet } from 'svelte';
2
2
  import { type ContextmenuRun } from './contextmenu_state.svelte.js';
3
+ import type { SvgData } from './svg.js';
3
4
  type $$ComponentProps = {
4
5
  run: ContextmenuRun;
5
- icon?: string | Snippet;
6
+ icon?: SvgData | string | Snippet;
6
7
  children: Snippet;
7
8
  disabled?: boolean;
8
9
  };
@@ -1 +1 @@
1
- {"version":3,"file":"ContextmenuEntry.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/ContextmenuEntry.svelte"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,QAAQ,CAAC;AAIpC,OAAO,EAAsB,KAAK,cAAc,EAAC,MAAM,+BAA+B,CAAC;AAEtF,KAAK,gBAAgB,GAAI;IACxB,GAAG,EAAE,cAAc,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IACxB,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC;AA2DH,QAAA,MAAM,gBAAgB,sDAAwC,CAAC;AAC/D,KAAK,gBAAgB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC5D,eAAe,gBAAgB,CAAC"}
1
+ {"version":3,"file":"ContextmenuEntry.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/ContextmenuEntry.svelte"],"names":[],"mappings":"AAGA,OAAO,EAAY,KAAK,OAAO,EAAC,MAAM,QAAQ,CAAC;AAI/C,OAAO,EAAsB,KAAK,cAAc,EAAC,MAAM,+BAA+B,CAAC;AACvF,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,UAAU,CAAC;AAGrC,KAAK,gBAAgB,GAAI;IACxB,GAAG,EAAE,cAAc,CAAC;IACpB,IAAI,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,OAAO,CAAC;IAClC,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC;AAgEH,QAAA,MAAM,gBAAgB,sDAAwC,CAAC;AAC/D,KAAK,gBAAgB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC5D,eAAe,gBAAgB,CAAC"}
@@ -0,0 +1,30 @@
1
+ <script lang="ts">
2
+ import type {Snippet} from 'svelte';
3
+
4
+ import type {SvgData} from './svg.js';
5
+ import Svg from './Svg.svelte';
6
+
7
+ /**
8
+ * Renders a contextmenu entry's `icon` value, shared by `ContextmenuEntry`,
9
+ * `ContextmenuSubmenu`, and `ContextmenuLinkEntry`. Renders nothing when
10
+ * `icon` is `undefined`.
11
+ *
12
+ * @module
13
+ */
14
+
15
+ const {
16
+ icon,
17
+ }: {
18
+ icon?: SvgData | string | Snippet;
19
+ } = $props();
20
+ </script>
21
+
22
+ {#if typeof icon === 'string'}
23
+ {icon}
24
+ {:else if typeof icon === 'object'}
25
+ <!-- `SvgData` is a plain object; detect it positively by shape rather than
26
+ introspecting the snippet (whose runtime form is opaque/unsupported). -->
27
+ <Svg data={icon} />
28
+ {:else if icon}
29
+ {@render icon()}
30
+ {/if}
@@ -0,0 +1,9 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { SvgData } from './svg.js';
3
+ type $$ComponentProps = {
4
+ icon?: SvgData | string | Snippet;
5
+ };
6
+ declare const ContextmenuIcon: import("svelte").Component<$$ComponentProps, {}, "">;
7
+ type ContextmenuIcon = ReturnType<typeof ContextmenuIcon>;
8
+ export default ContextmenuIcon;
9
+ //# sourceMappingURL=ContextmenuIcon.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ContextmenuIcon.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/ContextmenuIcon.svelte"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,QAAQ,CAAC;AAEpC,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,UAAU,CAAC;AAGrC,KAAK,gBAAgB,GAAI;IACxB,IAAI,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,OAAO,CAAC;CAClC,CAAC;AA+BH,QAAA,MAAM,eAAe,sDAAwC,CAAC;AAC9D,KAAK,eAAe,GAAG,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC;AAC1D,eAAe,eAAe,CAAC"}
@@ -5,6 +5,10 @@
5
5
  import {page} from '$app/state';
6
6
 
7
7
  import {contextmenu_context} from './contextmenu_state.svelte.js';
8
+ import {icon_external_link, icon_link} from './icons.js';
9
+ import type {SvgData} from './svg.js';
10
+ import Svg from './Svg.svelte';
11
+ import ContextmenuIcon from './ContextmenuIcon.svelte';
8
12
 
9
13
  const {
10
14
  href,
@@ -14,7 +18,7 @@
14
18
  external_rel = 'noreferrer', // TODO smarter defaults
15
19
  }: {
16
20
  href: string;
17
- icon?: string | Snippet; // TODO @many rethink this API, add svg icon fallback
21
+ icon?: SvgData | string | Snippet;
18
22
  children?: Snippet; // TODO @many rethink this API
19
23
  disabled?: boolean;
20
24
  external_rel?: string;
@@ -35,22 +39,27 @@
35
39
  () => disabled_prop,
36
40
  );
37
41
 
42
+ // keyboard selection, like `ContextmenuEntry` - distinct from `current_page` below
43
+ const {selected} = $derived(entry);
38
44
  const disabled = $derived(entry.disabled());
39
45
 
40
- // TODO move or upstream? rename? `print_url`?
41
- const format_url = (url: string, host: string = location.host): string => {
46
+ // TODO move or upstream?
47
+ const print_url = (url: string, host: string = location.host): string => {
42
48
  const formatted = strip_start(strip_start(url, 'https://'), 'http://');
43
49
  return formatted.startsWith(host + '/') ? strip_start(formatted, host) : formatted;
44
50
  };
45
51
 
46
- const text = $derived(format_url(href));
52
+ const text = $derived(print_url(href));
47
53
  const external = $derived(!(text[0] === '.' || (text[0] === '/' && text[1] !== '/')));
48
54
  const rel = $derived(external ? external_rel : undefined);
49
55
 
50
- const selected = $derived(page.url.pathname === href);
56
+ const current_page = $derived(page.url.pathname === href);
51
57
  </script>
52
58
 
53
59
  <li role="none">
60
+ <!-- `deselectable` is a hack to recover `cursor: pointer` when selected: these links always
61
+ navigate, but `.menuitem.selected` forces `cursor: default`. The real fix is a `.menuitem`
62
+ semantics cleanup (selected-as-active vs selected-as-highlight) - TODO @many -->
54
63
  <!-- TODO -next-line doesnt work? -->
55
64
  <!-- eslint-disable svelte/no-navigation-without-resolve -->
56
65
  <a
@@ -58,8 +67,10 @@
58
67
  class="menuitem plain"
59
68
  class:selected
60
69
  class:disabled
70
+ class:deselectable={!disabled}
61
71
  role="menuitem"
62
72
  aria-disabled={disabled}
73
+ aria-current={current_page ? 'page' : undefined}
63
74
  tabindex="-1"
64
75
  {href}
65
76
  {rel}
@@ -81,10 +92,10 @@
81
92
  >
82
93
  <div class="content">
83
94
  <div class="icon">
84
- {#if typeof icon === 'string'}
85
- {icon}
86
- {:else if icon}
87
- {@render icon()}
95
+ {#if icon}
96
+ <ContextmenuIcon {icon} />
97
+ {:else}
98
+ <Svg data={external ? icon_external_link : icon_link} />
88
99
  {/if}
89
100
  </div>
90
101
  <div class="title">
@@ -107,8 +118,8 @@
107
118
  a:hover .text {
108
119
  text-decoration: underline;
109
120
  }
110
- /* TODO hacky, needed because the base `.menuitem` added z-index */
111
- .menuitem {
112
- z-index: unset;
121
+ /* Mark the current page distinctly from the keyboard selection highlight. */
122
+ a[aria-current='page'] .text {
123
+ font-weight: 700;
113
124
  }
114
125
  </style>
@@ -1,7 +1,8 @@
1
1
  import type { Snippet } from 'svelte';
2
+ import type { SvgData } from './svg.js';
2
3
  type $$ComponentProps = {
3
4
  href: string;
4
- icon?: string | Snippet;
5
+ icon?: SvgData | string | Snippet;
5
6
  children?: Snippet;
6
7
  disabled?: boolean;
7
8
  external_rel?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"ContextmenuLinkEntry.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/ContextmenuLinkEntry.svelte"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,QAAQ,CAAC;AAKnC,KAAK,gBAAgB,GAAI;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IACxB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAkFH,QAAA,MAAM,oBAAoB,sDAAwC,CAAC;AACnE,KAAK,oBAAoB,GAAG,UAAU,CAAC,OAAO,oBAAoB,CAAC,CAAC;AACpE,eAAe,oBAAoB,CAAC"}
1
+ {"version":3,"file":"ContextmenuLinkEntry.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/ContextmenuLinkEntry.svelte"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,QAAQ,CAAC;AAKpC,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,UAAU,CAAC;AAIrC,KAAK,gBAAgB,GAAI;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,OAAO,CAAC;IAClC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAyFH,QAAA,MAAM,oBAAoB,sDAAwC,CAAC;AACnE,KAAK,oBAAoB,GAAG,UAAU,CAAC,OAAO,oBAAoB,CAAC,CAAC;AACpE,eAAe,oBAAoB,CAAC"}
@@ -0,0 +1,207 @@
1
+ <script module lang="ts">
2
+ // The default entry snippets are shared by `ContextmenuRoot.svelte`
3
+ // and `ContextmenuRootForSafariCompatibility.svelte`.
4
+ export {link_entry_default, text_entry_default, separator_entry_default};
5
+ </script>
6
+
7
+ <script lang="ts">
8
+ /**
9
+ * The menu surface shared by `ContextmenuRoot.svelte` and
10
+ * `ContextmenuRootForSafariCompatibility.svelte`. Owns everything that happens
11
+ * while the menu is open - positioning, keyboard navigation, dismissal on
12
+ * outside presses, popover top-layer promotion, and entry rendering - while
13
+ * the roots own gesture detection, i.e. how the menu opens. Holds no gesture
14
+ * state of its own; the `open_guard` it consults is owned and armed by the root.
15
+ */
16
+ import {swallow} from '@fuzdev/fuz_util/dom.js';
17
+ import type {ComponentProps, Snippet} from 'svelte';
18
+
19
+ import {
20
+ contextmenu_dimensions_context,
21
+ type ContextmenuState,
22
+ } from './contextmenu_state.svelte.js';
23
+ import ContextmenuLinkEntry from './ContextmenuLinkEntry.svelte';
24
+ import ContextmenuTextEntry from './ContextmenuTextEntry.svelte';
25
+ import ContextmenuSeparator from './ContextmenuSeparator.svelte';
26
+ import {
27
+ contextmenu_create_keyboard_handlers,
28
+ contextmenu_create_keydown_handler,
29
+ contextmenu_create_mousedown_handler,
30
+ contextmenu_calculate_constrained_x,
31
+ contextmenu_calculate_constrained_y,
32
+ contextmenu_popover_attachment,
33
+ type ContextmenuOpenGuard,
34
+ } from './contextmenu_helpers.js';
35
+
36
+ let {
37
+ contextmenu,
38
+ open_guard,
39
+ link_entry,
40
+ text_entry,
41
+ separator_entry,
42
+ el = $bindable(),
43
+ }: {
44
+ /**
45
+ * The contextmenu state owned by the root.
46
+ */
47
+ contextmenu: ContextmenuState;
48
+ /**
49
+ * Guards the menu from the residual events of the gesture that opened it.
50
+ * Owned by the root, which arms it when its gesture detection opens the menu.
51
+ */
52
+ open_guard: ContextmenuOpenGuard;
53
+ /**
54
+ * Snippet for rendering link entries, or `null` to disable them.
55
+ */
56
+ link_entry: Snippet<[ComponentProps<typeof ContextmenuLinkEntry>]> | null;
57
+ /**
58
+ * Snippet for rendering copy text entries, or `null` to disable them.
59
+ */
60
+ text_entry: Snippet<[ComponentProps<typeof ContextmenuTextEntry>]> | null;
61
+ /**
62
+ * Snippet for rendering separator entries, or `null` to disable them.
63
+ */
64
+ separator_entry: Snippet<[ComponentProps<typeof ContextmenuSeparator>]> | null;
65
+ /**
66
+ * The menu element while opened - bound by the roots for their gesture handlers.
67
+ */
68
+ el?: HTMLElement;
69
+ } = $props();
70
+
71
+ const {layout} = $derived(contextmenu);
72
+
73
+ const dimensions = contextmenu_dimensions_context.set();
74
+
75
+ const x = $derived(
76
+ contextmenu_calculate_constrained_x(contextmenu.x, dimensions.width, layout.width),
77
+ );
78
+ const y = $derived(
79
+ contextmenu_calculate_constrained_y(contextmenu.y, dimensions.height, layout.height),
80
+ );
81
+
82
+ // Closes the contextmenu on presses outside of it.
83
+ const mousedown = $derived(
84
+ contextmenu_create_mousedown_handler(contextmenu, () => el, open_guard),
85
+ );
86
+
87
+ const keyboard_handlers = $derived(contextmenu_create_keyboard_handlers(contextmenu));
88
+ const keydown = $derived(contextmenu_create_keydown_handler(keyboard_handlers));
89
+ </script>
90
+
91
+ <svelte:window
92
+ onmousedown={!contextmenu.opened ? undefined : mousedown}
93
+ onkeydown={!contextmenu.opened ? undefined : keydown}
94
+ onmouseupcapture={(e) => open_guard.track_mouseup(e)}
95
+ />
96
+
97
+ {#if !contextmenu.has_custom_layout}
98
+ <div
99
+ class="contextmenu-layout"
100
+ bind:clientWidth={layout.width}
101
+ bind:clientHeight={layout.height}
102
+ aria-hidden="true"
103
+ ></div>
104
+ {/if}
105
+
106
+ <!-- TODO animate the contextmenu as it appears somehow, maybe a subtle highlight -->
107
+ <!-- TODO implement focus management per APG: store `document.activeElement` when opening, focus menu on mount, restore focus on close if element `isConnected` -->
108
+ {#if contextmenu.opened}
109
+ <!-- Keyed on `params` (a fresh array every open) so reopening while open - which swaps
110
+ `params` without toggling `opened` - remounts the menu: entries register fresh state and
111
+ the popover attachment re-resolves its host (e.g. a modal dialog opened since the first
112
+ open). -->
113
+ {#key contextmenu.params}
114
+ <ul
115
+ class="contextmenu unstyled pane"
116
+ role="menu"
117
+ aria-label="context menu"
118
+ tabindex="-1"
119
+ popover="manual"
120
+ {@attach contextmenu_popover_attachment(contextmenu)}
121
+ bind:this={el}
122
+ bind:offsetWidth={dimensions.width}
123
+ bind:offsetHeight={dimensions.height}
124
+ style:transform="translate3d({x}px, {y}px, 0)"
125
+ onclickcapture={(e) => {
126
+ // blocks a click synthesized from the touch gesture that opened the menu,
127
+ // which would activate the first item (iOS can synthesize it even when
128
+ // the release was swallowed)
129
+ if (open_guard.consume_blocked_click()) swallow(e);
130
+ }}
131
+ >
132
+ <!-- TODO maybe this should be generic? -->
133
+ <!-- Index-keyed deliberately: `params` is immutable for the life of the key block
134
+ above, and identity keys would collide when the same snippet registers on nested
135
+ elements. -->
136
+ {#each contextmenu.params as p, i (i)}
137
+ {#if typeof p === 'function'}
138
+ {@render p()}
139
+ {:else if p.snippet === 'link'}
140
+ {@render link_entry?.(p.props)}
141
+ {:else if p.snippet === 'text'}
142
+ {@render text_entry?.(p.props)}
143
+ {:else if p.snippet === 'separator'}
144
+ {@render separator_entry?.(p.props)}
145
+ {/if}
146
+ {/each}
147
+ </ul>
148
+ {/key}
149
+ {/if}
150
+
151
+ {#snippet link_entry_default(props: ComponentProps<typeof ContextmenuLinkEntry>)}
152
+ <ContextmenuLinkEntry {...props} />
153
+ {/snippet}
154
+
155
+ {#snippet text_entry_default(props: ComponentProps<typeof ContextmenuTextEntry>)}
156
+ <ContextmenuTextEntry {...props} />
157
+ {/snippet}
158
+
159
+ {#snippet separator_entry_default(props: ComponentProps<typeof ContextmenuSeparator>)}
160
+ <ContextmenuSeparator {...props} />
161
+ {/snippet}
162
+
163
+ <style>
164
+ .contextmenu {
165
+ --icon_size: var(--icon_size_xs);
166
+ /* TODO maybe make this responsive or a max of the page width
167
+ minus some space to tap items covered by the menu on the side,
168
+ or consider a totally different design for small screens (more dialog-like) */
169
+ --contextmenu_width: 320px;
170
+ position: fixed;
171
+ left: 0;
172
+ top: 0;
173
+ /* the z-index is the fallback for browsers without the Popover API,
174
+ where the menu can't layer over modal dialogs */
175
+ z-index: var(--contextmenu_z_index, 200);
176
+ max-width: var(--contextmenu_width);
177
+ width: 100%;
178
+ /* reset the UA `[popover]` styles; positioning stays transform-based */
179
+ right: auto;
180
+ bottom: auto;
181
+ margin: 0;
182
+ padding: 0;
183
+ color: inherit;
184
+ /* the UA popover style sets `overflow: auto`, which would clip submenu flyouts */
185
+ overflow: visible;
186
+ /* Re-enable callouts on the menu itself to allow the native contextmenu (for dev
187
+ tools) and prevent the menu from being selected. Only meaningful under
188
+ `ContextmenuRootForSafariCompatibility`'s `body.contextmenu-pending` blocking -
189
+ inert under `ContextmenuRoot`, which never renders on iOS Safari. */
190
+ -webkit-touch-callout: initial !important;
191
+ }
192
+ /* TODO hacky */
193
+ .contextmenu,
194
+ .contextmenu :global(menu.pane) {
195
+ border: var(--contextmenu_border_width, var(--border_width))
196
+ var(--contextmenu_border_style, var(--border_style))
197
+ var(--contextmenu_border_color, var(--border_color));
198
+ border-radius: var(--contextmenu_border_radius, var(--border_radius_xs));
199
+ }
200
+
201
+ /* TODO better way to do this? */
202
+ .contextmenu-layout {
203
+ z-index: -200;
204
+ position: fixed;
205
+ inset: 0;
206
+ }
207
+ </style>