blume 1.6.5 → 1.7.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 (179) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/bin/blume.mjs +3 -2
  3. package/dist/cli/chunk-0qhq7b8q.js +111 -0
  4. package/dist/cli/chunk-0qhq7b8q.js.map +11 -0
  5. package/dist/cli/chunk-18tjv4f7.js +96 -0
  6. package/dist/cli/chunk-18tjv4f7.js.map +10 -0
  7. package/dist/cli/chunk-27gtm2ym.js +69 -0
  8. package/dist/cli/chunk-27gtm2ym.js.map +11 -0
  9. package/dist/cli/chunk-2aj8ddew.js +72 -0
  10. package/dist/cli/chunk-2aj8ddew.js.map +10 -0
  11. package/dist/cli/chunk-3r94j3tc.js +221 -0
  12. package/dist/cli/chunk-3r94j3tc.js.map +10 -0
  13. package/dist/cli/chunk-4trphnvy.js +102 -0
  14. package/dist/cli/chunk-4trphnvy.js.map +11 -0
  15. package/dist/cli/chunk-4xyggvgf.js +21 -0
  16. package/dist/cli/chunk-4xyggvgf.js.map +10 -0
  17. package/dist/cli/chunk-5d4q7121.js +4064 -0
  18. package/dist/cli/chunk-5d4q7121.js.map +40 -0
  19. package/dist/cli/chunk-5hs6gb7n.js +32 -0
  20. package/dist/cli/chunk-5hs6gb7n.js.map +10 -0
  21. package/dist/cli/chunk-6kzzpsx8.js +26 -0
  22. package/dist/cli/chunk-6kzzpsx8.js.map +10 -0
  23. package/dist/cli/chunk-8gnpdsn1.js +952 -0
  24. package/dist/cli/chunk-8gnpdsn1.js.map +12 -0
  25. package/dist/cli/chunk-9qs6acpw.js +176 -0
  26. package/dist/cli/chunk-9qs6acpw.js.map +10 -0
  27. package/dist/cli/chunk-agy5rzxy.js +2453 -0
  28. package/dist/cli/chunk-agy5rzxy.js.map +15 -0
  29. package/dist/cli/chunk-bcy492zc.js +16 -0
  30. package/dist/cli/chunk-bcy492zc.js.map +10 -0
  31. package/dist/cli/chunk-btfr9yvw.js +41 -0
  32. package/dist/cli/chunk-btfr9yvw.js.map +10 -0
  33. package/dist/cli/chunk-cbjnx4s8.js +73 -0
  34. package/dist/cli/chunk-cbjnx4s8.js.map +10 -0
  35. package/dist/cli/chunk-cfw6x4rm.js +1967 -0
  36. package/dist/cli/chunk-cfw6x4rm.js.map +34 -0
  37. package/dist/cli/chunk-ckh3a410.js +277 -0
  38. package/dist/cli/chunk-ckh3a410.js.map +11 -0
  39. package/dist/cli/chunk-drke6t0h.js +259 -0
  40. package/dist/cli/chunk-drke6t0h.js.map +11 -0
  41. package/dist/cli/chunk-ev67ycx0.js +15 -0
  42. package/dist/cli/chunk-ev67ycx0.js.map +10 -0
  43. package/dist/cli/chunk-ey89bjj1.js +209 -0
  44. package/dist/cli/chunk-ey89bjj1.js.map +11 -0
  45. package/dist/cli/chunk-j6pxe0dt.js +69 -0
  46. package/dist/cli/chunk-j6pxe0dt.js.map +11 -0
  47. package/dist/cli/chunk-jk1zwka1.js +387 -0
  48. package/dist/cli/chunk-jk1zwka1.js.map +12 -0
  49. package/dist/cli/chunk-jtb45atp.js +467 -0
  50. package/dist/cli/chunk-jtb45atp.js.map +14 -0
  51. package/dist/cli/chunk-jxkxjsc1.js +76 -0
  52. package/dist/cli/chunk-jxkxjsc1.js.map +10 -0
  53. package/dist/cli/chunk-kwx90v78.js +81 -0
  54. package/dist/cli/chunk-kwx90v78.js.map +10 -0
  55. package/dist/cli/chunk-n0nyat6g.js +30 -0
  56. package/dist/cli/chunk-n0nyat6g.js.map +10 -0
  57. package/dist/cli/chunk-pxj10x8y.js +35 -0
  58. package/dist/cli/chunk-pxj10x8y.js.map +10 -0
  59. package/dist/cli/chunk-qq9nm3qd.js +1141 -0
  60. package/dist/cli/chunk-qq9nm3qd.js.map +19 -0
  61. package/dist/cli/chunk-s102bysw.js +5170 -0
  62. package/dist/cli/chunk-s102bysw.js.map +47 -0
  63. package/dist/cli/chunk-s5dsk8bj.js +769 -0
  64. package/dist/cli/chunk-s5dsk8bj.js.map +13 -0
  65. package/dist/cli/chunk-s5e5jt53.js +227 -0
  66. package/dist/cli/chunk-s5e5jt53.js.map +11 -0
  67. package/dist/cli/chunk-sbdqrjbb.js +81 -0
  68. package/dist/cli/chunk-sbdqrjbb.js.map +10 -0
  69. package/dist/cli/chunk-tnskyrej.js +117 -0
  70. package/dist/cli/chunk-tnskyrej.js.map +10 -0
  71. package/dist/cli/chunk-v2ymm99c.js +1016 -0
  72. package/dist/cli/chunk-v2ymm99c.js.map +13 -0
  73. package/dist/cli/chunk-v5mm027v.js +185 -0
  74. package/dist/cli/chunk-v5mm027v.js.map +11 -0
  75. package/dist/cli/chunk-vt8fgygt.js +23 -0
  76. package/dist/cli/chunk-vt8fgygt.js.map +10 -0
  77. package/dist/cli/chunk-vxv4x1n8.js +17 -0
  78. package/dist/cli/chunk-vxv4x1n8.js.map +10 -0
  79. package/dist/cli/chunk-wd27zjcz.js +60 -0
  80. package/dist/cli/chunk-wd27zjcz.js.map +10 -0
  81. package/dist/cli/chunk-x66c5yjn.js +23 -0
  82. package/dist/cli/chunk-x66c5yjn.js.map +10 -0
  83. package/dist/cli/chunk-xv91q4nm.js +5314 -0
  84. package/dist/cli/chunk-xv91q4nm.js.map +58 -0
  85. package/dist/cli/chunk-y3g15rvv.js +679 -0
  86. package/dist/cli/chunk-y3g15rvv.js.map +15 -0
  87. package/dist/cli/chunk-ye9zdkgv.js +136 -0
  88. package/dist/cli/chunk-ye9zdkgv.js.map +10 -0
  89. package/dist/cli/chunk-ynacq3ev.js +1062 -0
  90. package/dist/cli/chunk-ynacq3ev.js.map +25 -0
  91. package/dist/cli/chunk-zr3ygrq3.js +54 -0
  92. package/dist/cli/chunk-zr3ygrq3.js.map +10 -0
  93. package/dist/cli/index.js +55 -27597
  94. package/dist/cli/index.js.map +5 -243
  95. package/dist/types/ai/ask-context.d.ts +26 -0
  96. package/dist/types/components/layout/nav-utils.d.ts +33 -1
  97. package/dist/types/core/code-fences.d.ts +11 -0
  98. package/dist/types/core/package-root.d.ts +1 -1
  99. package/dist/types/core/schema.d.ts +70 -0
  100. package/dist/types/theme/fonts.d.ts +22 -22
  101. package/docs/02-deployment.mdx +22 -1
  102. package/docs/configuration/ask-ai.mdx +1 -1
  103. package/docs/configuration/customization.mdx +2 -9
  104. package/docs/content/navigation.mdx +2 -0
  105. package/docs/content/syntax.mdx +1 -1
  106. package/docs/discoverability/open-graph.mdx +4 -0
  107. package/docs/reference/cli.mdx +1 -1
  108. package/package.json +4 -2
  109. package/src/ai/api/handlers.ts +4 -7
  110. package/src/ai/api/paths.ts +8 -0
  111. package/src/ai/api/spec.ts +2 -1
  112. package/src/ai/ask-context.ts +378 -22
  113. package/src/astro/generate.ts +161 -28
  114. package/src/astro/include-hmr.ts +10 -13
  115. package/src/astro/include-refresh.ts +0 -0
  116. package/src/astro/index.ts +6 -1
  117. package/src/astro/integration.ts +280 -53
  118. package/src/astro/module-types.ts +83 -0
  119. package/src/astro/templates.ts +256 -108
  120. package/src/audit/image-size.ts +10 -8
  121. package/src/cli/command-meta.ts +77 -0
  122. package/src/cli/commands/add.ts +2 -4
  123. package/src/cli/commands/audit.ts +2 -4
  124. package/src/cli/commands/build.ts +70 -346
  125. package/src/cli/commands/check.ts +2 -4
  126. package/src/cli/commands/dev.ts +31 -42
  127. package/src/cli/commands/doctor.ts +2 -4
  128. package/src/cli/commands/eject.ts +3 -41
  129. package/src/cli/commands/eval.ts +2 -5
  130. package/src/cli/commands/init.ts +2 -4
  131. package/src/cli/commands/mcp-stdio.ts +2 -5
  132. package/src/cli/commands/preview.ts +3 -5
  133. package/src/cli/commands/sync.ts +2 -4
  134. package/src/cli/commands/translate.ts +2 -5
  135. package/src/cli/commands/validate.ts +2 -4
  136. package/src/cli/commands/version.ts +2 -4
  137. package/src/cli/eject-scripts.ts +0 -45
  138. package/src/cli/host-args.ts +16 -0
  139. package/src/cli/index.ts +84 -35
  140. package/src/cli/lazy-command.ts +47 -0
  141. package/src/components/Icon.astro +24 -0
  142. package/src/components/content/GithubInfo.astro +4 -1
  143. package/src/components/icon-sprite-middleware.ts +41 -0
  144. package/src/components/icon-sprite.ts +93 -0
  145. package/src/components/layout/IconSprite.astro +11 -0
  146. package/src/components/layout/NavTree.astro +156 -188
  147. package/src/components/layout/NavTreeCache.astro +45 -0
  148. package/src/components/layout/NavTreeScript.astro +256 -0
  149. package/src/components/layout/PageActions.astro +11 -5
  150. package/src/components/layout/PageLayout.astro +21 -3
  151. package/src/components/layout/ReferenceLayout.astro +21 -4
  152. package/src/components/layout/RootLayout.astro +44 -6
  153. package/src/components/layout/nav-cache.ts +49 -0
  154. package/src/components/layout/nav-utils.ts +69 -1
  155. package/src/components/layout/page-locale.ts +29 -0
  156. package/src/core/api-name.ts +18 -0
  157. package/src/core/code-fences.ts +48 -0
  158. package/src/core/content-assets.ts +3 -7
  159. package/src/core/includes.ts +3 -7
  160. package/src/core/package-root.ts +1 -1
  161. package/src/core/schema.ts +19 -0
  162. package/src/core/sources/normalize.ts +2 -37
  163. package/src/core/sources/obsidian.ts +3 -2
  164. package/src/core/svg-dimensions.ts +97 -0
  165. package/src/core/version-cut.ts +2 -2
  166. package/src/deploy/artifacts.ts +370 -0
  167. package/src/deploy/cloudflare-negotiation.ts +97 -32
  168. package/src/deploy/function-bundle.ts +66 -20
  169. package/src/deploy/sitemap.ts +6 -0
  170. package/src/deploy/vercel-negotiation.ts +8 -30
  171. package/src/markdown/language-icon.ts +64 -20
  172. package/src/markdown/mermaid.ts +11 -0
  173. package/src/og/cache.ts +236 -0
  174. package/src/og/card.ts +18 -16
  175. package/src/og/index.ts +8 -1
  176. package/src/openapi/render-mdx.ts +9 -5
  177. package/src/registry/eject.ts +23 -10
  178. package/src/theme/entry.ts +41 -7
  179. package/src/theme/fonts.ts +30 -23
