@dogsbay/docs-layout 0.2.0-beta.14 → 0.2.0-beta.140

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.
@@ -33,6 +33,29 @@ export interface BuildChipsOptions {
33
33
  * text. URL still uses the slug.
34
34
  */
35
35
  labels?: Record<string, string>;
36
+ /**
37
+ * Color chips whose prefix has no `color` with a palette color
38
+ * hashed from the first segment (`autoTagColor`). Default false.
39
+ */
40
+ autoColor?: boolean;
41
+ }
42
+
43
+ /** Palette order for automatic colors. */
44
+ const AUTO_PALETTE: readonly TagPaletteName[] = ["blue", "amber", "emerald", "violet", "rose", "slate"];
45
+
46
+ /**
47
+ * A stable palette color for a tag segment: FNV-1a hash of the
48
+ * lowercased segment, modulo the palette. The same segment gets the
49
+ * same color on every page and every build; with more distinct tags
50
+ * than colors, colors repeat.
51
+ */
52
+ export function autoTagColor(segment: string): TagPaletteName {
53
+ let hash = 0x811c9dc5;
54
+ for (const ch of segment.toLowerCase()) {
55
+ hash ^= ch.codePointAt(0)!;
56
+ hash = Math.imul(hash, 0x01000193) >>> 0;
57
+ }
58
+ return AUTO_PALETTE[hash % AUTO_PALETTE.length];
36
59
  }
37
60
 
38
61
  export interface ChipModel {
@@ -164,7 +187,7 @@ export function buildChips(
164
187
  options: BuildChipsOptions,
165
188
  ): ChipModel[] {
166
189
  const safe = filterTags(tags);
167
- const { indexPath, prefixes, labels } = options;
190
+ const { indexPath, prefixes, labels, autoColor = false } = options;
168
191
 
169
192
  return safe.map((tag): ChipModel => {
170
193
  const href = `${indexPath}/${tag}/`;
@@ -182,7 +205,7 @@ export function buildChips(
182
205
  href,
183
206
  prefixLabel: null,
184
207
  leafLabel: overrideLeaf ?? tag,
185
- color: null,
208
+ color: autoColor ? autoTagColor(firstSegment) : null,
186
209
  };
187
210
  }
188
211
 
@@ -196,7 +219,7 @@ export function buildChips(
196
219
  href,
197
220
  prefixLabel: prefixEntry.label ?? null,
198
221
  leafLabel: leafLabel.length > 0 ? leafLabel : tag,
199
- color: prefixEntry.color ?? null,
222
+ color: prefixEntry.color ?? (autoColor ? autoTagColor(firstSegment) : null),
200
223
  };
201
224
  });
202
225
  }
package/src/toc-kind.css CHANGED
@@ -11,40 +11,40 @@
11
11
  line-height: 1.4;
12
12
  }
13
13
  .toc-kind-class {
14
- background-color: oklch(0.55 0.18 265 / 0.15);
15
- color: oklch(0.55 0.18 265);
14
+ background-color: oklch(0.44 0.18 265 / 0.15);
15
+ color: oklch(0.44 0.18 265);
16
16
  }
17
17
  .dark .toc-kind-class {
18
18
  background-color: oklch(0.7 0.15 265 / 0.2);
19
19
  color: oklch(0.7 0.15 265);
20
20
  }
21
21
  .toc-kind-meth {
22
- background-color: oklch(0.55 0.15 155 / 0.15);
23
- color: oklch(0.55 0.15 155);
22
+ background-color: oklch(0.44 0.15 155 / 0.15);
23
+ color: oklch(0.44 0.15 155);
24
24
  }
25
25
  .dark .toc-kind-meth {
26
26
  background-color: oklch(0.7 0.15 155 / 0.2);
27
27
  color: oklch(0.7 0.15 155);
28
28
  }
