@takazudo/zudo-doc 5.26.5 → 5.28.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 (39) hide show
  1. package/CHANGELOG.md +32 -1
  2. package/dist/compiled.css +18 -0
  3. package/dist/config-assertions/index.d.ts +13 -0
  4. package/dist/config-assertions/index.js +9 -0
  5. package/dist/config.d.ts +14 -0
  6. package/dist/config.js +4 -1
  7. package/dist/extract-headings/index.d.ts +2 -0
  8. package/dist/extract-headings/index.js +165 -16
  9. package/dist/header/header.js +47 -19
  10. package/dist/header-with-defaults/index.js +5 -1
  11. package/dist/i18n-defaults/index.js +10 -0
  12. package/dist/plugins/internal/search-index/collect.js +15 -12
  13. package/dist/plugins/internal/search-index/types.d.ts +25 -3
  14. package/dist/plugins/internal/search-index/types.js +1 -1
  15. package/dist/preset.d.ts +8 -0
  16. package/dist/preset.js +4 -1
  17. package/dist/safelist.css +1 -1
  18. package/dist/search-widget/index.js +1 -1
  19. package/dist/settings.d.ts +1 -0
  20. package/dist/sidebar-toggle-island/index.d.ts +4 -1
  21. package/dist/sidebar-toggle-island/index.js +5 -0
  22. package/dist/sidebar-tree-island/index.d.ts +4 -1
  23. package/dist/sidebar-tree-island/index.js +5 -5
  24. package/dist/sidebar-with-defaults/index.js +3 -0
  25. package/dist/theme/color-scheme-provider.js +40 -10
  26. package/dist/theme/theme-toggle.d.ts +1 -1
  27. package/dist/theme-toggle/color-scheme-sync.d.ts +19 -39
  28. package/dist/theme-toggle/color-scheme-sync.js +81 -6
  29. package/dist/theme-toggle/index.d.ts +10 -1
  30. package/dist/theme-toggle/index.js +219 -58
  31. package/dist/theme-toggle/labels.d.ts +3 -0
  32. package/dist/theme-toggle/labels.js +12 -0
  33. package/eject/header/header.tsx +56 -19
  34. package/eject/sidebar-toggle-island/index.tsx +14 -0
  35. package/eject/sidebar-tree-island/index.tsx +8 -6
  36. package/eject/theme-toggle/color-scheme-sync.ts +108 -63
  37. package/eject/theme-toggle/index.tsx +217 -89
  38. package/eject/theme-toggle/labels.ts +15 -0
  39. package/package.json +9 -9
package/CHANGELOG.md CHANGED
@@ -8,11 +8,42 @@ The format is based on Keep a Changelog, and release notes are generated from th
8
8
 
9
9
  No unreleased changes.
10
10
 
11
+ ## [5.28.0] - 2026-09-27
12
+
13
+ ### Features
14
+
15
+ - Search now indexes up to 3000 characters per page instead of 300, making longer pages searchable beyond their opening paragraphs. Set `searchMaxBodyLength` to a positive integer to adjust the limit; result snippets retain their existing display length. (332a6184b)
16
+
17
+ ### Bug Fixes
18
+
19
+ - Heading extraction for the table of contents and link checks ignores heading-like text inside multiline JSX expressions, comments, quoted attributes, and template-literal props, while preserving headings in JSX children. Multiline inline-code spans no longer confuse JSX detection. (b85fe3469, 99ad4252b)
20
+ - Escape closes the mobile drawer's Appearance menu and returns focus to its trigger; a second Escape closes the drawer and focuses the hamburger button. (66359d27c)
21
+
22
+ ### Other Changes
23
+
24
+ - Raise the zfb, zfb-runtime, and zfb-md-wasm peer floors to `^2.21.1`, bringing single-Preact-runtime SSR fixes, collection include-filter handling, faster startup, and workspace-package staging and reload fixes. No configuration migration is required. (6686f2da6, e82e57551)
25
+
26
+ ## [5.27.0] - 2026-09-24
27
+
28
+ ### Features
29
+
30
+ - Added an accessible Appearance menu with Light, Dark, and System choices. System follows device changes, and the selected preference persists across navigation (5bb3d2175, d2146b6e0).
31
+
32
+ ### Bug Fixes
33
+
34
+ - Grouped header icons into 40 px controls with no internal gap and 12 px boundaries beside other controls; the Appearance menu also returns keyboard focus to its trigger after closing (4c718aa02).
35
+ - Kept the Appearance menu above the design token panel and within the available viewport (1773a3a21).
36
+ - Corrected heading IDs and table-of-contents links when headings contain escaped punctuation or code spans (59d47f68a).
37
+
38
+ ### Other Changes
39
+
40
+ - Raised the minimum supported zfb toolchain peer version to 2.20.2 (e22acafdf).
41
+
11
42
  ## [5.26.5] - 2026-09-23
