@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.
- package/README.md +29 -15
- package/README.zh-CN.md +27 -15
- package/extensions/bytefly/README.md +195 -115
- package/extensions/bytefly/footer/collect/entries.ts +1 -6
- package/extensions/bytefly/footer/collect/snapshot.ts +23 -9
- package/extensions/bytefly/footer/price/price.ts +12 -8
- package/extensions/bytefly/footer/register.ts +13 -1
- package/extensions/bytefly/footer/types.ts +11 -0
- package/extensions/bytefly/footer/unpriced.ts +36 -0
- package/extensions/bytefly/header/component.ts +15 -29
- package/extensions/bytefly/header/identity.ts +71 -0
- package/extensions/bytefly/header/register.ts +9 -3
- package/extensions/bytefly/index.ts +13 -5
- package/extensions/bytefly/message/format.ts +20 -13
- package/extensions/bytefly/message/register.ts +8 -7
- package/extensions/bytefly/notify/types.ts +1 -1
- package/extensions/bytefly/panel/commands.ts +46 -25
- package/extensions/bytefly/panel/format.ts +49 -14
- package/extensions/bytefly/panel/join.ts +35 -9
- package/extensions/bytefly/panel/settings.ts +29 -39
- package/extensions/bytefly/panel/skills.ts +51 -0
- package/extensions/bytefly/panel/tui/extensions-view.ts +7 -61
- package/extensions/bytefly/panel/tui/frame.ts +9 -1
- package/extensions/bytefly/panel/tui/list-view.ts +72 -0
- package/extensions/bytefly/panel/tui/nav.ts +71 -3
- package/extensions/bytefly/panel/tui/settings-section.ts +250 -0
- package/extensions/bytefly/panel/tui/skills-view.ts +50 -0
- package/extensions/bytefly/panel/tui.ts +223 -331
- package/extensions/bytefly/panel/types.ts +26 -8
- package/extensions/bytefly/picker/discovery.ts +32 -27
- package/extensions/bytefly/picker/resources.ts +16 -8
- package/extensions/bytefly/picker/runtime.ts +62 -34
- package/extensions/bytefly/picker/toggle.ts +16 -10
- package/extensions/bytefly/picker/types.ts +31 -13
- package/extensions/bytefly/update/format.ts +66 -8
- package/extensions/bytefly/update/patch.ts +66 -0
- package/extensions/bytefly/update/register.ts +4 -11
- package/extensions/bytefly/update/release.ts +65 -0
- package/extensions/bytefly/update/state.ts +14 -0
- package/extensions/bytefly/update/suppress.ts +47 -29
- package/extensions/bytefly/update/types.ts +15 -5
- package/extensions/check/tui.ts +2 -0
- package/extensions/context/README.md +21 -11
- package/extensions/context/format.ts +10 -8
- package/extensions/context/report.ts +21 -8
- package/extensions/context/tui.ts +26 -10
- package/extensions/context/types.ts +5 -3
- package/extensions/file-browse/README.md +89 -22
- package/extensions/file-browse/chrome.ts +83 -0
- package/extensions/file-browse/client.ts +74 -459
- package/extensions/file-browse/config.ts +12 -3
- package/extensions/file-browse/diff-html.ts +94 -0
- package/extensions/file-browse/html.ts +26 -152
- package/extensions/file-browse/live-client.ts +47 -1
- package/extensions/file-browse/live.ts +35 -14
- package/extensions/file-browse/palette.ts +224 -0
- package/extensions/file-browse/rev.ts +42 -0
- package/extensions/file-browse/router.ts +27 -6
- package/extensions/file-browse/server.ts +7 -0
- package/extensions/file-browse/styles.ts +10 -3
- package/extensions/file-browse/tree.ts +381 -0
- package/extensions/file-read/README.md +4 -1
- package/extensions/file-read/extract/odt.ts +7 -5
- package/extensions/goal/plan-route.ts +8 -2
- package/extensions/goal/tools.ts +6 -11
- package/extensions/job/README.md +7 -5
- package/extensions/job/index.ts +35 -52
- package/extensions/job/tools.ts +51 -77
- package/extensions/memory/README.md +3 -3
- package/extensions/memory/tools.ts +4 -6
- package/extensions/memory/tui.ts +5 -1
- package/extensions/plan/README.md +333 -84
- package/extensions/plan/commands.ts +73 -7
- package/extensions/plan/feedback.ts +300 -63
- package/extensions/plan/index.ts +1 -1
- package/extensions/plan/plan-index.ts +4 -2
- package/extensions/plan/plan-text.ts +18 -1
- package/extensions/plan/plans.ts +184 -58
- package/extensions/plan/review-prompt.ts +93 -0
- package/extensions/plan/review.ts +95 -95
- package/extensions/plan/runtime.ts +4 -2
- package/extensions/plan/split.ts +87 -0
- package/extensions/plan/steps.ts +123 -107
- package/extensions/plan/tools/cancel.ts +79 -0
- package/extensions/plan/tools/enter.ts +17 -12
- package/extensions/plan/tools/exit.ts +148 -59
- package/extensions/plan/tools/steps.ts +7 -19
- package/extensions/plan/tools/write.ts +29 -31
- package/extensions/plan/tools.ts +6 -3
- package/extensions/plan/tui.ts +60 -15
- package/extensions/plan/types.ts +45 -3
- package/extensions/plan/web-chrome.ts +102 -0
- package/extensions/plan/web-client.ts +177 -446
- package/extensions/plan/web-collab.ts +207 -0
- package/extensions/plan/web-comments.ts +269 -0
- package/extensions/plan/web-markdown.ts +222 -0
- package/extensions/plan/web-page.ts +131 -19
- package/extensions/plan/web-router.ts +148 -32
- package/extensions/plan/web-share-session.ts +197 -0
- package/extensions/plan/web-share.ts +169 -0
- package/extensions/plan/web-streams.ts +124 -0
- package/extensions/plan/web-tokens.ts +63 -0
- package/extensions/plan/web.ts +193 -45
- package/extensions/question/README.md +3 -0
- package/extensions/question/dialogs.ts +91 -1
- package/extensions/question/tools.ts +2 -1
- package/extensions/rewind/checklist.ts +4 -0
- package/extensions/rewind/review-tui.ts +3 -1
- package/extensions/rewind/rewind-tui.ts +4 -0
- package/extensions/session-bus/README.md +247 -0
- package/extensions/session-bus/commands.ts +165 -0
- package/extensions/session-bus/format.ts +124 -0
- package/extensions/session-bus/index.ts +69 -0
- package/extensions/session-bus/notice.ts +58 -0
- package/extensions/session-bus/protocol.ts +134 -0
- package/extensions/session-bus/result.ts +243 -0
- package/extensions/session-bus/rows.ts +169 -0
- package/extensions/session-bus/runtime.ts +575 -0
- package/extensions/session-bus/store.ts +329 -0
- package/extensions/session-bus/team.ts +102 -0
- package/extensions/session-bus/tools.ts +227 -0
- package/extensions/session-bus/tui.ts +394 -0
- package/extensions/session-bus/types.ts +98 -0
- package/extensions/subagent/README.md +26 -13
- package/extensions/subagent/format.ts +72 -25
- package/extensions/subagent/index.ts +21 -47
- package/extensions/subagent/orchestrate.ts +18 -41
- package/extensions/subagent/render.ts +29 -42
- package/extensions/subagent/stream.ts +24 -18
- package/extensions/subagent/types.ts +7 -0
- package/extensions/todo/tools.ts +5 -6
- package/extensions/todo/tui.ts +5 -1
- package/extensions/watch/README.md +9 -5
- package/extensions/watch/index.ts +25 -44
- package/extensions/watch/tools.ts +31 -59
- package/extensions/wechat/README.md +45 -36
- package/extensions/wechat/bridge.ts +46 -48
- package/extensions/wechat/client.ts +1 -4
- package/extensions/wechat/commands.ts +3 -3
- package/extensions/wechat/deliver.ts +57 -37
- package/extensions/wechat/interaction.ts +54 -18
- package/extensions/wechat/login-screen.ts +2 -3
- package/extensions/wechat/notices.ts +9 -7
- package/extensions/wechat/peer-worker.ts +19 -34
- package/extensions/wechat/presence.ts +3 -4
- package/extensions/wechat/prompt.ts +53 -23
- package/extensions/wechat/reply-pipeline.ts +12 -4
- package/extensions/wechat/slash.ts +14 -0
- package/extensions/wechat/status-screen.ts +2 -2
- package/extensions/wechat/tools.ts +3 -11
- package/extensions/worktree/README.md +48 -12
- package/extensions/worktree/commands.ts +109 -65
- package/extensions/worktree/enter.ts +1 -1
- package/extensions/worktree/exit.ts +11 -1
- package/extensions/worktree/git.ts +5 -0
- package/extensions/worktree/lifecycle.ts +9 -1
- package/extensions/worktree/registry.ts +11 -5
- package/extensions/worktree/screen.ts +263 -0
- package/extensions/worktree/status.ts +84 -40
- package/lib/README.md +12 -7
- package/lib/claims/overlap.ts +43 -0
- package/lib/config.ts +12 -0
- package/lib/format.ts +6 -2
- package/lib/http/router.ts +40 -13
- package/lib/http/server.ts +27 -13
- package/lib/interaction.ts +28 -1
- package/lib/notice.ts +29 -2
- package/lib/plan-entry.ts +7 -2
- package/lib/plans/sections.ts +172 -0
- package/lib/plans/setting.ts +52 -0
- package/lib/plans/split.ts +215 -0
- package/lib/plans/store.ts +19 -0
- package/lib/plans/team.ts +151 -0
- package/lib/qr.ts +64 -0
- package/lib/repo-key.ts +74 -0
- package/lib/report.ts +117 -25
- package/lib/requirements.ts +1 -2
- package/lib/tool-result.ts +54 -4
- package/lib/tui/README.md +30 -8
- package/lib/tui/chrome.ts +27 -6
- package/lib/tui/dock-screen.ts +91 -19
- package/lib/tui/gauge.ts +9 -5
- package/lib/tui/line-pane.ts +15 -4
- package/lib/tui/tool.ts +23 -4
- package/lib/usage.ts +26 -0
- package/lib/web/README.md +131 -0
- package/lib/web/base.ts +19 -2
- package/lib/web/components.ts +10 -6
- package/lib/web/layout.ts +2 -0
- package/lib/web/markup.ts +2 -1
- package/lib/web/runtime.ts +103 -25
- package/lib/web/tokens.ts +23 -7
- package/package.json +5 -2
- package/skills/changelog/SKILL.md +57 -45
- package/skills/init/SKILL.md +80 -45
- package/skills/init/references/agents-md-quality.md +26 -2
- package/skills/init/references/interview-questions.md +36 -14
- package/skills/next/SKILL.md +209 -0
- package/skills/parallel-dev/SKILL.md +150 -0
- 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
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
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
|
|
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
|
|
package/lib/tui/line-pane.ts
CHANGED
|
@@ -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.
|
|
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
|
-
/**
|
|
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,
|
|
213
|
-
* "earlier" note. The caller owns the row
|
|
214
|
-
* grammar — a peer-session tag, a status
|
|
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,
|
|
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
|
+
}`;
|
package/lib/web/components.ts
CHANGED
|
@@ -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
|
|
21
|
-
.bf-btn--primary:hover{filter:brightness(1.06);color
|
|
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:
|
|
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:
|
|
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="
|
|
31
|
+
<script src="${THEME_JS_PATH}"></script>`;
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
/**
|
package/lib/web/runtime.ts
CHANGED
|
@@ -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
|
|
16
|
-
*
|
|
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
|
|
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
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
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,
|
|
84
|
-
*
|
|
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
|
|
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("
|
|
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
|
-
|
|
100
|
-
|
|
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 =
|
|
144
|
+
var trigger = ${trigger};
|
|
105
145
|
if (trigger) trigger.addEventListener("click", function () { toggle(); });
|
|
106
|
-
var closer =
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
|
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}}`;
|