29
29
  .toc-kind-attr {
30
- background-color: oklch(0.6 0.15 55 / 0.15);
31
- color: oklch(0.6 0.15 55);
30
+ background-color: oklch(0.44 0.15 55 / 0.15);
31
+ color: oklch(0.44 0.15 55);
32
32
  }
33
33
  .dark .toc-kind-attr {
34
34
  background-color: oklch(0.75 0.15 55 / 0.2);
35
35
  color: oklch(0.75 0.15 55);
36
36
  }
37
37
  .toc-kind-func {
38
- background-color: oklch(0.55 0.18 330 / 0.15);
39
- color: oklch(0.55 0.18 330);
38
+ background-color: oklch(0.44 0.18 330 / 0.15);
39
+ color: oklch(0.44 0.18 330);
40
40
  }
41
41
  .dark .toc-kind-func {
42
42
  background-color: oklch(0.7 0.15 330 / 0.2);
43
43
  color: oklch(0.7 0.15 330);
44
44
  }
45
45
  .toc-kind-module {
46
- background-color: oklch(0.5 0.12 200 / 0.15);
47
- color: oklch(0.5 0.12 200);
46
+ background-color: oklch(0.44 0.12 200 / 0.15);
47
+ color: oklch(0.44 0.12 200);
48
48
  }