12
43
 
13
44
  ### Bug Fixes
14
45
 
15
- - Fixed package-owned route injection in nested pnpm workspace consumers. Published route sources are staged under the project root so the build shadow can resolve the generated 404, robots, and docs routes (848264b9c).
46
+ - Fixed package-owned route injection in nested pnpm workspace consumers. Published route sources are staged under the project root so the build shadow can resolve the generated 404, robots, and docs routes (848264b9c). Projects scaffolded on 5.26.4 or earlier should add `.zudo-doc/` to their `.gitignore`: the build now writes the staged route sources to `.zudo-doc/routes-src/`, and without the ignore entry `git status` will show it as an untracked directory. New scaffolds already ignore it; see the `create-zudo-doc` changelog.
16
47
 
17
48
  ## [5.26.4] - 2026-09-22
18
49
 
package/dist/compiled.css CHANGED
@@ -387,6 +387,9 @@
387
387
  .z-toolbar {
388
388
  z-index: 20;
389
389
  }
390
+ .z-tooltip {
391
+ z-index: 80;
392
+ }
390
393
  .col-span-full {
391
394
  grid-column: 1 / -1;
392
395
  }
@@ -604,6 +607,9 @@
604
607
  .h-\[14px\] {
605
608
  height: 14px;
606
609
  }
610
+ .h-\[40px\] {
611
+ height: 40px;
612
+ }
607
613
  .h-\[70vh\] {
608
614
  height: 70vh;
609
615
  }
@@ -715,6 +721,9 @@
715
721
  .w-\[18px\] {
716
722
  width: 18px;
717
723
  }
724
+ .w-\[40px\] {
725
+ width: 40px;
726
+ }
718
727
  .w-\[90vw\] {
719
728
  width: 90vw;
720
729
  }
@@ -1152,6 +1161,12 @@
1152
1161
  .bg-accent {
1153
1162
  background-color: var(--color-accent);
1154
1163
  }
1164
+ .bg-accent\/10 {
1165
+ background-color: var(--color-accent);
1166
+ @supports (color: color-mix(in lab, red, red)) {
1167
+ background-color: color-mix(in oklab, var(--color-accent) 10%, transparent);
1168
+ }
1169
+ }
1155
1170
  .bg-bg {
1156
1171
  background-color: var(--color-bg);
1157
1172
  }
@@ -1233,6 +1248,9 @@
1233
1248
  .p-hsp-xl {
1234
1249
  padding: var(--spacing-hsp-xl);
1235
1250
  }
1251
+ .p-hsp-xs {
1252
+ padding: var(--spacing-hsp-xs);
1253
+ }
1236
1254
  .px-hsp-2xl {
1237
1255
  padding-inline: var(--spacing-hsp-2xl);
1238
1256
  }
