@takazudo/zudo-doc 5.14.0 → 5.16.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 (101) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/dist/asset-components/asset-card.js +3 -1
  3. package/dist/asset-components/asset-code.js +2 -0
  4. package/dist/asset-components/shared.d.ts +5 -0
  5. package/dist/asset-components/shared.js +7 -0
  6. package/dist/asset-index-page/index.d.ts +1 -0
  7. package/dist/asset-index-page/index.js +10 -9
  8. package/dist/asset-page/index.d.ts +20 -2
  9. package/dist/asset-page/index.js +43 -24
  10. package/dist/asset-path/index.d.ts +2 -0
  11. package/dist/asset-path/index.js +11 -1
  12. package/dist/chrome/derive.js +31 -6
  13. package/dist/compiled.css +12 -0
  14. package/dist/config.d.ts +15 -6
  15. package/dist/config.js +2 -0
  16. package/dist/content/content-link.d.ts +5 -1
  17. package/dist/content/content-link.js +4 -1
  18. package/dist/doc-content-header/index.js +1 -1
  19. package/dist/doc-page-shell/index.js +1 -1
  20. package/dist/factory-context/index.d.ts +7 -3
  21. package/dist/header-with-defaults/index.js +2 -0
  22. package/dist/home-page/index.js +1 -1
  23. package/dist/html-preview-wrapper/html-preview-wrapper.d.ts +41 -7
  24. package/dist/html-preview-wrapper/html-preview-wrapper.js +86 -2
  25. package/dist/html-preview-wrapper/html-preview.d.ts +29 -4
  26. package/dist/html-preview-wrapper/html-preview.js +64 -6
  27. package/dist/html-preview-wrapper/index.d.ts +1 -1
  28. package/dist/html-preview-wrapper/preview-auto-height.d.ts +24 -0
  29. package/dist/html-preview-wrapper/preview-auto-height.js +120 -0
  30. package/dist/html-preview-wrapper/preview-base.d.ts +21 -1
  31. package/dist/html-preview-wrapper/preview-base.js +99 -50
  32. package/dist/i18n-defaults/index.d.ts +5 -3
  33. package/dist/i18n-defaults/index.js +89 -0
  34. package/dist/i18n-version/language-switcher.d.ts +10 -5
  35. package/dist/i18n-version/language-switcher.js +115 -22
  36. package/dist/i18n-version/types.d.ts +1 -1
  37. package/dist/mdx-components/index.d.ts +4 -0
  38. package/dist/mdx-components/index.js +11 -2
  39. package/dist/nav-indexing/versions-page-content.js +1 -1
  40. package/dist/plugins/claude-resources.js +15 -1
  41. package/dist/plugins/codex-resources.js +15 -1
  42. package/dist/plugins/internal/asset-viewer/asset-pages.d.ts +53 -0
  43. package/dist/plugins/internal/asset-viewer/asset-pages.js +98 -0
  44. package/dist/plugins/internal/asset-viewer/scan.d.ts +18 -1
  45. package/dist/plugins/internal/asset-viewer/scan.js +34 -27
  46. package/dist/plugins/internal/claude-resources/generate.d.ts +9 -0
  47. package/dist/plugins/internal/claude-resources/generate.js +188 -18
  48. package/dist/plugins/internal/claude-resources/index.d.ts +9 -0
  49. package/dist/plugins/internal/claude-resources/index.js +13 -1
  50. package/dist/plugins/internal/codex-resources/agents-md.js +15 -2
  51. package/dist/plugins/internal/codex-resources/agents.js +15 -2
  52. package/dist/plugins/internal/codex-resources/config.js +15 -2
  53. package/dist/plugins/internal/codex-resources/generate.d.ts +9 -0
  54. package/dist/plugins/internal/codex-resources/generate.js +19 -7
  55. package/dist/plugins/internal/codex-resources/hooks.js +15 -2
  56. package/dist/plugins/internal/codex-resources/index.d.ts +9 -0
  57. package/dist/plugins/internal/codex-resources/index.js +13 -1
  58. package/dist/plugins/internal/codex-resources/overview.js +136 -7
  59. package/dist/plugins/internal/codex-resources/rules.js +15 -2
  60. package/dist/plugins/internal/codex-resources/skills.js +16 -3
  61. package/dist/plugins/internal/llms-txt/assets.d.ts +24 -0
  62. package/dist/plugins/internal/llms-txt/assets.js +65 -0
  63. package/dist/plugins/internal/llms-txt/dev-middleware.d.ts +5 -0
  64. package/dist/plugins/internal/llms-txt/dev-middleware.js +9 -1
  65. package/dist/plugins/internal/llms-txt/emit.js +21 -5
  66. package/dist/plugins/internal/llms-txt/generate.d.ts +3 -3
  67. package/dist/plugins/internal/llms-txt/generate.js +55 -2
  68. package/dist/plugins/internal/llms-txt/index.d.ts +4 -1
  69. package/dist/plugins/internal/llms-txt/index.js +4 -0
  70. package/dist/plugins/internal/llms-txt/types.d.ts +24 -0
  71. package/dist/plugins/internal/llms-txt/types.js +4 -0
  72. package/dist/plugins/internal/resource-docs-shared/fs.d.ts +33 -0
  73. package/dist/plugins/internal/resource-docs-shared/fs.js +82 -1
  74. package/dist/plugins/internal/resource-docs-shared/index.d.ts +4 -2
  75. package/dist/plugins/internal/resource-docs-shared/index.js +21 -1
  76. package/dist/plugins/internal/resource-docs-shared/labels.d.ts +24 -0
  77. package/dist/plugins/internal/resource-docs-shared/labels.js +14 -0
  78. package/dist/plugins/internal/resource-docs-shared/locale-routes.d.ts +16 -0
  79. package/dist/plugins/internal/resource-docs-shared/locale-routes.js +14 -0
  80. package/dist/plugins/internal/resource-docs-shared/mdx.d.ts +7 -1
  81. package/dist/plugins/internal/resource-docs-shared/mdx.js +2 -2
  82. package/dist/plugins/internal/search-index/collect.js +46 -1
  83. package/dist/plugins/internal/search-index/types.d.ts +6 -1
  84. package/dist/plugins/llms-txt.js +9 -1
  85. package/dist/plugins/routes.js +14 -0
  86. package/dist/plugins/search-index.js +10 -1
  87. package/dist/preset.d.ts +9 -1
  88. package/dist/preset.js +25 -4
  89. package/dist/route-context/index.js +5 -17
  90. package/dist/route-enumerators/index.d.ts +14 -1
  91. package/dist/route-enumerators/index.js +38 -0
  92. package/dist/routes/locale-files-path.d.ts +29 -0
  93. package/dist/routes/locale-files-path.js +52 -0
  94. package/dist/safelist.css +1 -1
  95. package/dist/settings.d.ts +15 -0
  96. package/dist/tag-pages/index.js +2 -2
  97. package/eject/asset-components/asset-card.tsx +2 -0
  98. package/eject/asset-components/asset-code.tsx +2 -0
  99. package/eject/asset-components/shared.tsx +16 -0
  100. package/package.json +13 -9
  101. package/routes-src/locale-files-path.tsx +73 -0
