devcoach 2.6.1 → 2.6.2
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/THIRD_PARTY_NOTICES.md +26 -0
- package/assets/references/course.md +115 -25
- package/assets/static/course-frame.js +23 -5
- package/assets/static/course-viewer.js +15 -2
- package/assets/static/style.css +87 -32
- package/assets/static/vendor/highlight.LICENSE.txt +29 -0
- package/dist/{app-HPKJDE5T.js → app-L3Y3NV4O.js} +197 -154
- package/dist/bin.js +1 -1
- package/dist/chunk-QFIAOASG.js +15 -0
- package/dist/chunk-XLFZCPHN.js +2 -0
- package/dist/chunk-YMXRXYRU.js +118 -0
- package/dist/{commands-J252OWKT.js → commands-55T3DDPV.js} +13 -13
- package/dist/{hooks-5Y2QJNJJ.js → hooks-7KTW5KRO.js} +1 -1
- package/dist/server-UN3IURFP.js +9 -0
- package/package.json +3 -2
- package/dist/chunk-2OKF6FRF.js +0 -15
- package/dist/chunk-54UDP4GL.js +0 -2
- package/dist/chunk-QWZZWWAN.js +0 -111
- package/dist/server-3EYIKYWM.js +0 -9
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Third-party notices
|
|
2
|
+
|
|
3
|
+
devcoach is licensed under AGPL-3.0-only (see `LICENSE`). It redistributes the libraries below,
|
|
4
|
+
vendored as files under `assets/static/vendor/`. Each keeps its own licence; all of them are
|
|
5
|
+
compatible with distribution under the AGPL.
|
|
6
|
+
|
|
7
|
+
| Library | Version | Licence | Where it is used | Notice |
|
|
8
|
+
|---|---|---|---|---|
|
|
9
|
+
| [highlight.js](https://github.com/highlightjs/highlight.js) | 11.10.0 | BSD-3-Clause | code blocks in lesson pages; **inlined into course documents** | header of `highlight.min.js`, full text in `highlight.LICENSE.txt` |
|
|
10
|
+
| [marked](https://github.com/markedjs/marked) | 4.3.0 | MIT | lesson markdown in the dashboard | header of `marked.min.js` |
|
|
11
|
+
| [DOMPurify](https://github.com/cure53/DOMPurify) | 3.4.13 | Apache-2.0 OR MPL-2.0 | sanitising rendered markdown | header of `purify.min.js` |
|
|
12
|
+
| [flatpickr](https://github.com/flatpickr/flatpickr) | 4.6.13 | MIT | the date range picker | header of `flatpickr.min.js` |
|
|
13
|
+
| [Alpine.js](https://github.com/alpinejs/alpine) | 3.14.1 | MIT | dashboard interactions | upstream repository (the minified file carries no header) |
|
|
14
|
+
| [htmx](https://github.com/bigskysoftware/htmx) | 1.9.12 | BSD-2-Clause | partial page updates | upstream repository (the minified file carries no header) |
|
|
15
|
+
| [Tailwind CSS](https://github.com/tailwindlabs/tailwindcss) (Play CDN build) | 3.4.17 | MIT | dashboard styling | upstream repository (the file carries no header) |
|
|
16
|
+
|
|
17
|
+
## Course documents and artifacts
|
|
18
|
+
|
|
19
|
+
A course document that shows code carries highlight.js **inside the file**: devcoach fills the
|
|
20
|
+
document's `<script data-devcoach="highlighter">` placeholder with the vendored build. The
|
|
21
|
+
BSD-3-Clause terms ask that the copyright notice, the conditions and the disclaimer travel with
|
|
22
|
+
every redistribution in binary form, so the inlined block starts with the full licence text.
|
|
23
|
+
Keep that comment when you copy, publish or share a course document — publishing it as a Claude
|
|
24
|
+
artifact redistributes the library too.
|
|
25
|
+
|
|
26
|
+
The course document itself (the text, examples and scripts written for you) is yours.
|
|
@@ -86,9 +86,10 @@ under 900 px the menu becomes a strip above the content. Routing by `location.ha
|
|
|
86
86
|
(`#step-N`, no hash → step 1): on `hashchange` show that section, scroll to the top. Every
|
|
87
87
|
section ends with *← Previous · Next →* buttons and the line *"Stuck? Ask in the chat — that's
|
|
88
88
|
where the course is being taught."* Mark `:root[data-embedded] nav.steps { display: none }`:
|
|
89
|
-
the dashboard injects `data-embedded`, has its own step list, makes the canvas transparent
|
|
90
|
-
lifts the `max-width` of `.course`, `section` and `header.intro` to use the width it has
|
|
91
|
-
keep those class names
|
|
89
|
+
the dashboard injects `data-embedded`, has its own step list, makes the canvas transparent,
|
|
90
|
+
lifts the `max-width` of `.course`, `section` and `header.intro` to use the width it has, and
|
|
91
|
+
sets `data-theme` from its own light/dark toggle — so keep those class names and the
|
|
92
|
+
`data-theme` selectors.
|
|
92
93
|
|
|
93
94
|
**Each section**, in order: **Goal** (one line) → **The idea** (short, one concept, an analogy
|
|
94
95
|
where it helps) → **Worked example** → **Try it** (see the ladder below) → **Check yourself**
|
|
@@ -109,56 +110,145 @@ translated into JS just to make it runnable:
|
|
|
109
110
|
output** (typed or picked, then Reveal), **fill the blank**, or **spot the bug** (3–4
|
|
110
111
|
candidates). No button may pretend to execute Java, Rust or Go.
|
|
111
112
|
|
|
112
|
-
**
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
`@media (prefers-color-scheme: dark)` guarded by `:root:not([data-theme="light"])` and again
|
|
117
|
-
under `:root[data-theme="dark"]`, `body` has an explicit background, 16 px side gutters at
|
|
118
|
-
phone width, no horizontal page scroll.
|
|
113
|
+
**Code and highlighting.** Every snippet is a `<pre><code class="language-…">` with the language
|
|
114
|
+
named (`js ts python java kotlin csharp go rust swift sql bash json xml css yaml diff`, 36 in
|
|
115
|
+
all) and `&`, `<`, `>` escaped in its text. The document carries ONE placeholder, before your
|
|
116
|
+
own script:
|
|
119
117
|
|
|
120
|
-
|
|
118
|
+
```html
|
|
119
|
+
<script data-devcoach="highlighter"></script>
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
devcoach fills it when you register steps, in a document that shows code (a course without
|
|
123
|
+
code never carries the library; leave the placeholder in anyway) — with highlight.js (BSD-3-Clause, the licence text
|
|
124
|
+
travels in the block), the stylesheet that colours it, and the call that highlights every
|
|
125
|
+
`pre code`. The library then lives inside the file, so the course is highlighted opened from
|
|
126
|
+
disk and as an artifact, not only in the dashboard.
|
|
127
|
+
|
|
128
|
+
- **Never write, paste or load a highlighter yourself**, and never colour code by hand with
|
|
129
|
+
`<span>`s: write plain escaped text and the language class.
|
|
130
|
+
- Colours are tokens with defaults matched to the palette: `--syn-keyword --syn-string
|
|
131
|
+
--syn-comment --syn-number --syn-title --syn-type --syn-attr --syn-meta`. Define them in your
|
|
132
|
+
token blocks only to retune them, each at 4.5:1 or better on `--code-bg`.
|
|
133
|
+
- `class="nohighlight"` on a `<code>` keeps it plain: terminal transcripts, program output. An
|
|
134
|
+
editable `<textarea>` is never highlighted.
|
|
135
|
+
- Once filled, the file holds the library (about 120 KB on a few very long lines). Change the
|
|
136
|
+
document with **targeted edits**; do not rewrite it whole and do not quote the block. If a
|
|
137
|
+
rewrite is unavoidable, write the empty placeholder again: it is refilled on the next
|
|
138
|
+
`add_course_step` or `update_course_progress`.
|
|
139
|
+
|
|
140
|
+
**Design — conform to the dashboard, do not copy it.** One measure (~68ch), generous spacing,
|
|
141
|
+
quiet Tufte-like typography, light + dark. Every colour is a **token** on `:root`; nothing is
|
|
142
|
+
hard-coded further down (no stray `#fff` on a button, no grey typed by hand in a rule):
|
|
143
|
+
|
|
144
|
+
- **Neutrals are the dashboard's** — `--bg --panel --text --muted --line --code-bg`, with the
|
|
145
|
+
values of the skeleton. They are what makes an embedded course sit on the dashboard's canvas
|
|
146
|
+
without a seam (embedded, the viewer sets these six itself, so keep the names).
|
|
147
|
+
- **The accent is yours** — the brand teal of the skeleton by default, or ONE accent that suits
|
|
148
|
+
the subject (Rust's orange, Swift's orange-red, a database blue…), as long as `--accent` reads
|
|
149
|
+
at 4.5:1 or better on `--panel` in both modes and `--on-accent` reads on `--accent`.
|
|
150
|
+
- `--ok` / `--warn` keep their meaning (right / wrong, safe / dangerous). Colours inside a
|
|
151
|
+
diagram or an illustration are free.
|
|
152
|
+
|
|
153
|
+
It must also pass as a Claude artifact page: `<title>` is a 2–4 word name (the long title is
|
|
154
|
+
the `<h1>`), dark tokens are set under `@media (prefers-color-scheme: dark)` guarded by
|
|
155
|
+
`:root:not([data-theme="light"])` and again under `:root[data-theme="dark"]`, `body` has an
|
|
156
|
+
explicit background, 16 px side gutters at phone width, no horizontal page scroll.
|
|
157
|
+
|
|
158
|
+
**Theme switch.** Opened on its own (the file, the artifact) the document follows the OS until
|
|
159
|
+
the reader uses its switch: a `<button class="theme-switch">` in the step menu that sets
|
|
160
|
+
`data-theme` on the root to `dark` or `light`, shows the **current** scheme, carries an
|
|
161
|
+
`aria-label` saying what a click does, and remembers the choice with `localStorage` inside
|
|
162
|
+
`try/catch` (storage is unavailable in a sandboxed open — the switch must still work for the
|
|
163
|
+
visit). Embedded in the dashboard the switch is hidden and the stored choice is NOT applied:
|
|
164
|
+
the dashboard's own toggle drives `data-theme` there.
|
|
165
|
+
|
|
166
|
+
Skeleton (adapt, keep the ids, the class names and the token names):
|
|
121
167
|
|
|
122
168
|
```html
|
|
123
169
|
<!doctype html><html lang="en"><head><meta charset="utf-8">
|
|
124
170
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
125
171
|
<title>Node error events</title>
|
|
126
172
|
<style>
|
|
127
|
-
:root { --bg:#
|
|
128
|
-
@media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) { --bg:#
|
|
129
|
-
:root[data-theme="dark"] { --bg:#
|
|
173
|
+
:root { --bg:#f9fafb; --panel:#ffffff; --text:#111827; --muted:#6b7280; --line:#e5e7eb; --code-bg:#f3f4f6; --accent:#0f766e; --accent-soft:#f0fdfa; --on-accent:#ffffff; --ok:#047857; --ok-soft:#d1fae5; --warn:#be123c; --warn-soft:#ffe4e6; --log-bg:#1e1f22; --log-text:#dfe1e5; }
|
|
174
|
+
@media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) { --bg:#1e1f22; --panel:#2b2d30; --text:#dfe1e5; --muted:#9da0a8; --line:#43454a; --code-bg:#1e1f22; --accent:#5eead4; --accent-soft:#134e4a; --on-accent:#0b2b28; --ok:#5fad65; --ok-soft:#1f3a24; --warn:#f97583; --warn-soft:#4a1f26; --log-bg:#18191b; --log-text:#dfe1e5; } }
|
|
175
|
+
:root[data-theme="dark"] { --bg:#1e1f22; --panel:#2b2d30; --text:#dfe1e5; --muted:#9da0a8; --line:#43454a; --code-bg:#1e1f22; --accent:#5eead4; --accent-soft:#134e4a; --on-accent:#0b2b28; --ok:#5fad65; --ok-soft:#1f3a24; --warn:#f97583; --warn-soft:#4a1f26; --log-bg:#18191b; --log-text:#dfe1e5; }
|
|
130
176
|
body { margin:0; background:var(--bg); color:var(--text); font:16px/1.6 system-ui, sans-serif; }
|
|
131
177
|
.course { display:grid; grid-template-columns:16rem minmax(0,1fr); gap:24px; max-width:1100px; margin:0 auto; padding:24px 16px; }
|
|
132
178
|
nav.steps { position:sticky; top:16px; align-self:start; } nav.steps a { display:block; padding:6px 10px; border-radius:8px; color:var(--muted); text-decoration:none; }
|
|
133
179
|
nav.steps a.current { background:var(--accent-soft); color:var(--accent); }
|
|
134
|
-
|
|
135
|
-
|
|
180
|
+
.theme-switch { margin-top:12px; background:transparent; color:var(--muted); border:1px solid var(--line); border-radius:8px; padding:4px 10px; cursor:pointer; }
|
|
181
|
+
:root[data-embedded] nav.steps, :root[data-embedded] .theme-switch { display:none } :root[data-embedded] .course { grid-template-columns:minmax(0,1fr); }
|
|
182
|
+
section { display:none; max-width:68ch; background:var(--panel); border:1px solid var(--line); border-radius:12px; padding:22px 24px; } section.current { display:block; }
|
|
183
|
+
button.primary { background:var(--accent); color:var(--on-accent); border:1px solid var(--accent); }
|
|
136
184
|
@media (max-width: 900px) { .course { grid-template-columns:minmax(0,1fr); } nav.steps { position:static; display:flex; flex-wrap:wrap; gap:4px; } }
|
|
137
|
-
/* .goal, .try, .check, .pager
|
|
185
|
+
/* .goal, .try, .check, .pager … — tokens only */
|
|
138
186
|
</style></head><body>
|
|
139
187
|
<div class="course">
|
|
140
|
-
<nav class="steps"><a href="#step-1">1 · Sums</a><a href="#step-2">2 · Powers</a
|
|
188
|
+
<nav class="steps"><a href="#step-1">1 · Sums</a><a href="#step-2">2 · Powers</a>
|
|
189
|
+
<button class="theme-switch" type="button"></button></nav>
|
|
141
190
|
<main>
|
|
142
|
-
<section id="step-1"><h2>1 · Sums</h2><p class="goal">…</p> …
|
|
191
|
+
<section id="step-1"><h2>1 · Sums</h2><p class="goal">…</p> …
|
|
192
|
+
<pre><code class="language-js">const total = [1, 2, 3].reduce((a, b) => a + b, 0);</code></pre>
|
|
193
|
+
<div class="check">…<button>Reveal</button></div>
|
|
143
194
|
<div class="pager"><a href="#step-1">← Previous</a><a href="#step-2">Next →</a></div>
|
|
144
195
|
<p class="ask">Stuck? Ask in the chat — that's where the course is being taught.</p></section>
|
|
145
196
|
<section id="step-2">…</section>
|
|
146
197
|
</main>
|
|
147
198
|
</div>
|
|
199
|
+
<script data-devcoach="highlighter"></script>
|
|
148
200
|
<script>
|
|
149
201
|
// show one section: on load + hashchange, toggle .current on the section and its nav link, scrollTo(0,0)
|
|
150
202
|
// reveal buttons; the live examples (new Function inside try/catch) or the simulations
|
|
203
|
+
// theme switch — standalone only; embedded, the dashboard sets data-theme:
|
|
204
|
+
(function () {
|
|
205
|
+
var root = document.documentElement, btn = document.querySelector(".theme-switch"), KEY = "course-theme";
|
|
206
|
+
var isDark = function () {
|
|
207
|
+
var t = root.getAttribute("data-theme");
|
|
208
|
+
return t ? t === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
209
|
+
};
|
|
210
|
+
var paint = function () {
|
|
211
|
+
btn.textContent = isDark() ? "🌙 Dark" : "☀️ Light";
|
|
212
|
+
btn.setAttribute("aria-label", isDark() ? "Dark theme — switch to light" : "Light theme — switch to dark");
|
|
213
|
+
};
|
|
214
|
+
if (window.parent === window) {
|
|
215
|
+
try { var saved = localStorage.getItem(KEY); if (saved === "dark" || saved === "light") root.setAttribute("data-theme", saved); } catch (e) {}
|
|
216
|
+
}
|
|
217
|
+
btn.addEventListener("click", function () {
|
|
218
|
+
var next = isDark() ? "light" : "dark";
|
|
219
|
+
root.setAttribute("data-theme", next);
|
|
220
|
+
try { localStorage.setItem(KEY, next); } catch (e) {}
|
|
221
|
+
paint();
|
|
222
|
+
});
|
|
223
|
+
matchMedia("(prefers-color-scheme: dark)").addEventListener("change", paint);
|
|
224
|
+
paint();
|
|
225
|
+
})();
|
|
151
226
|
</script></body></html>
|
|
152
227
|
```
|
|
153
228
|
|
|
154
|
-
### 4b. Share as an artifact — an offer,
|
|
229
|
+
### 4b. Share as an artifact — an offer, with the disclosure
|
|
230
|
+
|
|
231
|
+
Publishing is the one moment course content **leaves the machine**: the Artifact tool uploads
|
|
232
|
+
a copy of the document to claude.ai, under the user's account. The offer must say so — a
|
|
233
|
+
"private" link describes who can open it, not where the file lives.
|
|
155
234
|
|
|
156
235
|
After the last `add_course_step`, **if the Artifact tool is available**, offer ONCE, as a
|
|
157
|
-
choice
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
236
|
+
choice, in these terms:
|
|
237
|
+
|
|
238
|
+
> *Publish this course as a Claude artifact too? That **uploads a copy to claude.ai** — it
|
|
239
|
+
> leaves this machine. Only you can open it until you share the link.*
|
|
240
|
+
|
|
241
|
+
- A course usually carries the user's own work (examples come from `seed_context`: project and
|
|
242
|
+
repository names, file paths, snippets, the bug that started it). When this one does, name
|
|
243
|
+
what it contains **in the same question** — *"It mentions lolcoach and the hang you
|
|
244
|
+
debugged."* — so the yes is an informed one. Never publish without that yes.
|
|
245
|
+
- **Yes** → publish `document_path` as it is (`icon: "course"`, `description` = the goal) and
|
|
246
|
+
reply with the link in one line. **No**, or no Artifact tool → nothing, and do not ask again.
|
|
247
|
+
- The local file stays the course; the artifact is a copy for sharing. The user can ask for it
|
|
248
|
+
any time ("publish the course as an artifact") — the same disclosure applies each time, and
|
|
249
|
+
to every republish after an edit. devcoach stores nothing about the artifact, so it cannot
|
|
250
|
+
delete it either: if asked to take it down, use the Artifact tool or point the user to their
|
|
251
|
+
artifacts on claude.ai.
|
|
162
252
|
|
|
163
253
|
## 5. Teach — one step per message
|
|
164
254
|
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
// Injected by the dashboard at the end of every course document it serves (never part of the
|
|
2
2
|
// file on disk). Inside the sandboxed frame the parent cannot measure the document, so the
|
|
3
3
|
// document reports its own height and the step it is showing; the parent grows the frame to fit
|
|
4
|
-
// (the page scrolls, never the frame) and moves its step highlight.
|
|
5
|
-
//
|
|
4
|
+
// (the page scrolls, never the frame) and moves its step highlight. The dashboard in turn tells
|
|
5
|
+
// the document which theme it is in: framed, the document only sees the OS preference, not the
|
|
6
|
+
// dashboard's toggle. Standalone opens (a pasted URL, the artifact copy) never run this:
|
|
7
|
+
// `parent === window` exits at once, and the document's own theme switch is in charge.
|
|
6
8
|
(function () {
|
|
7
9
|
if (window.parent === window) return;
|
|
8
10
|
var root = document.documentElement;
|
|
@@ -10,12 +12,19 @@
|
|
|
10
12
|
// Embedded, the page canvas is the dashboard's: let it show through (sections keep their own
|
|
11
13
|
// panel colours), so the frame reads as part of the page instead of a box inside it.
|
|
12
14
|
root.style.background = "transparent";
|
|
13
|
-
// …
|
|
14
|
-
//
|
|
15
|
+
// …use the width the dashboard gives it (the reading-column caps of the contract's `.course` /
|
|
16
|
+
// `section` / `header.intro` are for the standalone page), and take the NEUTRALS from the
|
|
17
|
+
// dashboard palette so panels and text sit on its canvas without a seam. Accent and semantic
|
|
18
|
+
// tokens stay the document's own: a course conforms to the palette, it does not copy it.
|
|
15
19
|
var style = document.createElement("style");
|
|
16
20
|
style.textContent =
|
|
17
21
|
":root[data-embedded] .course{max-width:none;padding:4px 0 32px}" +
|
|
18
|
-
":root[data-embedded] section,:root[data-embedded] header.intro{max-width:none}"
|
|
22
|
+
":root[data-embedded] section,:root[data-embedded] header.intro{max-width:none}" +
|
|
23
|
+
":root[data-embedded] .theme-switch{display:none}" +
|
|
24
|
+
':root[data-embedded][data-theme="light"]{--bg:#f9fafb;--panel:#ffffff;--text:#111827;' +
|
|
25
|
+
"--muted:#6b7280;--line:#e5e7eb;--code-bg:#f3f4f6}" +
|
|
26
|
+
':root[data-embedded][data-theme="dark"]{--bg:#1e1f22;--panel:#2b2d30;--text:#dfe1e5;' +
|
|
27
|
+
"--muted:#9da0a8;--line:#43454a;--code-bg:#1e1f22}";
|
|
19
28
|
(document.head || root).appendChild(style);
|
|
20
29
|
function clearBody() { document.body.style.background = "transparent"; }
|
|
21
30
|
if (document.body) clearBody(); else document.addEventListener("DOMContentLoaded", clearBody);
|
|
@@ -27,6 +36,15 @@
|
|
|
27
36
|
"*",
|
|
28
37
|
);
|
|
29
38
|
}
|
|
39
|
+
// The dashboard answers every report with its theme (the first report is the handshake).
|
|
40
|
+
window.addEventListener("message", function (event) {
|
|
41
|
+
var data = event.data;
|
|
42
|
+
if (event.source !== window.parent || !data || data.type !== "devcoach:theme") return;
|
|
43
|
+
if (data.theme !== "dark" && data.theme !== "light") return;
|
|
44
|
+
if (root.getAttribute("data-theme") === data.theme) return;
|
|
45
|
+
root.setAttribute("data-theme", data.theme);
|
|
46
|
+
report();
|
|
47
|
+
});
|
|
30
48
|
window.addEventListener("load", report);
|
|
31
49
|
window.addEventListener("hashchange", report);
|
|
32
50
|
if (typeof ResizeObserver === "function") new ResizeObserver(report).observe(root);
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
// Course page: the sandboxed frame reports its height and current step (course-frame.js, injected
|
|
2
2
|
// by the server); this side sizes the frame to the document so the page scrolls, keeps the step
|
|
3
3
|
// list in sync, and turns step clicks into in-frame navigation (a fragment navigation of the
|
|
4
|
-
// cross-origin frame) instead of a page reload.
|
|
4
|
+
// cross-origin frame) instead of a page reload. It also tells the frame which theme the
|
|
5
|
+
// dashboard is in: the framed document cannot see the toggle, only the OS preference.
|
|
5
6
|
(function () {
|
|
6
7
|
var frame = document.getElementById("course-frame");
|
|
7
8
|
var list = document.getElementById("course-steps");
|
|
8
9
|
if (!frame || !list) return;
|
|
9
|
-
var CURRENT = ["bg-
|
|
10
|
+
var CURRENT = ["bg-accent-50", "dark:bg-accent-900/30"];
|
|
10
11
|
var shown = null;
|
|
11
12
|
|
|
12
13
|
function select(anchor) {
|
|
@@ -22,6 +23,15 @@
|
|
|
22
23
|
});
|
|
23
24
|
}
|
|
24
25
|
|
|
26
|
+
// The frame's origin is opaque (sandbox without allow-same-origin): "*" is the only target
|
|
27
|
+
// that reaches it, and the payload is one word.
|
|
28
|
+
function sendTheme() {
|
|
29
|
+
if (!frame.contentWindow) return;
|
|
30
|
+
var dark = document.documentElement.classList.contains("dark");
|
|
31
|
+
frame.contentWindow.postMessage({ type: "devcoach:theme", theme: dark ? "dark" : "light" }, "*");
|
|
32
|
+
}
|
|
33
|
+
document.addEventListener("dc:theme", sendTheme);
|
|
34
|
+
|
|
25
35
|
function scrollToFrame() {
|
|
26
36
|
var top = frame.getBoundingClientRect().top + window.scrollY - 16;
|
|
27
37
|
window.scrollTo({ top: Math.max(0, top), behavior: "smooth" });
|
|
@@ -35,6 +45,9 @@
|
|
|
35
45
|
frame.style.height = Math.min(50000, Math.max(320, Math.ceil(height))) + "px";
|
|
36
46
|
}
|
|
37
47
|
if (typeof data.anchor === "string") select(data.anchor);
|
|
48
|
+
// Every report is answered with the theme: the first one is the handshake, so the message
|
|
49
|
+
// can never arrive before the frame listens.
|
|
50
|
+
sendTheme();
|
|
38
51
|
});
|
|
39
52
|
|
|
40
53
|
list.addEventListener("click", function (event) {
|
package/assets/static/style.css
CHANGED
|
@@ -1,5 +1,56 @@
|
|
|
1
1
|
/* devcoach — style overrides with light/dark theme support */
|
|
2
2
|
|
|
3
|
+
/* ── Palette tokens ─────────────────────────────────────────────────────────
|
|
4
|
+
The one place colours are decided. Values are space-separated RGB triplets so Tailwind can
|
|
5
|
+
apply its opacity modifiers: layout() in src/web/views.ts points Tailwind's `gray` and
|
|
6
|
+
`accent` scales at these variables (`rgb(var(--dc-gray-900) / <alpha-value>)`), which is how
|
|
7
|
+
every `dark:bg-gray-900` in the views picks the palette up without being rewritten.
|
|
8
|
+
- Light neutrals: Tailwind's own gray.
|
|
9
|
+
- Dark neutrals: an IDE ramp (JetBrains New UI dark) — page → panel → raised → border.
|
|
10
|
+
- Accent: the brand teal of the wordmark and the docs site, one step darker from 500 up so
|
|
11
|
+
white text on accent-600 and accent-600 text on white both clear 4.5:1.
|
|
12
|
+
tests/theme.test.ts holds the contrast floor for the pairs the UI relies on. */
|
|
13
|
+
|
|
14
|
+
:root {
|
|
15
|
+
--dc-gray-50: 249 250 251;
|
|
16
|
+
--dc-gray-100: 243 244 246;
|
|
17
|
+
--dc-gray-200: 229 231 235;
|
|
18
|
+
--dc-gray-300: 209 213 219;
|
|
19
|
+
--dc-gray-400: 156 163 175;
|
|
20
|
+
--dc-gray-500: 107 114 128;
|
|
21
|
+
--dc-gray-600: 75 85 99;
|
|
22
|
+
--dc-gray-700: 55 65 81;
|
|
23
|
+
--dc-gray-800: 31 41 55;
|
|
24
|
+
--dc-gray-900: 17 24 39;
|
|
25
|
+
--dc-gray-950: 3 7 18;
|
|
26
|
+
|
|
27
|
+
--dc-accent-50: 240 253 250;
|
|
28
|
+
--dc-accent-100: 204 251 241;
|
|
29
|
+
--dc-accent-200: 153 246 228;
|
|
30
|
+
--dc-accent-300: 94 234 212;
|
|
31
|
+
--dc-accent-400: 45 212 191;
|
|
32
|
+
--dc-accent-500: 13 148 136;
|
|
33
|
+
--dc-accent-600: 15 118 110;
|
|
34
|
+
--dc-accent-700: 17 94 89;
|
|
35
|
+
--dc-accent-800: 19 78 74;
|
|
36
|
+
--dc-accent-900: 15 61 58;
|
|
37
|
+
--dc-accent-950: 4 47 46;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.dark {
|
|
41
|
+
--dc-gray-50: 245 246 247;
|
|
42
|
+
--dc-gray-100: 236 238 241;
|
|
43
|
+
--dc-gray-200: 223 225 229; /* text */
|
|
44
|
+
--dc-gray-300: 188 190 196;
|
|
45
|
+
--dc-gray-400: 157 160 168; /* muted text */
|
|
46
|
+
--dc-gray-500: 144 150 160; /* secondary text */
|
|
47
|
+
--dc-gray-600: 111 115 122; /* disabled, faint */
|
|
48
|
+
--dc-gray-700: 67 69 74; /* borders */
|
|
49
|
+
--dc-gray-800: 57 59 64; /* raised: inputs, hover rows */
|
|
50
|
+
--dc-gray-900: 43 45 48; /* panels, nav, cards */
|
|
51
|
+
--dc-gray-950: 30 31 34; /* page canvas */
|
|
52
|
+
}
|
|
53
|
+
|
|
3
54
|
/* ── Theme variables ────────────────────────────────────────────────────── */
|
|
4
55
|
|
|
5
56
|
:root {
|
|
@@ -17,10 +68,10 @@
|
|
|
17
68
|
--md-pre-bg: #f8fafc;
|
|
18
69
|
--md-pre-border: #e2e8f0;
|
|
19
70
|
--md-blockquote-color: #6b7280;
|
|
20
|
-
--md-blockquote-border:
|
|
71
|
+
--md-blockquote-border: rgb(var(--dc-accent-500));
|
|
21
72
|
--md-hr: #e5e7eb;
|
|
22
|
-
--md-link:
|
|
23
|
-
--md-link-hover:
|
|
73
|
+
--md-link: rgb(var(--dc-accent-600));
|
|
74
|
+
--md-link-hover: rgb(var(--dc-accent-500));
|
|
24
75
|
--md-table-th-bg: #f9fafb;
|
|
25
76
|
--md-table-th-color: #374151;
|
|
26
77
|
--md-table-border: #e5e7eb;
|
|
@@ -34,36 +85,40 @@
|
|
|
34
85
|
}
|
|
35
86
|
|
|
36
87
|
.dark {
|
|
37
|
-
--scrollbar-track:
|
|
38
|
-
--scrollbar-thumb:
|
|
39
|
-
--scrollbar-thumb-hover:
|
|
40
|
-
|
|
41
|
-
--md-text:
|
|
42
|
-
--md-heading:
|
|
43
|
-
--md-heading-h3:
|
|
44
|
-
--md-h2-border:
|
|
45
|
-
--md-code-bg:
|
|
88
|
+
--scrollbar-track: rgb(var(--dc-gray-950));
|
|
89
|
+
--scrollbar-thumb: rgb(var(--dc-gray-700));
|
|
90
|
+
--scrollbar-thumb-hover: rgb(var(--dc-gray-600));
|
|
91
|
+
|
|
92
|
+
--md-text: rgb(var(--dc-gray-200));
|
|
93
|
+
--md-heading: rgb(var(--dc-gray-100));
|
|
94
|
+
--md-heading-h3: rgb(var(--dc-gray-300));
|
|
95
|
+
--md-h2-border: rgb(var(--dc-gray-700));
|
|
96
|
+
--md-code-bg: rgb(var(--dc-gray-800));
|
|
46
97
|
--md-code-color: #f472b6;
|
|
47
|
-
--md-code-border:
|
|
48
|
-
--md-pre-bg:
|
|
49
|
-
--md-pre-border:
|
|
50
|
-
--md-blockquote-color:
|
|
51
|
-
--md-blockquote-border:
|
|
52
|
-
--md-hr:
|
|
53
|
-
--md-link:
|
|
54
|
-
--md-link-hover:
|
|
55
|
-
--md-table-th-bg:
|
|
56
|
-
--md-table-th-color:
|
|
57
|
-
--md-table-border:
|
|
58
|
-
--md-table-td-color:
|
|
59
|
-
--md-table-even-bg:
|
|
60
|
-
--md-strong:
|
|
98
|
+
--md-code-border: rgb(var(--dc-gray-700));
|
|
99
|
+
--md-pre-bg: rgb(var(--dc-gray-950));
|
|
100
|
+
--md-pre-border: rgb(var(--dc-gray-700));
|
|
101
|
+
--md-blockquote-color: rgb(var(--dc-gray-400));
|
|
102
|
+
--md-blockquote-border: rgb(var(--dc-accent-400));
|
|
103
|
+
--md-hr: rgb(var(--dc-gray-700));
|
|
104
|
+
--md-link: rgb(var(--dc-accent-300));
|
|
105
|
+
--md-link-hover: rgb(var(--dc-accent-200));
|
|
106
|
+
--md-table-th-bg: rgb(var(--dc-gray-800));
|
|
107
|
+
--md-table-th-color: rgb(var(--dc-gray-300));
|
|
108
|
+
--md-table-border: rgb(var(--dc-gray-700));
|
|
109
|
+
--md-table-td-color: rgb(var(--dc-gray-200));
|
|
110
|
+
--md-table-even-bg: rgb(var(--dc-gray-800) / 0.4);
|
|
111
|
+
--md-strong: rgb(var(--dc-gray-100));
|
|
61
112
|
--md-em: #c4b5fd;
|
|
62
113
|
|
|
63
|
-
--check-border:
|
|
64
|
-
--check-bg:
|
|
114
|
+
--check-border: rgb(var(--dc-gray-600));
|
|
115
|
+
--check-bg: rgb(var(--dc-gray-900));
|
|
65
116
|
}
|
|
66
117
|
|
|
118
|
+
/* Code blocks: keep the highlighter's token colours, sit them on the editor background. */
|
|
119
|
+
.dark .markdown-body pre code.hljs,
|
|
120
|
+
.dark .hljs { background: var(--md-pre-bg); }
|
|
121
|
+
|
|
67
122
|
/* ── Scrollbar ──────────────────────────────────────────────────────────── */
|
|
68
123
|
|
|
69
124
|
::-webkit-scrollbar { width: 6px; height: 6px; }
|
|
@@ -184,15 +239,15 @@
|
|
|
184
239
|
transition: background-color 0.12s, border-color 0.12s;
|
|
185
240
|
}
|
|
186
241
|
.dc-check:checked {
|
|
187
|
-
background-color:
|
|
188
|
-
border-color:
|
|
242
|
+
background-color: rgb(var(--dc-accent-600));
|
|
243
|
+
border-color: rgb(var(--dc-accent-600));
|
|
189
244
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='white' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 10.5l3.5 3.5L15 7'/%3E%3C/svg%3E");
|
|
190
245
|
background-size: 72%;
|
|
191
246
|
background-position: center;
|
|
192
247
|
background-repeat: no-repeat;
|
|
193
248
|
}
|
|
194
249
|
.dc-check:focus-visible {
|
|
195
|
-
outline: 2px solid
|
|
250
|
+
outline: 2px solid rgb(var(--dc-accent-500));
|
|
196
251
|
outline-offset: 2px;
|
|
197
252
|
}
|
|
198
253
|
|
|
@@ -220,7 +275,7 @@
|
|
|
220
275
|
}
|
|
221
276
|
.dc-resize-handle:hover::after,
|
|
222
277
|
.dc-resizing .dc-resize-handle:active::after {
|
|
223
|
-
background-color:
|
|
278
|
+
background-color: rgb(var(--dc-accent-500) / 0.85);
|
|
224
279
|
}
|
|
225
280
|
.dc-resizing {
|
|
226
281
|
user-select: none;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2006, Ivan Sagalaev.
|
|
4
|
+
All rights reserved.
|
|
5
|
+
|
|
6
|
+
Redistribution and use in source and binary forms, with or without
|
|
7
|
+
modification, are permitted provided that the following conditions are met:
|
|
8
|
+
|
|
9
|
+
* Redistributions of source code must retain the above copyright notice, this
|
|
10
|
+
list of conditions and the following disclaimer.
|
|
11
|
+
|
|
12
|
+
* Redistributions in binary form must reproduce the above copyright notice,
|
|
13
|
+
this list of conditions and the following disclaimer in the documentation
|
|
14
|
+
and/or other materials provided with the distribution.
|
|
15
|
+
|
|
16
|
+
* Neither the name of the copyright holder nor the names of its
|
|
17
|
+
contributors may be used to endorse or promote products derived from
|
|
18
|
+
this software without specific prior written permission.
|
|
19
|
+
|
|
20
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
21
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
22
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
23
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
24
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
25
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
26
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
27
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
28
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
29
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|