@takazudo/zudo-doc 5.12.0 → 5.13.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.
- package/CHANGELOG.md +27 -0
- package/bin/tags-audit-runner.ts +2 -2
- package/dist/catalog.d.ts +42 -10
- package/dist/catalog.js +66 -2
- package/dist/chrome/derive.js +4 -2
- package/dist/color-scheme-utils.d.ts +2 -50
- package/dist/compiled.css +3622 -0
- package/dist/doc-body-end-islands/index.d.ts +6 -0
- package/dist/doc-body-end-islands/index.js +2 -1
- package/dist/doc-body-end-islands/theme-pack-switcher-island.d.ts +6 -0
- package/dist/doc-body-end-islands/theme-pack-switcher-island.js +8 -1
- package/dist/factory-context/index.d.ts +3 -35
- package/dist/features.css +5 -0
- package/dist/header-with-defaults/index.js +1 -1
- package/dist/hydration-pending.d.ts +5 -0
- package/dist/hydration-pending.js +9 -0
- package/dist/plugins/internal/theme-packs/resolve.d.ts +1 -1
- package/dist/plugins/internal/theme-packs/resolve.js +2 -4
- package/dist/plugins/routes.js +2 -4
- package/dist/route-context-payload/index.d.ts +60 -0
- package/dist/route-context-payload/index.js +48 -0
- package/dist/route-context-payload/types.d.ts +115 -0
- package/dist/route-context-payload/types.js +0 -0
- package/dist/safelist.css +1 -1
- package/dist/settings.d.ts +1 -17
- package/dist/sidebar-tree-island/index.js +1 -1
- package/dist/tags-audit.d.ts +3 -2
- package/dist/tags-audit.js +21 -0
- package/dist/theme-cli/registry.d.ts +1 -1
- package/dist/theme-cli/registry.js +1 -3
- package/dist/theme-pack-switcher/index.d.ts +3 -1
- package/dist/theme-pack-switcher/index.js +14 -2
- package/dist/theme-packs-registry/build-registry.d.ts +41 -0
- package/dist/theme-packs-registry/build-registry.js +62 -0
- package/dist/theme-packs-registry/index.d.ts +2 -1
- package/dist/theme-packs-registry/index.js +6 -4
- package/dist/theme-packs-registry/load-registry.d.ts +2 -13
- package/dist/theme-packs-registry/meta-schema.d.ts +1 -21
- package/dist/theme-packs-registry/resolve-enabled-packs.d.ts +1 -1
- package/dist/theme-packs-registry/types.d.ts +1 -0
- package/dist/theme-packs-registry/types.js +0 -0
- package/dist/theme-toggle/index.d.ts +3 -1
- package/dist/theme-toggle/index.js +7 -1
- package/dist/transitions/nested-island-props-refresh.d.ts +3 -1
- package/dist/transitions/nested-island-props-refresh.js +89 -13
- package/eject/sidebar-tree-island/index.tsx +3 -1
- package/eject/theme-toggle/hydration-pending.ts +13 -0
- package/eject/theme-toggle/index.tsx +8 -0
- package/package.json +19 -12
|
@@ -18,6 +18,8 @@ export interface ThemePackSwitcherProps {
|
|
|
18
18
|
/** Base prefix WITH trailing slash (`ctx.withBase("/")`) — forwarded to
|
|
19
19
|
* the browse-all dialog for its `{base}theme-packs/index.json` fetch. */
|
|
20
20
|
base: string;
|
|
21
|
+
/** Keep launcher activation pending until the first successful mount. @default true */
|
|
22
|
+
pendingUntilHydrated?: boolean;
|
|
21
23
|
}
|
|
22
24
|
/** Props of the browse-all dialog mounted through {@link ThemePackDialogSlot}
|
|
23
25
|
* — the #2825 contract. The dialog fetches `{base}theme-packs/index.json`
|
|
@@ -43,7 +45,7 @@ export type ThemePackDialogComponent = (props: ThemePackDialogProps) => JSX.Elem
|
|
|
43
45
|
* `Island({ when: "load" })` — see
|
|
44
46
|
* `../doc-body-end-islands/theme-pack-switcher-island.tsx`.
|
|
45
47
|
*/
|
|
46
|
-
export declare function ThemePackSwitcher({ active, order, base }: ThemePackSwitcherProps): JSX.Element;
|
|
48
|
+
export declare function ThemePackSwitcher({ active, order, base, pendingUntilHydrated, }: ThemePackSwitcherProps): JSX.Element;
|
|
47
49
|
export declare namespace ThemePackSwitcher {
|
|
48
50
|
var displayName: string;
|
|
49
51
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { jsx, jsxs } from "preact/jsx-runtime";
|
|
3
3
|
import { useEffect, useRef, useState } from "preact/hooks";
|
|
4
4
|
import { ChevronLeft, ChevronRight, Close } from "../icons/index.js";
|
|
5
|
+
import { useHydrationPending } from "../hydration-pending.js";
|
|
5
6
|
import { ThemePackDialog } from "../theme-pack-dialog/index.js";
|
|
6
7
|
import { applyThemePack } from "./theme-pack-sync.js";
|
|
7
8
|
import {
|
|
@@ -61,7 +62,13 @@ function PaletteIcon({ className }) {
|
|
|
61
62
|
}
|
|
62
63
|
const CONTROL_BUTTON_CLASS = "flex items-center gap-hsp-xs rounded border border-muted bg-bg px-hsp-md py-hsp-2xs text-caption text-fg transition-colors hover:border-accent focus-visible:outline-2 focus-visible:outline-accent focus-visible:outline-offset-2 disabled:opacity-50 disabled:pointer-events-none";
|
|
63
64
|
const ICON_BUTTON_CLASS = "flex items-center justify-center rounded p-hsp-2xs text-muted transition-colors hover:text-fg focus-visible:outline-2 focus-visible:outline-accent focus-visible:outline-offset-2 disabled:opacity-50 disabled:pointer-events-none";
|
|
64
|
-
function ThemePackSwitcher({
|
|
65
|
+
function ThemePackSwitcher({
|
|
66
|
+
active,
|
|
67
|
+
order,
|
|
68
|
+
base,
|
|
69
|
+
pendingUntilHydrated = true
|
|
70
|
+
}) {
|
|
71
|
+
const pending = useHydrationPending(pendingUntilHydrated);
|
|
65
72
|
const [open, setOpen] = useState(false);
|
|
66
73
|
const [dialogOpen, setDialogOpen] = useState(false);
|
|
67
74
|
const [activeSlug, setActiveSlug] = useState(active);
|
|
@@ -178,9 +185,14 @@ function ThemePackSwitcher({ active, order, base }) {
|
|
|
178
185
|
"aria-haspopup": "dialog",
|
|
179
186
|
"aria-expanded": open,
|
|
180
187
|
"aria-label": "Theme pack switcher",
|
|
188
|
+
"aria-disabled": pending ? "true" : void 0,
|
|
181
189
|
title: "Theme packs",
|
|
182
190
|
"data-switcher-launcher": true,
|
|
183
|
-
|
|
191
|
+
"data-zd-pending": pending ? "" : void 0,
|
|
192
|
+
onClick: () => {
|
|
193
|
+
if (pending) return;
|
|
194
|
+
setOpen(!open);
|
|
195
|
+
},
|
|
184
196
|
class: "flex h-[2.5rem] w-[2.5rem] items-center justify-center rounded-full border border-muted bg-surface text-fg shadow-lg transition-colors hover:text-accent focus-visible:outline-2 focus-visible:outline-accent focus-visible:outline-offset-2",
|
|
185
197
|
children: /* @__PURE__ */ jsx(PaletteIcon, { className: "h-icon-md w-icon-md" })
|
|
186
198
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type ResolveEnabledPacksSettings } from "./resolve-enabled-packs.js";
|
|
2
|
+
import type { ThemePackMeta, ThemePackRegistry } from "../route-context-payload/types.js";
|
|
3
|
+
/** Contract version for the `./theme-packs-registry` public surface. */
|
|
4
|
+
export declare const schemaVersion = 1;
|
|
5
|
+
/** One entry in the browser-facing catalog v2 manifest. */
|
|
6
|
+
export interface ThemePackCatalogEntry {
|
|
7
|
+
/** The pack directory slug. */
|
|
8
|
+
slug: string;
|
|
9
|
+
/** The schema-validated metadata for this pack. */
|
|
10
|
+
meta: ThemePackMeta;
|
|
11
|
+
/** Whether the pack ships a CSS stylesheet. */
|
|
12
|
+
hasStylesheet: boolean;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The complete catalog manifest accepted by {@link buildThemePackRegistry}.
|
|
16
|
+
*
|
|
17
|
+
* This is intentionally a whole-manifest parameter rather than a bare
|
|
18
|
+
* `packs` array: the catalog schema version is part of the input contract and
|
|
19
|
+
* must be checked before its entries are interpreted.
|
|
20
|
+
*/
|
|
21
|
+
export interface ThemePacksCatalogManifest {
|
|
22
|
+
/** Catalog schema version, distinct from each meta.json schema version. */
|
|
23
|
+
schemaVersion: 2;
|
|
24
|
+
packs: ThemePackCatalogEntry[];
|
|
25
|
+
}
|
|
26
|
+
/** The `{ themePack, themePacks }` settings projection used by the resolver. */
|
|
27
|
+
export type ThemePackSettingsProjection = ResolveEnabledPacksSettings;
|
|
28
|
+
/** Alias emphasizing that this is the registry builder's settings input. */
|
|
29
|
+
export type ThemePackRegistrySettings = ThemePackSettingsProjection;
|
|
30
|
+
/**
|
|
31
|
+
* Build the resolved, enabled, ordered theme-pack registry for a browser
|
|
32
|
+
* consumer.
|
|
33
|
+
*
|
|
34
|
+
* The input is the complete catalog v2 manifest and the settings projection
|
|
35
|
+
* `{ themePack, themePacks }`. Catalog schema versions other than `2` are
|
|
36
|
+
* rejected with an error naming the received value. Once the boundary is
|
|
37
|
+
* validated, {@link resolveEnabledPacks} remains the single source of truth
|
|
38
|
+
* for default-first/alphabetical ordering and loud duplicate, unknown, and
|
|
39
|
+
* excluded-active-slug failures.
|
|
40
|
+
*/
|
|
41
|
+
export declare function buildThemePackRegistry(catalog: ThemePacksCatalogManifest, settings: ThemePackSettingsProjection): ThemePackRegistry;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { resolveEnabledPacks } from "./resolve-enabled-packs.js";
|
|
2
|
+
const schemaVersion = 1;
|
|
3
|
+
function describeCatalogValue(value) {
|
|
4
|
+
if (value === void 0) return "undefined";
|
|
5
|
+
try {
|
|
6
|
+
const json = JSON.stringify(value);
|
|
7
|
+
return json === void 0 ? String(value) : json;
|
|
8
|
+
} catch {
|
|
9
|
+
return String(value);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function isRecord(value) {
|
|
13
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
14
|
+
}
|
|
15
|
+
function validateCatalogManifest(value) {
|
|
16
|
+
if (!isRecord(value)) {
|
|
17
|
+
throw new TypeError(
|
|
18
|
+
`@takazudo/zudo-doc/theme-packs-registry: expected a catalog manifest object with schemaVersion 2; got ${describeCatalogValue(value)}.`
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
if (value.schemaVersion !== 2) {
|
|
22
|
+
throw new Error(
|
|
23
|
+
`@takazudo/zudo-doc/theme-packs-registry: unsupported catalog schemaVersion ${describeCatalogValue(value.schemaVersion)}; expected 2.`
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
if (!Array.isArray(value.packs)) {
|
|
27
|
+
throw new TypeError(
|
|
28
|
+
`@takazudo/zudo-doc/theme-packs-registry: catalog.packs must be an array; got ${describeCatalogValue(value.packs)}.`
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
for (const [index, entry] of value.packs.entries()) {
|
|
32
|
+
if (!isRecord(entry)) {
|
|
33
|
+
throw new TypeError(
|
|
34
|
+
`@takazudo/zudo-doc/theme-packs-registry: catalog.packs[${index}] must be an object; got ${describeCatalogValue(entry)}.`
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
if (typeof entry.slug !== "string") {
|
|
38
|
+
throw new TypeError(
|
|
39
|
+
`@takazudo/zudo-doc/theme-packs-registry: catalog.packs[${index}].slug must be a string; got ${describeCatalogValue(entry.slug)}.`
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
if (!isRecord(entry.meta)) {
|
|
43
|
+
throw new TypeError(
|
|
44
|
+
`@takazudo/zudo-doc/theme-packs-registry: catalog.packs[${index}].meta must be an object; got ${describeCatalogValue(entry.meta)}.`
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
if (typeof entry.hasStylesheet !== "boolean") {
|
|
48
|
+
throw new TypeError(
|
|
49
|
+
`@takazudo/zudo-doc/theme-packs-registry: catalog.packs[${index}].hasStylesheet must be a boolean; got ${describeCatalogValue(entry.hasStylesheet)}.`
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
55
|
+
function buildThemePackRegistry(catalog, settings) {
|
|
56
|
+
const manifest = validateCatalogManifest(catalog);
|
|
57
|
+
return resolveEnabledPacks(manifest.packs, settings);
|
|
58
|
+
}
|
|
59
|
+
export {
|
|
60
|
+
buildThemePackRegistry,
|
|
61
|
+
schemaVersion
|
|
62
|
+
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { THEME_PACK_SLUG_RE, DEFAULT_THEME_PACK_SLUG, themePackMetaSchema, type ThemePackMeta, type ThemePackSwatches, } from "./meta-schema.js";
|
|
2
2
|
export { KNOWN_THEME_PACK_TOKEN_NAMES, COMMERCIAL_FONT_DENYLIST, THEME_PACK_SOFT_BUDGET_BYTES, } from "./token-manifest.js";
|
|
3
3
|
export { validateThemePack, type ThemePackValidationInput, type ThemePackValidationIssue, type ThemePackValidationResult, type ThemePackValidationSeverity, } from "./validator.js";
|
|
4
|
-
export {
|
|
4
|
+
export type { ThemePackRegistry, ThemePackRegistryEntry } from "./types.js";
|
|
5
5
|
export { resolveEnabledPacks, type ResolveEnabledPacksSettings, } from "./resolve-enabled-packs.js";
|
|
6
|
+
export { buildThemePackRegistry, schemaVersion, type ThemePackCatalogEntry, type ThemePackRegistrySettings, type ThemePackSettingsProjection, type ThemePacksCatalogManifest, } from "./build-registry.js";
|
|
@@ -11,20 +11,22 @@ import {
|
|
|
11
11
|
import {
|
|
12
12
|
validateThemePack
|
|
13
13
|
} from "./validator.js";
|
|
14
|
-
import {
|
|
15
|
-
loadThemePackRegistry
|
|
16
|
-
} from "./load-registry.js";
|
|
17
14
|
import {
|
|
18
15
|
resolveEnabledPacks
|
|
19
16
|
} from "./resolve-enabled-packs.js";
|
|
17
|
+
import {
|
|
18
|
+
buildThemePackRegistry,
|
|
19
|
+
schemaVersion
|
|
20
|
+
} from "./build-registry.js";
|
|
20
21
|
export {
|
|
21
22
|
COMMERCIAL_FONT_DENYLIST,
|
|
22
23
|
DEFAULT_THEME_PACK_SLUG,
|
|
23
24
|
KNOWN_THEME_PACK_TOKEN_NAMES,
|
|
24
25
|
THEME_PACK_SLUG_RE,
|
|
25
26
|
THEME_PACK_SOFT_BUDGET_BYTES,
|
|
26
|
-
|
|
27
|
+
buildThemePackRegistry,
|
|
27
28
|
resolveEnabledPacks,
|
|
29
|
+
schemaVersion,
|
|
28
30
|
themePackMetaSchema,
|
|
29
31
|
validateThemePack
|
|
30
32
|
};
|
|
@@ -1,16 +1,5 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
export interface ThemePackRegistryEntry {
|
|
4
|
-
/** The pack's directory name (equals `meta.slug`). */
|
|
5
|
-
slug: string;
|
|
6
|
-
/** The pack's schema-validated `meta.json`. */
|
|
7
|
-
meta: ThemePackMeta;
|
|
8
|
-
/** Whether this pack ships a `pack.css` (always `false` for `"default"`). */
|
|
9
|
-
hasStylesheet: boolean;
|
|
10
|
-
}
|
|
11
|
-
/** The full bundled registry, or a subset of it (after `resolveEnabledPacks`)
|
|
12
|
-
* — an ordered array; order carries meaning (canonical / switcher order). */
|
|
13
|
-
export type ThemePackRegistry = ThemePackRegistryEntry[];
|
|
1
|
+
import type { ThemePackRegistry } from "../route-context-payload/types.js";
|
|
2
|
+
export type { ThemePackRegistry, ThemePackRegistryEntry, } from "../route-context-payload/types.js";
|
|
14
3
|
/**
|
|
15
4
|
* Scan `dir` (a `theme-packs/` directory — `src/theme-packs/` in the
|
|
16
5
|
* workspace, `dist/theme-packs/` once shipped) for per-slug pack directories,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
export type { ThemePackMeta, ThemePackSwatches } from "../route-context-payload/types.js";
|
|
2
3
|
/** `[a-z0-9][a-z0-9-]*` — a pack's slug MUST equal its directory name
|
|
3
4
|
* (Decision 6.7 "slug regex + dir-name parity"). */
|
|
4
5
|
export declare const THEME_PACK_SLUG_RE: RegExp;
|
|
@@ -6,20 +7,6 @@ export declare const THEME_PACK_SLUG_RE: RegExp;
|
|
|
6
7
|
* ONLY (no `pack.css`); the runtime never inserts a stylesheet link for it
|
|
7
8
|
* (ADR Decision 1, "The reserved `default` pack"). */
|
|
8
9
|
export declare const DEFAULT_THEME_PACK_SLUG = "default";
|
|
9
|
-
declare const themePackSwatchesSchema: z.ZodObject<{
|
|
10
|
-
bg: z.ZodString;
|
|
11
|
-
fg: z.ZodString;
|
|
12
|
-
accent: z.ZodString;
|
|
13
|
-
syntax: z.ZodObject<{
|
|
14
|
-
keyword: z.ZodString;
|
|
15
|
-
string: z.ZodString;
|
|
16
|
-
comment: z.ZodString;
|
|
17
|
-
callable: z.ZodString;
|
|
18
|
-
}, z.core.$strip>;
|
|
19
|
-
}, z.core.$strip>;
|
|
20
|
-
/** Preview swatches for the dialog grid — RESOLVED plain CSS colors, one set
|
|
21
|
-
* per mode. See {@link themePackMetaSchema}'s `preview` field. */
|
|
22
|
-
export type ThemePackSwatches = z.infer<typeof themePackSwatchesSchema>;
|
|
23
10
|
export declare const themePackMetaSchema: z.ZodObject<{
|
|
24
11
|
schemaVersion: z.ZodLiteral<1>;
|
|
25
12
|
slug: z.ZodString;
|
|
@@ -61,10 +48,3 @@ export declare const themePackMetaSchema: z.ZodObject<{
|
|
|
61
48
|
}, z.core.$strip>;
|
|
62
49
|
}, z.core.$strip>;
|
|
63
50
|
}, z.core.$strip>;
|
|
64
|
-
/**
|
|
65
|
-
* A pack's `meta.json`, schema-validated (ADR Decision 1). `mode` is a
|
|
66
|
-
* designed-primary badge only — every pack still defines BOTH modes via
|
|
67
|
-
* `light-dark()` in `pack.css`.
|
|
68
|
-
*/
|
|
69
|
-
export type ThemePackMeta = z.infer<typeof themePackMetaSchema>;
|
|
70
|
-
export {};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ThemePackRegistry } from "./
|
|
1
|
+
import type { ThemePackRegistry } from "./types.js";
|
|
2
2
|
/** The subset of settings this resolver reads. */
|
|
3
3
|
export interface ResolveEnabledPacksSettings {
|
|
4
4
|
/** Active pack slug — must end up a member of the resolved list. */
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type { ThemePackRegistry, ThemePackRegistryEntry, } from "../route-context-payload/types.js";
|
|
File without changes
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { type ColorSchemeMode } from "./color-scheme-sync.js";
|
|
2
2
|
export interface ThemeToggleProps {
|
|
3
3
|
defaultMode?: ColorSchemeMode;
|
|
4
|
+
/** Keep activation pending until the first successful mount. @default true */
|
|
5
|
+
pendingUntilHydrated?: boolean;
|
|
4
6
|
}
|
|
5
|
-
export declare function ThemeToggle({ defaultMode, }: ThemeToggleProps): import("preact").JSX.Element;
|
|
7
|
+
export declare function ThemeToggle({ defaultMode, pendingUntilHydrated, }: ThemeToggleProps): import("preact").JSX.Element;
|
|
6
8
|
export declare namespace ThemeToggle {
|
|
7
9
|
var displayName: string;
|
|
8
10
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from "preact/jsx-runtime";
|
|
3
3
|
import { useState, useEffect } from "preact/hooks";
|
|
4
|
+
import { useHydrationPending } from "../hydration-pending.js";
|
|
4
5
|
import {
|
|
5
6
|
applyColorScheme,
|
|
6
7
|
readColorSchemeFromDom,
|
|
@@ -53,8 +54,10 @@ function MoonIcon() {
|
|
|
53
54
|
);
|
|
54
55
|
}
|
|
55
56
|
function ThemeToggle({
|
|
56
|
-
defaultMode = "dark"
|
|
57
|
+
defaultMode = "dark",
|
|
58
|
+
pendingUntilHydrated = true
|
|
57
59
|
}) {
|
|
60
|
+
const pending = useHydrationPending(pendingUntilHydrated);
|
|
58
61
|
const [mode, setMode] = useState(defaultMode);
|
|
59
62
|
useEffect(() => {
|
|
60
63
|
const sync = () => setMode(readColorSchemeFromDom(defaultMode));
|
|
@@ -62,6 +65,7 @@ function ThemeToggle({
|
|
|
62
65
|
return subscribeColorSchemeChanged(sync);
|
|
63
66
|
}, []);
|
|
64
67
|
function toggle() {
|
|
68
|
+
if (pending) return;
|
|
65
69
|
const next = mode === "dark" ? "light" : "dark";
|
|
66
70
|
setMode(next);
|
|
67
71
|
applyColorScheme(next);
|
|
@@ -72,6 +76,8 @@ function ThemeToggle({
|
|
|
72
76
|
{
|
|
73
77
|
onClick: toggle,
|
|
74
78
|
"aria-label": `Switch to ${nextMode} mode`,
|
|
79
|
+
"aria-disabled": pending ? "true" : void 0,
|
|
80
|
+
"data-zd-pending": pending ? "" : void 0,
|
|
75
81
|
className: "text-muted hover:text-fg transition-colors p-hsp-sm focus-visible:outline-2 focus-visible:outline-accent focus-visible:outline-offset-2",
|
|
76
82
|
children: mode === "dark" ? /* @__PURE__ */ jsx(SunIcon, {}) : /* @__PURE__ */ jsx(MoonIcon, {})
|
|
77
83
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
interface NestedIslandPropsRefreshOptions {
|
|
2
2
|
document: Document;
|
|
3
|
+
/** Explicit error seam for hosts/tests; omitted errors surface in a microtask. */
|
|
4
|
+
reportError?: (error: unknown) => void;
|
|
3
5
|
}
|
|
4
6
|
/**
|
|
5
7
|
* Install the before-swap props-refresh listener on a document.
|
|
@@ -8,7 +10,7 @@ interface NestedIslandPropsRefreshOptions {
|
|
|
8
10
|
* non-singleton hosts. Browser modules use the durable, duplicate-safe
|
|
9
11
|
* `ensureNestedIslandPropsRefresh` wrapper below instead.
|
|
10
12
|
*/
|
|
11
|
-
export declare function installNestedIslandPropsRefresh({ document, }: NestedIslandPropsRefreshOptions): () => void;
|
|
13
|
+
export declare function installNestedIslandPropsRefresh({ document, reportError, }: NestedIslandPropsRefreshOptions): () => void;
|
|
12
14
|
/**
|
|
13
15
|
* Install exactly one document-lifetime controller for a browser document.
|
|
14
16
|
* Repeated calls from component renders or duplicate boot paths are no-ops.
|
|
@@ -7,12 +7,47 @@ const ISLAND_REMOUNT_ATTR = "data-zfb-island-remount";
|
|
|
7
7
|
const DOCUMENT_NODE = 9;
|
|
8
8
|
const installedControllers = /* @__PURE__ */ new WeakMap();
|
|
9
9
|
function installNestedIslandPropsRefresh({
|
|
10
|
-
document: document2
|
|
10
|
+
document: document2,
|
|
11
|
+
reportError
|
|
11
12
|
}) {
|
|
12
13
|
const onBeforeSwap = (event) => {
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
const swapEvent = event;
|
|
15
|
+
const incoming = swapEvent.newDocument;
|
|
16
|
+
if (!isDocument(incoming) || incoming === document2 || typeof swapEvent.swap !== "function") {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const capturedSwap = swapEvent.swap;
|
|
20
|
+
const plan = buildNestedIslandPropsMutationPlan(document2, incoming);
|
|
21
|
+
let commitStarted = false;
|
|
22
|
+
let commitOutcome;
|
|
23
|
+
const composedSwap = function(...args) {
|
|
24
|
+
if (commitStarted) {
|
|
25
|
+
if (!commitOutcome) return void 0;
|
|
26
|
+
if (!commitOutcome.ok) throw commitOutcome.error;
|
|
27
|
+
return commitOutcome.value;
|
|
28
|
+
}
|
|
29
|
+
commitStarted = true;
|
|
30
|
+
try {
|
|
31
|
+
const value = commitMutationPlanAndDelegate(
|
|
32
|
+
plan,
|
|
33
|
+
capturedSwap,
|
|
34
|
+
this,
|
|
35
|
+
args,
|
|
36
|
+
reportError
|
|
37
|
+
);
|
|
38
|
+
commitOutcome = { ok: true, value };
|
|
39
|
+
return value;
|
|
40
|
+
} catch (error) {
|
|
41
|
+
commitOutcome = { ok: false, error };
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
try {
|
|
46
|
+
swapEvent.swap = composedSwap;
|
|
47
|
+
} catch {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (swapEvent.swap !== composedSwap) return;
|
|
16
51
|
};
|
|
17
52
|
document2.addEventListener(BEFORE_SWAP_EVENT, onBeforeSwap);
|
|
18
53
|
return () => document2.removeEventListener(BEFORE_SWAP_EVENT, onBeforeSwap);
|
|
@@ -29,9 +64,10 @@ function disposeNestedIslandPropsRefresh(document2) {
|
|
|
29
64
|
installedControllers.get(document2)?.();
|
|
30
65
|
installedControllers.delete(document2);
|
|
31
66
|
}
|
|
32
|
-
function
|
|
67
|
+
function buildNestedIslandPropsMutationPlan(liveDocument, incomingDocument) {
|
|
68
|
+
const plan = [];
|
|
33
69
|
const liveRoots = collectRefreshableRoots(liveDocument);
|
|
34
|
-
if (liveRoots.size === 0) return;
|
|
70
|
+
if (liveRoots.size === 0) return Object.freeze(plan);
|
|
35
71
|
const incomingRoots = collectRefreshableRoots(incomingDocument);
|
|
36
72
|
for (const [persistKey, liveRoot] of liveRoots) {
|
|
37
73
|
const incomingRoot = incomingRoots.get(persistKey);
|
|
@@ -41,9 +77,31 @@ function refreshNestedIslandProps(liveDocument, incomingDocument) {
|
|
|
41
77
|
for (const [name, liveIsland] of collectUniqueOwnedIslands(liveRoot, true)) {
|
|
42
78
|
const incomingIsland = incomingIslands.get(name);
|
|
43
79
|
if (!incomingIsland) continue;
|
|
44
|
-
|
|
80
|
+
const incomingProps = incomingIsland.getAttribute(PROPS_ATTR);
|
|
81
|
+
if (incomingProps === liveIsland.getAttribute(PROPS_ATTR)) continue;
|
|
82
|
+
plan.push(Object.freeze({ liveIsland, incomingProps }));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return Object.freeze(plan);
|
|
86
|
+
}
|
|
87
|
+
function commitMutationPlanAndDelegate(plan, delegate, receiver, args, reportError) {
|
|
88
|
+
const helperErrors = [];
|
|
89
|
+
for (const mutation of plan) {
|
|
90
|
+
try {
|
|
91
|
+
applyPropsMutation(mutation);
|
|
92
|
+
} catch (error) {
|
|
93
|
+
helperErrors.push(error);
|
|
45
94
|
}
|
|
46
95
|
}
|
|
96
|
+
let delegateResult;
|
|
97
|
+
try {
|
|
98
|
+
delegateResult = delegate.apply(receiver, args);
|
|
99
|
+
} catch (delegateError) {
|
|
100
|
+
reportRefreshErrors(helperErrors, reportError);
|
|
101
|
+
throw delegateError;
|
|
102
|
+
}
|
|
103
|
+
reportRefreshErrors(helperErrors, reportError);
|
|
104
|
+
return delegateResult;
|
|
47
105
|
}
|
|
48
106
|
function collectRefreshableRoots(doc) {
|
|
49
107
|
const roots = indexUniquely(
|
|
@@ -83,17 +141,35 @@ function indexUniquely(elements, attribute) {
|
|
|
83
141
|
for (const value of duplicated) byValue.delete(value);
|
|
84
142
|
return byValue;
|
|
85
143
|
}
|
|
86
|
-
function
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
if (
|
|
144
|
+
function applyPropsMutation({
|
|
145
|
+
liveIsland,
|
|
146
|
+
incomingProps
|
|
147
|
+
}) {
|
|
148
|
+
if (incomingProps === null) {
|
|
91
149
|
liveIsland.removeAttribute(PROPS_ATTR);
|
|
92
150
|
} else {
|
|
93
|
-
liveIsland.setAttribute(PROPS_ATTR,
|
|
151
|
+
liveIsland.setAttribute(PROPS_ATTR, incomingProps);
|
|
94
152
|
}
|
|
95
153
|
liveIsland.setAttribute(ISLAND_REMOUNT_ATTR, "");
|
|
96
154
|
}
|
|
155
|
+
function reportRefreshErrors(errors, reportError) {
|
|
156
|
+
if (errors.length === 0) return;
|
|
157
|
+
const error = errors.length === 1 ? errors[0] : new AggregateError(errors, "Nested-island props refresh failed");
|
|
158
|
+
if (reportError) {
|
|
159
|
+
try {
|
|
160
|
+
reportError(error);
|
|
161
|
+
} catch (reporterError) {
|
|
162
|
+
surfaceAsynchronously(reporterError);
|
|
163
|
+
}
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
surfaceAsynchronously(error);
|
|
167
|
+
}
|
|
168
|
+
function surfaceAsynchronously(error) {
|
|
169
|
+
queueMicrotask(() => {
|
|
170
|
+
throw error;
|
|
171
|
+
});
|
|
172
|
+
}
|
|
97
173
|
function isDocument(value) {
|
|
98
174
|
if (typeof value !== "object" || value === null) return false;
|
|
99
175
|
const candidate = value;
|
|
@@ -187,7 +187,9 @@ function SidebarFooter({ links, themeDefaultMode }: { links?: SidebarLocaleLink[
|
|
|
187
187
|
return (
|
|
188
188
|
// pb-[50vh] provides scroll room so the footer doesn't sit at the very bottom of the viewport
|
|
189
189
|
<div className="lg:hidden flex items-center gap-hsp-md border-t border-muted px-hsp-sm py-vsp-xs pb-[50vh] text-small">
|
|
190
|
-
{themeDefaultMode &&
|
|
190
|
+
{themeDefaultMode && (
|
|
191
|
+
<ThemeToggle defaultMode={themeDefaultMode} pendingUntilHydrated={true} />
|
|
192
|
+
)}
|
|
191
193
|
{links && links.map((link, i) => (
|
|
192
194
|
<span key={link.href} className="flex items-center gap-hsp-xs">
|
|
193
195
|
{i > 0 && <span className="text-muted">/</span>}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { useEffect, useState } from "preact/hooks";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Keep an interactive control pending until its first successful client mount.
|
|
5
|
+
* Initializing from `enabled` preserves server/client-first-render parity.
|
|
6
|
+
*/
|
|
7
|
+
export function useHydrationPending(enabled: boolean): boolean {
|
|
8
|
+
const [pending, setPending] = useState(enabled);
|
|
9
|
+
|
|
10
|
+
useEffect(() => setPending(false), []);
|
|
11
|
+
|
|
12
|
+
return pending;
|
|
13
|
+
}
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
// not alias "react" to "preact/compat", so importing from "react" here
|
|
15
15
|
// would fail to resolve.
|
|
16
16
|
import { useState, useEffect } from "preact/hooks";
|
|
17
|
+
import { useHydrationPending } from "./hydration-pending.js";
|
|
17
18
|
import {
|
|
18
19
|
applyColorScheme,
|
|
19
20
|
readColorSchemeFromDom,
|
|
@@ -69,6 +70,8 @@ function MoonIcon() {
|
|
|
69
70
|
|
|
70
71
|
export interface ThemeToggleProps {
|
|
71
72
|
defaultMode?: ColorSchemeMode;
|
|
73
|
+
/** Keep activation pending until the first successful mount. @default true */
|
|
74
|
+
pendingUntilHydrated?: boolean;
|
|
72
75
|
}
|
|
73
76
|
|
|
74
77
|
// NAMED export (not default) on purpose: tsup compiles a default export
|
|
@@ -79,7 +82,9 @@ export interface ThemeToggleProps {
|
|
|
79
82
|
// the package's MobileToc island).
|
|
80
83
|
export function ThemeToggle({
|
|
81
84
|
defaultMode = "dark",
|
|
85
|
+
pendingUntilHydrated = true,
|
|
82
86
|
}: ThemeToggleProps) {
|
|
87
|
+
const pending = useHydrationPending(pendingUntilHydrated);
|
|
83
88
|
// Initial state must match server render to avoid hydration mismatch.
|
|
84
89
|
// Actual theme is synced from DOM in useEffect below.
|
|
85
90
|
const [mode, setMode] = useState<ColorSchemeMode>(defaultMode);
|
|
@@ -94,6 +99,7 @@ export function ThemeToggle({
|
|
|
94
99
|
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
95
100
|
|
|
96
101
|
function toggle() {
|
|
102
|
+
if (pending) return;
|
|
97
103
|
const next = mode === "dark" ? "light" : "dark";
|
|
98
104
|
setMode(next);
|
|
99
105
|
applyColorScheme(next);
|
|
@@ -105,6 +111,8 @@ export function ThemeToggle({
|
|
|
105
111
|
<button
|
|
106
112
|
onClick={toggle}
|
|
107
113
|
aria-label={`Switch to ${nextMode} mode`}
|
|
114
|
+
aria-disabled={pending ? "true" : undefined}
|
|
115
|
+
data-zd-pending={pending ? "" : undefined}
|
|
108
116
|
className="text-muted hover:text-fg transition-colors p-hsp-sm focus-visible:outline-2 focus-visible:outline-accent focus-visible:outline-offset-2"
|
|
109
117
|
>
|
|
110
118
|
{mode === "dark" ? <SunIcon /> : <MoonIcon />}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@takazudo/zudo-doc",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.13.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "zudo-doc framework primitives layer that sits on top of zfb's engine — sidebar, theme, TOC, breadcrumb, layouts, head injection, View Transitions, SSR-skip wrappers (per ADR-003).",
|
|
6
6
|
"license": "MIT",
|
|
@@ -247,11 +247,16 @@
|
|
|
247
247
|
"types": "./dist/site-schema/index.d.ts",
|
|
248
248
|
"default": "./dist/site-schema/index.js"
|
|
249
249
|
},
|
|
250
|
+
"./theme-packs-registry": {
|
|
251
|
+
"types": "./dist/theme-packs-registry/index.d.ts",
|
|
252
|
+
"default": "./dist/theme-packs-registry/index.js"
|
|
253
|
+
},
|
|
250
254
|
"./theme.css": "./dist/theme.css",
|
|
251
255
|
"./safelist.css": "./dist/safelist.css",
|
|
252
256
|
"./content.css": "./dist/content.css",
|
|
253
257
|
"./page-loading.css": "./dist/page-loading.css",
|
|
254
258
|
"./features.css": "./dist/features.css",
|
|
259
|
+
"./compiled.css": "./dist/compiled.css",
|
|
255
260
|
"./theme-packs/*": "./dist/theme-packs/*",
|
|
256
261
|
"./routes-src/*": "./routes-src/*",
|
|
257
262
|
"./tsconfig.base.json": "./tsconfig.base.json",
|
|
@@ -612,6 +617,10 @@
|
|
|
612
617
|
"./note-tray-model": {
|
|
613
618
|
"types": "./dist/note-tray-model/index.d.ts",
|
|
614
619
|
"default": "./dist/note-tray-model/index.js"
|
|
620
|
+
},
|
|
621
|
+
"./route-context-payload": {
|
|
622
|
+
"types": "./dist/route-context-payload/index.d.ts",
|
|
623
|
+
"default": "./dist/route-context-payload/index.js"
|
|
615
624
|
}
|
|
616
625
|
},
|
|
617
626
|
"bin": {
|
|
@@ -634,10 +643,10 @@
|
|
|
634
643
|
],
|
|
635
644
|
"peerDependencies": {
|
|
636
645
|
"@takazudo/zdtp": "^0.4.12",
|
|
637
|
-
"@takazudo/zfb": "^2.
|
|
638
|
-
"@takazudo/zfb-md-wasm": "^2.
|
|
639
|
-
"@takazudo/zfb-runtime": "^2.
|
|
640
|
-
"@takazudo/zudo-doc-history-server": "^5.
|
|
646
|
+
"@takazudo/zfb": "^2.12.0",
|
|
647
|
+
"@takazudo/zfb-md-wasm": "^2.12.0",
|
|
648
|
+
"@takazudo/zfb-runtime": "^2.12.0",
|
|
649
|
+
"@takazudo/zudo-doc-history-server": "^5.12.1",
|
|
641
650
|
"diff": "^8.0.0",
|
|
642
651
|
"katex": "^0.16.0",
|
|
643
652
|
"preact": "^10.29.1",
|
|
@@ -669,18 +678,16 @@
|
|
|
669
678
|
"picocolors": "^1.1.1",
|
|
670
679
|
"pluralize": "^8.0.0",
|
|
671
680
|
"smol-toml": "^1.8.0",
|
|
672
|
-
"string-similarity": "^4.0.4",
|
|
673
681
|
"tsx": "^4.21.0"
|
|
674
682
|
},
|
|
675
683
|
"devDependencies": {
|
|
676
|
-
"@takazudo/zfb": "2.
|
|
677
|
-
"@takazudo/zfb-md-wasm": "2.
|
|
678
|
-
"@takazudo/zfb-runtime": "2.
|
|
684
|
+
"@takazudo/zfb": "2.12.0",
|
|
685
|
+
"@takazudo/zfb-md-wasm": "2.12.0",
|
|
686
|
+
"@takazudo/zfb-runtime": "2.12.0",
|
|
679
687
|
"@types/fs-extra": "^11.0.4",
|
|
680
688
|
"@types/minimist": "^1.2.5",
|
|
681
689
|
"@types/node": "^25.3.5",
|
|
682
690
|
"@types/pluralize": "^0.0.33",
|
|
683
|
-
"@types/string-similarity": "^4.0.2",
|
|
684
691
|
"happy-dom": "^20.10.6",
|
|
685
692
|
"npm-run-all2": "^7.0.2",
|
|
686
693
|
"preact": "^10.29.1",
|
|
@@ -689,7 +696,7 @@
|
|
|
689
696
|
"typescript": "^5.0.0",
|
|
690
697
|
"vitest": "^4.1.0",
|
|
691
698
|
"zod": "^4.3.6",
|
|
692
|
-
"@takazudo/zudo-doc-history-server": "5.
|
|
699
|
+
"@takazudo/zudo-doc-history-server": "5.13.0"
|
|
693
700
|
},
|
|
694
701
|
"scripts": {
|
|
695
702
|
"gen:search-widget-script": "node scripts/gen-search-widget-script.mjs",
|
|
@@ -701,7 +708,7 @@
|
|
|
701
708
|
"dev:js": "tsup --watch",
|
|
702
709
|
"dev:dts": "tsc -p tsconfig.build.json --watch --preserveWatchOutput",
|
|
703
710
|
"// check:prepack-contract": "check:search-widget-drift and check:nav-overflow-drift run here (not just b4push/CI) because `prepare` regenerates BOTH frozen literals unconditionally — without a prepack gate, packing from a tree where a source changed but a literal was never re-committed would silently publish new script bytes and break every consumer's pinned CSP hash (#3534/#3535 for nav-overflow; #3540 extended the same gate to search-widget, the exact failure class the freezes exist to prevent).",
|
|
704
|
-
"check:prepack-contract": "pnpm --dir ../.. check:search-widget-drift && pnpm --dir ../.. check:nav-overflow-drift && pnpm --dir ../.. gen:changelog && node scripts/check-theme-css.mjs && node scripts/check-safelist.mjs && node scripts/check-content-css.mjs && node scripts/check-page-loading-css.mjs && node scripts/check-features-css.mjs && node scripts/check-theme-packs.mjs && node scripts/check-catalog.mjs && node scripts/check-site-schema.mjs && node scripts/check-plugins.mjs && node scripts/check-plugin-resolution.mjs && node scripts/check-eject-sources.mjs && node scripts/check-routes-src.mjs && node scripts/check-shim-artifacts.mjs && node scripts/check-virtual-modules.mjs && node bin/gen-component-tokens.mjs --check",
|
|
711
|
+
"check:prepack-contract": "pnpm --dir ../.. check:search-widget-drift && pnpm --dir ../.. check:nav-overflow-drift && pnpm --dir ../.. gen:changelog && node scripts/check-theme-css.mjs && node scripts/check-safelist.mjs && node scripts/check-content-css.mjs && node scripts/check-page-loading-css.mjs && node scripts/check-features-css.mjs && node scripts/check-compiled-css.mjs && node scripts/check-theme-packs.mjs && node scripts/check-catalog.mjs && node scripts/check-site-schema.mjs && node scripts/check-theme-packs-registry.mjs && node scripts/check-plugins.mjs && node scripts/check-plugin-resolution.mjs && node scripts/check-eject-sources.mjs && node scripts/check-routes-src.mjs && node scripts/check-shim-artifacts.mjs && node scripts/check-virtual-modules.mjs && node bin/gen-component-tokens.mjs --check && node scripts/check-route-context-payload.mjs",
|
|
705
712
|
"test:plugin-resolution": "node scripts/check-plugin-resolution.mjs",
|
|
706
713
|
"test": "vitest run --config vitest.config.ts",
|
|
707
714
|
"test:slow": "vitest run --config vitest.slow.config.ts",
|