49
49
  .dark .toc-kind-module {
50
50
  background-color: oklch(0.65 0.12 200 / 0.2);
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Decide which TOC container(s) and right-rail region `DocsLayout` renders,
3
+ * given the `toc` placement mode and per-page facts. Factored out of
4
+ * `DocsLayout.astro` so the branching logic is unit-testable (the .astro
5
+ * file just consumes the result) — same pattern as `nav-filter.ts` etc.
6
+ *
7
+ * See plans/ask-branch1-placement-toc.md.
8
+ */
9
+
10
+ export type TocMode = "top" | "popover" | "rail" | "off";
11
+
12
+ export interface TocPlacement {
13
+ /** Classic right-hand TOC sidebar (the pre-`toc`-option layout). */
14
+ railToc: boolean;
15
+ /** Expandable "On this page" disclosure at the top of the article. */
16
+ topToc: boolean;
17
+ /** "On this page" dropdown in the header. */
18
+ popoverToc: boolean;
19
+ /**
20
+ * The right-rail plugin region (host for the `right-rail` named slot, e.g.
21
+ * an Ask AI panel). Present whenever the rail isn't the classic TOC's, and
22
+ * deliberately NOT gated on headings — so a plugin can dock on heading-less
23
+ * pages. Never shown on wide/API pages (no rail there).
24
+ */
25
+ regionRail: boolean;
26
+ }
27
+
28
+ /**
29
+ * Whether a page has enough displayable headings to warrant a TOC. The TOC
30
+ * (`DocsToc`) only lists depth `minDepth`–`maxDepth` (2–3 by default), so the
31
+ * page H1 never counts; and a TOC of a single entry isn't useful, so require
32
+ * `min` (2) by default. Pages like a landing/welcome page — H1 + prose, no
33
+ * sub-sections — therefore get no "On this page".
34
+ */
35
+ export function hasDisplayableToc(
36
+ headings: { depth: number }[],
37
+ opts: { minDepth?: number; maxDepth?: number; min?: number } = {},
38
+ ): boolean {
39
+ const { minDepth = 2, maxDepth = 3, min = 2 } = opts;
40
+ let count = 0;
41
+ for (const h of headings) {
42
+ if (h.depth >= minDepth && h.depth <= maxDepth) count++;
43
+ if (count >= min) return true;
44
+ }
45
+ return false;
46
+ }
47
+
48
+ export interface TocPlacementInput {
49
+ /** Page has at least one heading to list. */
50
+ hasHeadings: boolean;
51
+ /** Wide/API layout — composes its own columns, so no right rail. */
52
+ wideLayout: boolean;
53
+ }
54
+
55
+ /**
56
+ * Resolve the placement flags. A TOC container only renders when there are
57
+ * headings to show; the region rail is independent of headings. `rail` and
58
+ * the region rail are mutually exclusive (they compete for the same column),
59
+ * and both are suppressed on wide pages.
60
+ */
61
+ export function resolveTocPlacement(
62
+ toc: TocMode,
63
+ { hasHeadings, wideLayout }: TocPlacementInput,
64
+ ): TocPlacement {
65
+ return {
66
+ railToc: toc === "rail" && hasHeadings && !wideLayout,
67
+ topToc: toc === "top" && hasHeadings,
68
+ popoverToc: toc === "popover" && hasHeadings,
69
+ regionRail: toc !== "rail" && !wideLayout,
70
+ };
71
+ }
@@ -31,6 +31,15 @@ export interface AxisRedirectConfig {
31
31
  defaultVersion?: string;
32
32
  /** Full set of declared version ids. Empty/undefined → axis inactive. */
33
33
  knownVersions?: string[];
34
+ /**
35
+ * The version served at the site root (`versions[].path: ""`, the
36
+ * default version). A URL with no version segment is one of its pages,
37
+ * so the version is never injected; a URL starting with another known
38
+ * version's segment is that version's (the longest matching prefix
39
+ * wins). Only the locale redirect remains. format-astro emits no version
40
+ * config at all in that case; this keeps the helper right when given one.
41
+ */
42
+ rootVersion?: string;
34
43
  /** Default locale id; required when locale axis is active. */
35
44
  defaultLocale?: string;
36
45
  /** Full set of declared locale ids. Empty/undefined → axis inactive. */
@@ -126,7 +135,8 @@ export function shouldRedirectToDefaultVersion(
126
135
  if (
127
136
  versionAxisActive &&
128
137
  cursor < segments.length &&
129
- knownVersions.has(segments[cursor])
138
+ knownVersions.has(segments[cursor]) &&
139
+ segments[cursor] !== config.rootVersion
130
140
  ) {
131
141
  presentVersion = segments[cursor];
132
142
  cursor += 1;
@@ -134,13 +144,16 @@ export function shouldRedirectToDefaultVersion(
134
144
 
135
145
  // What's the rest of the path beyond the recognised axis prefixes?
136
146
  const tailSegments = segments.slice(cursor);
147
+ // With a root version, a URL without a version segment is already complete.
148
+ const versionMissing =
149
+ versionAxisActive && presentVersion === undefined && !config.rootVersion;
137
150
  if (tailSegments.length === 0) {
138
151
  // The path is JUST a known axis prefix (e.g. /docs/en/ or
139
152
  // /docs/v1/). Treat as the axis's landing page; no redirect
140
153
  // needed unless other axes are MISSING from the URL too.
141
154
  if (
142
155
  (localeAxisActive && presentLocale === undefined) ||
143
- (versionAxisActive && presentVersion === undefined)
156
+ versionMissing
144
157
  ) {
145
158
  // Some axis still missing — fall through to redirect.
146
159
  } else {
@@ -150,7 +163,7 @@ export function shouldRedirectToDefaultVersion(
150
163
 
151
164
  // If all required-active axes are present, no redirect needed.
152
165
  const localeNeeded = localeAxisActive && presentLocale === undefined;
153
- const versionNeeded = versionAxisActive && presentVersion === undefined;
166
+ const versionNeeded = versionMissing;
154
167
  if (!localeNeeded && !versionNeeded) return null;
155
168
 
156
169
  // Compose the redirect target with missing segments injected
@@ -159,7 +172,7 @@ export function shouldRedirectToDefaultVersion(
159
172
  if (localeAxisActive) {
160
173
  out.push(presentLocale ?? config.defaultLocale!);
161
174
  }
162
- if (versionAxisActive) {
175
+ if (versionAxisActive && (presentVersion !== undefined || !config.rootVersion)) {
163
176
  out.push(presentVersion ?? config.defaultVersion!);
164
177
  }
165
178
  out.push(...tailSegments);