@@ -24,7 +24,9 @@ function createContentLink({
24
24
  base,
25
25
  assetManifest,
26
26
  routePrefix,
27
- dir
27
+ dir,
28
+ locale,
29
+ isDefaultLocaleOnlyPath
28
30
  }) {
29
31
  return function ManifestAwareContentLink(props) {
30
32
  const { href, className, children } = props;
@@ -43,6 +45,7 @@ function createContentLink({
43
45
  base,
44
46
  routePrefix,
45
47
  path: entry.path,
48
+ locale: isDefaultLocaleOnlyPath?.(`/${routePrefix}/${entry.path}`) ? void 0 : locale,
46
49
  fragment: decoded.fragment
47
50
  }),
48
51
  children: /* @__PURE__ */ jsxs("span", { className: "inline-flex items-baseline gap-x-hsp-xs font-mono", children: [
@@ -20,7 +20,7 @@ function createDocContentHeader(ctx) {
20
20
  version
21
21
  }) {
22
22
  return /* @__PURE__ */ jsxs(Fragment, { children: [
23
- /* @__PURE__ */ jsx("h1", { class: "text-heading font-bold border-b border-fg pb-vsp-xs mb-vsp-xs", children: entry.data.title }),
23
+ /* @__PURE__ */ jsx("h1", { class: "text-heading font-bold border-b border-fg pb-vsp-xs mb-vsp-xs break-words", children: entry.data.title }),
24
24
  docContentHeaderExtras?.({
25
25
  entry,
26
26
  slug,
@@ -176,7 +176,7 @@ function createDocPageShell(ctx) {
176
176
  Fragment (not <div>) so children become direct children of
177
177
  <article class="zd-content">, picking up the flow-space rule. */
178
178
  /* @__PURE__ */ jsxs(Fragment, { children: [
179
- /* @__PURE__ */ jsx("h1", { class: "text-heading font-bold border-b border-fg pb-vsp-xs mb-vsp-xs", children: autoIndexLabel }),
179
+ /* @__PURE__ */ jsx("h1", { class: "text-heading font-bold border-b border-fg pb-vsp-xs mb-vsp-xs break-words", children: autoIndexLabel }),
180
180
  metainfoSlot,
181
181
  description && /* @__PURE__ */ jsx("p", { class: "mb-vsp-lg text-title text-muted", "data-doc-description": true, children: description }),
182
182
  /* @__PURE__ */ jsx(NavCardGrid, { children: autoIndexChildren ?? [] })
@@ -27,8 +27,9 @@ export interface FactoryI18n {
27
27
  /** Display label for a locale (e.g. `"EN"`, `"日本語"`). */
28
28
  getLocaleLabel: (locale: string) => string;
29
29
  /**
30
- * Translate a UI string key for a locale, falling back to the default locale
31
- * then the key itself. Optional: factories that emit no UI strings omit it.
30
+ * Translate a UI string key for a locale, falling back through the configured
31
+ * default locale, package English, then the raw key. Optional: factories
32
+ * that emit no UI strings omit it.
32
33
  */
33
34
  t?: (key: string, locale?: string) => string;
34
35
  }
@@ -136,7 +137,10 @@ export interface RouteContext<S = Settings> extends UrlHelpers, NavSourceDocsAPI
136
137
  } | undefined;
137
138
  /** Display label for a locale. */
138
139
  getLocaleLabel(locale: string): string;
139
- /** Translate a UI string key for a locale (falls back to default then key). */
140
+ /**
141
+ * Translate a UI string key for a locale (falls back through the configured
142
+ * default locale, package English, then the raw key).
143
+ */
140
144
  t(key: string, locale?: string): string;
141
145
  /** The full URL-helper bag (also spread as own members via `extends`). */
142
146
  urlHelpers: UrlHelpers;
@@ -120,6 +120,8 @@ function createHeaderWithDefaults(ctx) {
120
120
  LanguageSwitcher,
121
121
  {
122
122
  links: localeLinks,
123
+ accessibleLabel: t("language.switcher.label", lang),
124
+ idSuffix: "header",
123
125
  config: {
124
126
  base: settings.base.replace(/\/+$/, ""),
125
127
  defaultLocale,
@@ -117,7 +117,7 @@ function createHomePageView(ctx) {
117
117
  }
118
118
  ) : null,
119
119
  /* @__PURE__ */ jsxs("div", { children: [
120
- /* @__PURE__ */ jsx("h1", { class: "text-heading font-bold mb-vsp-2xs", children: settings.siteName }),
120
+ /* @__PURE__ */ jsx("h1", { class: "text-heading font-bold mb-vsp-2xs break-words", children: settings.siteName }),
121
121
  /* @__PURE__ */ jsx("p", { class: "text-muted text-small mb-vsp-sm", children: settings.siteDescription }),
122
122
  /* @__PURE__ */ jsx("div", { class: "flex items-center justify-center lg:justify-start gap-hsp-md text-small", children: rowItems.map((item, index) => /* @__PURE__ */ jsxs(Fragment2, { children: [
123
123
  index > 0 && /* @__PURE__ */ jsx("span", { class: "text-muted", children: "/" }),
@@ -1,6 +1,7 @@
1
1
  /** @jsxRuntime automatic */
2
2
  /** @jsxImportSource preact */
3
3
  import type { VNode } from "preact";
4
+ import { type HtmlPreviewLabels } from "./html-preview.js";
4
5
  /**
5
6
  * Global HTML preview configuration. Mirrors the `settings.htmlPreview`
6
7
  * shape from the host project so callers can pass the resolved config
@@ -15,6 +16,20 @@ export interface HtmlPreviewGlobalConfig {
15
16
  js?: string;
16
17
  }
17
18
  export interface HtmlPreviewWrapperProps {
19
+ /**
20
+ * Controls when the preview subtree is rendered.
21
+ *
22
+ * `"eager"` (and omission) preserves the server-rendered iframe and
23
+ * hydrates it when visible. `"visible"` emits only an inert height
24
+ * reservation during SSR and renders the preview on the client through
25
+ * zfb's skip-SSR island path.
26
+ *
27
+ * This is a component lifecycle policy and is not forwarded as the native
28
+ * iframe `loading` attribute.
29
+ *
30
+ * @default "eager"
31
+ */
32
+ loading?: "eager" | "visible";
18
33
  /**
19
34
  * Site-wide HTML preview configuration (resolved from
20
35
  * `settings.htmlPreview` by the caller). When provided, its
@@ -36,10 +51,24 @@ export interface HtmlPreviewWrapperProps {
36
51
  js?: string;
37
52
  /** Optional title displayed in the preview title bar. */
38
53
  title?: string;
54
+ /**
55
+ * Language tag for the generated preview document. Any nonblank tag is
56
+ * forwarded as-is; omission or a blank value falls back to `"en"` in
57
+ * `<HtmlPreview>`. Bound consumers should pass their active route locale.
58
+ *
59
+ * @default "en"
60
+ */
61
+ lang?: string;
39
62
  /** Fixed iframe height in pixels. Auto-sizes when omitted. */
40
63
  height?: number;
41
64
  /** When true, the code section is expanded by default. */
42
65
  defaultOpen?: boolean;
66
+ /** Localized labels for viewport, source, and iframe controls. */
67
+ labels?: Partial<HtmlPreviewLabels>;
68
+ /** Whether the source toggle and code panel are rendered. @default true */
69
+ showSource?: boolean;
70
+ /** Whether the viewport preset controls are rendered. @default true */
71
+ showViewportControls?: boolean;
43
72
  /**
44
73
  * Forwarded to `<HtmlPreview>`. When true, makes the preview document's
45
74
  * `html`/`body` stretch to 100% height. Interacts with auto-height — pair
@@ -94,6 +123,7 @@ export interface HtmlPreviewWrapperProps {
94
123
  */
95
124
  showResources?: boolean;
96
125
  }
126
+ type HtmlPreviewWrapperInnerProps = Omit<HtmlPreviewWrapperProps, "loading">;
97
127
  /**
98
128
  * Bare HTML preview body — the actual island **hydration target**.
99
129
  *
@@ -121,7 +151,7 @@ export interface HtmlPreviewWrapperProps {
121
151
  * (`HtmlPreviewWrapperInner`) and is exported, so the marker resolves to
122
152
  * THIS bare component and the bundle hydrates it in-place.
123
153
  */
124
- export declare function HtmlPreviewWrapperInner(props: HtmlPreviewWrapperProps): VNode;
154
+ export declare function HtmlPreviewWrapperInner(props: HtmlPreviewWrapperInnerProps): VNode;
125
155
  export declare namespace HtmlPreviewWrapperInner {
126
156
  var displayName: string;
127
157
  }
@@ -129,12 +159,15 @@ export declare namespace HtmlPreviewWrapperInner {
129
159
  * HTML preview wrapper component — the public MDX-registered binding
130
160
  * (`HtmlPreview: HtmlPreviewWrapper`).
131
161
  *
132
- * Wraps the bare `HtmlPreviewWrapperInner` in `<Island when="visible">`,
133
- * mirroring the legacy `client:visible` hydration timing — the iframe is
134
- * heavy and not on the critical path, so hydration is deferred until the
135
- * preview enters the viewport. The SSG output emits
136
- * `data-zfb-island="HtmlPreviewWrapperInner"` around the bare tree, and the
137
- * client bundle hydrates `HtmlPreviewWrapperInner` against it in-place.
162
+ * Eager mode wraps the bare `HtmlPreviewWrapperInner` in
163
+ * `<Island when="visible">`, mirroring the legacy `client:visible` hydration
164
+ * timing while preserving the complete server-rendered preview. Visible mode
165
+ * uses zfb's skip-SSR fallback path: static output contains only an inert
166
+ * nonzero reservation, while the real serializable inner props remain on the
167
+ * island marker for the client mount. zfb intentionally mounts skip-SSR
168
+ * islands immediately, so the bare inner target keeps that reservation in
169
+ * place and applies its own one-shot IntersectionObserver gate before it
170
+ * instantiates the preview subtree; missing observer support fails open.
138
171
  *
139
172
  * The public export name and signature are unchanged from before the
140
173
  * zudolab/zudo-doc#1925 fix, so existing consumers that register
@@ -142,3 +175,4 @@ export declare namespace HtmlPreviewWrapperInner {
142
175
  * with no call-site change.
143
176
  */
144
177
  export declare function HtmlPreviewWrapper(props: HtmlPreviewWrapperProps): VNode;
178
+ export {};
@@ -1,24 +1,91 @@
1
1
  "use client";
2
2
  import { jsx } from "preact/jsx-runtime";
3
+ import { useEffect, useRef, useState } from "preact/hooks";
3
4
  import { Island } from "@takazudo/zfb";
4
5
  import { HtmlPreview } from "./html-preview.js";
6
+ const VISIBLE_MOUNT_PROP = "__zudoDocVisibleMount";
7
+ function reservationHeight(height) {
8
+ return height != null && height > 0 ? height : 200;
9
+ }
10
+ function HtmlPreviewReservation({
11
+ height,
12
+ reservationRef
13
+ }) {
14
+ return /* @__PURE__ */ jsx(
15
+ "div",
16
+ {
17
+ ref: reservationRef,
18
+ "aria-hidden": "true",
19
+ "data-zd-html-preview-reservation": true,
20
+ style: { height: reservationHeight(height) }
21
+ }
22
+ );
23
+ }
5
24
  function HtmlPreviewWrapperInner(props) {
25
+ const runtimeProps = props;
26
+ const deferUntilVisible = runtimeProps[VISIBLE_MOUNT_PROP] === true;
27
+ const reservationRef = useRef(null);
28
+ const [shouldRenderPreview, setShouldRenderPreview] = useState(
29
+ !deferUntilVisible || typeof globalThis.IntersectionObserver !== "function"
30
+ );
31
+ useEffect(() => {
32
+ if (shouldRenderPreview) return;
33
+ if (!deferUntilVisible) {
34
+ setShouldRenderPreview(true);
35
+ return;
36
+ }
37
+ const target = reservationRef.current;
38
+ const Observer = globalThis.IntersectionObserver;
39
+ if (!target || typeof Observer !== "function") {
40
+ setShouldRenderPreview(true);
41
+ return;
42
+ }
43
+ let fired = false;
44
+ const observer = new Observer(
45
+ (entries) => {
46
+ if (fired || !entries.some((entry) => entry.isIntersecting)) return;
47
+ fired = true;
48
+ observer.disconnect();
49
+ setShouldRenderPreview(true);
50
+ },
51
+ { threshold: 0 }
52
+ );
53
+ observer.observe(target);
54
+ return () => {
55
+ fired = true;
56
+ observer.disconnect();
57
+ };
58
+ }, [deferUntilVisible, shouldRenderPreview]);
6
59
  const {
60
+ [VISIBLE_MOUNT_PROP]: _visibleMount,
7
61
  globalConfig,
8
62
  html,
9
63
  css,
10
64
  head,
11
65
  js,
12
66
  title,
67
+ lang,
13
68
  height,
14
69
  defaultOpen,
70
+ labels,
71
+ showSource,
72
+ showViewportControls,
15
73
  fullHeight,
16
74
  sandbox,
17
75
  externalStyles,
18
76
  externalScripts,
19
77
  preflight,
20
78
  showResources
21
- } = props;
79
+ } = runtimeProps;
80
+ if (!shouldRenderPreview) {
81
+ return /* @__PURE__ */ jsx(
82
+ HtmlPreviewReservation,
83
+ {
84
+ height,
85
+ reservationRef
86
+ }
87
+ );
88
+ }
22
89
  const mergedHead = [globalConfig?.head, head].filter(Boolean).join("\n") || void 0;
23
90
  const mergedCss = [globalConfig?.css, css].filter(Boolean).join("\n") || void 0;
24
91
  const mergedJs = [globalConfig?.js, js].filter(Boolean).join("\n") || void 0;
@@ -30,8 +97,12 @@ function HtmlPreviewWrapperInner(props) {
30
97
  head: mergedHead,
31
98
  js: mergedJs,
32
99
  title,
100
+ lang,
33
101
  height,
34
102
  defaultOpen,
103
+ labels,
104
+ showSource,
105
+ showViewportControls,
35
106
  fullHeight,
36
107
  sandbox,
37
108
  componentCss: css,
@@ -46,9 +117,22 @@ function HtmlPreviewWrapperInner(props) {
46
117
  }
47
118
  HtmlPreviewWrapperInner.displayName = "HtmlPreviewWrapperInner";
48
119
  function HtmlPreviewWrapper(props) {
120
+ const { loading = "eager", ...innerProps } = props;
121
+ if (loading === "visible") {
122
+ const visibleInnerProps = {
123
+ ...innerProps,
124
+ [VISIBLE_MOUNT_PROP]: true
125
+ };
126
+ const rendered2 = Island({
127
+ when: "visible",
128
+ ssrFallback: /* @__PURE__ */ jsx(HtmlPreviewReservation, { height: innerProps.height }),
129
+ children: /* @__PURE__ */ jsx(HtmlPreviewWrapperInner, { ...visibleInnerProps })
130
+ });
131
+ return rendered2;
132
+ }
49
133
  const rendered = Island({
50
134
  when: "visible",
51
- children: /* @__PURE__ */ jsx(HtmlPreviewWrapperInner, { ...props })
135
+ children: /* @__PURE__ */ jsx(HtmlPreviewWrapperInner, { ...innerProps })
52
136
  });
53
137
  return rendered;
54
138
  }
@@ -1,14 +1,30 @@
1
1
  /** @jsxRuntime automatic */
2
2
  /** @jsxImportSource preact */
3
3
  import type { VNode } from "preact";
4
+ import { type HtmlPreviewLabels } from "./preview-base.js";
5
+ export type { HtmlPreviewLabels } from "./preview-base.js";
4
6
  export interface HtmlPreviewProps {
5
7
  html: string;
6
8
  css?: string;
7
9
  head?: string;
8
10
  js?: string;
9
11
  title?: string;
12
+ /**
13
+ * Language tag for the generated preview document's `<html lang>`
14
+ * attribute. Any nonblank tag is accepted; omission or a blank value falls
15
+ * back to `"en"`. Bound consumers should pass their active route locale.
16
+ *
17
+ * @default "en"
18
+ */
19
+ lang?: string;
10
20
  height?: number;
11
21
  defaultOpen?: boolean;
22
+ /** Localized labels for viewport, source, and iframe controls. */
23
+ labels?: Partial<HtmlPreviewLabels>;
24
+ /** Whether the source toggle and code panel are rendered. @default true */
25
+ showSource?: boolean;
26
+ /** Whether the viewport preset controls are rendered. @default true */
27
+ showViewportControls?: boolean;
12
28
  /**
13
29
  * When true, injects `<style>html,body{height:100%}</style>` into the
14
30
  * preview document so the preview's content can stretch to fill the
@@ -20,8 +36,9 @@ export interface HtmlPreviewProps {
20
36
  * `iframe.contentDocument.body.scrollHeight` and resizes the iframe to
21
37
  * fit; `fullHeight` makes the body's height derive FROM the iframe's own
22
38
  * height instead, which creates a feedback loop when the iframe height is
23
- * itself derived from the body. This component does not attempt to
24
- * detect or break that loop — always set `height` alongside `fullHeight`.
39
+ * itself derived from the body. Auto-height is therefore disabled when
40
+ * `fullHeight` is used without a fixed height. Pair both props when the
41
+ * preview needs a height other than the default 200px reservation.
25
42
  *
26
43
  * @default false
27
44
  */
@@ -121,7 +138,15 @@ export declare function containsScript(head?: string, js?: string, externalScrip
121
138
  * trust note.
122
139
  */
123
140
  export declare function resolveSandbox(sandbox: string | undefined, hasScripts: boolean): string;
124
- export declare function buildSrcdoc(html: string, css?: string, head?: string, js?: string, fullHeight?: boolean, externalStyles?: string[], externalScripts?: string[], preflight?: boolean): string;
141
+ export interface BuildSrcdocMetadata {
142
+ /** Language for the generated document. Blank values fall back to `"en"`. */
143
+ lang?: string;
144
+ /** Visible preview title, used when the trusted head has no opening title. */
145
+ title?: string;
146
+ /** Localized preview label used after a blank visible title. */
147
+ previewLabel?: string;
148
+ }
149
+ export declare function buildSrcdoc(html: string, css?: string, head?: string, js?: string, fullHeight?: boolean, externalStyles?: string[], externalScripts?: string[], preflight?: boolean, metadata?: BuildSrcdocMetadata): string;
125
150
  /**
126
151
  * HTML preview widget — renders an isolated iframe with viewport
127
152
  * controls and a collapsible code section.
@@ -134,4 +159,4 @@ export declare function buildSrcdoc(html: string, css?: string, head?: string, j
134
159
  * Astro, or wire up the SSR-skip placeholder pattern for non-Astro
135
160
  * consumers.
136
161
  */
137
- export declare function HtmlPreview({ html, css, head, js, title, height, defaultOpen, fullHeight, sandbox, componentCss, componentHead, componentJs, externalStyles, externalScripts, preflight, showResources, }: HtmlPreviewProps): VNode;
162
+ export declare function HtmlPreview({ html, css, head, js, title, lang, height, defaultOpen, labels, showSource, showViewportControls, fullHeight, sandbox, componentCss, componentHead, componentJs, externalStyles, externalScripts, preflight, showResources, }: HtmlPreviewProps): VNode;
@@ -1,6 +1,8 @@
1
1
  import { jsx } from "preact/jsx-runtime";
2
2
  import { useMemo } from "preact/hooks";
3
- import { PreviewBase } from "./preview-base.js";
3
+ import {
4
+ PreviewBase
5
+ } from "./preview-base.js";
4
6
  import { dedent } from "./dedent.js";
5
7
  import { preflightCss } from "./preflight.js";
6
8
  function containsScript(head, js, externalScripts) {
@@ -13,15 +15,49 @@ function resolveSandbox(sandbox, hasScripts) {
13
15
  return sandbox ?? (hasScripts ? "allow-scripts allow-same-origin" : "allow-same-origin");
14
16
  }
15
17
  const fullHeightStyle = "<style>html,body{height:100%}</style>";
16
- function buildSrcdoc(html, css, head, js, fullHeight, externalStyles, externalScripts, preflight) {
18
+ function nonBlank(value) {
19
+ return value?.trim() ? value : void 0;
20
+ }
21
+ function escapeHtmlAttribute(value) {
22
+ return value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
23
+ }
24
+ function escapeHtmlText(value) {
25
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
26
+ }
27
+ function hasAuthorTitle(head) {
28
+ if (!head) return false;
29
+ const withoutComments = head.replace(/<!--[\s\S]*?-->/g, "");
30
+ const openingTitle = /<title(?=[\s>])/gi;
31
+ for (const match of withoutComments.matchAll(openingTitle)) {
32
+ let quote;
33
+ for (let index = (match.index ?? 0) + match[0].length; index < withoutComments.length; index += 1) {
34
+ const character = withoutComments[index];
35
+ if (quote) {
36
+ if (character === quote) quote = void 0;
37
+ continue;
38
+ }
39
+ if (character === '"' || character === "'") {
40
+ quote = character;
41
+ continue;
42
+ }
43
+ if (character === ">") return true;
44
+ if (character === "<") break;
45
+ }
46
+ }
47
+ return false;
48
+ }
49
+ function buildSrcdoc(html, css, head, js, fullHeight, externalStyles, externalScripts, preflight, metadata) {
17
50
  const includePreflight = preflight ?? true;
51
+ const lang = nonBlank(metadata?.lang) ?? "en";
52
+ const generatedTitle = hasAuthorTitle(head) ? void 0 : nonBlank(metadata?.title) ?? nonBlank(metadata?.previewLabel) ?? "Preview";
18
53
  const externalStylesHtml = (externalStyles ?? []).map((href) => `<link rel="stylesheet" href="${href}">`).join("\n");
19
54
  const externalScriptsHtml = (externalScripts ?? []).map((src) => `<script src="${src}"></script>`).join("\n");
20
55
  return `<!doctype html>
21
- <html>
56
+ <html lang="${escapeHtmlAttribute(lang)}">
22
57
  <head>
23
58
  <meta charset="utf-8">
24
59
  <meta name="viewport" content="width=device-width,initial-scale=1">
60
+ ${generatedTitle ? `<title>${escapeHtmlText(generatedTitle)}</title>` : ""}
25
61
  ${includePreflight ? `<style>${preflightCss}</style>` : ""}
26
62
  ${fullHeight ? fullHeightStyle : ""}
27
63
  ${externalStylesHtml}
@@ -40,8 +76,12 @@ function HtmlPreview({
40
76
  head,
41
77
  js,
42
78
  title,
79
+ lang,
43
80
  height,
44
81
  defaultOpen,
82
+ labels,
83
+ showSource,
84
+ showViewportControls,
45
85
  fullHeight,
46
86
  sandbox,
47
87
  componentCss,
@@ -61,13 +101,27 @@ function HtmlPreview({
61
101
  fullHeight,
62
102
  externalStyles,
63
103
  externalScripts,
64
- preflight
104
+ preflight,
105
+ { lang, title, previewLabel: labels?.preview }
65
106
  ),
66
- [html, css, head, js, fullHeight, externalStyles, externalScripts, preflight]
107
+ [
108
+ html,
109
+ css,
110
+ head,
111
+ js,
112
+ fullHeight,
113
+ externalStyles,
114
+ externalScripts,
115
+ preflight,
116
+ lang,
117
+ title,
118
+ labels?.preview
119
+ ]
67
120
  );
68
121
  const hasScripts = containsScript(head, js, externalScripts);
69
122
  const syncDelay = hasScripts ? 300 : 0;
70
123
  const sandboxValue = resolveSandbox(sandbox, hasScripts);
124
+ const sandboxAttributeValue = sandboxValue === "" ? " " : sandboxValue;
71
125
  const codeBlocks = useMemo(() => {
72
126
  const resourceLines = showResources ? [
73
127
  ...(externalStyles ?? []).map(
@@ -108,7 +162,11 @@ ${dedent(html)}` : dedent(html);
108
162
  height,
109
163
  srcdoc,
110
164
  defaultOpen,
111
- sandbox: sandboxValue,
165
+ labels,
166
+ showSource,
167
+ showViewportControls,
168
+ autoHeight: height == null && !fullHeight,
169
+ sandbox: sandboxAttributeValue,
112
170
  syncDelay,
113
171
  codeBlocks
114
172
  }
@@ -3,6 +3,6 @@ export type { HtmlPreviewWrapperProps, HtmlPreviewGlobalConfig, } from "./html-p
3
3
  export { HtmlPreview } from "./html-preview.js";
4
4
  export type { HtmlPreviewProps } from "./html-preview.js";
5
5
  export { PreviewBase } from "./preview-base.js";
6
- export type { PreviewBaseProps, CodeBlockData } from "./preview-base.js";
6
+ export type { PreviewBaseProps, CodeBlockData, HtmlPreviewLabels, } from "./preview-base.js";
7
7
  export { HighlightedCode } from "./highlighted-code.js";
8
8
  export type { HighlightedCodeProps } from "./highlighted-code.js";
@@ -0,0 +1,24 @@
1
+ interface PreviewAutoHeightRuntime {
2
+ setTimeout(callback: () => void, delay: number): number;
3
+ clearTimeout(id: number): void;
4
+ requestAnimationFrame(callback: FrameRequestCallback): number;
5
+ cancelAnimationFrame(id: number): void;
6
+ }
7
+ export interface PreviewAutoHeightOptions {
8
+ iframe: HTMLIFrameElement;
9
+ syncDelay: number;
10
+ getCurrentHeight: () => number;
11
+ setHeight: (height: number) => void;
12
+ runtime?: PreviewAutoHeightRuntime;
13
+ }
14
+ export interface PreviewAutoHeightController {
15
+ handleLoad(): void;
16
+ schedule(): void;
17
+ destroy(): void;
18
+ }
19
+ /**
20
+ * Owns the readable-iframe auto-height lifecycle. Kept separate from Preact so
21
+ * reload generations, observer callbacks, and teardown remain deterministic.
22
+ */
23
+ export declare function createPreviewAutoHeightController({ iframe, syncDelay, getCurrentHeight, setHeight, runtime, }: PreviewAutoHeightOptions): PreviewAutoHeightController;
24
+ export {};
@@ -0,0 +1,120 @@
1
+ const MIN_PREVIEW_HEIGHT = 200;
2
+ const PREVIEW_HEIGHT_BUFFER = 16;
3
+ const SELF_SIZING_EPSILON = 1;
4
+ function defaultRuntime(iframe) {
5
+ const view = iframe.ownerDocument.defaultView ?? window;
6
+ return {
7
+ setTimeout: (callback, delay) => view.setTimeout(callback, delay),
8
+ clearTimeout: (id) => view.clearTimeout(id),
9
+ requestAnimationFrame: (callback) => view.requestAnimationFrame(callback),
10
+ cancelAnimationFrame: (id) => view.cancelAnimationFrame(id)
11
+ };
12
+ }
13
+ function createPreviewAutoHeightController({
14
+ iframe,
15
+ syncDelay,
16
+ getCurrentHeight,
17
+ setHeight,
18
+ runtime = defaultRuntime(iframe)
19
+ }) {
20
+ let generation = 0;
21
+ let document = null;
22
+ let observer = null;
23
+ let timeoutId = null;
24
+ let frameId = null;
25
+ let lastAppliedHeight = getCurrentHeight();
26
+ let lastWriteSample;
27
+ let destroyed = false;
28
+ const cancelPending = () => {
29
+ observer?.disconnect();
30
+ observer = null;
31
+ if (timeoutId != null) {
32
+ runtime.clearTimeout(timeoutId);
33
+ timeoutId = null;
34
+ }
35
+ if (frameId != null) {
36
+ runtime.cancelAnimationFrame(frameId);
37
+ frameId = null;
38
+ }
39
+ document = null;
40
+ };
41
+ const measure = (expectedGeneration) => {
42
+ if (destroyed || expectedGeneration !== generation || !document?.body) {
43
+ return;
44
+ }
45
+ const bodyHeight = document.body.scrollHeight;
46
+ if (!Number.isFinite(bodyHeight) || bodyHeight <= 0) return;
47
+ const currentHeight = getCurrentHeight();
48
+ const targetHeight = Math.max(
49
+ bodyHeight + PREVIEW_HEIGHT_BUFFER,
50
+ MIN_PREVIEW_HEIGHT
51
+ );
52
+ const isSelfSizingGrowth = targetHeight > currentHeight && Math.abs(bodyHeight - currentHeight) <= SELF_SIZING_EPSILON;
53
+ const followedPreviousWrite = targetHeight > currentHeight && lastWriteSample != null && Math.abs(currentHeight - lastWriteSample.targetHeight) <= SELF_SIZING_EPSILON && Math.abs(
54
+ bodyHeight - lastWriteSample.bodyHeight - (currentHeight - lastWriteSample.currentHeight)
55
+ ) <= SELF_SIZING_EPSILON;
56
+ if (isSelfSizingGrowth || followedPreviousWrite || targetHeight === lastAppliedHeight) {
57
+ return;
58
+ }
59
+ lastAppliedHeight = targetHeight;
60
+ lastWriteSample = { bodyHeight, currentHeight, targetHeight };
61
+ setHeight(targetHeight);
62
+ };
63
+ const scheduleFor = (expectedGeneration) => {
64
+ if (destroyed || expectedGeneration !== generation || document == null || frameId != null) {
65
+ return;
66
+ }
67
+ frameId = runtime.requestAnimationFrame(() => {
68
+ frameId = null;
69
+ measure(expectedGeneration);
70
+ });
71
+ };
72
+ const handleLoad = () => {
73
+ generation += 1;
74
+ cancelPending();
75
+ if (destroyed) return;
76
+ const expectedGeneration = generation;
77
+ lastAppliedHeight = getCurrentHeight();
78
+ lastWriteSample = void 0;
79
+ try {
80
+ const nextDocument = iframe.contentDocument;
81
+ if (!nextDocument?.body) return;
82
+ void nextDocument.body.scrollHeight;
83
+ document = nextDocument;
84
+ } catch {
85
+ return;
86
+ }
87
+ measure(expectedGeneration);
88
+ if (syncDelay > 0) {
89
+ timeoutId = runtime.setTimeout(() => {
90
+ timeoutId = null;
91
+ scheduleFor(expectedGeneration);
92
+ }, syncDelay);
93
+ }
94
+ const iframeWindow = iframe.contentWindow;
95
+ const ResizeObserverConstructor = iframeWindow?.ResizeObserver;
96
+ if (ResizeObserverConstructor) {
97
+ const nextObserver = new ResizeObserverConstructor(() => {
98
+ scheduleFor(expectedGeneration);
99
+ });
100
+ observer = nextObserver;
101
+ nextObserver.observe(document.body);
102
+ if (document.documentElement !== document.body) {
103
+ nextObserver.observe(document.documentElement);
104
+ }
105
+ }
106
+ };
107
+ return {
108
+ handleLoad,
109
+ schedule: () => scheduleFor(generation),
110
+ destroy: () => {
111
+ if (destroyed) return;
112
+ destroyed = true;
113
+ generation += 1;
114
+ cancelPending();
115
+ }
116
+ };
117
+ }
118
+ export {
119
+ createPreviewAutoHeightController
120
+ };