@@ -68,6 +68,19 @@ export declare function resolvesBundleZdtp(settings: ZdtpBundlingSubject): boole
68
68
  * in the browser. Fail loudly at config resolution instead (#4261).
69
69
  */
70
70
  export declare function assertZdtpBundlingConsistent(settings: ZdtpBundlingSubject): void;
71
+ /**
72
+ * Reject a `searchMaxBodyLength` that is not a positive integer.
73
+ *
74
+ * `undefined` no-ops (the field is optional at this layer — the search-index
75
+ * collector applies its own `MAX_BODY_LENGTH` default). Shared between
76
+ * `zudoDoc()` (`../config.ts`) and `zudoDocPreset()` (same rationale as the
77
+ * other guards above) AND the search-index collector itself
78
+ * (`../plugins/internal/search-index/collect.ts`), which is also reachable
79
+ * directly (tests, or a consumer calling `collectSearchEntries()` outside
80
+ * `zudoDoc()`) — clamping silently there would hide a config typo behind a
81
+ * quietly-wrong index instead of failing loudly (zudolab/zudo-doc#4407).
82
+ */
83
+ export declare function assertValidSearchMaxBodyLength(searchMaxBodyLength: number | undefined): void;
71
84
  /**
72
85
  * Warn about `categoryMatch` values that cannot express the intended
73
86
  * dropdown grouping.
@@ -29,6 +29,14 @@ function assertZdtpBundlingConsistent(settings) {
29
29
  );
30
30
  }
31
31
  }
32
+ function assertValidSearchMaxBodyLength(searchMaxBodyLength) {
33
+ if (searchMaxBodyLength === void 0) return;
34
+ if (!Number.isInteger(searchMaxBodyLength) || searchMaxBodyLength <= 0) {
35
+ throw new TypeError(
36
+ `Invalid searchMaxBodyLength ${JSON.stringify(searchMaxBodyLength)}: must be a positive integer (e.g. 3000).`
37
+ );
38
+ }
39
+ }
32
40
  function warnAmbiguousDropdownCategoryMatch(headerNav, logger = console) {
33
41
  if (!headerNav) return;
34
42
  for (const item of headerNav) {
@@ -70,6 +78,7 @@ function warnAmbiguousDropdownCategoryMatch(headerNav, logger = console) {
70
78
  }
71
79
  export {
72
80
  assertNoEmptyStringFaviconOrLogo,
81
+ assertValidSearchMaxBodyLength,
73
82
  assertZdtpBundlingConsistent,
74
83
  resolvesBundleZdtp,
75
84
  warnAmbiguousDropdownCategoryMatch
package/dist/config.d.ts CHANGED
@@ -368,6 +368,20 @@ export interface ZudoDocConfig {
368
368
  * @default 4
369
369
  */
370
370
  tocMaxDepth?: number;
371
+ /**
372
+ * How many characters of each page's plain-text body are indexed for
373
+ * search *matching* (`search-index.json`'s per-entry `body` field). This is
374
+ * a match-depth cap, not a display cap — the search widget already renders
375
+ * only a short match-centred window (`truncate(text, query, 200)`), so
376
+ * raising this value makes long pages searchable past their opening
377
+ * paragraphs without changing what a result snippet shows. Also caps how
378
+ * many bytes are read from a public text asset for its search excerpt (the
379
+ * asset-viewer indexing path in `plugins/internal/search-index/collect.ts`).
380
+ * Must be a positive integer — a non-integer or ≤ 0 value throws at config
381
+ * resolution.
382
+ * @default 3000
383
+ */
384
+ searchMaxBodyLength?: number;
371
385
  /**
372
386
  * Enable the draggable sidebar resizer.
373
387
  * @default false
package/dist/config.js CHANGED
@@ -6,7 +6,8 @@ import { defaultColorSchemes } from "./color-schemes-defaults/index.js";
6
6
  import { assertNoCommaInVersionSlugs } from "./version-availability/index.js";
7
7
  import {
8
8
  assertNoEmptyStringFaviconOrLogo,
9
- assertZdtpBundlingConsistent
9
+ assertZdtpBundlingConsistent,
10
+ assertValidSearchMaxBodyLength
10
11
  } from "./config-assertions/index.js";
11
12
  import { validateAssetViewerSettings } from "./asset-path/index.js";
12
13
  function mergeDefaultTranslations(overrides) {
@@ -91,6 +92,7 @@ const DEFAULT_SETTINGS = {
91
92
  designTokenPanel: false,
92
93
  tocMinDepth: 2,
93
94
  tocMaxDepth: 4,
95
+ searchMaxBodyLength: 3e3,
94
96
  sidebarResizer: false,
95
97
  sidebarToggle: false,
96
98
  tocToggle: false,
@@ -152,6 +154,7 @@ function zudoDoc(user = {}) {
152
154
  assertNoCommaInVersionSlugs(settings.versions);
153
155
  assertNoEmptyStringFaviconOrLogo(settings);
154
156
  assertZdtpBundlingConsistent(settings);
157
+ assertValidSearchMaxBodyLength(settings.searchMaxBodyLength);
155
158
  const fragment = zudoDocPreset({
156
159
  settings,
157
160
  buildDocsSchema: userBuildDocsSchema ?? (() => defaultBuildDocsSchema({
@@ -38,3 +38,5 @@ export declare function extractHeadings(body: string, opts?: {
38
38
  tocMinDepth?: number;
39
39
  tocMaxDepth?: number;
40
40
  }): HeadingItem[];
41
+ /** All rendered h2–h6 IDs, including headings below the TOC depth window. */
42
+ export declare function extractAllHeadingIds(body: string): string[];
@@ -52,7 +52,39 @@ class SlugAllocator {
52
52
  }
53
53
  }
