devcoach 2.6.1 → 2.6.3

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.
@@ -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 and
90
- lifts the `max-width` of `.course`, `section` and `header.intro` to use the width it has — so
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
- **Design.** One measure (~68ch), generous spacing, quiet Tufte-like typography; light + dark
113
- using the dashboard's palette (greys, indigo `#4f46e5` accents, rose for warnings). It must
114
- also pass as a Claude artifact page: `<title>` is a 2–4 word name (the long title is the
115
- `<h1>`), colour tokens live on `:root`, dark tokens are set under
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
- Skeleton (adapt, keep the ids and the class names):
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:#f8fafc; --panel:#fff; --text:#0f172a; --muted:#64748b; --line:#e2e8f0; --accent:#4f46e5; --accent-soft:#eef2ff; --warn:#e11d48; }
128
- @media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) { --bg:#0b1220; --panel:#111a2e; --text:#e2e8f0; --muted:#94a3b8; --line:#1e293b; --accent:#818cf8; --accent-soft:#1e1b4b; --warn:#fb7185; } }
129
- :root[data-theme="dark"] { --bg:#0b1220; --panel:#111a2e; --text:#e2e8f0; --muted:#94a3b8; --line:#1e293b; --accent:#818cf8; --accent-soft:#1e1b4b; --warn:#fb7185; }
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
- :root[data-embedded] nav.steps { display:none } :root[data-embedded] .course { grid-template-columns:minmax(0,1fr); }
135
- section { display:none; max-width:68ch; } section.current { display:block; }
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, buttons … */
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></nav>
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> … <div class="check">…<button>Reveal</button></div>
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) =&gt; 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, nothing more
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: *"Publish this course as a Claude artifact too? (private until you share the
158
- link)"* — **Yes** → publish `document_path` as it is (`icon: "course"`, `description` = the
159
- goal) and reply with the link in one line; **No**, or no Artifact tool → nothing. The local
160
- file stays the course; the artifact is a copy for sharing, and the user can ask for it again
161
- any time ("publish the course as an artifact"). Never store the link anywhere.
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. Standalone opens (a pasted
5
- // URL, the artifact copy) never run this: `parent === window` exits at once.
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
- // …and use the width the dashboard gives it: the reading-column caps of the contract's
14
- // `.course` / `section` / `header.intro` are for the standalone page.
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-indigo-50", "dark:bg-indigo-900/30"];
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) {
@@ -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: #6366f1;
71
+ --md-blockquote-border: rgb(var(--dc-accent-500));
21
72
  --md-hr: #e5e7eb;
22
- --md-link: #4f46e5;
23
- --md-link-hover: #6366f1;
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: #111827;
38
- --scrollbar-thumb: #374151;
39
- --scrollbar-thumb-hover: #4b5563;
40
-
41
- --md-text: #e5e7eb;
42
- --md-heading: #f9fafb;
43
- --md-heading-h3: #d1d5db;
44
- --md-h2-border: #374151;
45
- --md-code-bg: #1f2937;
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: #374151;
48
- --md-pre-bg: #282c34;
49
- --md-pre-border: #374151;
50
- --md-blockquote-color: #9ca3af;
51
- --md-blockquote-border: #6366f1;
52
- --md-hr: #374151;
53
- --md-link: #818cf8;
54
- --md-link-hover: #a5b4fc;
55
- --md-table-th-bg: #1f2937;
56
- --md-table-th-color: #d1d5db;
57
- --md-table-border: #374151;
58
- --md-table-td-color: #e5e7eb;
59
- --md-table-even-bg: #111827;
60
- --md-strong: #f3f4f6;
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: #4b5563;
64
- --check-bg: #111827;
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: #4f46e5;
188
- border-color: #4f46e5;
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 #6366f1;
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: rgba(99, 102, 241, 0.85);
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.