@gavin-hu/my-pi-agent 0.16.0 → 0.18.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 (200) hide show
  1. package/README.md +29 -15
  2. package/README.zh-CN.md +27 -15
  3. package/extensions/bytefly/README.md +195 -115
  4. package/extensions/bytefly/footer/collect/entries.ts +1 -6
  5. package/extensions/bytefly/footer/collect/snapshot.ts +23 -9
  6. package/extensions/bytefly/footer/price/price.ts +12 -8
  7. package/extensions/bytefly/footer/register.ts +13 -1
  8. package/extensions/bytefly/footer/types.ts +11 -0
  9. package/extensions/bytefly/footer/unpriced.ts +36 -0
  10. package/extensions/bytefly/header/component.ts +15 -29
  11. package/extensions/bytefly/header/identity.ts +71 -0
  12. package/extensions/bytefly/header/register.ts +9 -3
  13. package/extensions/bytefly/index.ts +13 -5
  14. package/extensions/bytefly/message/format.ts +20 -13
  15. package/extensions/bytefly/message/register.ts +8 -7
  16. package/extensions/bytefly/notify/types.ts +1 -1
  17. package/extensions/bytefly/panel/commands.ts +46 -25
  18. package/extensions/bytefly/panel/format.ts +49 -14
  19. package/extensions/bytefly/panel/join.ts +35 -9
  20. package/extensions/bytefly/panel/settings.ts +29 -39
  21. package/extensions/bytefly/panel/skills.ts +51 -0
  22. package/extensions/bytefly/panel/tui/extensions-view.ts +7 -61
  23. package/extensions/bytefly/panel/tui/frame.ts +9 -1
  24. package/extensions/bytefly/panel/tui/list-view.ts +72 -0
  25. package/extensions/bytefly/panel/tui/nav.ts +71 -3
  26. package/extensions/bytefly/panel/tui/settings-section.ts +250 -0
  27. package/extensions/bytefly/panel/tui/skills-view.ts +50 -0
  28. package/extensions/bytefly/panel/tui.ts +223 -331
  29. package/extensions/bytefly/panel/types.ts +26 -8
  30. package/extensions/bytefly/picker/discovery.ts +32 -27
  31. package/extensions/bytefly/picker/resources.ts +16 -8
  32. package/extensions/bytefly/picker/runtime.ts +62 -34
  33. package/extensions/bytefly/picker/toggle.ts +16 -10
  34. package/extensions/bytefly/picker/types.ts +31 -13
  35. package/extensions/bytefly/update/format.ts +66 -8
  36. package/extensions/bytefly/update/patch.ts +66 -0
  37. package/extensions/bytefly/update/register.ts +4 -11
  38. package/extensions/bytefly/update/release.ts +65 -0
  39. package/extensions/bytefly/update/state.ts +14 -0
  40. package/extensions/bytefly/update/suppress.ts +47 -29
  41. package/extensions/bytefly/update/types.ts +15 -5
  42. package/extensions/check/tui.ts +2 -0
  43. package/extensions/context/README.md +21 -11
  44. package/extensions/context/format.ts +10 -8
  45. package/extensions/context/report.ts +21 -8
  46. package/extensions/context/tui.ts +26 -10
  47. package/extensions/context/types.ts +5 -3
  48. package/extensions/file-browse/README.md +89 -22
  49. package/extensions/file-browse/chrome.ts +83 -0
  50. package/extensions/file-browse/client.ts +74 -459
  51. package/extensions/file-browse/config.ts +12 -3
  52. package/extensions/file-browse/diff-html.ts +94 -0
  53. package/extensions/file-browse/html.ts +26 -152
  54. package/extensions/file-browse/live-client.ts +47 -1
  55. package/extensions/file-browse/live.ts +35 -14
  56. package/extensions/file-browse/palette.ts +224 -0
  57. package/extensions/file-browse/rev.ts +42 -0
  58. package/extensions/file-browse/router.ts +27 -6
  59. package/extensions/file-browse/server.ts +7 -0
  60. package/extensions/file-browse/styles.ts +10 -3
  61. package/extensions/file-browse/tree.ts +381 -0
  62. package/extensions/file-read/README.md +4 -1
  63. package/extensions/file-read/extract/odt.ts +7 -5
  64. package/extensions/goal/plan-route.ts +8 -2
  65. package/extensions/goal/tools.ts +6 -11
  66. package/extensions/job/README.md +7 -5
  67. package/extensions/job/index.ts +35 -52
  68. package/extensions/job/tools.ts +51 -77
  69. package/extensions/memory/README.md +3 -3
  70. package/extensions/memory/tools.ts +4 -6
  71. package/extensions/memory/tui.ts +5 -1
  72. package/extensions/plan/README.md +333 -84
  73. package/extensions/plan/commands.ts +73 -7
  74. package/extensions/plan/feedback.ts +300 -63
  75. package/extensions/plan/index.ts +1 -1
  76. package/extensions/plan/plan-index.ts +4 -2
  77. package/extensions/plan/plan-text.ts +18 -1
  78. package/extensions/plan/plans.ts +184 -58
  79. package/extensions/plan/review-prompt.ts +93 -0
  80. package/extensions/plan/review.ts +95 -95
  81. package/extensions/plan/runtime.ts +4 -2
  82. package/extensions/plan/split.ts +87 -0
  83. package/extensions/plan/steps.ts +123 -107
  84. package/extensions/plan/tools/cancel.ts +79 -0
  85. package/extensions/plan/tools/enter.ts +17 -12
  86. package/extensions/plan/tools/exit.ts +148 -59
  87. package/extensions/plan/tools/steps.ts +7 -19
  88. package/extensions/plan/tools/write.ts +29 -31
  89. package/extensions/plan/tools.ts +6 -3
  90. package/extensions/plan/tui.ts +60 -15
  91. package/extensions/plan/types.ts +45 -3
  92. package/extensions/plan/web-chrome.ts +102 -0
  93. package/extensions/plan/web-client.ts +177 -446
  94. package/extensions/plan/web-collab.ts +207 -0
  95. package/extensions/plan/web-comments.ts +269 -0
  96. package/extensions/plan/web-markdown.ts +222 -0
  97. package/extensions/plan/web-page.ts +131 -19
  98. package/extensions/plan/web-router.ts +148 -32
  99. package/extensions/plan/web-share-session.ts +197 -0
  100. package/extensions/plan/web-share.ts +169 -0
  101. package/extensions/plan/web-streams.ts +124 -0
  102. package/extensions/plan/web-tokens.ts +63 -0
  103. package/extensions/plan/web.ts +193 -45
  104. package/extensions/question/README.md +3 -0
  105. package/extensions/question/dialogs.ts +91 -1
  106. package/extensions/question/tools.ts +2 -1
  107. package/extensions/rewind/checklist.ts +4 -0
  108. package/extensions/rewind/review-tui.ts +3 -1
  109. package/extensions/rewind/rewind-tui.ts +4 -0
  110. package/extensions/session-bus/README.md +247 -0
  111. package/extensions/session-bus/commands.ts +165 -0
  112. package/extensions/session-bus/format.ts +124 -0
  113. package/extensions/session-bus/index.ts +69 -0
  114. package/extensions/session-bus/notice.ts +58 -0
  115. package/extensions/session-bus/protocol.ts +134 -0
  116. package/extensions/session-bus/result.ts +243 -0
  117. package/extensions/session-bus/rows.ts +169 -0
  118. package/extensions/session-bus/runtime.ts +575 -0
  119. package/extensions/session-bus/store.ts +329 -0
  120. package/extensions/session-bus/team.ts +102 -0
  121. package/extensions/session-bus/tools.ts +227 -0
  122. package/extensions/session-bus/tui.ts +394 -0
  123. package/extensions/session-bus/types.ts +98 -0
  124. package/extensions/subagent/README.md +26 -13
  125. package/extensions/subagent/format.ts +72 -25
  126. package/extensions/subagent/index.ts +21 -47
  127. package/extensions/subagent/orchestrate.ts +18 -41
  128. package/extensions/subagent/render.ts +29 -42
  129. package/extensions/subagent/stream.ts +24 -18
  130. package/extensions/subagent/types.ts +7 -0
  131. package/extensions/todo/tools.ts +5 -6
  132. package/extensions/todo/tui.ts +5 -1
  133. package/extensions/watch/README.md +9 -5
  134. package/extensions/watch/index.ts +25 -44
  135. package/extensions/watch/tools.ts +31 -59
  136. package/extensions/wechat/README.md +45 -36
  137. package/extensions/wechat/bridge.ts +46 -48
  138. package/extensions/wechat/client.ts +1 -4
  139. package/extensions/wechat/commands.ts +3 -3
  140. package/extensions/wechat/deliver.ts +57 -37
  141. package/extensions/wechat/interaction.ts +54 -18
  142. package/extensions/wechat/login-screen.ts +2 -3
  143. package/extensions/wechat/notices.ts +9 -7
  144. package/extensions/wechat/peer-worker.ts +19 -34
  145. package/extensions/wechat/presence.ts +3 -4
  146. package/extensions/wechat/prompt.ts +53 -23
  147. package/extensions/wechat/reply-pipeline.ts +12 -4
  148. package/extensions/wechat/slash.ts +14 -0
  149. package/extensions/wechat/status-screen.ts +2 -2
  150. package/extensions/wechat/tools.ts +3 -11
  151. package/extensions/worktree/README.md +48 -12
  152. package/extensions/worktree/commands.ts +109 -65
  153. package/extensions/worktree/enter.ts +1 -1
  154. package/extensions/worktree/exit.ts +11 -1
  155. package/extensions/worktree/git.ts +5 -0
  156. package/extensions/worktree/lifecycle.ts +9 -1
  157. package/extensions/worktree/registry.ts +11 -5
  158. package/extensions/worktree/screen.ts +263 -0
  159. package/extensions/worktree/status.ts +84 -40
  160. package/lib/README.md +12 -7
  161. package/lib/claims/overlap.ts +43 -0
  162. package/lib/config.ts +12 -0
  163. package/lib/format.ts +6 -2
  164. package/lib/http/router.ts +40 -13
  165. package/lib/http/server.ts +27 -13
  166. package/lib/interaction.ts +28 -1
  167. package/lib/notice.ts +29 -2
  168. package/lib/plan-entry.ts +7 -2
  169. package/lib/plans/sections.ts +172 -0
  170. package/lib/plans/setting.ts +52 -0
  171. package/lib/plans/split.ts +215 -0
  172. package/lib/plans/store.ts +19 -0
  173. package/lib/plans/team.ts +151 -0
  174. package/lib/qr.ts +64 -0
  175. package/lib/repo-key.ts +74 -0
  176. package/lib/report.ts +117 -25
  177. package/lib/requirements.ts +1 -2
  178. package/lib/tool-result.ts +54 -4
  179. package/lib/tui/README.md +30 -8
  180. package/lib/tui/chrome.ts +27 -6
  181. package/lib/tui/dock-screen.ts +91 -19
  182. package/lib/tui/gauge.ts +9 -5
  183. package/lib/tui/line-pane.ts +15 -4
  184. package/lib/tui/tool.ts +23 -4
  185. package/lib/usage.ts +26 -0
  186. package/lib/web/README.md +131 -0
  187. package/lib/web/base.ts +19 -2
  188. package/lib/web/components.ts +10 -6
  189. package/lib/web/layout.ts +2 -0
  190. package/lib/web/markup.ts +2 -1
  191. package/lib/web/runtime.ts +103 -25
  192. package/lib/web/tokens.ts +23 -7
  193. package/package.json +5 -2
  194. package/skills/changelog/SKILL.md +57 -45
  195. package/skills/init/SKILL.md +80 -45
  196. package/skills/init/references/agents-md-quality.md +26 -2
  197. package/skills/init/references/interview-questions.md +36 -14
  198. package/skills/next/SKILL.md +209 -0
  199. package/skills/parallel-dev/SKILL.md +150 -0
  200. package/extensions/wechat/qrcode.ts +0 -46