@@ -6,15 +6,20 @@ import { methodColor } from "../colors.ts";
6
6
  import Icon from "../Icon.astro";
7
7
  import { withBase } from "../islands/base-path.ts";
8
8
  import Self from "./NavTree.astro";
9
+ import NavTreeCache from "./NavTreeCache.astro";
10
+ import NavTreeScript from "./NavTreeScript.astro";
9
11
 
10
12
  interface PagePanel {
11
13
  id: string;
12
14
  parentId: string;
15
+ /** Nesting level in the panel stack (a root-level page group is 1). */
16
+ depth: number;
13
17
  label: string;
14
18
  route?: string;
15
19
  icon?: string;
16
20
  children: NavNode[];
17
21
  active: boolean;
22
+ node: NavNode;
18
23
  }
19
24
 
20
25
  interface Props {
@@ -27,6 +32,20 @@ interface Props {
27
32
  root?: boolean;
28
33
  /** Localized labels ("Back", the deprecated badge); English when omitted. */
29
34
  strings?: UIStrings["nav"];
35
+ /**
36
+ * Stable ids for the full sidebar's groups (`navGroupIds`), so panel and
37
+ * fragment ids match across the scoped views the layout renders. Absent,
38
+ * ids fall back to positions within this render.
39
+ */
40
+ ids?: Map<NavNode, string>;
41
+ /**
42
+ * URL prefix of the deferred-section fragments (`/blume-nav/<version>/
43
+ * <locale>`). When set, a collapsed group's children and an inactive
44
+ * drill-in panel's contents are left out of the page and fetched from
45
+ * `<fragmentBase>/<id>` on first open (see NavTreeScript); absent, every
46
+ * section renders in full.
47
+ */
48
+ fragmentBase?: string;
30
49
  }
31
50
 
32
51
  const {
@@ -36,8 +55,18 @@ const {
36
55
  idPrefix = "n",
37
56
  root = depth === 0,
38
57
  strings,
58
+ ids,
59
+ fragmentBase,
39
60
  } = Astro.props;
40
61
 
62
+ /** A group's id: its stable full-tree id, else its position in this render. */
63
+ const idOf = (node: NavNode, positional: string): string =>
64
+ ids?.get(node) ?? positional;
65
+
66
+ /** The fragment a deferred section loads from, when sections are deferred. */
67
+ const fragmentFor = (id: string): string | undefined =>
68
+ fragmentBase ? `${fragmentBase}/${id}` : undefined;
69
+
41
70
  // Merge over the English defaults so a label missing from a translation (or
42
71
  // from a not-yet-regenerated snapshot) still renders instead of coming out
43
72
  // blank — the PageActions pattern.
@@ -66,27 +95,30 @@ const hasActivePage = (item: NavNode): boolean =>
66
95
  const collectPanels = (
67
96
  nodes: NavNode[],
68
97
  prefix: string,
69
- parentId: string
98
+ parentId: string,
99
+ level = 1
70
100
  ): PagePanel[] => {
71
101
  const out: PagePanel[] = [];
72
102
  nodes.forEach((node, index) => {
73
103
  if (node.kind !== "group") {
74
104
  return;
75
105
  }
76
- const id = `${prefix}.${index}`;
106
+ const id = idOf(node, `${prefix}.${index}`);
77
107
  if ((node.display ?? "flat") === "page") {
78
108
  out.push({
79
109
  active: hasActivePage(node),
80
110
  children: node.children,
111
+ depth: level,
81
112
  icon: node.icon,
82
113
  id,
83
114
  label: node.label,
115
+ node,
84
116
  parentId,
85
117
  route: node.route,
86
118
  });
87
- out.push(...collectPanels(node.children, id, id));
119
+ out.push(...collectPanels(node.children, id, id, level + 1));
88
120
  } else {
89
- out.push(...collectPanels(node.children, id, parentId));
121
+ out.push(...collectPanels(node.children, id, parentId, level));
90
122
  }
91
123
  });
92
124
  return out;
@@ -98,27 +130,33 @@ const panels = root ? collectPanels(items, idPrefix, "root") : [];
98
130
  const activePanels = panels.filter((panel) => panel.active);
99
131
  const initialId =
100
132
  activePanels.length > 0
101
- ? activePanels.reduce((a, b) =>
102
- b.id.split(".").length > a.id.split(".").length ? b : a
103
- ).id
133
+ ? activePanels.reduce((a, b) => (b.depth > a.depth ? b : a)).id
104
134
  : "root";
105
135
  ---
106
136
 
137
+ {root && (panels.length > 0 || fragmentBase) && <NavTreeScript />}
107
138
  {
108
139
  root && panels.length > 0 ? (
109
140
  <blume-nav class="block" data-initial={initialId}>
110
- <div data-nav-panel="root" hidden={initialId !== "root"}>
141
+ <div data-nav-depth="0" data-nav-panel="root" hidden={initialId !== "root"}>
111
142
  <Self
112
143
  currentRoute={currentRoute}
113
144
  depth={0}
145
+ fragmentBase={fragmentBase}
114
146
  idPrefix={idPrefix}
147
+ ids={ids}
115
148
  items={items}
116
149
  root={false}
117
150
  strings={n}
118
151
  />
119
152
  </div>
120
153
  {panels.map((panel) => (
121
- <div data-nav-panel={panel.id} hidden={panel.id !== initialId}>
154
+ <div
155
+ data-nav-depth={panel.depth}
156
+ data-nav-panel={panel.id}
157
+ data-nav-src={panel.active ? undefined : fragmentFor(panel.id)}
158
+ hidden={panel.id !== initialId}
159
+ >
122
160
  {/* The title is the only sidebar link to the section's own page, so
123
161
  a routed panel keeps it as a link; without a route the whole row
124
162
  becomes the back button. Either way every part of the row is
@@ -156,14 +194,34 @@ const initialId =
156
194
  <span class="flex-1 truncate">{panel.label}</span>
157
195
  </button>
158
196
  )}
159
- <Self
160
- currentRoute={currentRoute}
161
- depth={1}
162
- idPrefix={panel.id}
163
- items={panel.children}
164
- root={false}
165
- strings={n}
166
- />
197
+ {/* An inactive panel's contents are deferred (fetched into this
198
+ element on drill-in) when fragments are on; otherwise the
199
+ build-time cache renders them once. */}
200
+ {panel.active ? (
201
+ <Self
202
+ currentRoute={currentRoute}
203
+ depth={1}
204
+ fragmentBase={fragmentBase}
205
+ idPrefix={panel.id}
206
+ ids={ids}
207
+ items={panel.children}
208
+ root={false}
209
+ strings={n}
210
+ />
211
+ ) : fragmentBase ? null : (
212
+ <NavTreeCache node={panel.node} variant={`${panel.id}|${n.back}|${n.deprecated}`}>
213
+ <Self
214
+ currentRoute={currentRoute}
215
+ depth={1}
216
+ fragmentBase={fragmentBase}
217
+ idPrefix={panel.id}
218
+ ids={ids}
219
+ items={panel.children}
220
+ root={false}
221
+ strings={n}
222
+ />
223
+ </NavTreeCache>
224
+ )}
167
225
  </div>
168
226
  ))}
169
227
  </blume-nav>
@@ -171,11 +229,28 @@ const initialId =
171
229
  <ul class="m-0 list-none space-y-px p-0">
172
230
  {items.map((item, index) => {
173
231
  if (item.kind === "page") {
232
+ const current = item.route === currentRoute ? "page" : undefined;
233
+ // A bare row (no icon, badge, or deprecation) is the common case and
234
+ // the bulk of every page's HTML on a large site, so it renders as a
235
+ // single truncating link with no inner row markup.
236
+ if (!(item.icon || item.badge || item.deprecated)) {
237
+ return (
238
+ <li>
239
+ <a
240
+ aria-current={current}
241
+ class="blume-nav-link truncate"
242
+ href={withBase(item.route)}
243
+ >
244
+ {item.label}
245
+ </a>
246
+ </li>
247
+ );
248
+ }
174
249
  return (
175
250
  <li>
176
251
  <a
177
- aria-current={item.route === currentRoute ? "page" : undefined}
178
- class="block rounded-[0.65rem] px-2.5 py-1.5 text-muted-foreground text-sm transition-colors hover:bg-muted hover:text-foreground aria-[current=page]:bg-muted aria-[current=page]:font-medium aria-[current=page]:text-foreground"
252
+ aria-current={current}
253
+ class="blume-nav-link"
179
254
  href={withBase(item.route)}
180
255
  >
181
256
  <span class="flex items-center gap-2">
@@ -198,7 +273,7 @@ const initialId =
198
273
  }
199
274
 
200
275
  const display = item.display ?? "flat";
201
- const id = `${idPrefix}.${index}`;
276
+ const id = idOf(item, `${idPrefix}.${index}`);
202
277
  // Flat groups are section headers and want breathing room; collapsible
203
278
  // `group`/`page` rows are single rows and should stack tightly.
204
279
  const spacing =
@@ -209,7 +284,7 @@ const initialId =
209
284
  return (
210
285
  <li class={spacing}>
211
286
  <button
212
- class="flex w-full items-center gap-2 rounded-[0.65rem] px-2.5 py-1.5 text-left font-medium text-foreground text-sm transition-colors hover:bg-muted"
287
+ class="blume-nav-drill"
213
288
  data-nav-to={id}
214
289
  type="button"
215
290
  >
@@ -241,13 +316,13 @@ const initialId =
241
316
  return (
242
317
  <li class={spacing}>
243
318
  <details open={open}>
244
- <summary class="flex cursor-pointer list-none items-center gap-1.5 rounded-[0.65rem] px-2.5 py-1.5 text-muted-foreground text-sm transition-colors hover:bg-muted hover:text-foreground [&::-webkit-details-marker]:hidden">
319
+ <summary class="blume-nav-summary">
245
320
  {item.route ? (
246
321
  <a
247
322
  aria-current={
248
323
  item.route === currentRoute ? "page" : undefined
249
324
  }
250
- class="-my-1 flex flex-1 items-center gap-1.5 rounded py-1 transition-colors aria-[current=page]:text-foreground aria-[current=page]:font-semibold"
325
+ class="blume-nav-summary-link"
251
326
  href={withBase(item.route)}
252
327
  >
253
328
  {item.icon && (
@@ -285,15 +360,37 @@ const initialId =
285
360
  <Icon name="chevron-right" size={13} />
286
361
  </span>
287
362
  </summary>
288
- <div class="space-y-0.5 border-border border-l pl-3">
289
- <Self
290
- currentRoute={currentRoute}
291
- depth={depth + 1}
292
- idPrefix={id}
293
- items={item.children}
294
- root={false}
295
- strings={n}
296
- />
363
+ <div
364
+ class="space-y-0.5 border-border border-l pl-3"
365
+ data-nav-src={open ? undefined : fragmentFor(id)}
366
+ >
367
+ {/* A closed group's children are deferred (fetched into this
368
+ element on first open) when fragments are on. */}
369
+ {!open && fragmentBase ? null : active ? (
370
+ <Self
371
+ currentRoute={currentRoute}
372
+ depth={depth + 1}
373
+ fragmentBase={fragmentBase}
374
+ idPrefix={id}
375
+ ids={ids}
376
+ items={item.children}
377
+ root={false}
378
+ strings={n}
379
+ />
380
+ ) : (
381
+ <NavTreeCache node={item} variant={`${id}|${n.back}|${n.deprecated}`}>
382
+ <Self
383
+ currentRoute={currentRoute}
384
+ depth={depth + 1}
385
+ fragmentBase={fragmentBase}
386
+ idPrefix={id}
387
+ ids={ids}
388
+ items={item.children}
389
+ root={false}
390
+ strings={n}
391
+ />
392
+ </NavTreeCache>
393
+ )}
297
394
  </div>
298
395
  </details>
299
396
  </li>
@@ -303,11 +400,11 @@ const initialId =
303
400
  // `flat` (default): a non-collapsible header with its items beneath.
304
401
  return (
305
402
  <li class={spacing}>
306
- <p class="mb-2 flex items-center gap-1.5 px-2.5 font-medium text-foreground text-sm">
403
+ <p class="blume-nav-heading">
307
404
  {item.route ? (
308
405
  <a
309
406
  aria-current={item.route === currentRoute ? "page" : undefined}
310
- class="-ml-1 flex flex-1 items-center gap-1.5 rounded-[0.65rem] px-1 py-0.5 text-foreground transition-colors hover:bg-muted aria-[current=page]:bg-muted"
407
+ class="blume-nav-heading-link"
311
408
  href={withBase(item.route)}
312
409
  >
313
410
  {item.icon && (
@@ -339,14 +436,31 @@ const initialId =
339
436
  )}
340
437
  </p>
341
438
  <div class="space-y-0.5">
342
- <Self
343
- currentRoute={currentRoute}
344
- depth={depth + 1}
345
- idPrefix={id}
346
- items={item.children}
347
- root={false}
348
- strings={n}
349
- />
439
+ {hasActivePage(item) ? (
440
+ <Self
441
+ currentRoute={currentRoute}
442
+ depth={depth + 1}
443
+ fragmentBase={fragmentBase}
444
+ idPrefix={id}
445
+ ids={ids}
446
+ items={item.children}
447
+ root={false}
448
+ strings={n}
449
+ />
450
+ ) : (
451
+ <NavTreeCache node={item} variant={`${id}|${n.back}|${n.deprecated}`}>
452
+ <Self
453
+ currentRoute={currentRoute}
454
+ depth={depth + 1}
455
+ fragmentBase={fragmentBase}
456
+ idPrefix={id}
457
+ ids={ids}
458
+ items={item.children}
459
+ root={false}
460
+ strings={n}
461
+ />
462
+ </NavTreeCache>
463
+ )}
350
464
  </div>
351
465
  </li>
352
466
  );
@@ -355,149 +469,3 @@ const initialId =
355
469
  )
356
470
  }
357
471
 
358
- <script>
359
- const DURATION = 260;
360
- const EASING = "cubic-bezier(0.33, 1, 0.68, 1)";
361
-
362
- class BlumeNav extends HTMLElement {
363
- connectedCallback() {
364
- if (this.dataset.ready) {
365
- return;
366
- }
367
- this.dataset.ready = "1";
368
- this.addEventListener("click", (event) => {
369
- const target = event.target;
370
- if (!(target instanceof Element)) {
371
- return;
372
- }
373
- const drill = target.closest("[data-nav-to]");
374
- if (drill && this.contains(drill)) {
375
- const id = drill.getAttribute("data-nav-to");
376
- if (id && this.panel(id)) {
377
- event.preventDefault();
378
- this.show(id, true);
379
- }
380
- return;
381
- }
382
- const back = target.closest("[data-nav-back]");
383
- if (back && this.contains(back)) {
384
- event.preventDefault();
385
- this.show(back.getAttribute("data-nav-back") || "root", true);
386
- }
387
- });
388
- // Initial panel is route-driven, so it snaps into place without a slide.
389
- this.show(this.dataset.initial || "root", false);
390
- }
391
-
392
- panel(id) {
393
- return this.querySelector(`[data-nav-panel="${id}"]`);
394
- }
395
-
396
- depth(id) {
397
- return id === "root" ? 0 : id.split(".").length;
398
- }
399
-
400
- reduced() {
401
- return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
402
- }
403
-
404
- show(id, animate) {
405
- const next = this.panel(id) || this.panel("root");
406
- if (!next) {
407
- return;
408
- }
409
- const resolved = next.getAttribute("data-nav-panel");
410
- const prev = this.active;
411
- if (prev === next) {
412
- return;
413
- }
414
- // Settle any in-flight slide before starting the next one.
415
- if (this.finishSlide) {
416
- this.finishSlide();
417
- }
418
- this.active = next;
419
- this.dataset.active = resolved;
420
-
421
- if (!(animate && prev) || this.reduced()) {
422
- for (const panel of this.querySelectorAll("[data-nav-panel]")) {
423
- panel.hidden = panel !== next;
424
- }
425
- return;
426
- }
427
-
428
- const forward =
429
- this.depth(resolved) > this.depth(prev.getAttribute("data-nav-panel"));
430
- this.slide(prev, next, forward);
431
- }
432
-
433
- slide(out, into, forward) {
434
- const rtl = getComputedStyle(this).direction === "rtl" ? -1 : 1;
435
- const sign = (forward ? -1 : 1) * rtl;
436
-
437
- const fromHeight = out.offsetHeight;
438
- this.style.position = "relative";
439
- this.style.overflow = "hidden";
440
- this.style.height = `${fromHeight}px`;
441
- into.hidden = false;
442
- for (const panel of [out, into]) {
443
- panel.style.position = "absolute";
444
- panel.style.top = "0";
445
- panel.style.insetInlineStart = "0";
446
- panel.style.width = "100%";
447
- }
448
- const toHeight = into.offsetHeight;
449
- this.style.height = `${toHeight}px`;
450
-
451
- const options = { duration: DURATION, easing: EASING };
452
- const animations = [
453
- out.animate(
454
- [
455
- { transform: "translateX(0)" },
456
- { transform: `translateX(${sign * 100}%)` },
457
- ],
458
- options
459
- ),
460
- into.animate(
461
- [
462
- { transform: `translateX(${-sign * 100}%)` },
463
- { transform: "translateX(0)" },
464
- ],
465
- options
466
- ),
467
- this.animate(
468
- [{ height: `${fromHeight}px` }, { height: `${toHeight}px` }],
469
- options
470
- ),
471
- ];
472
-
473
- let done = false;
474
- const finish = () => {
475
- if (done) {
476
- return;
477
- }
478
- done = true;
479
- for (const animation of animations) {
480
- animation.cancel();
481
- }
482
- out.hidden = true;
483
- for (const panel of [out, into]) {
484
- panel.style.position = "";
485
- panel.style.top = "";
486
- panel.style.insetInlineStart = "";
487
- panel.style.width = "";
488
- panel.style.transform = "";
489
- }
490
- this.style.position = "";
491
- this.style.overflow = "";
492
- this.style.height = "";
493
- this.finishSlide = null;
494
- };
495
- this.finishSlide = finish;
496
- animations[0].finished.then(finish, finish);
497
- }
498
- }
499
-
500
- if (!customElements.get("blume-nav")) {
501
- customElements.define("blume-nav", BlumeNav);
502
- }
503
- </script>
@@ -0,0 +1,45 @@
1
+ ---
2
+ // Renders its slot — a sidebar subtree with no active page in it — through the
3
+ // build-time subtree cache (see nav-cache.ts), so a large sidebar is rendered
4
+ // once per group instead of once per page. Off in dev, where component edits
5
+ // must show on the next request. The cached HTML references the page's icon
6
+ // sprite (see icon-sprite.ts), so the symbols it used ride along and are
7
+ // registered into whichever page reuses it.
8
+ import type { NavNode } from "../../core/types.ts";
9
+ import { iconSpriteFor, referencedIconSymbols } from "../icon-sprite.ts";
10
+ import type { IconSymbol } from "../icon-sprite.ts";
11
+ import { cachedNavSubtree } from "./nav-cache.ts";
12
+
13
+ interface Props {
14
+ node: NavNode;
15
+ variant: string;
16
+ }
17
+
18
+ const { node, variant } = Astro.props;
19
+ const sprite = iconSpriteFor(Astro.locals);
20
+ const { html, icons } = await cachedNavSubtree(
21
+ node,
22
+ variant,
23
+ async () => {
24
+ const rendered = await Astro.slots.render("default");
25
+ const used: [string, IconSymbol][] = [];
26
+ for (const id of referencedIconSymbols(rendered)) {
27
+ const symbol = sprite?.symbols.get(id);
28
+ if (symbol) {
29
+ used.push([id, symbol]);
30
+ }
31
+ }
32
+ return { html: rendered, icons: used };
33
+ },
34
+ !import.meta.env.DEV
35
+ );
36
+ if (sprite) {
37
+ for (const [id, symbol] of icons) {
38
+ if (!sprite.symbols.has(id)) {
39
+ sprite.symbols.set(id, symbol);
40
+ }
41
+ }
42
+ }
43
+ ---
44
+
45
+ <Fragment set:html={html} />