54
54
  function stripInlineMarkdown(raw) {
55
- return raw.replace(/\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/`([^`]+)`/g, "$1").replace(/\*\*([^*]+)\*\*/g, "$1").replace(/(?<![\w])__([^_]+)__(?![\w])/g, "$1").replace(/\*([^*]+)\*/g, "$1").replace(/(?<![\w])_([^_]+)_(?![\w])/g, "$1").trim();
55
+ const protectedText = [];
56
+ const protect = (value) => {
57
+ const index = protectedText.push(value) - 1;
58
+ return `\uE000${index}\uE001`;
59
+ };
60
+ let masked = "";
61
+ for (let i = 0; i < raw.length; ) {
62
+ if (raw[i] === "\\" && i + 1 < raw.length && /[!-/:-@[-`{-~]/.test(raw[i + 1] ?? "")) {
63
+ masked += protect(raw[i + 1] ?? "");
64
+ i += 2;
65
+ continue;
66
+ }
67
+ if (raw[i] === "`") {
68
+ const opener = /^`+/.exec(raw.slice(i))?.[0] ?? "`";
69
+ const closer = /`+/g;
70
+ closer.lastIndex = i + opener.length;
71
+ let match;
72
+ while ((match = closer.exec(raw)) !== null && match[0].length !== opener.length) {
73
+ }
74
+ if (match !== null) {
75
+ let code = raw.slice(i + opener.length, match.index).replace(/\n/g, " ");
76
+ if (code.startsWith(" ") && code.endsWith(" ") && /[^ ]/.test(code)) {
77
+ code = code.slice(1, -1);
78
+ }
79
+ masked += protect(code);
80
+ i = match.index + opener.length;
81
+ continue;
82
+ }
83
+ }
84
+ masked += raw[i];
85
+ i++;
86
+ }
87
+ return masked.replace(/\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/\*\*([^*]+)\*\*/g, "$1").replace(/(?<![A-Za-z0-9])__([^_]+)__(?![A-Za-z0-9])/g, "$1").replace(/\*([^*]+)\*/g, "$1").replace(/(?<![A-Za-z0-9])_([^_]+)_(?![A-Za-z0-9])/g, "$1").replace(/\uE000(\d+)\uE001/g, (_match, index) => protectedText[Number(index)] ?? "").trim();
56
88
  }
57
89
  function resolveDepthWindow(rawMin, rawMax) {
58
90
  const min = Math.trunc(Number(rawMin));
@@ -67,23 +99,18 @@ function extractHeadings(body, opts) {
67
99
  opts?.tocMinDepth ?? 2,
68
100
  opts?.tocMaxDepth ?? 4
69
101
  );
102
+ return collectHeadings(body, lo, hi);
103
+ }
104
+ function extractAllHeadingIds(body) {
105
+ return collectHeadings(body, 2, 6).map((heading) => heading.slug).filter(Boolean);
106
+ }
107
+ function collectHeadings(body, lo, hi) {
70
108
  const allocator = new SlugAllocator();
71
109
  const headings = [];
72
- let codeFenceOpener = null;
73
- for (const line of body.split("\n")) {
74
- const trimmed = line.trimStart();
75
- const fenceMatch = /^([`~]{3,})/.exec(trimmed);
76
- if (fenceMatch) {
77
- const fence = fenceMatch[1];
78
- if (fence === void 0) continue;
79
- if (codeFenceOpener === null) {
80
- codeFenceOpener = fence;
81
- } else if (fence[0] === codeFenceOpener[0] && fence.length >= codeFenceOpener.length) {
82
- codeFenceOpener = null;
83
- }
84
- continue;
85
- }
86
- if (codeFenceOpener !== null) continue;
110
+ const lines = body.split("\n");
111
+ const candidate = classifyLines(lines);
112
+ for (const [index, line] of lines.entries()) {
113
+ if (candidate[index] !== true) continue;
87
114
  const match = /^(#{2,6})[ \t]+(.+)$/.exec(line.trim());
88
115
  if (!match) continue;
89
116
  const hashes = match[1];
@@ -98,7 +125,129 @@ function extractHeadings(body, opts) {
98
125
  }
99
126
  return headings;
100
127
  }
128
+ const JSX_TAG_CHAR = /[\p{ID_Continue}$\-.:=/>"'{\s]/u;
129
+ const JSX_TAG_START = /[\p{ID_Start}$_/>]/u;
130
+ const ASCII_PUNCTUATION = /[!-/:-@[-`{-~]/;
131
+ const ATX_HEADING = /^#{1,6}(?:[ \t]|$)/;
132
+ function findCodeSpanEnd(lines, from, runLength) {
133
+ for (let lineIndex = from; lineIndex < lines.length; lineIndex++) {
134
+ const line = lines[lineIndex] ?? "";
135
+ const trimmed = line.trimStart();
136
+ if (trimmed.trim() === "" || ATX_HEADING.test(trimmed) || /^(?:`{3,}|~{3,})/.test(trimmed)) {
137
+ return null;
138
+ }
139
+ for (const match of line.matchAll(/`+/g)) {
140
+ if (match[0].length === runLength) {
141
+ return { line: lineIndex, col: match.index + runLength };
142
+ }
143
+ }
144
+ }
145
+ return null;
146
+ }
147
+ function classifyLines(lines) {
148
+ const ignoredOpeners = /* @__PURE__ */ new Set();
149
+ for (; ; ) {
150
+ const result = scanLines(lines, ignoredOpeners);
151
+ if (result.unclosedOpener === null) return result.candidate;
152
+ ignoredOpeners.add(result.unclosedOpener);
153
+ }
154
+ }
155
+ function scanLines(lines, ignoredOpeners) {
156
+ const candidate = [];
157
+ let codeFenceOpener = null;
158
+ let exprDepth = 0;
159
+ let tag = null;
160
+ let opener = null;
161
+ let codeSpanEnd = null;
162
+ for (const [lineIndex, line] of lines.entries()) {
163
+ let start = 0;
164
+ if (codeSpanEnd !== null) {
165
+ candidate.push(false);
166
+ if (lineIndex < codeSpanEnd.line) continue;
167
+ start = codeSpanEnd.col;
168
+ codeSpanEnd = null;
169
+ }
170
+ const trimmed = line.trimStart();
171
+ if (start === 0 && tag !== null && tag.quote === null && exprDepth === 0) {
172
+ const first = trimmed[0];
173
+ if (first !== void 0 && !JSX_TAG_CHAR.test(first)) tag = null;
174
+ }
175
+ if (start > 0) {
176
+ } else if (exprDepth > 0 || tag !== null) {
177
+ candidate.push(false);
178
+ } else {
179
+ const fence = /^([`~]{3,})/.exec(trimmed)?.[1];
180
+ if (fence !== void 0) {
181
+ if (codeFenceOpener === null) {
182
+ codeFenceOpener = fence;
183
+ } else if (fence[0] === codeFenceOpener[0] && fence.length >= codeFenceOpener.length) {
184
+ codeFenceOpener = null;
185
+ }
186
+ candidate.push(false);
187
+ continue;
188
+ }
189
+ candidate.push(codeFenceOpener === null);
190
+ if (codeFenceOpener !== null) continue;
191
+ }
192
+ for (let i = start; i < line.length; i++) {
193
+ const ch = line[i] ?? "";
194
+ if (exprDepth > 0) {
195
+ if (ch === "{") exprDepth++;
196
+ else if (ch === "}") exprDepth--;
197
+ continue;
198
+ }
199
+ if (tag !== null) {
200
+ if (tag.quote !== null) {
201
+ if (ch === tag.quote) tag.quote = null;
202
+ } else if (ch === '"' || ch === "'") {
203
+ tag.quote = ch;
204
+ } else if (ch === "{") {
205
+ exprDepth = 1;
206
+ } else if (ch === ">") {
207
+ tag = null;
208
+ } else if (!JSX_TAG_CHAR.test(ch)) {
209
+ tag = null;
210
+ i--;
211
+ }
212
+ continue;
213
+ }
214
+ if (ch === "\\" && ASCII_PUNCTUATION.test(line[i + 1] ?? "")) {
215
+ i++;
216
+ continue;
217
+ }
218
+ if (ch === "`") {
219
+ const run = /^`+/.exec(line.slice(i))?.[0] ?? "`";
220
+ const closer = /`+/g;
221
+ closer.lastIndex = i + run.length;
222
+ let match;
223
+ while ((match = closer.exec(line)) !== null && match[0].length !== run.length) {
224
+ }
225
+ if (match === null && !ATX_HEADING.test(trimmed)) {
226
+ codeSpanEnd = findCodeSpanEnd(lines, lineIndex + 1, run.length);
227
+ if (codeSpanEnd !== null) break;
228
+ }
229
+ i = (match === null ? i : match.index) + run.length - 1;
230
+ continue;
231
+ }
232
+ const position = `${lineIndex}:${i}`;
233
+ if (ignoredOpeners.has(position)) continue;
234
+ if (ch === "{") {
235
+ exprDepth = 1;
236
+ opener = position;
237
+ } else if (ch === "<" && JSX_TAG_START.test(line[i + 1] ?? "")) {
238
+ tag = { quote: null };
239
+ opener = position;
240
+ }
241
+ }
242
+ if (exprDepth === 0 && tag === null) opener = null;
243
+ }
244
+ return {
245
+ candidate,
246
+ unclosedOpener: exprDepth > 0 || tag !== null ? opener : null
247
+ };
248
+ }
101
249
  export {
250
+ extractAllHeadingIds,
102
251
  extractHeadings,
103
252
  slugify
104
253
  };
@@ -49,6 +49,43 @@ function Header(props) {
49
49
  const matchPath = pathForMatch(pathWithoutVersion, lang, i18n.defaultLocale);
50
50
  const activeNavPath = computeActiveNavPath(headerNav, matchPath);
51
51
  const rightItemDispatch = createRightItemDispatch(headerRightComponents);
52
+ const rightItems = headerRightItems.map((item, i) => ({
53
+ item,
54
+ node: renderRightItem(
55
+ item,
56
+ i,
57
+ {
58
+ lang,
59
+ githubRepoUrl,
60
+ githubLabel,
61
+ themeToggle,
62
+ languageSwitcher,
63
+ versionSwitcher,
64
+ search,
65
+ colorModeEnabled,
66
+ hasLocales
67
+ },
68
+ rightItemDispatch
69
+ )
70
+ })).filter((entry) => entry.node !== null);
71
+ const rightGroups = [];
72
+ let iconGroup = [];
73
+ const flushIconGroup = () => {
74
+ if (iconGroup.length === 0) return;
75
+ rightGroups.push(
76
+ /* @__PURE__ */ jsx("div", { class: "flex items-center", "data-header-icon-group": true, children: iconGroup })
77
+ );
78
+ iconGroup = [];
79
+ };
80
+ for (const { item, node } of rightItems) {
81
+ if (isHeaderIconItem(item)) {
82
+ iconGroup.push(node);
83
+ } else {
84
+ flushIconGroup();
85
+ rightGroups.push(node);
86
+ }
87
+ }
88
+ flushIconGroup();
52
89
  return /* @__PURE__ */ jsxs(
53
90
  "header",
54
91
  {
@@ -123,22 +160,7 @@ function Header(props) {
123
160
  {
124
161
  class: "ml-auto flex shrink-0 items-center gap-x-hsp-md",
125
162
  "data-header-right": true,
126
- children: headerRightItems.map((item, i) => renderRightItem(
127
- item,
128
- i,
129
- {
130
- lang,
131
- githubRepoUrl,
132
- githubLabel,
133
- themeToggle,
134
- languageSwitcher,
135
- versionSwitcher,
136
- search,
137
- colorModeEnabled,
138
- hasLocales
139
- },
140
- rightItemDispatch
141
- ))
163
+ children: rightGroups
142
164
  }
143
165
  ),
144
166
  /* @__PURE__ */ jsx("script", { dangerouslySetInnerHTML: { __html: NAV_OVERFLOW_SCRIPT } }),
@@ -271,6 +293,12 @@ function stripCurrentVersionPrefix(path, currentVersion) {
271
293
  if (path.startsWith(`${prefix}/`)) return path.slice(prefix.length);
272
294
  return path;
273
295
  }
296
+ function isHeaderIconItem(item) {
297
+ if (item.type === "trigger") return true;
298
+ if (item.type === "link") return item.icon === "github";
299
+ if (item.type !== "component") return false;
300
+ return ["github-link", "theme-toggle", "search"].includes(item.component);
301
+ }
274
302
  function TriggerButton({
275
303
  index,
276
304
  id,
@@ -286,7 +314,7 @@ function TriggerButton({
286
314
  {
287
315
  id,
288
316
  type: "button",
289
- class: "flex items-center justify-center text-muted transition-colors hover:text-fg",
317
+ class: "flex h-[40px] w-[40px] shrink-0 items-center justify-center text-muted transition-colors hover:text-fg focus-visible:outline-2 focus-visible:outline-accent focus-visible:outline-offset-2",
290
318
  "aria-label": ariaLabel,
291
319
  ...inlineOnclick,
292
320
  children
@@ -373,7 +401,7 @@ const BASE_RIGHT_ITEM_DISPATCH = {
373
401
  href: ctx.githubRepoUrl,
374
402
  target: "_blank",
375
403
  rel: "noopener noreferrer",
376
- class: "flex items-center justify-center text-muted transition-colors hover:text-fg",
404
+ class: "flex h-[40px] w-[40px] shrink-0 items-center justify-center text-muted transition-colors hover:text-fg focus-visible:outline-2 focus-visible:outline-accent focus-visible:outline-offset-2",
377
405
  "aria-label": ctx.githubLabel,
378
406
  title: ctx.githubLabel,
379
407
  children: [
@@ -403,7 +431,7 @@ const BASE_RIGHT_ITEM_DISPATCH = {
403
431
  href: item.href,
404
432
  target: isExternal ? "_blank" : void 0,
405
433
  rel: isExternal ? "noopener noreferrer" : void 0,
406
- class: "flex items-center justify-center text-muted transition-colors hover:text-fg",
434
+ class: item.icon === "github" ? "flex h-[40px] w-[40px] shrink-0 items-center justify-center text-muted transition-colors hover:text-fg focus-visible:outline-2 focus-visible:outline-accent focus-visible:outline-offset-2" : "flex items-center justify-center text-muted transition-colors hover:text-fg",
407
435
  "aria-label": item.ariaLabel,
408
436
  title: label,
409
437
  children: item.icon === "github" ? /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -6,6 +6,7 @@ import {
6
6
  VersionSwitcher
7
7
  } from "../i18n-version/index.js";
8
8
  import { ThemeToggle } from "../theme-toggle/index.js";
9
+ import { themeToggleLabels } from "../theme-toggle/labels.js";
9
10
  import { SidebarToggle } from "../sidebar-toggle-island/index.js";
10
11
  import { buildGitHubRepoUrl as buildGitHubRepoUrlBase } from "../github-helpers/index.js";
11
12
  import { assertChromeContext } from "../chrome/assert-chrome-context.js";
@@ -47,6 +48,7 @@ function createHeaderWithDefaults(ctx) {
47
48
  const backToMenuLabel = t("nav.backToMenu", lang);
48
49
  const localeLinks = buildLocaleLinksForNav(currentPath, lang, locales.length);
49
50
  const themeDefaultMode = getThemeDefaultMode();
51
+ const themeRespectSystem = (ctx.settings.colorMode && ctx.settings.colorMode.respectPrefersColorScheme) ?? true;
50
52
  const buildDefaultSidebarNodes = () => buildSidebarNodes(lang, navSection, currentVersion);
51
53
  const sidebarNodes = sidebarNodesProp === void 0 ? buildDefaultSidebarNodes() : typeof sidebarNodesProp === "function" ? sidebarNodesProp({
52
54
  lang,
@@ -66,13 +68,15 @@ function createHeaderWithDefaults(ctx) {
66
68
  locale: lang,
67
69
  localeLinks,
68
70
  themeDefaultMode,
71
+ themeLabels: themeToggleLabels(t, lang),
72
+ themeRespectSystem,
69
73
  dateFormats: dateFormatsFor(lang)
70
74
  }
71
75
  )
72
76
  });
73
77
  const themeToggle = Island({
74
78
  when: "load",
75
- children: /* @__PURE__ */ jsx(ThemeToggle, { defaultMode: themeDefaultMode, pendingUntilHydrated: true })
79
+ children: /* @__PURE__ */ jsx(ThemeToggle, { defaultMode: themeDefaultMode, labels: themeToggleLabels(t, lang), respectPrefersColorScheme: themeRespectSystem, pendingUntilHydrated: true })
76
80
  });
77
81
  const searchWidget = /* @__PURE__ */ jsx(
78
82
  SearchWidget,
@@ -37,6 +37,11 @@ const defaultTranslations = {
37
37
  "doc.editPage": "Edit this page",
38
38
  "doc.viewSource": "View source on GitHub",
39
39
  "header.github": "GitHub repository",
40
+ "appearance.title": "Appearance",
41
+ "appearance.light": "Light",
42
+ "appearance.dark": "Dark",
43
+ "appearance.system": "System",
44
+ "appearance.systemHelper": "Follows device \xB7 currently {mode}",
40
45
  "language.switcher.label": "Language",
41
46
  "doc.tags": "Tags",
42
47
  "doc.taggedWith": "Pages tagged with",
@@ -167,6 +172,11 @@ const defaultTranslations = {
167
172
  "doc.editPage": "\u3053\u306E\u30DA\u30FC\u30B8\u3092\u7DE8\u96C6",
168
173
  "doc.viewSource": "GitHub \u3067\u30BD\u30FC\u30B9\u3092\u898B\u308B",
169
174
  "header.github": "GitHub \u30EA\u30DD\u30B8\u30C8\u30EA",
175
+ "appearance.title": "\u5916\u89B3",
176
+ "appearance.light": "\u30E9\u30A4\u30C8",
177
+ "appearance.dark": "\u30C0\u30FC\u30AF",
178
+ "appearance.system": "\u30B7\u30B9\u30C6\u30E0",
179
+ "appearance.systemHelper": "\u30C7\u30D0\u30A4\u30B9\u306E\u8A2D\u5B9A\u306B\u5F93\u3044\u307E\u3059 \xB7 \u73FE\u5728\u306F{mode}",
170
180
  "language.switcher.label": "\u8A00\u8A9E",
171
181
  "doc.tags": "\u30BF\u30B0",
172
182
  "doc.taggedWith": "\u30BF\u30B0\u4ED8\u304D\u30DA\u30FC\u30B8",
@@ -8,14 +8,15 @@ import {
8
8
  stripMarkdown
9
9
  } from "../../../md-utils/index.js";
10
10
  import { collectAssetPageDescriptors } from "../asset-viewer/asset-pages.js";
11
+ import { assertValidSearchMaxBodyLength } from "../../../config-assertions/index.js";
11
12
  import {
12
13
  MAX_BODY_LENGTH
13
14
  } from "./types.js";
14
- function truncateBody(text) {
15
- return text.length > MAX_BODY_LENGTH ? text.substring(0, MAX_BODY_LENGTH) : text;
15
+ function truncateBody(text, maxBodyLength) {
16
+ return text.length > maxBodyLength ? text.substring(0, maxBodyLength) : text;
16
17
  }
17
- function readAssetExcerpt(filePath) {
18
- const sample = Buffer.allocUnsafe(MAX_BODY_LENGTH * 4);
18
+ function readAssetExcerpt(filePath, maxBodyLength) {
19
+ const sample = Buffer.allocUnsafe(maxBodyLength * 4);
19
20
  const fd = openSync(filePath, "r");
20
21
  try {
21
22
  let offset = 0;
@@ -30,12 +31,12 @@ function readAssetExcerpt(filePath) {
30
31
  if (bytesRead === 0) break;
31
32
  offset += bytesRead;
32
33
  }
33
- return truncateBody(sample.subarray(0, offset).toString("utf8"));
34
+ return truncateBody(sample.subarray(0, offset).toString("utf8"), maxBodyLength);
34
35
  } finally {
35
36
  closeSync(fd);
36
37
  }
37
38
  }
38
- function buildEntries(contentDir, locale, base) {
39
+ function buildEntries(contentDir, locale, base, maxBodyLength) {
39
40
  const absDir = resolve(contentDir);
40
41
  const files = collectMdFiles(absDir);
41
42
  const entries = [];
@@ -49,14 +50,14 @@ function buildEntries(contentDir, locale, base) {
49
50
  entries.push({
50
51
  id,
51
52
  title: data.title ?? slug,
52
- body: truncateBody(stripMarkdown(content)),
53
+ body: truncateBody(stripMarkdown(content), maxBodyLength),
53
54
  url: slugToUrl(slug, locale, base),
54
55
  description: data.description ?? ""
55
56
  });
56
57
  }
57
58
  return entries;
58
59
  }
59
- function buildAssetEntries(config) {
60
+ function buildAssetEntries(config, maxBodyLength) {
60
61
  const { assetScan, projectRoot } = config;
61
62
  if (assetScan === void 0 || projectRoot === void 0) return [];
62
63
  const descriptors = collectAssetPageDescriptors({
@@ -67,7 +68,7 @@ function buildAssetEntries(config) {
67
68
  const assetRoot = resolve(projectRoot, "public", assetScan.assetViewerDir);
68
69
  return descriptors.map((descriptor) => {
69
70
  const localePrefix = descriptor.locale === void 0 ? "" : `${descriptor.locale}/`;
70
- const body = descriptor.isText ? readAssetExcerpt(resolve(assetRoot, descriptor.path)) : "";
71
+ const body = descriptor.isText ? readAssetExcerpt(resolve(assetRoot, descriptor.path), maxBodyLength) : "";
71
72
  return {
72
73
  id: `asset:${localePrefix}${assetScan.assetViewerRoutePrefix}/${descriptor.path}`,
73
74
  title: basename(descriptor.path),
@@ -78,15 +79,17 @@ function buildAssetEntries(config) {
78
79
  });
79
80
  }
80
81
  function collectSearchEntries(config) {
82
+ assertValidSearchMaxBodyLength(config.maxBodyLength);
83
+ const maxBodyLength = config.maxBodyLength ?? MAX_BODY_LENGTH;
81
84
  const base = config.base ?? "";
82
85
  const entries = [];
83
- entries.push(...buildEntries(config.docsDir, null, base));
86
+ entries.push(...buildEntries(config.docsDir, null, base, maxBodyLength));
84
87
  if (config.locales) {
85
88
  for (const [code, locale] of Object.entries(config.locales)) {
86
- entries.push(...buildEntries(locale.dir, code, base));
89
+ entries.push(...buildEntries(locale.dir, code, base, maxBodyLength));
87
90
  }
88
91
  }
89
- entries.push(...buildAssetEntries(config));
92
+ entries.push(...buildAssetEntries(config, maxBodyLength));
90
93
  return entries;
91
94
  }
92
95
  export {