@tenphi/renderer 0.25.3 → 0.26.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.
@@ -10,6 +10,9 @@ export const MainContentRoot = defineComponent("MainContent", {
10
10
  styles: {
11
11
  "@property": TASTY_SPACING_PROPERTIES,
12
12
  padding: "0 0 5rem",
13
+ // A focus target for the skip link and page swaps, not a control. Clicking
14
+ // its text focuses it, so keyboard scrolling would otherwise show a ring.
15
+ outline: "none",
13
16
  // Layout spacing is a default; generated Markdown owns prose spacing.
14
17
  ContentSpacing: {
15
18
  $: ":where(Panel > Container) > * + *",
@@ -22,7 +22,7 @@ export const PaginationRoot = defineComponent("Pagination", {
22
22
  gap: "$gap",
23
23
  inlineSize: "100%",
24
24
  padding: "($gap * 2)",
25
- border: "#clear",
25
+ border: true,
26
26
  color: "#text-soft",
27
27
  fill: "#surface-2",
28
28
  shadow: "none",
@@ -1,12 +1,17 @@
1
1
  ---
2
2
  import { messages, requestLanguage } from "../localization.js";
3
+ import { buildPreview } from "../preview-document.js";
3
4
  import { content } from "virtual:cookbook/config";
4
5
  import { PreviewRoot } from "./Preview.styles.js";
5
6
 
6
7
  import type { PreviewProps as Props } from "./props.js";
7
8
 
8
- const { title, html = "", css = "", javascript } = Astro.props;
9
- const source = `<!doctype html><meta charset="utf-8"><style>${css}</style>${html}${javascript ? `<script>${javascript}<\/script>` : ""}`;
9
+ const { title } = Astro.props;
10
+ const preview = await buildPreview(
11
+ Astro.props,
12
+ content.publicDir,
13
+ Astro.url.pathname,
14
+ );
10
15
  const ui = messages(
11
16
  requestLanguage(Astro.url.pathname, content.base, content),
12
17
  content,
@@ -18,12 +23,12 @@ const ui = messages(
18
23
  <PreviewRoot.Stage>
19
24
  <PreviewRoot.Frame
20
25
  title={title}
21
- sandbox={javascript ? "allow-scripts" : ""}
22
- srcDoc={source}
26
+ sandbox={preview.sandbox}
27
+ srcDoc={preview.document}
23
28
  />
24
29
  </PreviewRoot.Stage>
25
30
  <PreviewRoot.Code>
26
31
  <PreviewRoot.Summary>{ui.viewSource}</PreviewRoot.Summary>
27
- <PreviewRoot.Pre><code>{source}</code></PreviewRoot.Pre>
32
+ <PreviewRoot.Pre><code>{preview.source}</code></PreviewRoot.Pre>
28
33
  </PreviewRoot.Code>
29
34
  </PreviewRoot>
@@ -254,6 +254,9 @@ export const SidebarRoot = defineComponent("Sidebar", {
254
254
  '@own((:hover | :focus-visible) & ![aria-current="page"])':
255
255
  "#surface-2-hover",
256
256
  },
257
+ // Lift the focus ring above the next item's fill.
258
+ position: { "": null, "@own(:focus-visible)": "relative" },
259
+ zIndex: { "": null, "@own(:focus-visible)": "1" },
257
260
  },
258
261
  SummaryMarker: {
259
262
  $: "cookbook-sidebar summary::marker, summary::-webkit-details-marker",
@@ -1,21 +1,29 @@
1
1
  // Kept dependency-free for the page shell's synchronous, pre-paint script.
2
+ // Reads the documented storage contract in appearance-preferences.ts.
2
3
  (() => {
3
4
  const root = globalThis.document.documentElement;
5
+ let appearance = {};
4
6
  try {
5
- const scheme = globalThis.localStorage.getItem("cookbook-theme");
6
- if (scheme === "light" || scheme === "dark") root.dataset.theme = scheme;
7
- else delete root.dataset.theme;
8
- } catch {
9
- // Without storage, the document's system appearance rules remain active.
10
- }
11
- try {
12
- const appearance = JSON.parse(
13
- globalThis.localStorage.getItem("cookbook-appearance") ?? "{}",
14
- );
15
- if (appearance?.contrast === "normal" || appearance?.contrast === "more")
16
- root.dataset.contrast = appearance.contrast;
17
- else delete root.dataset.contrast;
7
+ appearance =
8
+ JSON.parse(
9
+ globalThis.localStorage.getItem("cookbook-appearance") ?? "{}",
10
+ ) ?? {};
18
11
  } catch {
19
12
  // Malformed or unavailable storage must not prevent the first paint.
20
13
  }
14
+ let theme = appearance.theme;
15
+ if (theme === undefined) {
16
+ try {
17
+ // A scheme saved before the appearance object held it.
18
+ theme = globalThis.localStorage.getItem("cookbook-theme");
19
+ } catch {
20
+ // Without storage, the document's system appearance rules remain active.
21
+ }
22
+ }
23
+ if (theme === "light" || theme === "dark") root.dataset.theme = theme;
24
+ else delete root.dataset.theme;
25
+ const contrast = appearance.contrast;
26
+ if (contrast === "normal" || contrast === "more")
27
+ root.dataset.contrast = contrast;
28
+ else delete root.dataset.contrast;
21
29
  })();
@@ -7,71 +7,104 @@ function storage(values: Record<string, string> = {}) {
7
7
  setItem: (key: string, value: string) => {
8
8
  values[key] = value;
9
9
  },
10
+ removeItem: (key: string) => {
11
+ delete values[key];
12
+ },
10
13
  };
11
14
  }
12
15
 
13
16
  describe("appearance preferences", () => {
14
- it("restores existing independent scheme and contrast choices", () => {
17
+ it("restores independent theme and contrast choices from one object", () => {
15
18
  const saved = storage({
16
- "cookbook-theme": "dark",
17
- "cookbook-appearance": JSON.stringify({ contrast: "more" }),
19
+ "cookbook-appearance": JSON.stringify({
20
+ theme: "dark",
21
+ contrast: "more",
22
+ }),
18
23
  });
19
24
  expect(loadPreferences(saved)).toEqual({
20
- scheme: "dark",
25
+ theme: "dark",
21
26
  contrast: "more",
22
27
  });
23
- storePreferences({ scheme: "light", contrast: "more" }, saved);
28
+ storePreferences({ theme: "light", contrast: "more" }, saved);
24
29
  expect(loadPreferences(saved)).toEqual({
25
- scheme: "light",
30
+ theme: "light",
26
31
  contrast: "more",
27
32
  });
28
- storePreferences({ scheme: "light", contrast: "normal" }, saved);
29
- expect(loadPreferences(saved)).toEqual({
30
- scheme: "light",
33
+ storePreferences({ theme: "light", contrast: "normal" }, saved);
34
+ expect(JSON.parse(saved.getItem("cookbook-appearance")!)).toEqual({
35
+ theme: "light",
31
36
  contrast: "normal",
32
37
  });
33
38
  });
34
39
 
35
- it("persists Auto using Cookbook storage without losing other preferences", () => {
40
+ it("persists system choices without losing other fields", () => {
36
41
  const saved = storage({
37
42
  "cookbook-appearance": JSON.stringify({ contrast: "more", other: true }),
38
43
  });
39
- storePreferences({ scheme: "auto", contrast: "system" }, saved);
40
- expect(saved.getItem("cookbook-theme")).toBe("");
44
+ storePreferences({ theme: "system", contrast: "system" }, saved);
41
45
  expect(JSON.parse(saved.getItem("cookbook-appearance")!)).toEqual({
46
+ theme: "system",
42
47
  contrast: "system",
43
48
  other: true,
44
49
  });
45
50
  expect(loadPreferences(saved)).toEqual({
46
- scheme: "auto",
51
+ theme: "system",
47
52
  contrast: "system",
48
53
  });
49
54
  });
50
55
 
56
+ it("reads a scheme saved under the legacy key and removes it on save", () => {
57
+ const saved = storage({
58
+ "cookbook-theme": "dark",
59
+ "cookbook-appearance": JSON.stringify({ contrast: "more" }),
60
+ });
61
+ expect(loadPreferences(saved)).toEqual({
62
+ theme: "dark",
63
+ contrast: "more",
64
+ });
65
+ storePreferences(loadPreferences(saved), saved);
66
+ expect(saved.getItem("cookbook-theme")).toBeNull();
67
+ expect(JSON.parse(saved.getItem("cookbook-appearance")!)).toEqual({
68
+ theme: "dark",
69
+ contrast: "more",
70
+ });
71
+ });
72
+
73
+ it("prefers the object's theme over the legacy key", () => {
74
+ const saved = storage({
75
+ "cookbook-theme": "dark",
76
+ "cookbook-appearance": JSON.stringify({ theme: "system" }),
77
+ });
78
+ expect(loadPreferences(saved).theme).toBe("system");
79
+ });
80
+
51
81
  it.each(["{broken", "null", "[]", '"more"', '{"contrast":"invalid"}'])(
52
- "recovers from malformed contrast data: %s",
82
+ "recovers from malformed appearance data: %s",
53
83
  (value) => {
54
- const saved = storage({
55
- "cookbook-theme": "light",
56
- "cookbook-appearance": value,
57
- });
84
+ const saved = storage({ "cookbook-appearance": value });
58
85
  expect(loadPreferences(saved)).toEqual({
59
- scheme: "light",
86
+ theme: "system",
60
87
  contrast: "system",
61
88
  });
62
- storePreferences({ scheme: "dark", contrast: "more" }, saved);
89
+ storePreferences({ theme: "dark", contrast: "more" }, saved);
63
90
  expect(loadPreferences(saved)).toEqual({
64
- scheme: "dark",
91
+ theme: "dark",
65
92
  contrast: "more",
66
93
  });
67
94
  },
68
95
  );
69
96
 
70
97
  it("uses system defaults for unknown values or unavailable storage", () => {
71
- expect(loadPreferences(storage({ "cookbook-theme": "invalid" }))).toEqual({
72
- scheme: "auto",
73
- contrast: "system",
74
- });
98
+ expect(
99
+ loadPreferences(
100
+ storage({
101
+ "cookbook-appearance": JSON.stringify({
102
+ theme: "invalid",
103
+ contrast: "invalid",
104
+ }),
105
+ }),
106
+ ),
107
+ ).toEqual({ theme: "system", contrast: "system" });
75
108
  const unavailable = {
76
109
  getItem: () => {
77
110
  throw new Error("Storage unavailable");
@@ -79,13 +112,16 @@ describe("appearance preferences", () => {
79
112
  setItem: () => {
80
113
  throw new Error("Storage unavailable");
81
114
  },
115
+ removeItem: () => {
116
+ throw new Error("Storage unavailable");
117
+ },
82
118
  };
83
119
  expect(loadPreferences(unavailable)).toEqual({
84
- scheme: "auto",
120
+ theme: "system",
85
121
  contrast: "system",
86
122
  });
87
123
  expect(() =>
88
- storePreferences({ scheme: "dark", contrast: "more" }, unavailable),
124
+ storePreferences({ theme: "dark", contrast: "more" }, unavailable),
89
125
  ).not.toThrow();
90
126
  });
91
127
  });
@@ -1,19 +1,25 @@
1
- type Scheme = "light" | "dark" | "auto";
1
+ // `cookbook-appearance` is a documented contract that other apps on the same
2
+ // origin read and write (docs/theme-and-components.md, "Reader appearance").
3
+ // Keep its key and value format compatible; appearance-bootstrap.js reads it too.
4
+ export const APPEARANCE_KEY = "cookbook-appearance";
5
+ // The color scheme used to live in its own key. It is still read when the
6
+ // object has no `theme`, and removed on the next save.
7
+ export const LEGACY_THEME_KEY = "cookbook-theme";
8
+
9
+ type Theme = "light" | "dark" | "system";
2
10
  type Contrast = "normal" | "more" | "system";
3
- type Preferences = { scheme: Scheme; contrast: Contrast };
4
- type PreferenceStorage = Pick<Storage, "getItem" | "setItem">;
11
+ type Preferences = { theme: Theme; contrast: Contrast };
12
+ type PreferenceStorage = Pick<Storage, "getItem" | "setItem" | "removeItem">;
5
13
 
6
- export const parseScheme = (value: unknown): Scheme =>
7
- value === "light" || value === "dark" ? value : "auto";
14
+ export const parseTheme = (value: unknown): Theme =>
15
+ value === "light" || value === "dark" ? value : "system";
8
16
 
9
17
  export const parseContrast = (value: unknown): Contrast =>
10
18
  value === "normal" || value === "more" ? value : "system";
11
19
 
12
20
  function storedAppearance(storage: PreferenceStorage): Record<string, unknown> {
13
21
  try {
14
- const value: unknown = JSON.parse(
15
- storage.getItem("cookbook-appearance") ?? "{}",
16
- );
22
+ const value: unknown = JSON.parse(storage.getItem(APPEARANCE_KEY) ?? "{}");
17
23
  return value && typeof value === "object" && !Array.isArray(value)
18
24
  ? (value as Record<string, unknown>)
19
25
  : {};
@@ -23,11 +29,14 @@ function storedAppearance(storage: PreferenceStorage): Record<string, unknown> {
23
29
  }
24
30
 
25
31
  export function loadPreferences(storage?: PreferenceStorage): Preferences {
26
- const preferences: Preferences = { scheme: "auto", contrast: "system" };
32
+ const preferences: Preferences = { theme: "system", contrast: "system" };
27
33
  try {
28
34
  storage ??= localStorage;
29
- preferences.scheme = parseScheme(storage.getItem("cookbook-theme"));
30
- preferences.contrast = parseContrast(storedAppearance(storage).contrast);
35
+ const stored = storedAppearance(storage);
36
+ preferences.theme = parseTheme(
37
+ "theme" in stored ? stored.theme : storage.getItem(LEGACY_THEME_KEY),
38
+ );
39
+ preferences.contrast = parseContrast(stored.contrast);
31
40
  } catch {
32
41
  // System defaults work even when browser storage is unavailable.
33
42
  }
@@ -41,16 +50,14 @@ export function storePreferences(
41
50
  try {
42
51
  storage ??= localStorage;
43
52
  storage.setItem(
44
- "cookbook-theme",
45
- value.scheme === "auto" ? "" : value.scheme,
46
- );
47
- storage.setItem(
48
- "cookbook-appearance",
53
+ APPEARANCE_KEY,
49
54
  JSON.stringify({
50
55
  ...storedAppearance(storage),
56
+ theme: value.theme,
51
57
  contrast: value.contrast,
52
58
  }),
53
59
  );
60
+ storage.removeItem(LEGACY_THEME_KEY);
54
61
  } catch {
55
62
  // The current page still updates when preferences cannot be persisted.
56
63
  }
@@ -13,6 +13,8 @@ interface PreviewProps {
13
13
  html?: string;
14
14
  css?: string;
15
15
  javascript?: string;
16
+ /** Public-directory paths, such as `/demo/button.js`, inlined as classic scripts before `javascript`. */
17
+ scripts?: string[];
16
18
  }
17
19
  type StepsProps = Record<string, never>;
18
20
  interface TabsProps {
@@ -1 +1 @@
1
- {"version":3,"file":"props.d.ts","names":[],"sources":["../../src/components/props.ts"],"mappings":";UAAiB;EACf;EACA;;UAEe;EACf;EACA;EACA;;UAEe;EACf;EACA;EACA;EACA;;KAEU,aAAa;UACR;EACf;;UAEe;EACf;;UAEe;EACf;EACA;;UAEe;EACf;EACA,OAAO;IAAQ;IAAe;IAAc"}
1
+ {"version":3,"file":"props.d.ts","names":[],"sources":["../../src/components/props.ts"],"mappings":";UAAiB;EACf;EACA;;UAEe;EACf;EACA;EACA;;UAEe;EACf;EACA;EACA;EACA;;EAEA;;KAEU,aAAa;UACR;EACf;;UAEe;EACf;;UAEe;EACf;EACA;;UAEe;EACf;EACA,OAAO;IAAQ;IAAe;IAAc"}
@@ -12,6 +12,8 @@ export interface PreviewProps {
12
12
  html?: string;
13
13
  css?: string;
14
14
  javascript?: string;
15
+ /** Public-directory paths, such as `/demo/button.js`, inlined as classic scripts before `javascript`. */
16
+ scripts?: string[];
15
17
  }
16
18
  export type StepsProps = Record<string, never>;
17
19
  export interface TabsProps {
package/dist/index.js CHANGED
@@ -1426,7 +1426,8 @@ function configuredCookbook(options) {
1426
1426
  locales: loaded.config.locales,
1427
1427
  defaultLocale: loaded.config.defaultLocale,
1428
1428
  translations: loaded.config.translations,
1429
- head: [...siteIcons?.head ?? [], ...options.config?.head ?? []]
1429
+ head: [...siteIcons?.head ?? [], ...options.config?.head ?? []],
1430
+ publicDir: fileURLToPath(context.config.publicDir)
1430
1431
  };
1431
1432
  }, navigation, () => [
1432
1433
  projectRoot,