claude-code-kanban 7.9.0 → 7.11.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/hub-app.json +1 -1
- package/package.json +1 -1
- package/plugin/plugins/claude-code-kanban/.claude-plugin/plugin.json +1 -1
- package/plugin/plugins/claude-code-kanban/skills/visualize/SKILL.md +57 -0
- package/plugin/plugins/claude-code-kanban/skills/visualize/template.html +215 -0
- package/public/app.js +116 -35
package/hub-app.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"icon": "columns",
|
|
6
6
|
"run": { "entry": "server.js", "defaultPort": 3541 },
|
|
7
7
|
"publishes": ["session.changed"],
|
|
8
|
-
"keys": { "keeps": ["ctrl+alt+n", "ctrl+alt+r", "ctrl+alt+s", "ctrl+shift+k", "ctrl+shift+p", "ctrl+shift+z"] },
|
|
8
|
+
"keys": { "keeps": ["ctrl+alt+n", "ctrl+alt+r", "ctrl+alt+s", "ctrl+shift+d", "ctrl+shift+k", "ctrl+shift+p", "ctrl+shift+z"] },
|
|
9
9
|
"plugin": {
|
|
10
10
|
"marketplace": "plugin",
|
|
11
11
|
"name": "claude-code-kanban",
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-code-kanban",
|
|
3
|
-
"version": "3.7.
|
|
3
|
+
"version": "3.7.5",
|
|
4
4
|
"description": "claude-code-kanban dashboard integration: agent activity tracking, context and cost tracking, board moves and reviews delivered to the session, and skills to drive the board from a session"
|
|
5
5
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: visualize
|
|
3
|
+
description: Present a topic as one static HTML page — findings, options and a pick, a system overview. Use when the user asks to visualize something, or when an answer outgrows chat (options plus a pick, a diagram plus a table).
|
|
4
|
+
argument-hint: '[what to present]'
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Visualize
|
|
8
|
+
|
|
9
|
+
Present this: $ARGUMENTS. With no argument, present the subject of this conversation.
|
|
10
|
+
|
|
11
|
+
The page is a document to read: every mark is in the HTML, and it works offline with scripts off. Two leading words run through it: the **pick** is the option the page recommends (on a page with no options, its main path), and an **exhibit** is the one table, figure or block that proves a section's claim.
|
|
12
|
+
|
|
13
|
+
## Steps
|
|
14
|
+
|
|
15
|
+
1. **Copy.** `cp` [template.html](template.html) from this skill's base directory to the output path. Edit the `<title>` and inside `<main>`; the `<style>` block stays byte for byte, and your own CSS goes in a second `<style>` after it.
|
|
16
|
+
2. **Write the story.** The `<h1>`, the lead and the `<h2>`s alone carry the whole point:
|
|
17
|
+
1. `.kicker`, then `<title>` and `<h1>`: a name of 2 to 5 words. `.sub`: the question, in one line.
|
|
18
|
+
2. `.lead`: the answer in 1 or 2 sentences, the pick in `<b>`.
|
|
19
|
+
3. Sections. Each `<h2>` is a claim that can be false ("A breaker alone trips again on release", not "Breaker"), with one exhibit and at most 2 short paragraphs under it.
|
|
20
|
+
4. Close with what to do next and `.sources`.
|
|
21
|
+
3. **Invent each exhibit.** Ask what form makes this claim obvious at a glance, then build that, the template's blocks or something new: a slope chart, an annotated timeline, small multiples, a cutaway, a ranked strip, a map. The Blocks table is a palette of ready parts, not a menu to choose from. Before you draw an SVG, read the matching guide in the `show` skill — [chart](../show/references/chart.md), [diagram](../show/references/diagram.md) and its type file, [mockup](../show/references/mockup.md) — for shapes, arrows and labels; the Fit rule below replaces their card sizes, and label masks fill with `--color-bg-subtle`, the figure's background.
|
|
22
|
+
4. **Check.** Render the page headless (Playwright) in light and in dark, and look at each screenshot. Done when no text is clipped, no arrow crosses a label, `scrollWidth` equals `clientWidth`, and, when the topic comes from a source such as a file or a conversation, every section and number of it is named where it landed on the page or reported to the user as left out.
|
|
23
|
+
5. **Show.** When `$CCK_URL` is set, the session runs in the cck board: run `claude-code-kanban pane add <page> --title '<h1>'` and tell the user the page waits in that tab. Else give the user the page path.
|
|
24
|
+
|
|
25
|
+
## Principles
|
|
26
|
+
|
|
27
|
+
Every exhibit, ready or new, keeps these:
|
|
28
|
+
|
|
29
|
+
- **Accent marks the pick**, one or two marks per exhibit: its tile, card, row, node, or the one bar or point in a chart. The rest stays muted. Status colors mean status.
|
|
30
|
+
- **Chart data takes series colors**: `--color-series-1` for the main series, `--color-text-muted` or `--color-series-3` for a second, up to 8 series; past that, fold the tail into "Other" or split into small multiples. The accent can sit close to `--color-danger`, so a whole data series in it reads as an alarm.
|
|
31
|
+
- **Tokens color everything**: `var(--color-*)`, tints by `color-mix(in srgb, var(--color-accent) 12%, var(--color-bg))`. Then the page follows light, dark and a host's theme with no script.
|
|
32
|
+
- **Fit**: an SVG has a `viewBox` up to 960 wide, no `width` attribute, text of 12 units or more. A flow runs left to right when it has 6 steps or fewer.
|
|
33
|
+
- **Remove test**: each node, arrow, label and decoration stays only if removing it loses meaning. A diagram past 9 nodes or 12 arrows splits into an overview and a detail. Each SVG has `role="img"` and `aria-labelledby` pointing at its `<title>`, with a per-figure id prefix.
|
|
34
|
+
- **Real**: real names, numbers and `file:line`, estimates and invented data marked, each claim cited. Repeated marks, such as small multiples, are generated while you write the page.
|
|
35
|
+
|
|
36
|
+
## Blocks
|
|
37
|
+
|
|
38
|
+
Ready parts in the template's CSS; its body has one of each.
|
|
39
|
+
|
|
40
|
+
| Block | Markup | Good for |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| Kicker | `p.kicker` | Ticket or kind of page |
|
|
43
|
+
| Lead | `p.lead` | The answer; `<b>` takes the accent |
|
|
44
|
+
| Lineup | `.lineup > div > b + span + small` | Every option as a tile: index, name, 2-word verdict |
|
|
45
|
+
| Stat tiles | `.kpis > .kpi > span + b + small` | 1 to 4 headline numbers |
|
|
46
|
+
| Section | `section > p.eyebrow + h2` | CSS numbers the eyebrow |
|
|
47
|
+
| Cards | `.cards > .card > h3 + p.verdict + p + pre + ul` | Trade-offs; `li.pro`, `li.con` |
|
|
48
|
+
| Table | `.table > table`; `td.num`, `td.c`, `tr[data-tone]` | A matrix |
|
|
49
|
+
| Heatmap | `td.yes`, `td.part`, `td.no` | Tinted cells: `yes` the good outcome, `no` the bad, whatever word the cell holds |
|
|
50
|
+
| Figure | `figure > svg + figcaption` | A diagram or a chart |
|
|
51
|
+
| Points | `ul.points > li > b + text` | Findings, each led by its claim |
|
|
52
|
+
| Callout | `div.callout` | A warning |
|
|
53
|
+
| Steps | `ol.steps` | What to do next |
|
|
54
|
+
| Tag, dot, legend | `span.tag`, `span.dot`, `.legend > span > i` | Labels and keys |
|
|
55
|
+
| Sources | `div.sources` | Where each claim comes from |
|
|
56
|
+
|
|
57
|
+
`data-tone` takes `accent`, `info`, `success`, `warning`, `danger` or `muted` on any block, legend keys included.
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Page Title</title>
|
|
7
|
+
<style>
|
|
8
|
+
/* Defaults in :where() have zero specificity, so a host that sets the same tokens on :root wins. */
|
|
9
|
+
:where(:root) {
|
|
10
|
+
color-scheme: light dark;
|
|
11
|
+
--color-bg: #f7f5f2; --color-bg-subtle: #ece9e4;
|
|
12
|
+
--color-text: #16130f; --color-text-muted: #5f5a53; --color-border: #d6d1c9;
|
|
13
|
+
--color-accent: #c4581c;
|
|
14
|
+
--color-info: #2563eb; --color-success: #1a8a5a; --color-warning: #b07d0a; --color-danger: #c0392b;
|
|
15
|
+
--color-series-1: #2a78d6; --color-series-2: #c4511c; --color-series-3: #008a72; --color-series-4: #ad6f00;
|
|
16
|
+
--color-series-5: #c4467a; --color-series-6: #008300; --color-series-7: #4a3aa7; --color-series-8: #d03a3a;
|
|
17
|
+
--color-ramp-1: #5598e7; --color-ramp-2: #2a78d6; --color-ramp-3: #1c5cab; --color-ramp-4: #104281; --color-ramp-5: #0a2b57;
|
|
18
|
+
--font-sans: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
19
|
+
--font-mono: "IBM Plex Mono", ui-monospace, "Cascadia Mono", Consolas, monospace;
|
|
20
|
+
--font-serif: "Playfair Display", Georgia, "Times New Roman", serif;
|
|
21
|
+
}
|
|
22
|
+
@media (prefers-color-scheme: dark) {
|
|
23
|
+
:where(:root) {
|
|
24
|
+
--color-bg: #16181c; --color-bg-subtle: #101114;
|
|
25
|
+
--color-text: #f0f1f3; --color-text-muted: #9a9da5; --color-border: #363840;
|
|
26
|
+
--color-accent: #e86f33;
|
|
27
|
+
--color-info: #60a5fa; --color-success: #3ecf8e; --color-warning: #f0b429; --color-danger: #ef5350;
|
|
28
|
+
--color-series-1: #3987e5; --color-series-2: #d95926; --color-series-3: #199e70; --color-series-4: #c98500;
|
|
29
|
+
--color-series-5: #d55181; --color-series-6: #2a9a2a; --color-series-7: #9085e9; --color-series-8: #e66767;
|
|
30
|
+
--color-ramp-1: #256abf; --color-ramp-2: #3987e5; --color-ramp-3: #6da7ec; --color-ramp-4: #9ec5f4; --color-ramp-5: #cde2fb;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
:root { --surface: color-mix(in srgb, var(--color-text) 4%, var(--color-bg)); }
|
|
34
|
+
:where(.dot, .tag) { --tone: var(--color-text-muted); }
|
|
35
|
+
:where(.callout) { --tone: var(--color-accent); }
|
|
36
|
+
[data-tone="accent"] { --tone: var(--color-accent); }
|
|
37
|
+
[data-tone="info"] { --tone: var(--color-info); }
|
|
38
|
+
[data-tone="success"] { --tone: var(--color-success); }
|
|
39
|
+
[data-tone="warning"] { --tone: var(--color-warning); }
|
|
40
|
+
[data-tone="danger"] { --tone: var(--color-danger); }
|
|
41
|
+
[data-tone="muted"] { --tone: var(--color-text-muted); }
|
|
42
|
+
|
|
43
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
44
|
+
body { margin: 0; background: var(--color-bg); color: var(--color-text); font: 16px/1.65 var(--font-sans); -webkit-text-size-adjust: 100%; }
|
|
45
|
+
main { max-width: 980px; margin: 0 auto; padding: 64px 16px 96px; counter-reset: section; }
|
|
46
|
+
p, li { max-width: 68ch; }
|
|
47
|
+
p { margin: 0 0 14px; }
|
|
48
|
+
ul, ol { margin: 0 0 16px; padding-left: 22px; }
|
|
49
|
+
li { margin: 4px 0; }
|
|
50
|
+
a { color: var(--color-accent); text-underline-offset: 3px; }
|
|
51
|
+
b, strong { font-weight: 600; }
|
|
52
|
+
code { font: .85em var(--font-mono); background: var(--surface); border: 1px solid var(--color-border); border-radius: 5px; padding: 1px 5px; }
|
|
53
|
+
pre { font: 13px/1.6 var(--font-mono); background: var(--color-bg-subtle); border: 1px solid var(--color-border); border-radius: 10px; padding: 12px 14px; margin: 0 0 16px; overflow-x: auto; }
|
|
54
|
+
pre code { font-size: inherit; background: none; border: 0; padding: 0; }
|
|
55
|
+
.muted { color: var(--color-text-muted); }
|
|
56
|
+
|
|
57
|
+
.kicker { font: 500 12px/1.4 var(--font-mono); letter-spacing: .12em; text-transform: uppercase; color: var(--color-accent); margin: 0 0 18px; }
|
|
58
|
+
h1 { font: 400 48px/1.08 var(--font-serif); letter-spacing: -.01em; margin: 0 0 16px; }
|
|
59
|
+
.sub { font-size: 17px; color: var(--color-text-muted); max-width: 46em; margin: 0 0 28px; }
|
|
60
|
+
.lead { font-size: 21px; line-height: 1.5; max-width: 40em; margin: 0 0 32px; }
|
|
61
|
+
.lead b { color: var(--color-accent); }
|
|
62
|
+
|
|
63
|
+
.lineup { display: grid; grid-template-columns: repeat(auto-fit, minmax(108px, 1fr)); gap: 8px; margin: 0 0 40px; }
|
|
64
|
+
.lineup > div { border: 1px solid var(--color-border); border-radius: 12px; padding: 12px 12px 10px; background: var(--surface); }
|
|
65
|
+
.lineup b { display: block; font: 400 28px/1 var(--font-serif); color: var(--color-text-muted); }
|
|
66
|
+
.lineup span { display: block; font-size: 13px; font-weight: 600; line-height: 1.3; margin-top: 8px; }
|
|
67
|
+
.lineup small { display: block; font: 11px/1.35 var(--font-mono); color: var(--color-text-muted); margin-top: 4px; }
|
|
68
|
+
.lineup > [data-tone] { border-color: var(--tone); background: color-mix(in srgb, var(--tone) 12%, var(--color-bg)); box-shadow: 0 10px 32px -14px var(--tone); }
|
|
69
|
+
.lineup > [data-tone] b { color: var(--tone); }
|
|
70
|
+
|
|
71
|
+
section { counter-increment: section; margin-top: 88px; }
|
|
72
|
+
.eyebrow { max-width: none; display: flex; align-items: center; gap: 12px; font: 500 12px/1.4 var(--font-mono); letter-spacing: .12em; text-transform: uppercase; color: var(--color-text-muted); margin: 0 0 14px; }
|
|
73
|
+
.eyebrow::before { content: counter(section, decimal-leading-zero); color: var(--color-accent); }
|
|
74
|
+
.eyebrow::after { content: ""; flex: 1; height: 1px; background: var(--color-border); }
|
|
75
|
+
h2 { font: 400 32px/1.2 var(--font-serif); letter-spacing: -.005em; max-width: 30em; margin: 0 0 22px; }
|
|
76
|
+
h3 { font-size: 17px; line-height: 1.4; font-weight: 600; margin: 36px 0 10px; }
|
|
77
|
+
|
|
78
|
+
.callout { border: 1px solid var(--color-border); border-radius: 12px; background: var(--surface); box-shadow: inset 3px 0 0 var(--tone); padding: 16px 20px; margin: 0 0 20px; }
|
|
79
|
+
.callout p { max-width: none; }
|
|
80
|
+
.callout > :last-child { margin-bottom: 0; }
|
|
81
|
+
|
|
82
|
+
.kpis { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 12px; margin: 0 0 16px; }
|
|
83
|
+
.kpi { border: 1px solid var(--color-border); border-radius: 12px; background: var(--surface); padding: 14px 16px; }
|
|
84
|
+
.kpi > span { display: block; font: 500 11px/1.4 var(--font-mono); letter-spacing: .08em; text-transform: uppercase; color: var(--color-text-muted); }
|
|
85
|
+
.kpi > b { display: block; font: 400 40px/1.1 var(--font-serif); font-variant-numeric: lining-nums tabular-nums; margin: 8px 0 4px; color: var(--tone, var(--color-text)); }
|
|
86
|
+
.kpi > small { display: block; font-size: 13px; color: var(--color-text-muted); }
|
|
87
|
+
|
|
88
|
+
.cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 14px; margin: 0 0 16px; }
|
|
89
|
+
.card { border: 1px solid var(--color-border); border-radius: 14px; padding: 20px 22px; background: var(--surface); }
|
|
90
|
+
.card[data-tone] { border-color: var(--tone); background: color-mix(in srgb, var(--tone) 7%, var(--color-bg)); box-shadow: 0 16px 40px -24px var(--tone); }
|
|
91
|
+
.card h3 { font-size: 14px; margin: 0 0 10px; color: var(--color-text-muted); }
|
|
92
|
+
.card .verdict { font: 400 20px/1.35 var(--font-serif); color: var(--color-text); margin: 0 0 12px; }
|
|
93
|
+
.card > p:not(.verdict), .card ul { font-size: 13px; line-height: 1.55; color: var(--color-text-muted); }
|
|
94
|
+
.card > p:not(.verdict) { margin: 0 0 12px; }
|
|
95
|
+
.card pre { font-size: 12px; padding: 8px 10px; margin: 0 0 12px; white-space: pre-wrap; }
|
|
96
|
+
.card ul { margin: 0; padding-left: 18px; }
|
|
97
|
+
.card li { margin: 3px 0; }
|
|
98
|
+
li.pro::marker { content: "+ "; color: var(--color-success); font-weight: 700; }
|
|
99
|
+
li.con::marker { content: "− "; color: var(--color-danger); font-weight: 700; }
|
|
100
|
+
|
|
101
|
+
.tag { display: inline-block; font: 500 10.5px/18px var(--font-mono); letter-spacing: .06em; text-transform: uppercase; padding: 0 7px; border-radius: 9px; margin-left: 8px; vertical-align: 1px; color: var(--tone); background: color-mix(in srgb, var(--tone) 15%, transparent); }
|
|
102
|
+
.dot { display: inline-grid; place-items: center; width: 22px; height: 22px; border-radius: 50%; margin-right: 8px; vertical-align: 1px; font: 600 11px var(--font-mono); color: var(--color-bg); background: var(--tone); }
|
|
103
|
+
|
|
104
|
+
.table { overflow-x: auto; margin: 0 0 20px; border: 1px solid var(--color-border); border-radius: 14px; background: var(--surface); }
|
|
105
|
+
table { border-collapse: collapse; width: 100%; font-size: 14px; }
|
|
106
|
+
th, td { padding: 10px 14px; text-align: left; vertical-align: top; border-bottom: 1px solid var(--color-border); }
|
|
107
|
+
tbody tr:last-child > td { border-bottom: 0; }
|
|
108
|
+
th { font: 500 11px/1.4 var(--font-mono); letter-spacing: .06em; text-transform: uppercase; color: var(--color-text-muted); background: var(--color-bg-subtle); }
|
|
109
|
+
td:first-child { white-space: nowrap; font-weight: 500; }
|
|
110
|
+
td.num { text-align: right; font-variant-numeric: tabular-nums; }
|
|
111
|
+
td.c { text-align: center; white-space: nowrap; }
|
|
112
|
+
td.yes { --tone: var(--color-success); } td.no { --tone: var(--color-danger); } td.part { --tone: var(--color-warning); }
|
|
113
|
+
td.yes, td.no, td.part { color: var(--tone); font-weight: 600; background: color-mix(in srgb, var(--tone) 13%, transparent); }
|
|
114
|
+
td.yes::before { content: "● "; } td.part::before { content: "◐ "; } td.no::before { content: "○ "; }
|
|
115
|
+
tr[data-tone] > td:first-child { box-shadow: inset 3px 0 0 var(--tone); background: color-mix(in srgb, var(--tone) 10%, transparent); }
|
|
116
|
+
|
|
117
|
+
figure { margin: 0 0 20px; padding: 24px; border: 1px solid var(--color-border); border-radius: 14px; background: var(--color-bg-subtle) radial-gradient(color-mix(in srgb, var(--color-text) 9%, transparent) 1px, transparent 1.5px) 0 0 / 18px 18px; }
|
|
118
|
+
figure svg { display: block; width: 100%; height: auto; }
|
|
119
|
+
figcaption { font-size: 13px; color: var(--color-text-muted); margin-top: 14px; }
|
|
120
|
+
@media (max-width: 600px) { figure.wide { overflow-x: auto; } figure.wide svg { min-width: 680px; } }
|
|
121
|
+
|
|
122
|
+
.legend { display: flex; flex-wrap: wrap; gap: 6px 18px; font: 12px var(--font-mono); color: var(--color-text-muted); margin: 0 0 14px; }
|
|
123
|
+
.legend i { display: inline-block; width: 14px; height: 10px; border-radius: 2px; margin-right: 6px; background: var(--tone, var(--color-text-muted)); }
|
|
124
|
+
|
|
125
|
+
ul.points { list-style: none; padding: 0; display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 8px 36px; }
|
|
126
|
+
ul.points > li { max-width: none; margin: 0; padding: 14px 0 0; border-top: 1px solid var(--color-border); color: var(--color-text-muted); font-size: 15px; }
|
|
127
|
+
ul.points > li > b:first-child { display: block; font: 400 19px/1.35 var(--font-serif); color: var(--color-text); margin-bottom: 6px; }
|
|
128
|
+
|
|
129
|
+
ol.steps { list-style: none; counter-reset: step; padding: 0; }
|
|
130
|
+
ol.steps > li { counter-increment: step; position: relative; padding: 2px 0 20px 48px; }
|
|
131
|
+
ol.steps > li::before { content: counter(step); position: absolute; left: 0; top: 0; width: 30px; height: 30px; border-radius: 50%; display: grid; place-items: center; font: 600 13px var(--font-mono); color: var(--color-bg); background: var(--color-accent); }
|
|
132
|
+
ol.steps > li:not(:last-child)::after { content: ""; position: absolute; left: 15px; top: 36px; bottom: 4px; width: 1px; background: var(--color-border); }
|
|
133
|
+
|
|
134
|
+
.sources { margin-top: 88px; padding-top: 16px; border-top: 1px solid var(--color-border); font: 12px/1.7 var(--font-mono); color: var(--color-text-muted); }
|
|
135
|
+
.sources p { max-width: none; }
|
|
136
|
+
.sources code { font-size: inherit; background: none; border: 0; padding: 0; }
|
|
137
|
+
|
|
138
|
+
@media (max-width: 600px) {
|
|
139
|
+
main { padding-top: 36px; }
|
|
140
|
+
h1 { font-size: 34px; }
|
|
141
|
+
.lead { font-size: 18px; }
|
|
142
|
+
h2 { font-size: 25px; }
|
|
143
|
+
section { margin-top: 64px; }
|
|
144
|
+
figure { padding: 14px; }
|
|
145
|
+
}
|
|
146
|
+
@media print { main { max-width: none; padding: 0; } figure, .card, .kpi, tr { break-inside: avoid; } .card, .lineup > div { box-shadow: none; } }
|
|
147
|
+
</style>
|
|
148
|
+
</head>
|
|
149
|
+
<body>
|
|
150
|
+
<main>
|
|
151
|
+
<p class="kicker">Ticket or context · kind of page</p>
|
|
152
|
+
<h1>Page Title</h1>
|
|
153
|
+
<p class="sub">The question this page answers.</p>
|
|
154
|
+
<p class="lead">The answer in one or two sentences, with <b>the pick</b> named, so a reader who stops here has it.</p>
|
|
155
|
+
|
|
156
|
+
<div class="lineup">
|
|
157
|
+
<div data-tone="accent"><b>1</b><span>Option A</span><small>the pick</small></div>
|
|
158
|
+
<div><b>2</b><span>Option B</span><small>too static</small></div>
|
|
159
|
+
<div><b>3</b><span>Option C</span><small>separate project</small></div>
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
<div class="kpis">
|
|
163
|
+
<div class="kpi"><span>Errors in 7 days</span><b>133.5K</b><small>26% of minutes [1]</small></div>
|
|
164
|
+
<div class="kpi" data-tone="danger"><span>Peak after release</span><b>30,000</b><small>RU per minute, estimate</small></div>
|
|
165
|
+
</div>
|
|
166
|
+
|
|
167
|
+
<section>
|
|
168
|
+
<p class="eyebrow">Overview</p>
|
|
169
|
+
<h2>Each heading is a claim that can be true or false</h2>
|
|
170
|
+
<figure class="wide">
|
|
171
|
+
<svg viewBox="0 0 900 200" role="img" aria-labelledby="f1-t"><title id="f1-t">What the diagram shows</title></svg>
|
|
172
|
+
<figcaption>One sentence: what to notice.</figcaption>
|
|
173
|
+
</figure>
|
|
174
|
+
</section>
|
|
175
|
+
|
|
176
|
+
<section>
|
|
177
|
+
<p class="eyebrow">Options</p>
|
|
178
|
+
<h2>Cards lead with the verdict</h2>
|
|
179
|
+
<div class="cards">
|
|
180
|
+
<div class="card" data-tone="accent">
|
|
181
|
+
<h3><span class="dot" data-tone="accent">1</span>Option A<span class="tag" data-tone="accent">pick</span></h3>
|
|
182
|
+
<p class="verdict">The verdict, in one line.</p>
|
|
183
|
+
<p>What it is, in one sentence.</p>
|
|
184
|
+
<pre>on 429: wait, then retry</pre>
|
|
185
|
+
<ul><li class="pro">A strong point.</li><li class="con">A weak point.</li></ul>
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
</section>
|
|
189
|
+
|
|
190
|
+
<section>
|
|
191
|
+
<p class="eyebrow">Compare</p>
|
|
192
|
+
<h2>A table holds the matrix</h2>
|
|
193
|
+
<div class="table"><table>
|
|
194
|
+
<thead><tr><th>Option</th><th>Global</th><th>Lines</th></tr></thead>
|
|
195
|
+
<tbody>
|
|
196
|
+
<tr data-tone="accent"><td><span class="dot" data-tone="accent">1</span>Option A</td><td class="c yes">yes</td><td class="num">1,229</td></tr>
|
|
197
|
+
<tr><td><span class="dot">2</span>Option B</td><td class="c no">no</td><td class="num">300</td></tr>
|
|
198
|
+
</tbody>
|
|
199
|
+
</table></div>
|
|
200
|
+
<ul class="points">
|
|
201
|
+
<li><b>A point leads with its claim.</b> One or two sentences of support.</li>
|
|
202
|
+
<li><b>Points sit side by side.</b> They wrap to one column on a phone.</li>
|
|
203
|
+
</ul>
|
|
204
|
+
</section>
|
|
205
|
+
|
|
206
|
+
<section>
|
|
207
|
+
<p class="eyebrow">Next</p>
|
|
208
|
+
<h2>Steps say what to do next</h2>
|
|
209
|
+
<ol class="steps"><li>First action.</li><li>Second action.</li></ol>
|
|
210
|
+
</section>
|
|
211
|
+
|
|
212
|
+
<div class="sources"><p>Sources: [1] <code>path/to/file.md</code> · [2] <a href="https://example.com">Title</a>. Estimates are marked as estimates.</p></div>
|
|
213
|
+
</main>
|
|
214
|
+
</body>
|
|
215
|
+
</html>
|
package/public/app.js
CHANGED
|
@@ -2565,20 +2565,28 @@ function unmountMsgReview() {
|
|
|
2565
2565
|
|
|
2566
2566
|
// Only the main session's replies: the agent has them in context, so a quote is enough.
|
|
2567
2567
|
function mountReplyReview(m, body, modal) {
|
|
2568
|
-
|
|
2568
|
+
const opts = !agentLogMode && replyReviewOpts(m, body, currentSessionId);
|
|
2569
|
+
if (!opts) return unmountMsgReview();
|
|
2569
2570
|
mountReview({
|
|
2570
|
-
|
|
2571
|
+
...opts,
|
|
2571
2572
|
panelEl: document.getElementById('msg-review-panel'),
|
|
2572
2573
|
hostEl: modal,
|
|
2574
|
+
onComment: () => setMsgDetailFollow(false),
|
|
2575
|
+
onSent: closeMsgDetailModal,
|
|
2576
|
+
});
|
|
2577
|
+
}
|
|
2578
|
+
|
|
2579
|
+
function replyReviewOpts(m, contentEl, sid) {
|
|
2580
|
+
if (m.type !== 'assistant' || !m.timestamp) return null;
|
|
2581
|
+
return {
|
|
2582
|
+
contentEl,
|
|
2573
2583
|
source: {
|
|
2574
2584
|
kind: 'reply',
|
|
2575
2585
|
id: m.timestamp,
|
|
2576
2586
|
label: replyReviewLabel(m.timestamp),
|
|
2577
|
-
locate: replyLocateHint(m.timestamp),
|
|
2587
|
+
locate: replyLocateHint(m.timestamp, sid),
|
|
2578
2588
|
},
|
|
2579
|
-
|
|
2580
|
-
onSent: closeMsgDetailModal,
|
|
2581
|
-
});
|
|
2589
|
+
};
|
|
2582
2590
|
}
|
|
2583
2591
|
|
|
2584
2592
|
function replyReviewLabel(timestamp) {
|
|
@@ -2590,8 +2598,8 @@ function replyReviewLabel(timestamp) {
|
|
|
2590
2598
|
return `your reply${day} at ${d.toLocaleTimeString()}`;
|
|
2591
2599
|
}
|
|
2592
2600
|
|
|
2593
|
-
function replyLocateHint(timestamp) {
|
|
2594
|
-
const transcript = sessions.find((s) => s.id ===
|
|
2601
|
+
function replyLocateHint(timestamp, sid) {
|
|
2602
|
+
const transcript = sessions.find((s) => s.id === sid)?.jsonlPath;
|
|
2595
2603
|
if (!transcript) return undefined;
|
|
2596
2604
|
return `If that reply is not in your context, it is in ${transcript}, the assistant entry with timestamp ${timestamp}.`;
|
|
2597
2605
|
}
|
|
@@ -7610,6 +7618,17 @@ async function dismissSession(sid) {
|
|
|
7610
7618
|
if (await confirmDismissBlockers(sid)) dismissSessionFromList(sid);
|
|
7611
7619
|
}
|
|
7612
7620
|
|
|
7621
|
+
function contextSessionId() {
|
|
7622
|
+
if (terminalPaneFocused()) return termState.sessionId || currentSessionId;
|
|
7623
|
+
if (focusZone !== 'sidebar') return selectedSessionId || currentSessionId;
|
|
7624
|
+
return sessionsList.querySelector('.kb-selected')?.dataset.sessionId || currentSessionId;
|
|
7625
|
+
}
|
|
7626
|
+
|
|
7627
|
+
function dismissContextSession() {
|
|
7628
|
+
const sid = contextSessionId();
|
|
7629
|
+
if (sid) dismissSession(sid);
|
|
7630
|
+
}
|
|
7631
|
+
|
|
7613
7632
|
const MODAL_ESC_PRIORITY = [
|
|
7614
7633
|
'preview-modal',
|
|
7615
7634
|
'msg-detail-modal',
|
|
@@ -7968,9 +7987,7 @@ document.addEventListener('keydown', (e) => {
|
|
|
7968
7987
|
}
|
|
7969
7988
|
|
|
7970
7989
|
// Shared actions — work in both sidebar and board
|
|
7971
|
-
const
|
|
7972
|
-
const contextSid =
|
|
7973
|
-
focusZone === 'sidebar' ? cursorEl?.dataset.sessionId || currentSessionId : selectedSessionId || currentSessionId;
|
|
7990
|
+
const contextSid = contextSessionId();
|
|
7974
7991
|
if (matchKey(e, 'KeyP')) {
|
|
7975
7992
|
e.preventDefault();
|
|
7976
7993
|
if (contextSid) openPlanForSession(contextSid);
|
|
@@ -7983,7 +8000,7 @@ document.addEventListener('keydown', (e) => {
|
|
|
7983
8000
|
}
|
|
7984
8001
|
if (matchKey(e, 'KeyN')) {
|
|
7985
8002
|
e.preventDefault();
|
|
7986
|
-
const cursorKey = _scratchpadKeyOf(
|
|
8003
|
+
const cursorKey = _scratchpadKeyOf(focusZone === 'sidebar' ? sessionsList.querySelector('.kb-selected') : null);
|
|
7987
8004
|
if (cursorKey) showScratchpad(cursorKey);
|
|
7988
8005
|
else toggleScratchpad();
|
|
7989
8006
|
return;
|
|
@@ -8007,8 +8024,7 @@ document.addEventListener('keydown', (e) => {
|
|
|
8007
8024
|
}
|
|
8008
8025
|
if (ClaudeHub.comboOf(e) === 'ctrl+d') {
|
|
8009
8026
|
e.preventDefault();
|
|
8010
|
-
|
|
8011
|
-
dismissSession(contextSid);
|
|
8027
|
+
dismissContextSession();
|
|
8012
8028
|
return;
|
|
8013
8029
|
}
|
|
8014
8030
|
if (e.code === 'KeyC' && e.shiftKey && !e.altKey && !e.metaKey) {
|
|
@@ -8554,6 +8570,8 @@ function reviewBridge(textBefore, headingBefore, rangeAt, comboOf, fieldSelector
|
|
|
8554
8570
|
marks.delete(e.data.id);
|
|
8555
8571
|
} else if (type === 'reveal' && range) {
|
|
8556
8572
|
range.startContainer.parentElement?.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
|
8573
|
+
} else if (type === 'scrollTo') {
|
|
8574
|
+
requestAnimationFrame(() => scrollTo(e.data.x, e.data.y));
|
|
8557
8575
|
}
|
|
8558
8576
|
});
|
|
8559
8577
|
}
|
|
@@ -8567,6 +8585,23 @@ const REVIEW_BRIDGE_TAG = `<script>(${reviewBridge})(${reviewTextBefore}, ${revi
|
|
|
8567
8585
|
// not drive the board.
|
|
8568
8586
|
const bridgedFrames = new WeakSet();
|
|
8569
8587
|
const paneFrameScroll = new WeakMap();
|
|
8588
|
+
const hiddenPaneFrames = new WeakSet();
|
|
8589
|
+
|
|
8590
|
+
// Chrome resets a frame's scroll to 0 while the frame is display:none and does not give it back.
|
|
8591
|
+
function restorePaneFrameScroll(view, shown) {
|
|
8592
|
+
const frame = view.querySelector(':scope > iframe[srcdoc]');
|
|
8593
|
+
if (!frame) return;
|
|
8594
|
+
if (!shown) {
|
|
8595
|
+
hiddenPaneFrames.add(frame);
|
|
8596
|
+
} else if (hiddenPaneFrames.delete(frame)) {
|
|
8597
|
+
const s = paneFrameScroll.get(frame);
|
|
8598
|
+
if (s) postPaneScroll(frame, s);
|
|
8599
|
+
}
|
|
8600
|
+
}
|
|
8601
|
+
|
|
8602
|
+
function postPaneScroll(frame, { x, y }) {
|
|
8603
|
+
frame.contentWindow?.postMessage({ type: `${REVIEW_MSG}scrollTo`, x: Number(x) || 0, y: Number(y) || 0 }, '*');
|
|
8604
|
+
}
|
|
8570
8605
|
|
|
8571
8606
|
function bridgedFrameEls() {
|
|
8572
8607
|
return [...document.querySelectorAll('iframe[srcdoc]')].filter((f) => bridgedFrames.has(f));
|
|
@@ -9084,7 +9119,7 @@ function renderHtmlPreview(bodyEl, content) {
|
|
|
9084
9119
|
bodyEl.innerHTML = '';
|
|
9085
9120
|
// After the document's end the parser still adds the script to <body>, and it runs after
|
|
9086
9121
|
// the page's own scripts.
|
|
9087
|
-
bodyEl.appendChild(createPreviewFrame('preview-html-frame', content
|
|
9122
|
+
bodyEl.appendChild(createPreviewFrame('preview-html-frame themed-frame', themedSrcdoc(content)));
|
|
9088
9123
|
}
|
|
9089
9124
|
|
|
9090
9125
|
function isAbsolutePath(p) {
|
|
@@ -13393,7 +13428,8 @@ function syncPanes() {
|
|
|
13393
13428
|
const boardSide = split ? splitSideOf(split, 'board') : null;
|
|
13394
13429
|
sessionView.classList.toggle('pane-mode', split ? !boardSide : !!pane);
|
|
13395
13430
|
sessionView.classList.toggle('split-mode', !!split);
|
|
13396
|
-
|
|
13431
|
+
const term = wantsTerminal();
|
|
13432
|
+
if (!layout || term) closePanePop();
|
|
13397
13433
|
// key → side ('' unsplit). The focused side mounts last, so the frame limit evicts it last.
|
|
13398
13434
|
const sides = new Map();
|
|
13399
13435
|
if (split) {
|
|
@@ -13411,14 +13447,15 @@ function syncPanes() {
|
|
|
13411
13447
|
el.classList.toggle('on', side !== undefined);
|
|
13412
13448
|
markPaneSide(el, side, el.dataset.key === focusKey);
|
|
13413
13449
|
if (paneLineWanted.size && side !== undefined) applyPaneLine(el);
|
|
13450
|
+
restorePaneFrameScroll(el, side !== undefined && !term);
|
|
13414
13451
|
}
|
|
13415
13452
|
const boardWasSplit = boardInSplit();
|
|
13416
13453
|
markPaneSide(paneBoard, boardSide, !!boardSide && !pane);
|
|
13417
13454
|
if (boardWasSplit !== !!boardSide) applyAgentFooterCollapsed();
|
|
13418
|
-
placeSessionLog(
|
|
13455
|
+
placeSessionLog(term ? null : boardSide);
|
|
13419
13456
|
// Off screen, a framed page keeps focus and takes keys the user aims at the board.
|
|
13420
13457
|
const focused = document.activeElement;
|
|
13421
|
-
if (paneViews.contains(focused) && (
|
|
13458
|
+
if (paneViews.contains(focused) && (term || !focused.closest('.pane-view.on'))) focused.blur();
|
|
13422
13459
|
syncPaneReview();
|
|
13423
13460
|
if (!layout) return;
|
|
13424
13461
|
for (const b of document.querySelectorAll('.pane-tool')) b.disabled = !pane;
|
|
@@ -13595,19 +13632,16 @@ async function loadPaneView(sid, pane, view, scroll) {
|
|
|
13595
13632
|
}
|
|
13596
13633
|
const data = await getJson('/api/preview', null, { query: { path: pane.target } });
|
|
13597
13634
|
if (data?.kind === 'html') {
|
|
13598
|
-
// The frame's origin is opaque, so the page restores its own scroll on load.
|
|
13599
|
-
const restore = scroll
|
|
13600
|
-
? `<script>addEventListener('load',()=>requestAnimationFrame(()=>scrollTo(${Number(scroll.x) || 0},${Number(scroll.y) || 0})))</script>`
|
|
13601
|
-
: '';
|
|
13602
13635
|
// A show card's own zoom follows the text zoom (showBridge), so its frame is not .modal-zoomable.
|
|
13603
13636
|
const [cls, doc] = pane.show
|
|
13604
13637
|
? ['pane-frame show-pane-frame', showSrcdoc(data.content, scroll?.y)]
|
|
13605
|
-
: ['pane-frame modal-zoomable', data.content
|
|
13638
|
+
: ['pane-frame modal-zoomable themed-frame', themedSrcdoc(data.content)];
|
|
13606
13639
|
const frame = createPreviewFrame(cls, doc);
|
|
13607
13640
|
frame.title = pane.title;
|
|
13608
13641
|
// Focus given before the document loads stays on the frame element, out of the document's keys.
|
|
13609
13642
|
frame.addEventListener('load', () => {
|
|
13610
13643
|
if (document.activeElement === frame) frame.contentWindow?.focus();
|
|
13644
|
+
if (scroll && !pane.show) postPaneScroll(frame, scroll);
|
|
13611
13645
|
});
|
|
13612
13646
|
view.appendChild(frame);
|
|
13613
13647
|
attachPaneReview(view, sid, fileReviewOpts(data.path, data.kind, frame, frame));
|
|
@@ -13671,7 +13705,9 @@ async function loadMessagePaneView(sid, pane, view) {
|
|
|
13671
13705
|
return;
|
|
13672
13706
|
}
|
|
13673
13707
|
const meta = [msgDetailTitle(m), ...msgDetailMeta(m)].join(' · ');
|
|
13674
|
-
|
|
13708
|
+
const body = appendPaneDoc(view);
|
|
13709
|
+
body.innerHTML = `<div class="pane-msg-meta">${escapeHtml(meta)}</div>${msgDetailContent(m)}`;
|
|
13710
|
+
attachPaneReview(view, sid, replyReviewOpts(m, body, sid));
|
|
13675
13711
|
}
|
|
13676
13712
|
|
|
13677
13713
|
function onPaneFileChanged(filePath) {
|
|
@@ -13824,8 +13860,13 @@ function syncPaneReview() {
|
|
|
13824
13860
|
|
|
13825
13861
|
function unmountPane(sid, paneId) {
|
|
13826
13862
|
const key = `${sid}/${paneId}`;
|
|
13827
|
-
paneFrames.get(key)
|
|
13863
|
+
const view = paneFrames.get(key);
|
|
13864
|
+
const hadFocus = view?.contains(document.activeElement);
|
|
13865
|
+
view?.remove();
|
|
13828
13866
|
paneFrames.delete(key);
|
|
13867
|
+
// Chrome leaves the window without focus when a focused frame is removed, so no key reaches
|
|
13868
|
+
// the board until a click. Only when it had focus: under the hub, cck may be off screen.
|
|
13869
|
+
if (hadFocus) window.focus();
|
|
13829
13870
|
}
|
|
13830
13871
|
|
|
13831
13872
|
function selectPane(id) {
|
|
@@ -13888,6 +13929,7 @@ function closeFocusedPane() {
|
|
|
13888
13929
|
const id = focusedPaneId();
|
|
13889
13930
|
if (!id || id === 'board') return false;
|
|
13890
13931
|
closePane(id);
|
|
13932
|
+
focusActivePane();
|
|
13891
13933
|
return true;
|
|
13892
13934
|
}
|
|
13893
13935
|
|
|
@@ -14738,6 +14780,7 @@ function terminalShortcut(e, probe = false) {
|
|
|
14738
14780
|
// Ctrl+Shift+Z is redo in a text field, so only the terminal's own textarea gives it up.
|
|
14739
14781
|
if (ctrlShift && e.code === 'KeyZ') return inPageField(e) ? null : toggleZenMode;
|
|
14740
14782
|
if (ctrlShift && e.code === 'KeyK') return kanbotAvailable() ? () => toggleKanbot() : null;
|
|
14783
|
+
if (ctrlShift && e.code === 'KeyD') return dismissContextSession;
|
|
14741
14784
|
if (ctrlAlt && (e.code === 'KeyN' || e.code === 'KeyR') && terminalAvailable()) {
|
|
14742
14785
|
return () => openNewSession(null, e.code === 'KeyR');
|
|
14743
14786
|
}
|
|
@@ -16333,22 +16376,49 @@ function showTokens() {
|
|
|
16333
16376
|
return out;
|
|
16334
16377
|
}
|
|
16335
16378
|
|
|
16336
|
-
|
|
16337
|
-
|
|
16338
|
-
const tokens = Object.entries(showTokens())
|
|
16379
|
+
function showTokenCss() {
|
|
16380
|
+
return Object.entries(showTokens())
|
|
16339
16381
|
.map(([k, v]) => `${k}:${v}`)
|
|
16340
16382
|
.join(';');
|
|
16341
|
-
|
|
16383
|
+
}
|
|
16384
|
+
|
|
16385
|
+
function withHeadFirst(content, head) {
|
|
16386
|
+
const at = /<head[^>]*>/i.exec(content) || /<html[^>]*>/i.exec(content) || /<!doctype[^>]*>/i.exec(content);
|
|
16387
|
+
const inject = at?.[0].toLowerCase().startsWith('<head') ? head : `<head>${head}</head>`;
|
|
16388
|
+
const cut = at ? at.index + at[0].length : 0;
|
|
16389
|
+
return content.slice(0, cut) + inject + content.slice(cut);
|
|
16390
|
+
}
|
|
16391
|
+
|
|
16392
|
+
// A whole document gets the tokens and base styles first in its head, so its own styles win.
|
|
16393
|
+
function showSrcdoc(content, restoreY) {
|
|
16394
|
+
const head = `<meta http-equiv="Content-Security-Policy" content="${SHOW_CSP}"><style>:root{${showTokenCss()}}${SHOW_FRAME_BASE}</style>`;
|
|
16342
16395
|
const bridge = `<script>(${showBridge})(${Math.max(0, Math.round(restoreY) || 0)}, ${modalZoom});</script>${REVIEW_BRIDGE_TAG}`;
|
|
16343
16396
|
if (!/^\s*(<!doctype|<html)/i.test(content)) {
|
|
16344
16397
|
return `<!doctype html><html><head><meta charset="utf-8">${head}</head><body>${content}${bridge}</body></html>`;
|
|
16345
16398
|
}
|
|
16346
|
-
|
|
16347
|
-
|
|
16348
|
-
|
|
16349
|
-
|
|
16399
|
+
return withHeadFirst(content, head) + bridge;
|
|
16400
|
+
}
|
|
16401
|
+
|
|
16402
|
+
// An HTML file preview gets only the tokens, first in its head: its own :root rules win, and
|
|
16403
|
+
// a page that declares its defaults in :where(:root) follows the board theme.
|
|
16404
|
+
function themedSrcdoc(content) {
|
|
16405
|
+
const head = `<style id="cck-tokens">:root{${showTokenCss()}}</style>`;
|
|
16406
|
+
return withHeadFirst(content, head) + THEME_BRIDGE_TAG + REVIEW_BRIDGE_TAG;
|
|
16350
16407
|
}
|
|
16351
16408
|
|
|
16409
|
+
// Runs inside the preview as source text, so it must not close over anything outside its own body.
|
|
16410
|
+
// It rewrites the injected rule, not inline styles, so the page's own :root still wins after a switch.
|
|
16411
|
+
function themeBridge(prefix) {
|
|
16412
|
+
addEventListener('message', (e) => {
|
|
16413
|
+
if (e.source !== parent || e.data?.type !== `${prefix}tokens`) return;
|
|
16414
|
+
const rule = document.getElementById('cck-tokens')?.sheet?.cssRules[0];
|
|
16415
|
+
if (!rule) return;
|
|
16416
|
+
for (const [k, v] of Object.entries(e.data.tokens)) rule.style.setProperty(k, v);
|
|
16417
|
+
});
|
|
16418
|
+
}
|
|
16419
|
+
|
|
16420
|
+
const THEME_BRIDGE_TAG = `<script>(${themeBridge})(${JSON.stringify(SHOW_MSG)});</script>`;
|
|
16421
|
+
|
|
16352
16422
|
// Runs inside the card as source text, so it must not close over anything outside its own body.
|
|
16353
16423
|
// Keys and links go through reviewBridge, which the card loads next to this.
|
|
16354
16424
|
function showBridge(restoreY, textZoom) {
|
|
@@ -16413,15 +16483,26 @@ window.addEventListener('message', (e) => {
|
|
|
16413
16483
|
});
|
|
16414
16484
|
|
|
16415
16485
|
// The same body attributes the terminal theme watches; a markdown card follows the board's CSS.
|
|
16486
|
+
// The body's style also changes on every drag, so a push with the same tokens is dropped.
|
|
16487
|
+
let sentTokens = '';
|
|
16416
16488
|
new MutationObserver(() => {
|
|
16417
|
-
postToShowFrames(() =>
|
|
16489
|
+
postToShowFrames(() => {
|
|
16490
|
+
const tokens = showTokens();
|
|
16491
|
+
const key = JSON.stringify(tokens);
|
|
16492
|
+
if (key === sentTokens) return null;
|
|
16493
|
+
sentTokens = key;
|
|
16494
|
+
return { type: `${SHOW_MSG}tokens`, tokens };
|
|
16495
|
+
});
|
|
16418
16496
|
}).observe(document.body, { attributes: true, attributeFilter: ['class', 'data-color-theme', 'style'] });
|
|
16419
16497
|
|
|
16420
16498
|
// Builds the message only when a frame is there, because showTokens forces a style recalc.
|
|
16421
16499
|
function postToShowFrames(makeMsg) {
|
|
16422
|
-
const frames = [showState.frame, ...document.querySelectorAll('.show-pane-frame')].filter(
|
|
16500
|
+
const frames = [showState.frame, ...document.querySelectorAll('.show-pane-frame, .themed-frame')].filter(
|
|
16501
|
+
(f) => f?.contentWindow,
|
|
16502
|
+
);
|
|
16423
16503
|
if (!frames.length) return;
|
|
16424
16504
|
const msg = makeMsg();
|
|
16505
|
+
if (!msg) return;
|
|
16425
16506
|
for (const f of frames) f.contentWindow.postMessage(msg, '*');
|
|
16426
16507
|
}
|
|
16427
16508
|
|