package/lib/tui/gauge.ts CHANGED
@@ -26,16 +26,20 @@ export const DEFAULT_GAUGE_THRESHOLDS: GaugeThresholds = { warn: 70, danger: 90
26
26
  /**
27
27
  * Filled and empty block counts for a percentage across `blocks` cells.
28
28
  *
29
- * The fill floors so the bar reads full only at 100%, and forces one block for
30
- * any non-zero usage so a small percentage is still visible. A `null` percent
31
- * (unknown) fills nothing, leaving the caller to decide whether to draw the
32
- * empty track; a known `0` draws it.
29
+ * The fill rounds to the nearest cell, so a bar sits no more than half a cell
30
+ * away from the number printed beside it, and the last cell is kept for 100% so
31
+ * a bar reads full only at a full value. Any non-zero usage still forces one
32
+ * block, so a small percentage stays visible. A `null` percent (unknown) fills
33
+ * nothing, leaving the caller to decide whether to draw the empty track; a known
34
+ * `0` draws it.
33
35
  */
34
36
  export function computeGauge(percent: number | null, blocks: number): { filled: number; empty: number } {
35
37
  if (blocks <= 0) return { filled: 0, empty: 0 };
36
38
  if (percent === null) return { filled: 0, empty: blocks };
37
39
  const clamped = Math.max(0, Math.min(100, percent));
38
- const filled = Math.max(clamped > 0 ? 1 : 0, Math.floor((clamped / 100) * blocks));
40
+ const nearest = Math.round((clamped / 100) * blocks);
41
+ const capped = clamped < 100 ? Math.min(blocks - 1, nearest) : nearest;
42
+ const filled = Math.max(clamped > 0 ? 1 : 0, capped);
39
43
  return { filled, empty: blocks - filled };
40
44
  }
41
45
 
@@ -45,6 +45,11 @@ export class LinePane {
45
45
  visible = 1;
46
46
  /** Whether the view is parked at the tail; set false once the user scrolls up. */
47
47
  follow = true;
48
+ /**
49
+ * Whether this pane follows its tail. A log does; a prose or list pane does not:
50
+ * it opens at the top and never reports `following`/`paused`.
51
+ */
52
+ tail = true;
48
53
 
49
54
  private buffer: string[] = [];
50
55
  private label = "";
@@ -66,16 +71,21 @@ export class LinePane {
66
71
  return Math.max(0, this.buffer.length - this.visible);
67
72
  }
68
73
 
69
- /** Open the pane on a row, reset to the tail. */
70
- open(id: string, source: PaneSource): void {
74
+ /** Open the pane on a row, reset to the tail for a log, or to the top for prose. */
75
+ open(id: string, source: PaneSource, tail = true): void {
71
76
  this.id = id;
72
77
  this.buffer = [];
73
78
  this.label = "";
74
79
  this.subtitle = undefined;
75
80
  this.more = false;
76
81
  this.scroll = 0;
77
- this.follow = true;
82
+ this.tail = tail;
83
+ this.follow = tail;
78
84
  this.update(source);
85
+ if (!tail) {
86
+ this.follow = false;
87
+ this.scroll = 0;
88
+ }
79
89
  }
80
90
 
81
91
  /** Close the pane and drop its buffer. */
@@ -87,6 +97,7 @@ export class LinePane {
87
97
  this.more = false;
88
98
  this.scroll = 0;
89
99
  this.follow = true;
100
+ this.tail = true;
90
101
  }
91
102
 
92
103
  /**
@@ -127,7 +138,7 @@ export class LinePane {
127
138
  const parts: string[] = [];
128
139
  if (this.subtitle) parts.push(this.subtitle);
129
140
  if (this.more && this.buffer.length > 0) parts.push(`showing last ${this.buffer.length} lines`);
130
- if (this.buffer.length > 0) parts.push(this.follow ? "following" : "paused");
141
+ if (this.tail && this.buffer.length > 0) parts.push(this.follow ? "following" : "paused");
131
142
  return parts.join(" · ");
132
143
  }
133
144
 
package/lib/tui/tool.ts CHANGED
@@ -200,18 +200,29 @@ export function earlierHint(theme: Theme, hidden: number, label: string): string
200
200
  /** The row renderer a {@link TailResult} list uses: the extension's row grammar at a width. */
201
201
  export type TailRowLine<Row> = (row: Row, width: number, theme: Theme) => string;
202
202
 
203
- /** What a {@link TailResult} draws: the caller's rows plus a hidden count, or a bounded tail. */
203
+ /** One group of rows inside a {@link TailResult}, under an optional faint heading. */
204
+ export interface TailSection<Row> {
205
+ /** A faint heading above the rows, e.g. `Claims`; omit for the block's primary list. */
206
+ label?: string;
207
+ rows: readonly Row[];
208
+ more: number;
209
+ rowLine: TailRowLine<Row>;
210
+ }
211
+
212
+ /** What a {@link TailResult} draws: the caller's rows plus a hidden count, a headed row list, or a bounded tail. */
204
213
  export type TailResultInput<Row> =
205
214
  | { kind: "list"; rows: readonly Row[]; more: number; rowLine: TailRowLine<Row> }
215
+ | { kind: "sections"; sections: readonly TailSection<Row>[] }
206
216
  | { kind: "lines"; lines: readonly string[]; earlier: number; earlierLabel: string };
207
217
 
208
218
  /**
209
219
  * The transcript result block a tool that lists rows or tails output shares.
210
220
  *
211
221
  * One leading blank line per the block grammar, then either the caller's rows
212
- * (already ordered and capped) with a "more" note, or the themed tail with an
213
- * "earlier" note. The caller owns the row renderer, so the extension's own row
214
- * grammar — a peer-session tag, a status glyph — stays with its vocabulary.
222
+ * (already ordered and capped) with a "more" note, one headed group of rows per
223
+ * section, or the themed tail with an "earlier" note. The caller owns the row
224
+ * renderer, so the extension's own row grammar — a peer-session tag, a status
225
+ * glyph — stays with its vocabulary.
215
226
  */
216
227
  export class TailResult<Row> implements Component {
217
228
  private input: TailResultInput<Row>;
@@ -239,6 +250,14 @@ export class TailResult<Row> implements Component {
239
250
  if (input.more > 0) lines.push(clip(` ${overflowHint(this.theme, input.more)}`, w));
240
251
  return lines;
241
252
  }
253
+ if (input.kind === "sections") {
254
+ for (const section of input.sections) {
255
+ if (section.label) lines.push(clip(paint(this.theme, "faint", section.label), w));
256
+ for (const row of section.rows) lines.push(section.rowLine(row, w, this.theme));
257
+ if (section.more > 0) lines.push(clip(` ${overflowHint(this.theme, section.more)}`, w));
258
+ }
259
+ return lines;
260
+ }
242
261
  if (input.earlier > 0) lines.push(clip(earlierHint(this.theme, input.earlier, input.earlierLabel), w));
243
262
  for (const line of input.lines) lines.push(this.theme.fg("toolOutput", clip(line, w)));
244
263
  return lines;
package/lib/usage.ts ADDED
@@ -0,0 +1,26 @@
1
+ /**
2
+ * The usage counters this package accounts in.
3
+ *
4
+ * A session, a delegated child run, and a priced record are all measured in the
5
+ * same five numbers, so the list lives here rather than in each caller:
6
+ * bytefly's footer accumulates them for the session and for its `↳` roll-up, and
7
+ * `subagent` accumulates them from a child's JSON events.
8
+ *
9
+ * `turns` and the context size are deliberately not counters — one is counted
10
+ * per message, the other is a window occupancy rather than something to sum — so
11
+ * each owner tracks those beside these.
12
+ */
13
+
14
+ /** The counters, in the order a usage line prints them. */
15
+ export const USAGE_COUNTERS = ["input", "output", "cacheRead", "cacheWrite", "cost"] as const;
16
+
17
+ /** One counter in a usage accumulator. */
18
+ export type UsageCounter = (typeof USAGE_COUNTERS)[number];
19
+
20
+ /** A usage accumulator: one running total per counter. */
21
+ export type UsageCounts = Record<UsageCounter, number>;
22
+
23
+ /** A zeroed accumulator, ready to sum into. */
24
+ export function zeroUsage(): UsageCounts {
25
+ return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
26
+ }
@@ -0,0 +1,131 @@
1
+ # lib/web — the shared browser design system
2
+
3
+ The tokens, base rules, app shell, `bf-*` components, HTML atoms, and client
4
+ runtime the two loopback pages draw with: the plan review
5
+ ([`../../extensions/plan/web-page.ts`](../../extensions/plan/web-page.ts)) and
6
+ file browse ([`../../extensions/file-browse/styles.ts`](../../extensions/file-browse/styles.ts)).
7
+ The module-by-module inventory is the `web/` row of [`../README.md`](../README.md);
8
+ this file is the conventions those modules exist to make easy.
9
+
10
+ Both pages serve a same-origin document through the shared HTTP layer
11
+ ([`../http/index.ts`](../http/index.ts)) under one CSP. The kit is the one policy
12
+ for how that document looks and moves — a second palette, focus ring, or dialog
13
+ is the bug this directory prevents.
14
+
15
+ ## Compose the layers in order
16
+
17
+ A page concatenates `WEB_BASE_CSS`, `WEB_THEME_CSS`, `WEB_LAYOUT_CSS`,
18
+ `WEB_COMPONENTS_CSS`, then its own page rules. Each block appears **once**: a
19
+ repeated block still renders, so only a count catches it, and every page suite
20
+ counts its own stylesheet (`pages.test.ts`, `web.test.ts`). A page rule is
21
+ namespaced (`review-*`, `browse-*`) and overrides a kit class rather than
22
+ re-declaring it.
23
+
24
+ ## Color is a role
25
+
26
+ `WEB_THEME_TOKENS` is derived from `themes/bytefly-dark.json` and
27
+ `themes/bytefly-light.json`, so the browser and the TUI cannot drift. Four values
28
+ are computed rather than mapped:
29
+
30
+ - `--accent-soft` — the accent at 15% (dark) or 10% (light), for a selection or
31
+ the active nav row.
32
+ - `--on-accent` — the appearance's canvas. A label on the accent fill is dark
33
+ text on the light-blue dark accent and white on the darker light one.
34
+ - `--scrim` and `--shadow` — per appearance, because a dimming layer and a lifted
35
+ panel need their own alpha rather than a palette entry.
36
+
37
+ Components read tokens only: no literal hex or `rgba()` in `components.ts`, and a
38
+ test fails one. Contrast is asserted, not assumed — text, muted, and accent at
39
+ 4.5:1 on both surfaces, the primary label at 4.5:1 on the accent fill, `--dim` at
40
+ 3:1 because it carries the line-number gutter, and the status colours at 3:1.
41
+
42
+ The theme layer emits the dark palette at `:root`, the explicit light override,
43
+ the system-light fallback, the `tok-*` syntax rules, and **last** a print block
44
+ that resets to the light palette: paper is white whatever the screen shows.
45
+
46
+ ## The shell
47
+
48
+ `.bf-app` is a definite-height column — `100vh`, then `100dvh` — so the header,
49
+ sidebar, and main pane scroll inside it instead of the document. `.bf-app__body`
50
+ is a 260px sidebar plus main; at `SIDEBAR_COLLAPSE_MAX` (860) the sidebar becomes
51
+ a `<details>` disclosure and the page returns to normal scrolling. That is the
52
+ only breakpoint the shell owns; a page's own (`820`, `1180`) stays in its
53
+ stylesheet, since a media query cannot read a custom property.
54
+
55
+ A `<details>` lays its content out in a wrapper with no definite height, so a
56
+ sidebar that must scroll on its own box positions its panel absolutely rather
57
+ than making the panel a flex child. `file-browse` does this; the rule and its
58
+ reason are in `layout.ts`.
59
+
60
+ ## The `bf-*` kit
61
+
62
+ `bf-` marks the kit: brand and mark, toolbar, title, buttons (`--primary`,
63
+ `--ghost`, `--icon`), chips and their tones, segmented control, field and input,
64
+ textarea, panel, progress, status, note, empty state, dialog (head, body, actions,
65
+ status, input, list, option), shortcut sheet and `bf-kbd`, tree, crumbs, table and
66
+ sort, card, code, media, Markdown body, comment, composer, and quote chip.
67
+
68
+ States are attributes, not classes, wherever a control has them:
69
+ `aria-pressed`, `aria-current`, `aria-sort`, `aria-expanded`, `hidden`. The base
70
+ layer's `[hidden]{display:none!important}` is what lets a `display:flex`
71
+ component disappear — do not add a page rule to hide it again.
72
+
73
+ ## Element and script contracts
74
+
75
+ These names are one agreement between a page, its client, and tests:
76
+
77
+ | Contract | Owner |
78
+ |---|---|
79
+ | `#theme-toggle` with a `.theme-glyph` | `themeToggle()` (`markup.ts`) |
80
+ | `THEME_JS_PATH` served by both routers | `runtime.ts` |
81
+ | `pi-web-theme` in `localStorage` | `WEB_THEME_STORAGE_KEY` |
82
+ | `#keys-dialog` (hidden, `aria-modal`) and `#keys-close` | `keysSheet` (`markup.ts`) |
83
+ | the page's own sheet trigger id | `keysDialogScript(triggerId)` |
84
+ | `window.bfKeys` — `open` / `toggle` / `close` / `isOpen` | `dialogScript` |
85
+
86
+ `dialogScript(options)` is the one modal implementation: it opens and closes,
87
+ closes on a backdrop click, traps `Tab` inside the panel, closes on `Esc` in the
88
+ capture phase (so a page's keymap cannot peel a different layer first), restores
89
+ focus to whatever opened it, and exposes `window[globalName]`. It renders nothing
90
+ when the element is absent, so a page can embed it unconditionally. A page adds
91
+ only its own confirm button's listener.
92
+
93
+ `THEME_JS` is the pre-paint bootstrap and lives in the head; `THEME_TOGGLE_JS`
94
+ paints the glyph, follows the system until the reader chooses, and then keeps the
95
+ choice. Neither page reloads for a theme.
96
+
97
+ ## Markup atoms and escaping
98
+
99
+ `docHead`, `brandLockup`, `themeToggle`, `chip`, `button`, and `keysSheet` return
100
+ strings. Every dynamic **value** goes through `escapeHtml` from
101
+ [`../escape.ts`](../escape.ts); the fields documented as trusted (`css`, an
102
+ `icon` glyph, an attribute key, a client id) are code-authored constants, never
103
+ model or file text. A page that renders its own document follows the same rule.
104
+
105
+ The kit is HTML and CSS only; plan text stays out of the page markup and arrives
106
+ through the review's JSON island, so the client writes `textContent` and never
107
+ markup.
108
+
109
+ ## Print and forced colours
110
+
111
+ `base.ts` owns both medias. Print un-constrains the shell and drops the toolbar,
112
+ sidebar, footer, progress bar, skip link, and dialogs; a page adds what its own
113
+ box clips (a card, a code pane, a sticky hunk header). Under
114
+ `forced-colors:active` the hand-drawn task checkbox names system colours and the
115
+ focus ring stays `CanvasText`, because Windows High Contrast replaces colour but
116
+ not a drawn control.
117
+
118
+ ## Keyboard
119
+
120
+ Every interactive element gets the one focus ring from `base.ts`; a component
121
+ must not declare its own, or the ring disappears surface by surface. A dialog
122
+ traps `Tab`. Within a page, `j`/`k`, arrows, and `?` follow the TUI's vocabulary
123
+ where they mean the same thing; the browse tree is one tab stop with arrows, and
124
+ `Enter` follows the focused link.
125
+
126
+ ## Tests
127
+
128
+ [`web.test.ts`](./web.test.ts) pins the token anchors, the contrast ratios, each
129
+ emitted block, the atoms' escaping, and every contract above. A page's suite adds
130
+ its own: the stylesheet is shipped once, and the ids its client greps for exist in
131
+ the markup.
package/lib/web/base.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Base rules shared by the Bytefly browser surfaces: box sizing, hidden,
3
- * links, the focus ring, and the skip-link / screen-reader helpers.
3
+ * links, the focus ring, the skip-link / screen-reader helpers, and the two
4
+ * media the kit answers for every page — print and forced colours.
4
5
  *
5
6
  * Page layout (body display, shells) stays with each page; this layer is the
6
7
  * smallest set both pages must agree on.
@@ -16,4 +17,20 @@ a:hover{text-decoration:underline}
16
17
  a:focus-visible,button:focus-visible,input:focus-visible,textarea:focus-visible,select:focus-visible,[tabindex]:focus-visible{outline:var(--focus-ring) solid var(--accent);outline-offset:2px}
17
18
  .skip{position:absolute;left:-9999px;top:0;z-index:3;padding:var(--space-2) var(--space-4);background:var(--surface);color:var(--text);border:1px solid var(--border);border-radius:var(--radius-md)}
18
19
  .skip:focus{left:var(--space-3);top:var(--space-3)}
19
- .sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}`;
20
+ .sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}
21
+ /* A printed page is not a viewport: the shell un-constrains, its chrome goes,
22
+ and the panes stop scrolling so the document prints whole. */
23
+ @media print{
24
+ .bf-app,.bf-app__body,.bf-app__main,.bf-app__sidebar,.bf-panel{display:block!important;height:auto!important;min-height:0!important;max-height:none!important;overflow:visible!important}
25
+ .bf-toolbar,.bf-app__sidebar,.bf-app__footer,.bf-progress,.skip,.bf-dialog{display:none!important}
26
+ .bf-app__header{position:static!important;border:0!important}
27
+ .bf-app__main{padding:0!important}
28
+ }
29
+ /* Windows High Contrast replaces colour but not a drawn control, so the one
30
+ hand-drawn box names a system colour and the ring stays visible. */
31
+ @media (forced-colors:active){
32
+ a{color:LinkText}
33
+ :focus-visible{outline-color:CanvasText}
34
+ .bf-md .md-check{border-color:CanvasText}
35
+ .bf-md .md-check:checked{forced-color-adjust:none;background:Highlight;border-color:Highlight}
36
+ }`;
@@ -17,14 +17,13 @@ export const WEB_COMPONENTS_CSS = `.bf-brand{display:inline-flex;align-items:cen
17
17
  .bf-btn{appearance:none;border:1px solid var(--border);border-radius:var(--radius-md);background:transparent;color:var(--text);font:var(--fs-sm) var(--font-ui);padding:5px var(--space-3);cursor:pointer}
18
18
  .bf-btn:hover{border-color:var(--accent);color:var(--accent);text-decoration:none}
19
19
  .bf-btn[aria-pressed="true"]{border-color:var(--accent);color:var(--accent);background:var(--accent-soft)}
20
- .bf-btn--primary{background:var(--accent);border-color:var(--accent);color:#fff}
21
- .bf-btn--primary:hover{filter:brightness(1.06);color:#fff}
20
+ .bf-btn--primary{background:var(--accent);border-color:var(--accent);color:var(--on-accent)}
21
+ .bf-btn--primary:hover{filter:brightness(1.06);color:var(--on-accent)}
22
22
  .bf-btn--ghost{border-color:transparent;background:none;padding:0;font:inherit;color:inherit}
23
23
  .bf-btn--ghost:hover{color:var(--accent)}
24
24
  .bf-btn--icon{min-width:36px;text-align:center;line-height:1}
25
+ .theme-glyph{font-size:var(--fs-md)}
25
26
  .bf-btn:disabled{opacity:.5;cursor:default}
26
- .bf-theme-toggle{min-width:36px;text-align:center;line-height:1}
27
- .bf-theme-toggle .theme-glyph{font-size:var(--fs-md)}
28
27
  .bf-chip{display:inline-flex;align-items:center;gap:5px;font-size:var(--fs-xs);color:var(--muted);white-space:nowrap}
29
28
  .bf-chip--accent{color:var(--accent);border:1px solid currentColor;border-radius:var(--radius-pill);padding:2px 10px}
30
29
  .bf-chip--success{color:var(--success)}
@@ -51,8 +50,8 @@ export const WEB_COMPONENTS_CSS = `.bf-brand{display:inline-flex;align-items:cen
51
50
  .bf-empty__title{font-size:var(--fs-base);margin:0 0 6px}
52
51
  .bf-empty__sub{color:var(--muted);font-size:var(--fs-sm);margin:0 0 var(--space-4)}
53
52
  .bf-empty__code{font:var(--fw-bold) 64px/1 var(--font-mono);color:var(--danger)}
54
- .bf-dialog{position:fixed;inset:0;z-index:5;display:flex;align-items:flex-start;justify-content:center;padding:10vh var(--space-4) var(--space-4);background:rgba(0,0,0,.45)}
55
- .bf-dialog__panel{width:min(640px,100%);max-height:70vh;display:flex;flex-direction:column;overflow:hidden;background:var(--surface);border:1px solid var(--border-strong);border-radius:var(--radius-lg);box-shadow:0 8px 24px rgba(0,0,0,.35)}
53
+ .bf-dialog{position:fixed;inset:0;z-index:5;display:flex;align-items:flex-start;justify-content:center;padding:10vh var(--space-4) var(--space-4);background:var(--scrim)}
54
+ .bf-dialog__panel{width:min(640px,100%);max-height:70vh;display:flex;flex-direction:column;overflow:hidden;background:var(--surface);border:1px solid var(--border-strong);border-radius:var(--radius-lg);box-shadow:var(--shadow)}
56
55
  .bf-dialog__status{margin:0;padding:var(--space-2) var(--space-4);border-top:1px solid var(--border);color:var(--muted);font-size:var(--fs-xs)}
57
56
  .bf-dialog__input{appearance:none;border:0;border-bottom:1px solid var(--border);background:transparent;color:var(--text);font:var(--fs-md) var(--font-mono);padding:var(--space-3) var(--space-4);outline:none}
58
57
  .bf-dialog__list{list-style:none;margin:0;padding:var(--space-1) 0;overflow:auto;min-height:0}
@@ -61,6 +60,11 @@ export const WEB_COMPONENTS_CSS = `.bf-brand{display:inline-flex;align-items:cen
61
60
  .bf-dialog mark{background:transparent;color:var(--warn);font-weight:var(--fw-semibold)}
62
61
  .bf-dialog__head{display:flex;align-items:center;gap:var(--space-3);padding:var(--space-3) var(--space-4);border-bottom:1px solid var(--border)}
63
62
  .bf-dialog__head h2{margin:0;font-size:var(--fs-lg)}
63
+ .bf-dialog__body{padding:var(--space-3) var(--space-4);font-size:var(--fs-sm);line-height:var(--lh-relaxed);overflow:auto;min-height:0}
64
+ .bf-dialog__body p{margin:0 0 var(--space-2)}
65
+ .bf-dialog__body p:last-child{margin:0}
66
+ .bf-dialog__hint{color:var(--muted)}
67
+ .bf-dialog__actions{display:flex;justify-content:flex-end;gap:var(--space-2);padding:var(--space-3) var(--space-4);border-top:1px solid var(--border)}
64
68
  .bf-keys{display:grid;grid-template-columns:auto 1fr;gap:8px var(--space-4);margin:0;padding:var(--space-4)}
65
69
  .bf-keys dt{white-space:nowrap}
66
70
  .bf-keys dd{margin:0;color:var(--muted);font-size:var(--fs-sm)}
package/lib/web/layout.ts CHANGED
@@ -27,6 +27,7 @@
27
27
  export const SIDEBAR_COLLAPSE_MAX = 860;
28
28
 
29
29
  export const WEB_LAYOUT_CSS = `.bf-app{display:flex;flex-direction:column;height:100vh;min-height:0;overflow:hidden;background:var(--canvas);color:var(--text);font:var(--fs-base)/var(--lh-normal) var(--font-ui)}
30
+ .bf-app{height:100dvh}
30
31
  .bf-app__header{position:sticky;top:0;z-index:2;flex:0 0 auto;background:var(--surface);border-bottom:1px solid var(--border)}
31
32
  .bf-app__body{flex:1 1 auto;min-height:0;display:grid;grid-template-columns:260px minmax(0,1fr)}
32
33
  .bf-app__sidebar{display:flex;flex-direction:column;min-width:0;min-height:0;overflow-x:hidden;overflow-y:auto;background:var(--surface);border-right:1px solid var(--border)}
@@ -34,6 +35,7 @@ export const WEB_LAYOUT_CSS = `.bf-app{display:flex;flex-direction:column;height
34
35
  .bf-app__footer{flex:0 0 auto;border-top:1px solid var(--border);color:var(--muted);font-size:var(--fs-sm);padding:var(--space-3) var(--space-6)}
35
36
  @media (max-width:${SIDEBAR_COLLAPSE_MAX}px){
36
37
  .bf-app{height:auto;min-height:100vh;overflow:visible}
38
+ .bf-app{min-height:100dvh}
37
39
  .bf-app__body{flex:none;grid-template-columns:1fr}
38
40
  .bf-app__sidebar{display:block;position:static;max-height:none;overflow:visible;border-right:0;border-bottom:1px solid var(--border)}
39
41
  .bf-app__main{overflow:visible}
package/lib/web/markup.ts CHANGED
@@ -7,6 +7,7 @@
7
7
  */
8
8
 
9
9
  import { escapeHtml } from "../escape.ts";
10
+ import { THEME_JS_PATH } from "./runtime.ts";
10
11
 
11
12
  /** Options for {@link docHead}. */
12
13
  export interface DocHeadOptions {
@@ -27,7 +28,7 @@ export function docHead(options: DocHeadOptions): string {
27
28
  <meta name="color-scheme" content="dark light">
28
29
  <title>${escapeHtml(options.title)}</title>
29
30
  <style>${options.css}</style>
30
- <script src="/theme.js"></script>`;
31
+ <script src="${THEME_JS_PATH}"></script>`;
31
32
  }
32
33
 
33
34
  /**
@@ -10,10 +10,16 @@
10
10
  /** localStorage key for the browser's explicit light/dark choice, shared by both surfaces. */
11
11
  export const WEB_THEME_STORAGE_KEY = "pi-web-theme";
12
12
 
13
+ /**
14
+ * Where both routers serve {@link THEME_JS}. `docHead` emits the same path, so
15
+ * the script tag and the route cannot drift apart.
16
+ */
17
+ export const THEME_JS_PATH = "/theme.js";
18
+
13
19
  /**
14
20
  * A tiny head script that applies the saved theme before the body is painted,
15
- * so an explicit choice does not flash the system theme on load. Served as
16
- * `/theme.js` and shared by both pages.
21
+ * so an explicit choice does not flash the system theme on load. Served at
22
+ * {@link THEME_JS_PATH} and shared by both pages.
17
23
  */
18
24
  export const THEME_JS = `(function(){try{var t=localStorage.getItem("${WEB_THEME_STORAGE_KEY}");if(t==="light"||t==="dark")document.documentElement.setAttribute("data-theme",t);}catch(e){}})();`;
19
25
 
@@ -67,52 +73,124 @@ export const THEME_TOGGLE_JS = String.raw`(function () {
67
73
  }
68
74
  })();`;
69
75
 
76
+ /** Options for {@link dialogScript}. */
77
+ export interface DialogOptions {
78
+ /** Id of the dialog root: the element carrying `.bf-dialog` and `hidden`. */
79
+ dialogId: string;
80
+ /** Id of the dialog's own close button, when the markup has one. */
81
+ closeId?: string;
82
+ /** Id of the page's trigger button, when the dialog opens from one. */
83
+ triggerId?: string;
84
+ /** Property to expose on `window`, so the page can open and close it. */
85
+ globalName?: string;
86
+ }
87
+
88
+ /** Everything inside a dialog that can take focus, in DOM order. */
89
+ const FOCUSABLE =
90
+ 'a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])';
91
+
70
92
  /**
71
- * The `?` keyboard sheet's wiring, shared by both clients.
72
- *
73
- * It owns the sheet's pointer behavior — the page's trigger button, the sheet's
74
- * own close button, and a click on the backdrop — and nothing else: each page
75
- * keeps its own keyboard policy in its keydown handler, because the two differ
76
- * (the review peels `Esc` through a composer first), and calls in through
77
- * `window.bfKeys`.
93
+ * The wiring every modal in the kit shares: open and close, the backdrop, the
94
+ * `Tab` trap, `Esc`, and focus returning to whatever opened it.
78
95
  *
79
- * `triggerId` is the page's own shortcut button, since browse opens the sheet
80
- * from `#keys-open` and review from `#shortcuts`. The sheet's ids are the fixed
81
- * contract in `keysSheet` (`lib/web/markup.ts`).
96
+ * A dialog is modal before this script runs too — `aria-modal` and `hidden` are
97
+ * in the markup — so a page without JavaScript still shows it closed rather
98
+ * than half-open.
82
99
  *
83
- * Value-only: the open state is the dialog's own `hidden` attribute, so this
84
- * module holds nothing.
100
+ * Value-only: the open state is the dialog's own `hidden` attribute, and the
101
+ * element that opened it lives in this script's closure.
85
102
  */
86
- export function keysDialogScript(triggerId: string): string {
103
+ export function dialogScript(options: DialogOptions): string {
104
+ const trigger = options.triggerId ? `document.getElementById("${options.triggerId}")` : "null";
105
+ const closer = options.closeId ? `document.getElementById("${options.closeId}")` : "null";
106
+ const expose = options.globalName
107
+ ? `window.${options.globalName} = {
108
+ open: function () { toggle(true); },
109
+ toggle: toggle,
110
+ close: function () { toggle(false); },
111
+ isOpen: isOpen
112
+ };`
113
+ : "";
87
114
  return `(function () {
88
115
  "use strict";
89
- var dialog = document.getElementById("keys-dialog");
116
+ var dialog = document.getElementById("${options.dialogId}");
90
117
  if (!dialog) return;
118
+ var opener = null;
91
119
 
92
120
  function isOpen() {
93
121
  return !dialog.hidden;
94
122
  }
123
+ // A button inside a closed dialog has no boxes, so the rects are the test that
124
+ // survives a hidden attribute, a collapsed panel, and a stylesheet swap alike.
125
+ function focusables() {
126
+ return Array.prototype.filter.call(dialog.querySelectorAll('${FOCUSABLE}'), function (node) {
127
+ return node.getClientRects && node.getClientRects().length > 0;
128
+ });
129
+ }
95
130
  function toggle(force) {
96
131
  var open = force === undefined ? dialog.hidden : force;
132
+ if (open === isOpen()) return;
97
133
  dialog.hidden = !open;
98
134
  if (open) {
99
- var close = document.getElementById("keys-close");
100
- if (close && close.focus) close.focus();
135
+ opener = document.activeElement;
136
+ var first = focusables()[0];
137
+ if (first) first.focus();
138
+ } else if (opener && opener.focus) {
139
+ opener.focus();
140
+ opener = null;
101
141
  }
102
142
  }
103
143
 
104
- var trigger = document.getElementById("${triggerId}");
144
+ var trigger = ${trigger};
105
145
  if (trigger) trigger.addEventListener("click", function () { toggle(); });
106
- var closer = document.getElementById("keys-close");
146
+ var closer = ${closer};
107
147
  if (closer) closer.addEventListener("click", function () { toggle(false); });
108
148
  dialog.addEventListener("click", function (event) {
109
149
  if (event.target === dialog) toggle(false);
110
150
  });
111
151
 
112
- window.bfKeys = {
113
- toggle: toggle,
114
- close: function () { toggle(false); },
115
- isOpen: isOpen
116
- };
152
+ // Tab cycles inside the dialog: leaving it would put focus behind the scrim,
153
+ // where the page looks inert but is not.
154
+ dialog.addEventListener("keydown", function (event) {
155
+ if (event.key !== "Tab") return;
156
+ var nodes = focusables();
157
+ if (nodes.length === 0) { event.preventDefault(); return; }
158
+ var first = nodes[0];
159
+ var last = nodes[nodes.length - 1];
160
+ if (event.shiftKey && document.activeElement === first) {
161
+ event.preventDefault();
162
+ last.focus();
163
+ } else if (!event.shiftKey && document.activeElement === last) {
164
+ event.preventDefault();
165
+ first.focus();
166
+ }
167
+ });
168
+
169
+ // Capture, so Esc closes the dialog before a page handler peels another layer.
170
+ document.addEventListener("keydown", function (event) {
171
+ if (event.key !== "Escape" || !isOpen()) return;
172
+ toggle(false);
173
+ event.preventDefault();
174
+ event.stopPropagation();
175
+ }, true);
176
+
177
+ ${expose}
117
178
  })();`;
118
179
  }
180
+
181
+ /**
182
+ * The `?` keyboard sheet's wiring, shared by both clients.
183
+ *
184
+ * `triggerId` is the page's own shortcut button, since browse opens the sheet
185
+ * from `#keys-open` and review from `#shortcuts`. The sheet's ids are the fixed
186
+ * contract in `keysSheet` (`lib/web/markup.ts`), and the page's keydown handler
187
+ * calls in through `window.bfKeys`.
188
+ */
189
+ export function keysDialogScript(triggerId: string): string {
190
+ return dialogScript({
191
+ dialogId: "keys-dialog",
192
+ closeId: "keys-close",
193
+ triggerId,
194
+ globalName: "bfKeys",
195
+ });
196
+ }
package/lib/web/tokens.ts CHANGED
@@ -21,12 +21,11 @@ interface ThemeJson {
21
21
  const DARK = darkTheme as unknown as ThemeJson;
22
22
  const LIGHT = lightTheme as unknown as ThemeJson;
23
23
 
24
- /** Web custom property -> Bytefly theme var. `--accent-soft` is derived. */
24
+ /** Web custom property -> Bytefly theme var. `--accent-soft`, `--on-accent`, `--scrim`,
25
+ * and `--shadow` are derived, so no appearance-only colour enters the kit. */
25
26
  const VAR_TOKENS: ReadonlyArray<readonly [string, string]> = [
26
27
  ["--canvas", "canvas"],
27
28
  ["--surface", "canvasSubtle"],
28
- ["--surface-raised", "selectionBg"],
29
- ["--selected", "selectionBg"],
30
29
  ["--border", "borderMuted"],
31
30
  ["--border-strong", "border"],
32
31
  ["--text", "text"],
@@ -90,19 +89,34 @@ function rgba(hex: string, alpha: number): string {
90
89
  return `rgba(${r},${g},${b},${alpha})`;
91
90
  }
92
91
 
92
+ /**
93
+ * Overlays are not palette entries: a scrim dims what is behind it and a shadow
94
+ * lifts a panel off it, so each appearance needs its own alpha rather than the
95
+ * theme's colours.
96
+ */
97
+ const OVERLAYS: Record<"dark" | "light", { scrim: string; shadow: string }> = {
98
+ dark: { scrim: "rgba(0,0,0,.5)", shadow: "0 8px 24px rgba(0,0,0,.4)" },
99
+ light: { scrim: "rgba(15,23,42,.3)", shadow: "0 8px 24px rgba(15,23,42,.14)" },
100
+ };
101
+
93
102
  /** The full custom-property set for one appearance. */
94
- function paletteOf(theme: ThemeJson, accentAlpha: number): Record<string, string> {
103
+ function paletteOf(theme: ThemeJson, appearance: "dark" | "light", accentAlpha: number): Record<string, string> {
95
104
  const palette: Record<string, string> = {};
96
105
  for (const [token, name] of VAR_TOKENS) palette[token] = theme.vars[name];
97
106
  palette["--accent-soft"] = rgba(palette["--accent"] ?? "#000000", accentAlpha);
107
+ // A label on the accent fill is the canvas colour: a light blue button takes dark
108
+ // text, and the same derivation gives white on the light theme's darker blue.
109
+ palette["--on-accent"] = palette["--canvas"];
110
+ palette["--scrim"] = OVERLAYS[appearance].scrim;
111
+ palette["--shadow"] = OVERLAYS[appearance].shadow;
98
112
  for (const [token, name] of SYNTAX_TOKENS) palette[token] = resolve(theme, theme.colors[name] ?? "");
99
113
  return palette;
100
114
  }
101
115
 
102
116
  /** Every token value per appearance, for tests and for building the CSS. */
103
117
  export const WEB_THEME_TOKENS: { dark: Record<string, string>; light: Record<string, string> } = {
104
- dark: paletteOf(DARK, 0.15),
105
- light: paletteOf(LIGHT, 0.1),
118
+ dark: paletteOf(DARK, "dark", 0.15),
119
+ light: paletteOf(LIGHT, "light", 0.1),
106
120
  };
107
121
 
108
122
  /** `a{color:var(--tok-keyword)}…` for each syntax token. */
@@ -125,4 +139,6 @@ export const WEB_THEME_CSS = `:root{${declarations(WEB_THEME_TOKENS.dark)};${SCA
125
139
  :root[data-theme="light"]{${declarations(WEB_THEME_TOKENS.light)};color-scheme:light}
126
140
  :root[data-theme="dark"]{color-scheme:dark}
127
141
  @media (prefers-color-scheme:light){:root:not([data-theme]){${declarations(WEB_THEME_TOKENS.light)};color-scheme:light}}
128
- ${SYNTAX_RULES}`;
142
+ ${SYNTAX_RULES}
143
+ /* Paper is white: print the light palette whatever the screen is showing. */
144
+ @media print{:root{${declarations(WEB_THEME_TOKENS.light)};color-scheme:light}}`;