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.
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import {a as a$2,c}from'./chunk-QU6UQJWC.js';import {p,q,h as h$1,m,a as a$1,e,g,j as j$1,l,n}from'./chunk-54UDP4GL.js';import {w,A,x,v,a,C,T,xa,Ba,ya,N,R,O,S,za,P as P$1,Q as Q$1,H,Ca,na,ra,J as J$1,I,L,D,G,Aa,F,K as K$1,U,t,wa,va,ka,ma,o,pa,oa,qa,ta,sa}from'./chunk-QWZZWWAN.js';import {existsSync,readFileSync,mkdirSync,writeFileSync}from'fs';import {dirname,join,extname}from'path';import {fileURLToPath}from'url';import {serve}from'@hono/node-server';import {Hono}from'hono';import {html,raw}from'hono/html';function j(r){return JSON.stringify(r??"").replace(/[<>&\u2028\u2029]/g,e=>String.raw`\u`+(e.codePointAt(0)??0).toString(16).padStart(4,"0"))}function Ye(r,e){return r+e.replace(/[^A-Za-z0-9_-]/g,g=>`_${(g.codePointAt(0)??0).toString(16)}`)}var nt=html`<svg class="w-4 h-4" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><circle cx="4" cy="10" r="1.75"/><circle cx="10" cy="10" r="1.75"/><circle cx="16" cy="10" r="1.75"/></svg>`;function Y(r,e="",g=""){return html`<div class="relative ${e}" ${raw(g)} x-data="{ open: false }" @click.outside="open = false" @keydown.escape="open = false">
3
- <button type="button" @click="open = !open" :aria-expanded="open" aria-label="More actions" title="More actions" class="w-8 h-8 inline-flex items-center justify-center rounded-lg border transition bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-500 dark:text-gray-400 hover:border-indigo-400 hover:text-gray-800 dark:hover:text-gray-100">${nt}</button>
2
+ import {a as a$2,c as c$1}from'./chunk-QU6UQJWC.js';import {p,q,h as h$1,m,a as a$1,e,g,j as j$1,l,n}from'./chunk-WETN2PWT.js';import {A,E as E$1,B as B$1,z,a,G,X as X$1,Ba,Fa,Ca,R,V as V$1,S,W as W$1,Da,T,U,L,Ga,ra,va,N,M,P as P$1,H,K as K$1,Ea,J as J$1,O,Y as Y$1,x,Aa,za,c,d,b as b$1,oa,qa,s,ta,sa,ua,xa,wa}from'./chunk-LNOPC5QA.js';import {existsSync,readFileSync,mkdirSync,writeFileSync}from'fs';import {dirname,join,extname}from'path';import {fileURLToPath}from'url';import {serve}from'@hono/node-server';import {Hono}from'hono';import {html,raw}from'hono/html';function j(r){return JSON.stringify(r??"").replace(/[<>&\u2028\u2029]/g,e=>String.raw`\u`+(e.codePointAt(0)??0).toString(16).padStart(4,"0"))}function We(r,e){return r+e.replace(/[^A-Za-z0-9_-]/g,g=>`_${(g.codePointAt(0)??0).toString(16)}`)}var lt=html`<svg class="w-4 h-4" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><circle cx="4" cy="10" r="1.75"/><circle cx="10" cy="10" r="1.75"/><circle cx="16" cy="10" r="1.75"/></svg>`;function Y(r,e="",g=""){return html`<div class="relative ${e}" ${raw(g)} x-data="{ open: false }" @click.outside="open = false" @keydown.escape="open = false">
3
+ <button type="button" @click="open = !open" :aria-expanded="open" aria-label="More actions" title="More actions" class="w-8 h-8 inline-flex items-center justify-center rounded-lg border transition bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-500 dark:text-gray-400 hover:border-accent-400 hover:text-gray-800 dark:hover:text-gray-100">${lt}</button>
4
4
  <div x-show="open" x-transition:enter="transition ease-out duration-100" x-transition:enter-start="opacity-0 scale-95" x-transition:enter-end="opacity-100 scale-100" class="absolute right-0 top-full mt-1 z-50 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-xl shadow-lg w-56 p-1" style="display:none">
5
5
  ${r}
6
6
  </div>
@@ -25,20 +25,44 @@ import {a as a$2,c}from'./chunk-QU6UQJWC.js';import {p,q,h as h$1,m,a as a$1,e,g
25
25
  <link rel="icon" type="image/svg+xml" href="/static/favicon.svg" />
26
26
  <meta name="devcoach-theme" content="${r.uiTheme}" />
27
27
  <script>
28
+ // Before first paint: the nav toggle's choice (this browser) wins over the saved setting;
29
+ // "system" follows the OS. Saving Settings clears the toggle's choice.
28
30
  (function () {
29
31
  var serverTheme = document.querySelector('meta[name="devcoach-theme"]').content;
30
- var session = localStorage.getItem('theme-override');
31
- var active = session || serverTheme;
32
+ var chosen = null;
33
+ try { chosen = localStorage.getItem('theme-override'); } catch (e) {}
34
+ var active = chosen || serverTheme;
32
35
  var dark = active === 'dark' || (active === 'system' && globalThis.matchMedia('(prefers-color-scheme: dark)').matches);
33
- if (dark) document.documentElement.classList.add('dark');
36
+ document.documentElement.classList.toggle('dark', dark);
34
37
  })();
35
38
  </script>
36
39
  <script src="/static/vendor/tailwind.js"></script>
37
- <script>tailwind.config = { darkMode: 'class' }</script>
40
+ <script>
41
+ // The palette lives in style.css as RGB triplets (--dc-gray-*, --dc-accent-*): Tailwind's
42
+ // gray scale and the accent family point at them, light and dark.
43
+ (function () {
44
+ var scale = function (name) {
45
+ var out = {};
46
+ [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950].forEach(function (n) {
47
+ out[n] = 'rgb(var(--dc-' + name + '-' + n + ') / <alpha-value>)';
48
+ });
49
+ return out;
50
+ };
51
+ tailwind.config = { darkMode: 'class', theme: { extend: { colors: { gray: scale('gray'), accent: scale('accent') } } } };
52
+ })();
53
+ </script>
38
54
  <script src="/static/vendor/htmx.min.js"></script>
39
55
  <script src="/static/vendor/alpinejs.min.js" defer></script>
40
56
  <link rel="stylesheet" href="/static/style.css" />
41
57
  ${r.head??""}
58
+ <script>
59
+ // Pages with code blocks ship the dark highlighter stylesheet: point it at the light one
60
+ // before first paint when the page is light.
61
+ (function () {
62
+ var sheet = document.getElementById('hljs-theme');
63
+ if (sheet && !document.documentElement.classList.contains('dark')) sheet.href = '/static/vendor/hljs-light.min.css';
64
+ })();
65
+ </script>
42
66
  <script>
43
67
  // Older devcoach on a newer database → the dashboard is read-only: one banner on every page.
44
68
  fetch('/ping').then(function (r) { return r.json(); }).then(function (p) {
@@ -62,75 +86,93 @@ import {a as a$2,c}from'./chunk-QU6UQJWC.js';import {p,q,h as h$1,m,a as a$1,e,g
62
86
  ${e("/lessons","Lessons",r.currentPath.includes("/lessons"))}
63
87
  ${e("/courses","Courses",r.currentPath.includes("/courses"))}
64
88
  <div class="ml-auto flex items-center gap-1.5">
65
- <a href="/settings" title="Settings" ${r.currentPath==="/settings"?raw('aria-current="page"'):""} class="h-8 px-2.5 inline-flex items-center gap-1.5 rounded-lg border text-sm font-medium transition ${r.currentPath==="/settings"?"bg-white dark:bg-gray-900 border-indigo-400 text-gray-900 dark:text-white":"bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-500 dark:text-gray-400 hover:border-indigo-400 hover:text-gray-800 dark:hover:text-gray-100"}"><svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 15.5a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7Z"/><path d="M19.4 15a1.7 1.7 0 0 0 .34 1.87l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.7 1.7 0 0 0-1.87-.34 1.7 1.7 0 0 0-1.03 1.56V21a2 2 0 1 1-4 0v-.09a1.7 1.7 0 0 0-1.11-1.56 1.7 1.7 0 0 0-1.87.34l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.7 1.7 0 0 0 4.6 15a1.7 1.7 0 0 0-1.56-1.03H3a2 2 0 1 1 0-4h.09A1.7 1.7 0 0 0 4.65 8.9a1.7 1.7 0 0 0-.34-1.87l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.7 1.7 0 0 0 9 4.6h.09A1.7 1.7 0 0 0 10.1 3.1V3a2 2 0 1 1 4 0v.09a1.7 1.7 0 0 0 1.03 1.56 1.7 1.7 0 0 0 1.87-.34l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.7 1.7 0 0 0 19.4 9v.09a1.7 1.7 0 0 0 1.56 1.03H21a2 2 0 1 1 0 4h-.09A1.7 1.7 0 0 0 19.4 15Z"/></svg><span>Settings</span></a>
66
- <button id="theme-toggle" onclick="toggleTheme()" title="Toggle theme" aria-label="Toggle theme"
67
- class="w-8 h-8 inline-flex items-center justify-center rounded-lg border transition bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-500 dark:text-gray-400 hover:border-indigo-400 text-base leading-none"></button>
89
+ <a href="/settings" title="Settings" ${r.currentPath==="/settings"?raw('aria-current="page"'):""} class="h-8 px-2.5 inline-flex items-center gap-1.5 rounded-lg border text-sm font-medium transition ${r.currentPath==="/settings"?"bg-white dark:bg-gray-900 border-accent-400 text-gray-900 dark:text-white":"bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-500 dark:text-gray-400 hover:border-accent-400 hover:text-gray-800 dark:hover:text-gray-100"}"><svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 15.5a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7Z"/><path d="M19.4 15a1.7 1.7 0 0 0 .34 1.87l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.7 1.7 0 0 0-1.87-.34 1.7 1.7 0 0 0-1.03 1.56V21a2 2 0 1 1-4 0v-.09a1.7 1.7 0 0 0-1.11-1.56 1.7 1.7 0 0 0-1.87.34l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.7 1.7 0 0 0 4.6 15a1.7 1.7 0 0 0-1.56-1.03H3a2 2 0 1 1 0-4h.09A1.7 1.7 0 0 0 4.65 8.9a1.7 1.7 0 0 0-.34-1.87l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.7 1.7 0 0 0 9 4.6h.09A1.7 1.7 0 0 0 10.1 3.1V3a2 2 0 1 1 4 0v.09a1.7 1.7 0 0 0 1.03 1.56 1.7 1.7 0 0 0 1.87-.34l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.7 1.7 0 0 0 19.4 9v.09a1.7 1.7 0 0 0 1.56 1.03H21a2 2 0 1 1 0 4h-.09A1.7 1.7 0 0 0 19.4 15Z"/></svg><span>Settings</span></a>
90
+ <!-- Shows the CURRENT scheme (both glyphs ship, the .dark class picks one: right from the
91
+ first paint, no script needed); the label says what a click does. -->
92
+ <button id="theme-toggle" onclick="toggleTheme()" title="Switch theme" aria-label="Switch theme"
93
+ class="w-8 h-8 inline-flex items-center justify-center rounded-lg border transition bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-500 dark:text-gray-400 hover:border-accent-400 text-base leading-none"><span class="dark:hidden" aria-hidden="true">☀️</span><span class="hidden dark:inline" aria-hidden="true">🌙</span></button>
68
94
  </div>
69
95
  </nav>
70
96
  <main class="${r.wide?"max-w-none":"max-w-7xl"} mx-auto px-4 sm:px-6 py-8">${r.body}</main>
71
97
  <script>
72
98
  function isDark() { return document.documentElement.classList.contains('dark'); }
73
- function updateThemeIcon() { var b = document.getElementById('theme-toggle'); if (b) b.textContent = isDark() ? '☀️' : '🌙'; }
74
99
  function updateHljsTheme() {
75
100
  var link = document.getElementById('hljs-theme'); if (!link) return;
76
101
  link.href = isDark() ? '/static/vendor/hljs-dark.min.css' : '/static/vendor/hljs-light.min.css';
77
102
  }
103
+ // The one place a theme is applied: class, the toggle's label, the highlighter, and the
104
+ // dc:theme event other scripts listen to (the course viewer forwards it into its frame).
105
+ function applyTheme(dark) {
106
+ document.documentElement.classList.toggle('dark', dark);
107
+ var b = document.getElementById('theme-toggle');
108
+ if (b) {
109
+ var label = dark ? 'Dark theme — switch to light' : 'Light theme — switch to dark';
110
+ b.title = label;
111
+ b.setAttribute('aria-label', label);
112
+ }
113
+ updateHljsTheme();
114
+ document.dispatchEvent(new CustomEvent('dc:theme', { detail: { dark: dark } }));
115
+ }
116
+ function themeChoice() { try { return localStorage.getItem('theme-override'); } catch (e) { return null; } }
78
117
  function toggleTheme() {
79
- document.documentElement.classList.toggle('dark');
80
- localStorage.setItem('theme-override', isDark() ? 'dark' : 'light');
81
- updateThemeIcon(); updateHljsTheme();
118
+ var dark = !isDark();
119
+ try { localStorage.setItem('theme-override', dark ? 'dark' : 'light'); } catch (e) {}
120
+ applyTheme(dark);
82
121
  }
83
122
  (function () {
84
123
  var serverTheme = document.querySelector('meta[name="devcoach-theme"]').content;
85
- if (serverTheme !== 'system') localStorage.removeItem('theme-override');
124
+ // "System" follows the OS live, until the toggle makes a choice in this browser.
125
+ globalThis.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function (e) {
126
+ if (serverTheme === 'system' && !themeChoice()) applyTheme(e.matches);
127
+ });
128
+ applyTheme(isDark());
86
129
  })();
87
- updateThemeIcon(); updateHljsTheme();
88
130
  </script>
89
131
  ${r.scripts??""}
90
132
  </body>
91
- </html>`}function Je(r){let e=d=>r.allGroups.filter(t=>t!=="Other").map(t=>html`<option value="${t}" ${t===d?"selected":""}>${t}</option>`),g=html`
133
+ </html>`}function Ze(r){let e=d=>r.allGroups.filter(t=>t!=="Other").map(t=>html`<option value="${t}" ${t===d?"selected":""}>${t}</option>`),g=html`
92
134
  <div id="knowledge-map" x-data="{ editMode: JSON.parse(localStorage.getItem('km-edit-mode') || 'false'), toggle() { this.editMode = !this.editMode; localStorage.setItem('km-edit-mode', this.editMode); } }">
93
135
 
94
136
  <div class="flex items-center justify-between mb-6">
95
- <h1 class="text-2xl font-bold text-indigo-600 dark:text-indigo-400">Knowledge Map</h1>
137
+ <h1 class="text-2xl font-bold text-accent-600 dark:text-accent-400">Knowledge Map</h1>
96
138
  <div class="flex items-center gap-2">
97
139
  <div x-show="editMode" style="display:none" x-data="{ open: false }" class="relative">
98
140
  <button type="button" @click="open = !open"
99
- class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-indigo-400">+ Add group</button>
141
+ class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-accent-400">+ Add group</button>
100
142
  <div x-show="open" @click.outside="open = false" class="absolute right-0 top-full mt-1 z-50 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-xl shadow-lg p-3 w-56" style="display:none">
101
143
  <form method="post" action="/groups" hx-post="/groups" hx-target="#knowledge-map" hx-select="#knowledge-map" hx-swap="outerHTML" class="flex gap-2">
102
- <input type="text" name="group_name" placeholder="Group name…" required class="flex-1 text-sm bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg px-2.5 py-1.5 text-gray-800 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-indigo-500" />
103
- <button type="submit" class="px-3 py-1.5 rounded-lg text-sm font-medium bg-indigo-600 text-white hover:bg-indigo-500 transition">Add</button>
144
+ <input type="text" name="group_name" placeholder="Group name…" required class="flex-1 text-sm bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg px-2.5 py-1.5 text-gray-800 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-accent-500" />
145
+ <button type="submit" class="px-3 py-1.5 rounded-lg text-sm font-medium bg-accent-600 text-white hover:bg-accent-500 transition">Add</button>
104
146
  </form>
105
147
  </div>
106
148
  </div>
107
149
  <div x-show="editMode" style="display:none" x-data="{ open: false }" class="relative">
108
- <button type="button" @click="open = !open" class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-indigo-400">+ Add topic</button>
150
+ <button type="button" @click="open = !open" class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-accent-400">+ Add topic</button>
109
151
  <div x-show="open" @click.outside="open = false" class="absolute right-0 top-full mt-1 z-50 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-xl shadow-lg p-4 w-72" style="display:none">
110
152
  <form method="post" action="/knowledge" hx-post="/knowledge" hx-target="#knowledge-map" hx-select="#knowledge-map" hx-swap="outerHTML" class="space-y-3">
111
153
  <div>
112
154
  <label for="add-topic-id" class="block text-xs text-gray-400 dark:text-gray-500 mb-1">Topic ID</label>
113
- <input id="add-topic-id" type="text" name="topic" placeholder="e.g. rust_lifetimes" required class="w-full text-sm bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg px-2.5 py-1.5 text-gray-800 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-indigo-500" />
155
+ <input id="add-topic-id" type="text" name="topic" placeholder="e.g. rust_lifetimes" required class="w-full text-sm bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg px-2.5 py-1.5 text-gray-800 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-accent-500" />
114
156
  </div>
115
157
  <div class="flex gap-2">
116
158
  <div class="w-24">
117
159
  <label for="add-topic-confidence" class="block text-xs text-gray-400 dark:text-gray-500 mb-1">Confidence</label>
118
- <input id="add-topic-confidence" type="number" name="confidence" value="5" min="0" max="10" class="w-full text-sm bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg px-2.5 py-1.5 text-gray-800 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-indigo-500" />
160
+ <input id="add-topic-confidence" type="number" name="confidence" value="5" min="0" max="10" class="w-full text-sm bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg px-2.5 py-1.5 text-gray-800 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-accent-500" />
119
161
  </div>
120
162
  <div class="flex-1">
121
163
  <label for="add-topic-group" class="block text-xs text-gray-400 dark:text-gray-500 mb-1">Group</label>
122
- <select id="add-topic-group" name="group" class="w-full text-sm bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg px-2.5 py-1.5 text-gray-800 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-indigo-500">
164
+ <select id="add-topic-group" name="group" class="w-full text-sm bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg px-2.5 py-1.5 text-gray-800 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-accent-500">
123
165
  <option value="">Other (ungrouped)</option>
124
166
  ${e("")}
125
167
  </select>
126
168
  </div>
127
169
  </div>
128
- <button type="submit" class="w-full px-3 py-1.5 rounded-lg text-sm font-medium bg-indigo-600 text-white hover:bg-indigo-500 transition">Add</button>
170
+ <button type="submit" class="w-full px-3 py-1.5 rounded-lg text-sm font-medium bg-accent-600 text-white hover:bg-accent-500 transition">Add</button>
129
171
  </form>
130
172
  </div>
131
173
  </div>
132
174
  <button type="button" @click="toggle()"
133
- :class="editMode ? 'bg-indigo-600 text-white border-indigo-600 hover:bg-indigo-500' : 'bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-indigo-400'"
175
+ :class="editMode ? 'bg-accent-600 text-white border-accent-600 hover:bg-accent-500' : 'bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-accent-400'"
134
176
  class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition">
135
177
  <span x-text="editMode ? '✓ Done' : '✎ Edit'">✎ Edit</span>
136
178
  </button>
@@ -157,27 +199,27 @@ ${Object.entries(r.categorised).map(([d,t])=>html`
157
199
  </form>`:""}
158
200
  </div>
159
201
  <div x-show="editMode" style="display:none" x-data="{ open: false }" class="relative">
160
- <button type="button" @click="open = !open" class="text-xs text-gray-300 dark:text-gray-600 hover:text-indigo-500 dark:hover:text-indigo-400 transition px-1">+ topic</button>
202
+ <button type="button" @click="open = !open" class="text-xs text-gray-300 dark:text-gray-600 hover:text-accent-500 dark:hover:text-accent-400 transition px-1">+ topic</button>
161
203
  <div x-show="open" @click.outside="open = false" class="absolute right-0 top-full mt-1 z-50 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-xl shadow-lg p-3 w-64" style="display:none">
162
204
  <form method="post" action="/knowledge" hx-post="/knowledge" hx-target="#knowledge-map" hx-select="#knowledge-map" hx-swap="outerHTML" class="space-y-2">
163
205
  <input type="hidden" name="group" value="${d!=="Other"?d:""}" />
164
- <input type="text" name="topic" placeholder="topic_id (e.g. rust_lifetimes)" required class="w-full text-sm bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg px-2.5 py-1.5 text-gray-800 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-indigo-500" />
206
+ <input type="text" name="topic" placeholder="topic_id (e.g. rust_lifetimes)" required class="w-full text-sm bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg px-2.5 py-1.5 text-gray-800 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-accent-500" />
165
207
  <div class="flex items-center gap-2">
166
208
  <label class="text-xs text-gray-400 dark:text-gray-500 shrink-0">Confidence</label>
167
- <input type="number" name="confidence" value="5" min="0" max="10" class="w-16 text-sm bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg px-2 py-1 text-gray-800 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-indigo-500" />
168
- <button type="submit" class="flex-1 px-3 py-1.5 rounded-lg text-sm font-medium bg-indigo-600 text-white hover:bg-indigo-500 transition">Add</button>
209
+ <input type="number" name="confidence" value="5" min="0" max="10" class="w-16 text-sm bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg px-2 py-1 text-gray-800 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-accent-500" />
210
+ <button type="submit" class="flex-1 px-3 py-1.5 rounded-lg text-sm font-medium bg-accent-600 text-white hover:bg-accent-500 transition">Add</button>
169
211
  </div>
170
212
  </form>
171
213
  </div>
172
214
  </div>
173
215
  </div>
174
216
  <div class="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-x-8 gap-y-0">
175
- ${t.map(o=>{let n=Ye("topic-row-",o.topic),i=o.confidence,s=Math.trunc(i/10*100),c=i>=7?"bg-green-500":i>=4?"bg-yellow-500":"bg-red-500",u=i>=7?"text-green-600 dark:text-green-400":i>=4?"text-yellow-600 dark:text-yellow-400":"text-red-600 dark:text-red-400";return html`
217
+ ${t.map(a=>{let n=We("topic-row-",a.topic),i=a.confidence,s=Math.trunc(i/10*100),c=i>=7?"bg-green-500":i>=4?"bg-yellow-500":"bg-red-500",u=i>=7?"text-green-600 dark:text-green-400":i>=4?"text-yellow-600 dark:text-yellow-400":"text-red-600 dark:text-red-400";return html`
176
218
  <div id="${n}" class="flex items-center gap-2 py-1.5 border-b border-gray-100 dark:border-gray-800/50 group/row">
177
219
  <div x-show="editMode" style="display:none" x-data="{ open: false }" class="relative shrink-0 flex items-center">
178
- <button type="button" @click="open = !open" class="text-xs text-gray-200 dark:text-gray-700 hover:text-indigo-500 dark:hover:text-indigo-400 transition w-4 text-center opacity-0 group-hover/row:opacity-100" title="Move to group">⇄</button>
220
+ <button type="button" @click="open = !open" class="text-xs text-gray-200 dark:text-gray-700 hover:text-accent-500 dark:hover:text-accent-400 transition w-4 text-center opacity-0 group-hover/row:opacity-100" title="Move to group">⇄</button>
179
221
  <div x-show="open" @click.outside="open = false" class="absolute left-0 top-full mt-1 z-50 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-xl shadow-lg p-1 w-40" style="display:none">
180
- <form method="post" action="/knowledge/${encodeURIComponent(o.topic)}/group" hx-post="/knowledge/${encodeURIComponent(o.topic)}/group" hx-target="#knowledge-map" hx-select="#knowledge-map" hx-swap="outerHTML">
222
+ <form method="post" action="/knowledge/${encodeURIComponent(a.topic)}/group" hx-post="/knowledge/${encodeURIComponent(a.topic)}/group" hx-target="#knowledge-map" hx-select="#knowledge-map" hx-swap="outerHTML">
181
223
  <select name="group" onchange="this.form.requestSubmit()" class="w-full text-xs bg-transparent text-gray-700 dark:text-gray-200 px-2 py-1 focus:outline-none">
182
224
  <option value="Other" ${d==="Other"?"selected":""}>Other (ungrouped)</option>
183
225
  ${e(d)}
@@ -186,20 +228,20 @@ ${Object.entries(r.categorised).map(([d,t])=>html`
186
228
  </div>
187
229
  </div>
188
230
  <div class="flex items-center gap-1 flex-1 min-w-0">
189
- <a x-show="!editMode" href="/lessons?search=${encodeURIComponent(o.topic)}" class="text-sm text-gray-700 dark:text-gray-200 min-w-0 truncate hover:text-indigo-600 dark:hover:text-indigo-400 hover:underline transition" title="${o.topic}">${o.topic}</a>
190
- <span x-show="editMode" style="display:none" class="text-sm text-gray-700 dark:text-gray-200 min-w-0 truncate" title="${o.topic}">${o.topic}</span>
191
- <form x-show="editMode" style="display:none" method="post" action="/knowledge/${encodeURIComponent(o.topic)}/delete" hx-post="/knowledge/${encodeURIComponent(o.topic)}/delete" hx-target="#knowledge-map" hx-select="#knowledge-map" hx-swap="outerHTML" hx-confirm="Remove ${o.topic} from your knowledge map?" class="shrink-0 flex items-center">
231
+ <a x-show="!editMode" href="/lessons?search=${encodeURIComponent(a.topic)}" class="text-sm text-gray-700 dark:text-gray-200 min-w-0 truncate hover:text-accent-600 dark:hover:text-accent-400 hover:underline transition" title="${a.topic}">${a.topic}</a>
232
+ <span x-show="editMode" style="display:none" class="text-sm text-gray-700 dark:text-gray-200 min-w-0 truncate" title="${a.topic}">${a.topic}</span>
233
+ <form x-show="editMode" style="display:none" method="post" action="/knowledge/${encodeURIComponent(a.topic)}/delete" hx-post="/knowledge/${encodeURIComponent(a.topic)}/delete" hx-target="#knowledge-map" hx-select="#knowledge-map" hx-swap="outerHTML" hx-confirm="Remove ${a.topic} from your knowledge map?" class="shrink-0 flex items-center">
192
234
  <button type="submit" class="text-xs text-gray-200 dark:text-gray-700 hover:text-red-500 dark:hover:text-red-400 transition w-4 text-center opacity-0 group-hover/row:opacity-100" title="Remove topic">×</button>
193
235
  </form>
194
236
  </div>
195
- <form x-show="editMode" style="display:none" method="post" action="/knowledge/${encodeURIComponent(o.topic)}" hx-post="/knowledge/${encodeURIComponent(o.topic)}" hx-target="#${n}" hx-select="#${n}" hx-swap="outerHTML" class="shrink-0 flex items-center">
237
+ <form x-show="editMode" style="display:none" method="post" action="/knowledge/${encodeURIComponent(a.topic)}" hx-post="/knowledge/${encodeURIComponent(a.topic)}" hx-target="#${n}" hx-select="#${n}" hx-swap="outerHTML" class="shrink-0 flex items-center">
196
238
  <input type="hidden" name="delta" value="-1" />
197
239
  <button class="text-xs text-gray-300 dark:text-gray-600 hover:text-red-500 dark:hover:text-red-400 transition w-5 text-center leading-none">−</button>
198
240
  </form>
199
241
  <div class="w-20 h-1.5 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden shrink-0">
200
242
  <div class="${c} h-full rounded-full transition-all" style="width: ${s}%"></div>
201
243
  </div>
202
- <form x-show="editMode" style="display:none" method="post" action="/knowledge/${encodeURIComponent(o.topic)}" hx-post="/knowledge/${encodeURIComponent(o.topic)}" hx-target="#${n}" hx-select="#${n}" hx-swap="outerHTML" class="shrink-0 flex items-center">
244
+ <form x-show="editMode" style="display:none" method="post" action="/knowledge/${encodeURIComponent(a.topic)}" hx-post="/knowledge/${encodeURIComponent(a.topic)}" hx-target="#${n}" hx-select="#${n}" hx-swap="outerHTML" class="shrink-0 flex items-center">
203
245
  <input type="hidden" name="delta" value="1" />
204
246
  <button class="text-xs text-gray-300 dark:text-gray-600 hover:text-green-600 dark:hover:text-green-400 transition w-5 text-center leading-none">+</button>
205
247
  </form>
@@ -207,21 +249,21 @@ ${Object.entries(r.categorised).map(([d,t])=>html`
207
249
  </div>`})}
208
250
  </div>
209
251
  </section>`)}
210
- </div>`;return P({title:"Profile \u2014 devcoach",currentPath:"/knowledge",uiTheme:r.uiTheme,body:g})}var Fe={all:"All time",today:"Today",week:"Last 7 days",month:"Last 30 days",year:"Last year"},Ge={know:"\u2713 Knew it",understood:"\u{1F4A1} Understood",dont_know:"\u2717 Couldn't follow",none:"\u2014 No response"},ze={junior:"\u{1F7E2}",mid:"\u{1F7E1}",senior:"\u{1F534}"};function h(r,e={}){let g={period:r.period,category:r.category,level:r.level,project:r.project,repository:r.repository,branch:r.branch,commit:r.commit,feedback:r.feedback,imported:r.imported,shared_by:r.shared_by,search:r.search,date_from:r.date_from,date_to:r.date_to,sort:r.sort,order:r.order};r.starred&&(g.starred="1"),Object.assign(g,e);let d=new URLSearchParams;for(let[t,o]of Object.entries(g))o&&d.set(t,o);return `?${d.toString()}`}function Ve(r){let e=r.s,g=!!(e.date_from||e.date_to),d=e.period!=="all"&&!g||g||!!(e.category||e.level||e.project||e.repository||e.branch||e.commit||e.starred||e.imported||e.shared_by||e.search||e.feedback),t=g?e.date_from&&e.date_to?`${e.date_from} \u2192 ${e.date_to}`:e.date_from?`From ${e.date_from}`:`Until ${e.date_to}`:Fe[e.period]??"All time",o=e.feedback?Ge[e.feedback]??"Feedback":"Feedback",n=e.shared_by?`\u{1F91D} from ${e.shared_by}`:e.imported==="1"?"\u{1F91D} Shared with me":e.imported==="0"?"\u{1F464} My own":"\u{1F91D} Shared",i=[["","","All lessons"],["0","","\u{1F464} My own"],["1","","\u{1F91D} Shared with me"],...r.allSharedBy.map(l=>["1",l,`\u{1F91D} from ${l}`])],s={junior:"text-teal-600 dark:text-teal-400 bg-teal-50 dark:bg-teal-900/20",mid:"text-amber-600 dark:text-amber-400 bg-amber-50 dark:bg-amber-900/20",senior:"text-rose-600 dark:text-rose-400 bg-rose-50 dark:bg-rose-900/20"},c=(l,x)=>html`<span class="inline-flex items-center gap-1 pl-2.5 pr-1 py-0.5 rounded-full text-xs bg-indigo-50 dark:bg-indigo-900/20 text-indigo-700 dark:text-indigo-300 border border-indigo-200 dark:border-indigo-800">${l}<a href="${x}" class="ml-0.5 w-4 h-4 flex items-center justify-center rounded-full hover:bg-indigo-200 dark:hover:bg-indigo-700 transition">×</a></span>`,u=(l,x,$="")=>{let S=e.sort===x,k=S&&e.order==="desc"?"asc":"desc";return html`<th class="px-3 py-3 whitespace-nowrap relative overflow-hidden text-ellipsis ${$}">
252
+ </div>`;return P({title:"Profile \u2014 devcoach",currentPath:"/knowledge",uiTheme:r.uiTheme,body:g})}var Ke={all:"All time",today:"Today",week:"Last 7 days",month:"Last 30 days",year:"Last year"},Ye={know:"\u2713 Knew it",understood:"\u{1F4A1} Understood",dont_know:"\u2717 Couldn't follow",none:"\u2014 No response"},Je={junior:"\u{1F7E2}",mid:"\u{1F7E1}",senior:"\u{1F534}"};function h(r,e={}){let g={period:r.period,category:r.category,level:r.level,project:r.project,repository:r.repository,branch:r.branch,commit:r.commit,feedback:r.feedback,imported:r.imported,shared_by:r.shared_by,search:r.search,date_from:r.date_from,date_to:r.date_to,sort:r.sort,order:r.order};r.starred&&(g.starred="1"),Object.assign(g,e);let d=new URLSearchParams;for(let[t,a]of Object.entries(g))a&&d.set(t,a);return `?${d.toString()}`}function Qe(r){let e=r.s,g=!!(e.date_from||e.date_to),d=e.period!=="all"&&!g||g||!!(e.category||e.level||e.project||e.repository||e.branch||e.commit||e.starred||e.imported||e.shared_by||e.search||e.feedback),t=g?e.date_from&&e.date_to?`${e.date_from} \u2192 ${e.date_to}`:e.date_from?`From ${e.date_from}`:`Until ${e.date_to}`:Ke[e.period]??"All time",a=e.feedback?Ye[e.feedback]??"Feedback":"Feedback",n=e.shared_by?`\u{1F91D} from ${e.shared_by}`:e.imported==="1"?"\u{1F91D} Shared with me":e.imported==="0"?"\u{1F464} My own":"\u{1F91D} Shared",i=[["","","All lessons"],["0","","\u{1F464} My own"],["1","","\u{1F91D} Shared with me"],...r.allSharedBy.map(l=>["1",l,`\u{1F91D} from ${l}`])],s={junior:"text-teal-600 dark:text-teal-400 bg-teal-50 dark:bg-teal-900/20",mid:"text-amber-600 dark:text-amber-400 bg-amber-50 dark:bg-amber-900/20",senior:"text-rose-600 dark:text-rose-400 bg-rose-50 dark:bg-rose-900/20"},c=(l,x)=>html`<span class="inline-flex items-center gap-1 pl-2.5 pr-1 py-0.5 rounded-full text-xs bg-accent-50 dark:bg-accent-900/20 text-accent-700 dark:text-accent-300 border border-accent-200 dark:border-accent-800">${l}<a href="${x}" class="ml-0.5 w-4 h-4 flex items-center justify-center rounded-full hover:bg-accent-200 dark:hover:bg-accent-700 transition">×</a></span>`,u=(l,x,$="")=>{let S=e.sort===x,k=S&&e.order==="desc"?"asc":"desc";return html`<th class="px-3 py-3 whitespace-nowrap relative overflow-hidden text-ellipsis ${$}">
211
253
  <a href="${h(e,{sort:x,order:k})}" class="hover:text-gray-700 dark:hover:text-gray-200 transition group/sort">${l}
212
- ${S?html`<span class="text-indigo-400">${e.order==="asc"?"\u2191":"\u2193"}</span>`:html`<span class="text-gray-200 dark:text-gray-700 group-hover/sort:text-gray-400 dark:group-hover/sort:text-gray-500 transition">↕</span>`}
254
+ ${S?html`<span class="text-accent-400">${e.order==="asc"?"\u2191":"\u2193"}</span>`:html`<span class="text-gray-200 dark:text-gray-700 group-hover/sort:text-gray-400 dark:group-hover/sort:text-gray-500 transition">↕</span>`}
213
255
  </a></th>`},p=html`<svg class="w-3 h-3 opacity-60" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M19 9l-7 7-7-7"/></svg>`,m=html`<link rel="stylesheet" href="/static/vendor/flatpickr.min.css" />
214
256
  <style>
215
- .dark .flatpickr-calendar { background:#1f2937; border-color:#374151; }
216
- .dark .flatpickr-day { color:#e5e7eb; }
217
- .dark .flatpickr-day:hover { background:#374151; }
218
- .dark .flatpickr-day.selected, .dark .flatpickr-day.startRange, .dark .flatpickr-day.endRange, .dark .flatpickr-day.inRange { background:#4f46e5; border-color:#4f46e5; color:#fff; }
219
- .dark .flatpickr-day.today { border-color:#6366f1; }
220
- .dark .flatpickr-months, .dark .flatpickr-month { background:#1f2937; color:#e5e7eb; fill:#e5e7eb; }
221
- .dark .flatpickr-current-month, .dark .flatpickr-monthDropdown-months { color:#e5e7eb; background:#1f2937; }
222
- .dark .flatpickr-weekday { color:#9ca3af; background:#1f2937; }
223
- .dark .flatpickr-prev-month svg, .dark .flatpickr-next-month svg { fill:#9ca3af; }
224
- </style>`,v=r.total===0?"No lessons":r.totalPages===1?`${r.total} lesson${r.total!==1?"s":""}`:`${(r.page-1)*r.perPage+1}\u2013${Math.min(r.page*r.perPage,r.total)} of ${r.total}`,I=JSON.stringify(r.lessons.map(l=>l.id)),R=html`
257
+ .dark .flatpickr-calendar { background:rgb(var(--dc-gray-900)); border-color:rgb(var(--dc-gray-700)); }
258
+ .dark .flatpickr-day { color:rgb(var(--dc-gray-200)); }
259
+ .dark .flatpickr-day:hover { background:rgb(var(--dc-gray-800)); }
260
+ .flatpickr-day.selected, .flatpickr-day.startRange, .flatpickr-day.endRange, .dark .flatpickr-day.inRange { background:rgb(var(--dc-accent-600)); border-color:rgb(var(--dc-accent-600)); color:white; }
261
+ .flatpickr-day.today { border-color:rgb(var(--dc-accent-500)); }
262
+ .dark .flatpickr-months, .dark .flatpickr-month { background:rgb(var(--dc-gray-900)); color:rgb(var(--dc-gray-200)); fill:rgb(var(--dc-gray-200)); }
263
+ .dark .flatpickr-current-month, .dark .flatpickr-monthDropdown-months { color:rgb(var(--dc-gray-200)); background:rgb(var(--dc-gray-900)); }
264
+ .dark .flatpickr-weekday { color:rgb(var(--dc-gray-400)); background:rgb(var(--dc-gray-900)); }
265
+ .dark .flatpickr-prev-month svg, .dark .flatpickr-next-month svg { fill:rgb(var(--dc-gray-400)); }
266
+ </style>`,v=r.total===0?"No lessons":r.totalPages===1?`${r.total} lesson${r.total!==1?"s":""}`:`${(r.page-1)*r.perPage+1}\u2013${Math.min(r.page*r.perPage,r.total)} of ${r.total}`,T=JSON.stringify(r.lessons.map(l=>l.id)),R=html`
225
267
  <div :class="selectMode && 'dc-selecting'" x-data="{ selectMode: false, selected: [], confirmOpen: false, shareOpen: false, sharePos: { left: 0, top: null, bottom: null }, openShare(btn) { const r = btn.getBoundingClientRect(); const w = 320; const left = Math.max(8, Math.min(r.right - w, innerWidth - w - 8)); const below = innerHeight - r.bottom > 380; this.sharePos = { left, top: below ? r.bottom + 6 : null, bottom: below ? null : innerHeight - r.top + 6 }; this.shareOpen = true }, toggle(id) { const i = this.selected.indexOf(id); if (i < 0) this.selected.push(id); else this.selected.splice(i, 1) }, setAll(ids, on) { this.selected = on ? ids.slice() : [] }, selectedTitles() { const rows = Array.from(document.querySelectorAll('tr[data-id]')); return this.selected.map((id) => { const r = rows.find((x) => x.dataset.id === id); return r ? r.dataset.title : id }) }, leave() { this.selectMode = false; this.selected = []; this.shareOpen = false; this.confirmOpen = false } }" @keydown.escape.window="leave()">
226
268
  <form id="filter-form" method="get" action="/lessons">
227
269
  <input type="hidden" name="period" id="h-period" value="${e.period}">
@@ -241,7 +283,7 @@ ${Object.entries(r.categorised).map(([d,t])=>html`
241
283
  <div class="flex items-center gap-3 mb-4">
242
284
  <div class="relative flex-1">
243
285
  <span class="absolute inset-y-0 left-3.5 flex items-center text-gray-400 pointer-events-none text-sm">🔍</span>
244
- <input type="text" name="search" value="${e.search}" placeholder="Search lessons…" autocomplete="off" class="w-full pl-9 pr-10 py-3 rounded-xl bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 text-gray-800 dark:text-gray-100 placeholder-gray-400 text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent" />
286
+ <input type="text" name="search" value="${e.search}" placeholder="Search lessons…" autocomplete="off" class="w-full pl-9 pr-10 py-3 rounded-xl bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 text-gray-800 dark:text-gray-100 placeholder-gray-400 text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-accent-500 focus:border-transparent" />
245
287
  ${e.search?html`<button type="submit" name="search" value="" class="absolute inset-y-0 right-3 flex items-center text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 text-xl leading-none">×</button>`:""}
246
288
  </div>
247
289
  <p class="text-sm text-gray-400 dark:text-gray-500 whitespace-nowrap shrink-0">${v}</p>
@@ -252,55 +294,55 @@ ${Object.entries(r.categorised).map(([d,t])=>html`
252
294
  class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition ${e.starred?"bg-yellow-400 text-yellow-900 border-yellow-400":"bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-500 dark:text-gray-400 hover:border-yellow-400 hover:text-yellow-500"}">★ Starred</button>
253
295
 
254
296
  <div class="relative" x-data="{ open: false }" @click.outside="open = false" @keydown.escape="open = false">
255
- <button type="button" @click="open = !open" class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition ${e.imported||e.shared_by?"bg-indigo-600 text-white border-indigo-600":"bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-indigo-400"}">${n}${p}</button>
297
+ <button type="button" @click="open = !open" class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition ${e.imported||e.shared_by?"bg-accent-600 text-white border-accent-600":"bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-accent-400"}">${n}${p}</button>
256
298
  <div x-show="open" x-transition:enter="transition ease-out duration-100" x-transition:enter-start="opacity-0 scale-95" x-transition:enter-end="opacity-100 scale-100" class="absolute left-0 top-full mt-1 z-50 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-xl shadow-lg w-52 p-1 overflow-hidden" style="display:none">
257
- ${i.map(([l,x,$])=>{let S=e.imported===l&&e.shared_by===x;return html`<button type="button" data-imported="${l}" data-shared-by="${x}" onclick="var d=this.dataset; document.getElementById('h-imported').value=d.imported; document.getElementById('h-shared-by').value=d.sharedBy; document.getElementById('filter-form').submit()" class="w-full text-left px-3 py-2 rounded-lg text-sm transition flex items-center justify-between ${S?"bg-indigo-50 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-300 font-medium":"text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800"}"><span class="truncate">${$}</span>${S?html`<span class="text-indigo-500 ml-auto">✓</span>`:""}</button>`})}
299
+ ${i.map(([l,x,$])=>{let S=e.imported===l&&e.shared_by===x;return html`<button type="button" data-imported="${l}" data-shared-by="${x}" onclick="var d=this.dataset; document.getElementById('h-imported').value=d.imported; document.getElementById('h-shared-by').value=d.sharedBy; document.getElementById('filter-form').submit()" class="w-full text-left px-3 py-2 rounded-lg text-sm transition flex items-center justify-between ${S?"bg-accent-50 dark:bg-accent-900/30 text-accent-700 dark:text-accent-300 font-medium":"text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800"}"><span class="truncate">${$}</span>${S?html`<span class="text-accent-500 ml-auto">✓</span>`:""}</button>`})}
258
300
  </div>
259
301
  </div>
260
302
 
261
303
  <div class="relative" x-data="periodPicker()" @keydown.escape="close()">
262
- <button type="button" @click="toggle()" class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition ${g||e.period!=="all"?"bg-indigo-600 text-white border-indigo-600":"bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-indigo-400"}">
304
+ <button type="button" @click="toggle()" class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition ${g||e.period!=="all"?"bg-accent-600 text-white border-accent-600":"bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-accent-400"}">
263
305
  <span>📅</span><span x-text="label">${t}</span>${p}
264
306
  </button>
265
307
  <div x-show="open" x-transition:enter="transition ease-out duration-100" x-transition:enter-start="opacity-0 scale-95" x-transition:enter-end="opacity-100 scale-100" x-transition:leave="transition ease-in duration-75" x-transition:leave-start="opacity-100 scale-100" x-transition:leave-end="opacity-0 scale-95" @click.outside="close()" class="absolute left-0 top-full mt-1 z-50 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-xl shadow-lg w-64 overflow-hidden" style="display:none">
266
308
  <div class="p-1">
267
- ${Object.entries(Fe).map(([l,x])=>{let $=e.period===l&&!g;return html`<button type="button" @click="selectPreset('${l}', '${x}')" class="w-full text-left px-3 py-2 rounded-lg text-sm transition flex items-center justify-between ${$?"bg-indigo-50 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-300 font-medium":"text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800"}">${x}${$?html`<span class="text-indigo-500">✓</span>`:""}</button>`})}
309
+ ${Object.entries(Ke).map(([l,x])=>{let $=e.period===l&&!g;return html`<button type="button" @click="selectPreset('${l}', '${x}')" class="w-full text-left px-3 py-2 rounded-lg text-sm transition flex items-center justify-between ${$?"bg-accent-50 dark:bg-accent-900/30 text-accent-700 dark:text-accent-300 font-medium":"text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800"}">${x}${$?html`<span class="text-accent-500">✓</span>`:""}</button>`})}
268
310
  </div>
269
311
  <div class="border-t border-gray-100 dark:border-gray-800 p-1">
270
- <button type="button" @click="showCustom = !showCustom" class="w-full text-left px-3 py-2 rounded-lg text-sm transition flex items-center justify-between ${g?"bg-indigo-50 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-300 font-medium":"text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800"}">
312
+ <button type="button" @click="showCustom = !showCustom" class="w-full text-left px-3 py-2 rounded-lg text-sm transition flex items-center justify-between ${g?"bg-accent-50 dark:bg-accent-900/30 text-accent-700 dark:text-accent-300 font-medium":"text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800"}">
271
313
  <span>Custom range</span>
272
314
  <svg class="w-3 h-3 opacity-60 transition-transform" :class="showCustom ? 'rotate-180' : ''" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M19 9l-7 7-7-7"/></svg>
273
315
  </button>
274
316
  <div x-show="showCustom" class="px-2 pb-2 pt-1">
275
- <input type="text" x-ref="fp" placeholder="Select date range…" class="w-full text-xs bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg px-2 py-1.5 text-gray-700 dark:text-gray-200 focus:outline-none focus:ring-2 focus:ring-indigo-500 cursor-pointer" />
317
+ <input type="text" x-ref="fp" placeholder="Select date range…" class="w-full text-xs bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg px-2 py-1.5 text-gray-700 dark:text-gray-200 focus:outline-none focus:ring-2 focus:ring-accent-500 cursor-pointer" />
276
318
  </div>
277
319
  </div>
278
320
  </div>
279
321
  </div>
280
322
 
281
323
  <div class="relative" x-data="{ open: false }" @click.outside="open = false" @keydown.escape="open = false">
282
- <button type="button" @click="open = !open" class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition ${e.feedback?"bg-indigo-600 text-white border-indigo-600":"bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-indigo-400"}">${o}${p}</button>
324
+ <button type="button" @click="open = !open" class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition ${e.feedback?"bg-accent-600 text-white border-accent-600":"bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-accent-400"}">${a}${p}</button>
283
325
  <div x-show="open" x-transition:enter="transition ease-out duration-100" x-transition:enter-start="opacity-0 scale-95" x-transition:enter-end="opacity-100 scale-100" class="absolute left-0 top-full mt-1 z-50 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-xl shadow-lg w-44 p-1 overflow-hidden" style="display:none">
284
- ${[["","All feedback"],["know","\u2713 Knew it"],["understood","\u{1F4A1} Understood"],["dont_know","\u2717 Couldn't follow"],["none","\u2014 No response"]].map(([l,x])=>html`<button type="button" onclick="document.getElementById('h-feedback').value='${l}'; document.getElementById('filter-form').submit()" class="w-full text-left px-3 py-2 rounded-lg text-sm transition flex items-center justify-between ${e.feedback===l?"bg-indigo-50 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-300 font-medium":"text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800"}">${x}${e.feedback===l?html`<span class="text-indigo-500 ml-auto">✓</span>`:""}</button>`)}
326
+ ${[["","All feedback"],["know","\u2713 Knew it"],["understood","\u{1F4A1} Understood"],["dont_know","\u2717 Couldn't follow"],["none","\u2014 No response"]].map(([l,x])=>html`<button type="button" onclick="document.getElementById('h-feedback').value='${l}'; document.getElementById('filter-form').submit()" class="w-full text-left px-3 py-2 rounded-lg text-sm transition flex items-center justify-between ${e.feedback===l?"bg-accent-50 dark:bg-accent-900/30 text-accent-700 dark:text-accent-300 font-medium":"text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800"}">${x}${e.feedback===l?html`<span class="text-accent-500 ml-auto">✓</span>`:""}</button>`)}
285
327
  </div>
286
328
  </div>
287
329
 
288
330
  <div class="relative" x-data="{ open: false }" @click.outside="open = false" @keydown.escape="open = false">
289
- <button type="button" @click="open = !open" class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition ${e.level?"bg-indigo-600 text-white border-indigo-600":"bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-indigo-400"}">${e.level?`${ze[e.level]??""} ${e.level}`:"Level"}${p}</button>
331
+ <button type="button" @click="open = !open" class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition ${e.level?"bg-accent-600 text-white border-accent-600":"bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-accent-400"}">${e.level?`${Je[e.level]??""} ${e.level}`:"Level"}${p}</button>
290
332
  <div x-show="open" x-transition:enter="transition ease-out duration-100" x-transition:enter-start="opacity-0 scale-95" x-transition:enter-end="opacity-100 scale-100" class="absolute left-0 top-full mt-1 z-50 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-xl shadow-lg w-40 p-1 overflow-hidden" style="display:none">
291
- ${[["","All levels"],["junior","\u{1F7E2} Junior"],["mid","\u{1F7E1} Mid"],["senior","\u{1F534} Senior"]].map(([l,x])=>html`<button type="button" onclick="document.getElementById('h-level').value='${l}'; document.getElementById('filter-form').submit()" class="w-full text-left px-3 py-2 rounded-lg text-sm transition flex items-center justify-between ${e.level===l?"bg-indigo-50 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-300 font-medium":"text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800"}">${x}${e.level===l?html`<span class="text-indigo-500 ml-auto">✓</span>`:""}</button>`)}
333
+ ${[["","All levels"],["junior","\u{1F7E2} Junior"],["mid","\u{1F7E1} Mid"],["senior","\u{1F534} Senior"]].map(([l,x])=>html`<button type="button" onclick="document.getElementById('h-level').value='${l}'; document.getElementById('filter-form').submit()" class="w-full text-left px-3 py-2 rounded-lg text-sm transition flex items-center justify-between ${e.level===l?"bg-accent-50 dark:bg-accent-900/30 text-accent-700 dark:text-accent-300 font-medium":"text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800"}">${x}${e.level===l?html`<span class="text-accent-500 ml-auto">✓</span>`:""}</button>`)}
292
334
  </div>
293
335
  </div>
294
336
 
295
337
  ${r.allCategories.length||r.allProjects.length||r.allRepositories.length||r.allBranches.length||r.allCommits.length?html`<div class="relative" x-data="{ open: false }" @click.outside="open = false" @keydown.escape="open = false">
296
- <button type="button" @click="open = !open" class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition ${e.category||e.project||e.repository||e.branch||e.commit?"bg-indigo-600 text-white border-indigo-600":"bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-indigo-400"}">Filters${p}</button>
338
+ <button type="button" @click="open = !open" class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition ${e.category||e.project||e.repository||e.branch||e.commit?"bg-accent-600 text-white border-accent-600":"bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-accent-400"}">Filters${p}</button>
297
339
  <div x-show="open" x-transition:enter="transition ease-out duration-100" x-transition:enter-start="opacity-0 scale-95" x-transition:enter-end="opacity-100 scale-100" class="absolute left-0 top-full mt-1 z-50 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-xl shadow-lg w-64 p-3 space-y-2.5" style="display:none">
298
- ${r.allCategories.length?html`<div><label class="block text-xs text-gray-400 dark:text-gray-500 mb-1">Category</label><select name="category" onchange="this.form.submit()" class="w-full bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 rounded-lg px-2.5 py-1.5 text-xs focus:outline-none focus:ring-2 focus:ring-indigo-500"><option value="">All categories</option>${r.allCategories.map(l=>html`<option value="${l}" ${e.category===l?"selected":""}>${l}</option>`)}</select></div>`:""}
299
- ${r.allProjects.length?html`<div><label class="block text-xs text-gray-400 dark:text-gray-500 mb-1">Project</label><select name="project" onchange="this.form.submit()" class="w-full bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 rounded-lg px-2.5 py-1.5 text-xs focus:outline-none focus:ring-2 focus:ring-indigo-500"><option value="">All projects</option>${r.allProjects.map(l=>html`<option value="${l}" ${e.project===l?"selected":""}>${l}</option>`)}</select></div>`:""}
300
- ${r.allRepositories.length?html`<div><label class="block text-xs text-gray-400 dark:text-gray-500 mb-1">Repository</label><select name="repository" onchange="this.form.submit()" class="w-full bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 rounded-lg px-2.5 py-1.5 text-xs focus:outline-none focus:ring-2 focus:ring-indigo-500"><option value="">All repositories</option>${r.allRepositories.map(l=>html`<option value="${l}" ${e.repository===l?"selected":""}>${l}</option>`)}</select></div>`:""}
301
- ${r.allBranches.length?html`<div><label class="block text-xs text-gray-400 dark:text-gray-500 mb-1">Branch</label><input type="text" name="branch" value="${e.branch}" placeholder="e.g. main" list="branch-list" autocomplete="off" class="w-full bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 rounded-lg px-2.5 py-1.5 text-xs focus:outline-none focus:ring-2 focus:ring-indigo-500" /><datalist id="branch-list">${r.allBranches.map(l=>html`<option value="${l}">`)}</datalist></div>`:""}
302
- ${r.allCommits.length?html`<div><label class="block text-xs text-gray-400 dark:text-gray-500 mb-1">Commit</label><input type="text" name="commit" value="${e.commit}" placeholder="hash prefix…" list="commit-list" autocomplete="off" class="w-full bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 rounded-lg px-2.5 py-1.5 text-xs font-mono focus:outline-none focus:ring-2 focus:ring-indigo-500" /><datalist id="commit-list">${r.allCommits.map(l=>html`<option value="${l.slice(0,7)}">`)}</datalist></div>`:""}
303
- <button type="submit" class="w-full bg-indigo-600 hover:bg-indigo-500 text-white rounded-lg px-3 py-1.5 text-xs font-medium transition">Apply</button>
340
+ ${r.allCategories.length?html`<div><label class="block text-xs text-gray-400 dark:text-gray-500 mb-1">Category</label><select name="category" onchange="this.form.submit()" class="w-full bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 rounded-lg px-2.5 py-1.5 text-xs focus:outline-none focus:ring-2 focus:ring-accent-500"><option value="">All categories</option>${r.allCategories.map(l=>html`<option value="${l}" ${e.category===l?"selected":""}>${l}</option>`)}</select></div>`:""}
341
+ ${r.allProjects.length?html`<div><label class="block text-xs text-gray-400 dark:text-gray-500 mb-1">Project</label><select name="project" onchange="this.form.submit()" class="w-full bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 rounded-lg px-2.5 py-1.5 text-xs focus:outline-none focus:ring-2 focus:ring-accent-500"><option value="">All projects</option>${r.allProjects.map(l=>html`<option value="${l}" ${e.project===l?"selected":""}>${l}</option>`)}</select></div>`:""}
342
+ ${r.allRepositories.length?html`<div><label class="block text-xs text-gray-400 dark:text-gray-500 mb-1">Repository</label><select name="repository" onchange="this.form.submit()" class="w-full bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 rounded-lg px-2.5 py-1.5 text-xs focus:outline-none focus:ring-2 focus:ring-accent-500"><option value="">All repositories</option>${r.allRepositories.map(l=>html`<option value="${l}" ${e.repository===l?"selected":""}>${l}</option>`)}</select></div>`:""}
343
+ ${r.allBranches.length?html`<div><label class="block text-xs text-gray-400 dark:text-gray-500 mb-1">Branch</label><input type="text" name="branch" value="${e.branch}" placeholder="e.g. main" list="branch-list" autocomplete="off" class="w-full bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 rounded-lg px-2.5 py-1.5 text-xs focus:outline-none focus:ring-2 focus:ring-accent-500" /><datalist id="branch-list">${r.allBranches.map(l=>html`<option value="${l}">`)}</datalist></div>`:""}
344
+ ${r.allCommits.length?html`<div><label class="block text-xs text-gray-400 dark:text-gray-500 mb-1">Commit</label><input type="text" name="commit" value="${e.commit}" placeholder="hash prefix…" list="commit-list" autocomplete="off" class="w-full bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 rounded-lg px-2.5 py-1.5 text-xs font-mono focus:outline-none focus:ring-2 focus:ring-accent-500" /><datalist id="commit-list">${r.allCommits.map(l=>html`<option value="${l.slice(0,7)}">`)}</datalist></div>`:""}
345
+ <button type="submit" class="w-full bg-accent-600 hover:bg-accent-500 text-white rounded-lg px-3 py-1.5 text-xs font-medium transition">Apply</button>
304
346
  </div>
305
347
  </div>`:""}
306
348
 
@@ -310,14 +352,14 @@ ${Object.entries(r.categorised).map(([d,t])=>html`
310
352
  <button type="button" @click="window.resetLessonColumns && window.resetLessonColumns(); open = false" class="w-full text-left px-3 py-2 rounded-lg text-sm transition flex items-center gap-2 text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800"><svg class="w-4 h-4 shrink-0 text-gray-400 dark:text-gray-500" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" aria-hidden="true"><rect x="2.5" y="4" width="15" height="12" rx="2"/><path d="M7.5 4v12M12.5 4v12"/></svg>Reset column widths</button>`,d?"":"ml-auto",'x-show="!selectMode"')}
311
353
  <button type="button" x-show="selectMode" style="display:none" @click="leave()" class="${d?"":"ml-auto"} inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-rose-400 hover:text-rose-600 dark:hover:text-rose-400">✕ Cancel</button>
312
354
  <div class="relative" x-data="{ open: ${String(r.importOpen)} }" @click.outside="open = false" @keydown.escape="open = false">
313
- <button type="button" @click="open = !open" title="Import a lesson someone shared" class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-indigo-400">+ Import</button>
355
+ <button type="button" @click="open = !open" title="Import a lesson someone shared" class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-accent-400">+ Import</button>
314
356
  <div x-show="open" x-transition:enter="transition ease-out duration-100" x-transition:enter-start="opacity-0 scale-95" x-transition:enter-end="opacity-100 scale-100" class="absolute right-0 top-full mt-1 z-50 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-xl shadow-lg w-80 p-3 space-y-2.5" style="display:none">
315
357
  <p class="text-xs text-gray-500 dark:text-gray-400">Paste the lesson code, the link, a URL or the whole text — or drop a <code>.devcoach.md</code> anywhere on this page.</p>
316
358
  ${r.importError==="invalid"?html`<p class="text-xs text-rose-600 dark:text-rose-400">That doesn't look like a devcoach lesson — copy the whole text again.</p>`:r.importError==="cross"?html`<p class="text-xs text-rose-600 dark:text-rose-400">Imports only work from this dashboard — paste the lesson here.</p>`:""}
317
- <textarea name="text" form="import-form" rows="4" placeholder="devcoach:lesson:1:…" autocomplete="off" class="w-full bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 rounded-lg px-2.5 py-1.5 text-xs focus:outline-none focus:ring-2 focus:ring-indigo-500 font-mono" x-ref="importText" x-effect="if (open) $nextTick(() => $refs.importText.focus())"></textarea>
359
+ <textarea name="text" form="import-form" rows="4" placeholder="devcoach:lesson:1:…" autocomplete="off" class="w-full bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 rounded-lg px-2.5 py-1.5 text-xs focus:outline-none focus:ring-2 focus:ring-accent-500 font-mono" x-ref="importText" x-effect="if (open) $nextTick(() => $refs.importText.focus())"></textarea>
318
360
  <div class="flex items-center gap-2">
319
- <label class="cursor-pointer text-xs text-gray-500 dark:text-gray-400 hover:text-indigo-600 dark:hover:text-indigo-400 transition">📄 Choose file<input id="import-file" type="file" name="file" form="import-form" accept=".md,.json,.txt" class="hidden" onchange="document.getElementById('import-form').submit()" /></label>
320
- <button type="submit" form="import-form" class="ml-auto bg-indigo-600 hover:bg-indigo-500 text-white rounded-lg px-3 py-1.5 text-xs font-medium transition">Import</button>
361
+ <label class="cursor-pointer text-xs text-gray-500 dark:text-gray-400 hover:text-accent-600 dark:hover:text-accent-400 transition">📄 Choose file<input id="import-file" type="file" name="file" form="import-form" accept=".md,.json,.txt" class="hidden" onchange="document.getElementById('import-form').submit()" /></label>
362
+ <button type="submit" form="import-form" class="ml-auto bg-accent-600 hover:bg-accent-500 text-white rounded-lg px-3 py-1.5 text-xs font-medium transition">Import</button>
321
363
  </div>
322
364
  </div>
323
365
  </div>
@@ -326,12 +368,12 @@ ${Object.entries(r.categorised).map(([d,t])=>html`
326
368
  ${d?html`<div class="flex flex-wrap gap-1.5 mb-4">
327
369
  ${g?c(`\u{1F4C5} ${t}`,h(e,{date_from:"",date_to:""})):""}
328
370
  ${e.period!=="all"&&!g?c(`\u{1F550} ${t}`,h(e,{period:"all"})):""}
329
- ${e.feedback?c(Ge[e.feedback]??e.feedback,h(e,{feedback:""})):""}
371
+ ${e.feedback?c(Ye[e.feedback]??e.feedback,h(e,{feedback:""})):""}
330
372
  ${e.starred?html`<span class="inline-flex items-center gap-1 pl-2.5 pr-1 py-0.5 rounded-full text-xs bg-yellow-50 dark:bg-yellow-900/20 text-yellow-700 dark:text-yellow-300 border border-yellow-200 dark:border-yellow-800">★ Starred<a href="${h({...e,starred:false})}" class="ml-0.5 w-4 h-4 flex items-center justify-center rounded-full hover:bg-yellow-200 dark:hover:bg-yellow-700 transition">×</a></span>`:""}
331
373
  ${e.imported||e.shared_by?c(n,h(e,{imported:"",shared_by:""})):""}
332
374
  ${e.search?c(`\u{1F50D} "${e.search}"`,h(e,{search:""})):""}
333
375
  ${e.category?c(e.category,h(e,{category:""})):""}
334
- ${e.level?c(`${ze[e.level]??""} ${e.level}`,h(e,{level:""})):""}
376
+ ${e.level?c(`${Je[e.level]??""} ${e.level}`,h(e,{level:""})):""}
335
377
  ${e.project?c(`\u{1F4C1} ${e.project}`,h(e,{project:""})):""}
336
378
  ${e.repository?c(`\u2387 ${e.repository}`,h(e,{repository:""})):""}
337
379
  ${e.branch?c(`\u2387 ${e.branch}`,h(e,{branch:""})):""}
@@ -339,7 +381,7 @@ ${Object.entries(r.categorised).map(([d,t])=>html`
339
381
  </div>`:""}
340
382
  </form>
341
383
  <form id="import-form" method="post" action="/lessons/import" enctype="multipart/form-data" class="hidden"><input type="hidden" name="from" value="lessons" /></form>
342
- <div id="drop-hint" class="hidden fixed inset-0 z-[60] bg-indigo-500/10 border-4 border-dashed border-indigo-400 pointer-events-none items-center justify-center"><p class="bg-white dark:bg-gray-900 text-indigo-700 dark:text-indigo-300 font-semibold rounded-xl px-6 py-3 shadow-lg">Drop to import the lesson</p></div>
384
+ <div id="drop-hint" class="hidden fixed inset-0 z-[60] bg-accent-500/10 border-4 border-dashed border-accent-400 pointer-events-none items-center justify-center"><p class="bg-white dark:bg-gray-900 text-accent-700 dark:text-accent-300 font-semibold rounded-xl px-6 py-3 shadow-lg">Drop to import the lesson</p></div>
343
385
 
344
386
  ${r.lessons.length?html`<div class="bg-white dark:bg-gray-900 rounded-xl border border-gray-200 dark:border-gray-800 shadow-sm overflow-hidden">
345
387
  <table class="w-full text-sm table-fixed" data-resizable="lessons">
@@ -355,7 +397,7 @@ ${r.lessons.length?html`<div class="bg-white dark:bg-gray-900 rounded-xl border
355
397
  </colgroup>
356
398
  <thead>
357
399
  <tr class="bg-gray-50 dark:bg-gray-800/60 border-b border-gray-200 dark:border-gray-800 text-left text-xs font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500">
358
- <th class="px-3 py-3"><input type="checkbox" tabindex="-1" aria-label="Select all on this page" class="dc-check" x-show="selectMode" style="display:none" :checked="selected.length > 0 && ${I}.every((id) => selected.includes(id))" @change="setAll(${I}, $event.target.checked)" /></th>
400
+ <th class="px-3 py-3"><input type="checkbox" tabindex="-1" aria-label="Select all on this page" class="dc-check" x-show="selectMode" style="display:none" :checked="selected.length > 0 && ${T}.every((id) => selected.includes(id))" @change="setAll(${T}, $event.target.checked)" /></th>
359
401
  ${u("Date","timestamp")}
360
402
  ${u("Topic","topic_id","hidden sm:table-cell")}
361
403
  ${u("Title","title")}
@@ -366,7 +408,7 @@ ${r.lessons.length?html`<div class="bg-white dark:bg-gray-900 rounded-xl border
366
408
  </tr>
367
409
  </thead>
368
410
  <tbody class="divide-y divide-gray-100 dark:divide-gray-800">
369
- ${r.lessons.map(l=>{let x=Ye("lesson-row-",l.id),$=l.timestamp.slice(0,10),S=l.timestamp.slice(0,16).replace("T"," ");return html`<tr id="${x}" data-id="${l.id}" data-title="${l.title}" data-href="/lessons/${encodeURIComponent(l.id)}" class="hover:bg-gray-50 dark:hover:bg-gray-800/40 transition-colors group cursor-pointer" tabindex="0" role="link" @click="if (selectMode) toggle($el.dataset.id); else if (!$event.target.closest('a, button, input, label')) window.location = $el.dataset.href" @keydown.enter="if (selectMode) toggle($el.dataset.id); else if ($event.target === $el) window.location = $el.dataset.href" @keydown.space.prevent="selectMode && toggle($el.dataset.id)" :class="selected.includes($el.dataset.id) && 'bg-indigo-50 dark:bg-indigo-900/20'">
411
+ ${r.lessons.map(l=>{let x=We("lesson-row-",l.id),$=l.timestamp.slice(0,10),S=l.timestamp.slice(0,16).replace("T"," ");return html`<tr id="${x}" data-id="${l.id}" data-title="${l.title}" data-href="/lessons/${encodeURIComponent(l.id)}" class="hover:bg-gray-50 dark:hover:bg-gray-800/40 transition-colors group cursor-pointer" tabindex="0" role="link" @click="if (selectMode) toggle($el.dataset.id); else if (!$event.target.closest('a, button, input, label')) window.location = $el.dataset.href" @keydown.enter="if (selectMode) toggle($el.dataset.id); else if ($event.target === $el) window.location = $el.dataset.href" @keydown.space.prevent="selectMode && toggle($el.dataset.id)" :class="selected.includes($el.dataset.id) && 'bg-accent-50 dark:bg-accent-900/20'">
370
412
  <td class="px-3 py-3">
371
413
  <form x-show="!selectMode" method="post" action="/lessons/${encodeURIComponent(l.id)}/star" hx-post="/lessons/${encodeURIComponent(l.id)}/star" hx-target="#${x}" hx-select="#${x}" hx-swap="outerHTML">
372
414
  <input type="hidden" name="starred" value="${l.starred?"0":"1"}" />
@@ -380,11 +422,11 @@ ${r.lessons.length?html`<div class="bg-white dark:bg-gray-900 rounded-xl border
380
422
  <div class="absolute z-10 bottom-full left-0 mb-1 px-2 py-1 rounded bg-gray-800 dark:bg-gray-700 text-white text-xs whitespace-nowrap pointer-events-none opacity-0 group-hover/date:opacity-100 transition-opacity duration-150">${S}</div>
381
423
  </td>
382
424
  <td class="px-3 py-3 hidden sm:table-cell"><span class="block truncate text-xs font-mono text-cyan-600 dark:text-cyan-400">${l.topic_id}</span>${l.imported?html`<span class="block text-[11px] text-gray-400 dark:text-gray-500 truncate max-w-[10rem]" title="This lesson was shared with you">🤝 ${l.shared_by??"anonymous"}</span>`:""}</td>
383
- <td class="px-3 py-3"><a href="/lessons/${encodeURIComponent(l.id)}" class="font-semibold text-[15px] leading-snug text-gray-800 dark:text-gray-100 hover:text-indigo-600 dark:hover:text-indigo-400 transition line-clamp-2">${l.title}</a></td>
425
+ <td class="px-3 py-3"><a href="/lessons/${encodeURIComponent(l.id)}" class="font-semibold text-[15px] leading-snug text-gray-800 dark:text-gray-100 hover:text-accent-600 dark:hover:text-accent-400 transition line-clamp-2">${l.title}</a></td>
384
426
  <td class="px-3 py-3"><a href="${h(e,{level:l.level})}" class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${s[l.level]??""} hover:ring-2 hover:ring-current hover:ring-offset-1 transition-shadow">${l.level}</a></td>
385
- <td class="px-3 py-3 hidden lg:table-cell"><div class="flex flex-wrap gap-1">${l.categories.map(k=>html`<a href="${h(e,{category:k})}" class="inline-flex items-center px-1.5 py-0.5 rounded text-xs bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400 hover:bg-indigo-100 dark:hover:bg-indigo-900/40 hover:text-indigo-700 dark:hover:text-indigo-300 transition-colors">${k}</a>`)}</div></td>
386
- <td class="px-3 py-3 hidden xl:table-cell">${l.feedback==="know"?html`<span class="text-xs text-teal-600 dark:text-teal-400 font-medium">✓ Knew it</span>`:l.feedback==="understood"?html`<span class="text-xs text-indigo-500 dark:text-indigo-400 font-medium">💡 Understood</span>`:l.feedback==="dont_know"?html`<span class="text-xs text-rose-500 dark:text-rose-400 font-medium">✗ Couldn't follow</span>`:""}</td>
387
- <td class="px-2 py-3 text-center"><button type="button" hx-get="/lessons/${encodeURIComponent(l.id)}/share?format=panel" hx-target="#share-modal-body" hx-swap="innerHTML" @click="openShare($el)" title="Share this lesson" aria-label="Share this lesson" class="inline-flex text-gray-400 dark:text-gray-500 hover:text-indigo-500 dark:hover:text-indigo-400 transition opacity-0 group-hover:opacity-100 focus-visible:opacity-100"><svg class="w-4 h-4" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 14 14 6M8 6h6v6"/></svg></button></td>
427
+ <td class="px-3 py-3 hidden lg:table-cell"><div class="flex flex-wrap gap-1">${l.categories.map(k=>html`<a href="${h(e,{category:k})}" class="inline-flex items-center px-1.5 py-0.5 rounded text-xs bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400 hover:bg-accent-100 dark:hover:bg-accent-900/40 hover:text-accent-700 dark:hover:text-accent-300 transition-colors">${k}</a>`)}</div></td>
428
+ <td class="px-3 py-3 hidden xl:table-cell">${l.feedback==="know"?html`<span class="text-xs text-teal-600 dark:text-teal-400 font-medium">✓ Knew it</span>`:l.feedback==="understood"?html`<span class="text-xs text-accent-500 dark:text-accent-400 font-medium">💡 Understood</span>`:l.feedback==="dont_know"?html`<span class="text-xs text-rose-500 dark:text-rose-400 font-medium">✗ Couldn't follow</span>`:""}</td>
429
+ <td class="px-2 py-3 text-center"><button type="button" hx-get="/lessons/${encodeURIComponent(l.id)}/share?format=panel" hx-target="#share-modal-body" hx-swap="innerHTML" @click="openShare($el)" title="Share this lesson" aria-label="Share this lesson" class="inline-flex text-gray-400 dark:text-gray-500 hover:text-accent-500 dark:hover:text-accent-400 transition opacity-0 group-hover:opacity-100 focus-visible:opacity-100"><svg class="w-4 h-4" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 14 14 6M8 6h6v6"/></svg></button></td>
388
430
  </tr>`})}
389
431
  </tbody>
390
432
  </table>
@@ -392,18 +434,18 @@ ${r.lessons.length?html`<div class="bg-white dark:bg-gray-900 rounded-xl border
392
434
  ${r.totalPages>1?html`<div class="flex items-center justify-between mt-4">
393
435
  <p class="text-xs text-gray-400 dark:text-gray-500">Page ${r.page} of ${r.totalPages}</p>
394
436
  <div class="flex items-center gap-1">
395
- ${r.page>1?html`<a href="${h(e,{page:String(r.page-1)})}" class="inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-medium bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 text-gray-600 dark:text-gray-300 hover:border-indigo-400 transition">← Prev</a>`:html`<span class="inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-medium border border-gray-100 dark:border-gray-800 text-gray-300 dark:text-gray-600 cursor-not-allowed">← Prev</span>`}
396
- ${it(r.page,r.totalPages).map(l=>l===0?html`<span class="text-gray-400 dark:text-gray-600 text-xs px-1">…</span>`:l===r.page?html`<span class="inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-medium bg-indigo-600 text-white border border-indigo-600">${l}</span>`:html`<a href="${h(e,{page:String(l)})}" class="inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-medium bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 text-gray-600 dark:text-gray-300 hover:border-indigo-400 transition">${l}</a>`)}
397
- ${r.page<r.totalPages?html`<a href="${h(e,{page:String(r.page+1)})}" class="inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-medium bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 text-gray-600 dark:text-gray-300 hover:border-indigo-400 transition">Next →</a>`:html`<span class="inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-medium border border-gray-100 dark:border-gray-800 text-gray-300 dark:text-gray-600 cursor-not-allowed">Next →</span>`}
437
+ ${r.page>1?html`<a href="${h(e,{page:String(r.page-1)})}" class="inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-medium bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 text-gray-600 dark:text-gray-300 hover:border-accent-400 transition">← Prev</a>`:html`<span class="inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-medium border border-gray-100 dark:border-gray-800 text-gray-300 dark:text-gray-600 cursor-not-allowed">← Prev</span>`}
438
+ ${ct(r.page,r.totalPages).map(l=>l===0?html`<span class="text-gray-400 dark:text-gray-600 text-xs px-1">…</span>`:l===r.page?html`<span class="inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-medium bg-accent-600 text-white border border-accent-600">${l}</span>`:html`<a href="${h(e,{page:String(l)})}" class="inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-medium bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 text-gray-600 dark:text-gray-300 hover:border-accent-400 transition">${l}</a>`)}
439
+ ${r.page<r.totalPages?html`<a href="${h(e,{page:String(r.page+1)})}" class="inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-medium bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 text-gray-600 dark:text-gray-300 hover:border-accent-400 transition">Next →</a>`:html`<span class="inline-flex items-center px-3 py-1.5 rounded-lg text-xs font-medium border border-gray-100 dark:border-gray-800 text-gray-300 dark:text-gray-600 cursor-not-allowed">Next →</span>`}
398
440
  </div>
399
441
  </div>`:""}`:html`<div class="flex flex-col items-center justify-center py-16 text-center">
400
442
  <p class="text-3xl mb-3">📭</p>
401
443
  <p class="text-gray-500 dark:text-gray-400 text-sm">No lessons match the current filters.</p>
402
- ${d?html`<a href="/lessons" class="mt-2 text-indigo-500 hover:text-indigo-400 text-sm transition">Clear all filters</a>`:""}
444
+ ${d?html`<a href="/lessons" class="mt-2 text-accent-500 hover:text-accent-400 text-sm transition">Clear all filters</a>`:""}
403
445
  </div>`}
404
446
  <div x-show="selectMode" style="display:none" x-transition:enter="transition ease-out duration-150" x-transition:enter-start="opacity-0 translate-y-2" x-transition:enter-end="opacity-100 translate-y-0" class="fixed inset-x-0 bottom-0 z-50 border-t border-gray-200 dark:border-gray-800 bg-white/95 dark:bg-gray-900/95 backdrop-blur px-4 sm:px-6 py-3">
405
447
  <div class="max-w-7xl mx-auto flex items-center gap-3">
406
- <button type="button" @click="leave()" class="inline-flex items-center px-3 py-1.5 rounded-lg text-sm font-medium border transition bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-indigo-400">Cancel</button>
448
+ <button type="button" @click="leave()" class="inline-flex items-center px-3 py-1.5 rounded-lg text-sm font-medium border transition bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-accent-400">Cancel</button>
407
449
  <div class="ml-auto flex items-center gap-3">
408
450
  <p class="text-sm text-gray-700 dark:text-gray-200 tabular-nums"><span x-text="selected.length"></span> selected</p>
409
451
  <button type="button" x-show="selected.length" style="display:none" @click="selected = []" class="text-xs text-gray-400 dark:text-gray-500 hover:text-gray-700 dark:hover:text-gray-200 transition">Clear</button>
@@ -415,7 +457,7 @@ ${J("confirmOpen","confirmOpen = false",html`<span x-text="selected.length === 1
415
457
  <template x-for="t in selectedTitles().slice(0, 6)" :key="t"><li class="truncate">· <span x-text="t"></span></li></template>
416
458
  </ul>
417
459
  <p x-show="selected.length > 6" style="display:none" x-text="'…and ' + (selected.length - 6) + ' more'"></p>
418
- <p>They will be removed from your log. This cannot be undone.</p>`,html`<button type="button" @click="confirmOpen = false" class="inline-flex items-center px-3 py-1.5 rounded-lg text-sm font-medium border transition bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-indigo-400">Cancel</button>
460
+ <p>They will be removed from your log. This cannot be undone.</p>`,html`<button type="button" @click="confirmOpen = false" class="inline-flex items-center px-3 py-1.5 rounded-lg text-sm font-medium border transition bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-accent-400">Cancel</button>
419
461
  <form method="post" action="/lessons/delete" class="contents">
420
462
  <template x-for="id in selected" :key="id"><input type="hidden" name="id" :value="id" /></template>
421
463
  <input type="hidden" name="next" value="/lessons${h(e,{page:String(r.page)})}" />
@@ -467,11 +509,11 @@ function periodPicker() {
467
509
  },
468
510
  };
469
511
  }
470
- </script>`;return P({title:"Lessons \u2014 devcoach",currentPath:"/lessons",uiTheme:r.uiTheme,head:m,body:R,scripts:L})}function it(r,e){let g=[],d=0;for(let t=1;t<=e;t++)(t===1||t===e||t>=r-2&&t<=r+2)&&(d&&t-d>1&&g.push(0),g.push(t),d=t);return g}var K={github:"github.com",gitlab:"gitlab.com",bitbucket:"bitbucket.org"};function V(r){let e=`/lessons/${encodeURIComponent(r.id)}/share?format=md&name=${encodeURIComponent(r.name)}&include_context=${r.includeContext?"1":"0"}`;return html`<div id="share-payloads" data-text="${r.text}" data-link="${r.link}" class="space-y-2">
512
+ </script>`;return P({title:"Lessons \u2014 devcoach",currentPath:"/lessons",uiTheme:r.uiTheme,head:m,body:R,scripts:L})}function ct(r,e){let g=[],d=0;for(let t=1;t<=e;t++)(t===1||t===e||t>=r-2&&t<=r+2)&&(d&&t-d>1&&g.push(0),g.push(t),d=t);return g}var K={github:"github.com",gitlab:"gitlab.com",bitbucket:"bitbucket.org"};function V(r){let e=`/lessons/${encodeURIComponent(r.id)}/share?format=md&name=${encodeURIComponent(r.name)}&include_context=${r.includeContext?"1":"0"}`;return html`<div id="share-payloads" data-text="${r.text}" data-link="${r.link}" class="space-y-2">
471
513
  <div class="flex flex-wrap gap-2">
472
- <button type="button" onclick="copyShare('text', this)" class="inline-flex items-center gap-1 px-2.5 py-1.5 rounded-lg text-xs font-medium bg-gray-100 dark:bg-gray-800 hover:bg-indigo-100 dark:hover:bg-indigo-900/40 text-gray-700 dark:text-gray-200 hover:text-indigo-700 dark:hover:text-indigo-300 transition">📋 Copy text</button>
473
- <button type="button" onclick="copyShare('link', this)" class="inline-flex items-center gap-1 px-2.5 py-1.5 rounded-lg text-xs font-medium bg-gray-100 dark:bg-gray-800 hover:bg-indigo-100 dark:hover:bg-indigo-900/40 text-gray-700 dark:text-gray-200 hover:text-indigo-700 dark:hover:text-indigo-300 transition">🔗 Copy link</button>
474
- <a href="${e}" class="inline-flex items-center gap-1 px-2.5 py-1.5 rounded-lg text-xs font-medium bg-gray-100 dark:bg-gray-800 hover:bg-indigo-100 dark:hover:bg-indigo-900/40 text-gray-700 dark:text-gray-200 hover:text-indigo-700 dark:hover:text-indigo-300 transition">⬇ Download .md</a>
514
+ <button type="button" onclick="copyShare('text', this)" class="inline-flex items-center gap-1 px-2.5 py-1.5 rounded-lg text-xs font-medium bg-gray-100 dark:bg-gray-800 hover:bg-accent-100 dark:hover:bg-accent-900/40 text-gray-700 dark:text-gray-200 hover:text-accent-700 dark:hover:text-accent-300 transition">📋 Copy text</button>
515
+ <button type="button" onclick="copyShare('link', this)" class="inline-flex items-center gap-1 px-2.5 py-1.5 rounded-lg text-xs font-medium bg-gray-100 dark:bg-gray-800 hover:bg-accent-100 dark:hover:bg-accent-900/40 text-gray-700 dark:text-gray-200 hover:text-accent-700 dark:hover:text-accent-300 transition">🔗 Copy link</button>
516
+ <a href="${e}" class="inline-flex items-center gap-1 px-2.5 py-1.5 rounded-lg text-xs font-medium bg-gray-100 dark:bg-gray-800 hover:bg-accent-100 dark:hover:bg-accent-900/40 text-gray-700 dark:text-gray-200 hover:text-accent-700 dark:hover:text-accent-300 transition">⬇ Download .md</a>
475
517
  </div>
476
518
  <p class="text-[11px] text-gray-400 dark:text-gray-500">${r.name?html`Shared by <span class="text-gray-600 dark:text-gray-300">${r.name}</span>`:"Shared anonymously"} · ${r.includeContext?"includes project, branch and commit":"only the lesson travels \u2014 no paths, no project"}.</p>
477
519
  </div>`}function W(r,e,g=null){return html`
@@ -481,12 +523,12 @@ function periodPicker() {
481
523
  <input type="hidden" name="persist" value="0" x-ref="persist" />
482
524
  <div>
483
525
  <label for="share-name" class="block text-xs text-gray-400 dark:text-gray-500 mb-1">Your name</label>
484
- <input id="share-name" type="text" name="name" value="${e.name}" maxlength="80" placeholder="anonymous" autocomplete="off" @input="$refs.persist.value = '0'" @change="$refs.persist.value = '1'" class="w-full bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 rounded-lg px-2.5 py-1.5 text-xs focus:outline-none focus:ring-2 focus:ring-indigo-500" />
526
+ <input id="share-name" type="text" name="name" value="${e.name}" maxlength="80" placeholder="anonymous" autocomplete="off" @input="$refs.persist.value = '0'" @change="$refs.persist.value = '1'" class="w-full bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 rounded-lg px-2.5 py-1.5 text-xs focus:outline-none focus:ring-2 focus:ring-accent-500" />
485
527
  </div>
486
528
  <label class="flex items-start gap-2 text-xs text-gray-600 dark:text-gray-300 cursor-pointer"><input type="checkbox" name="include_context" value="1" ${e.includeContext?"checked":""} class="mt-0.5" /><span>Include where it happened <span class="text-gray-400 dark:text-gray-500">(project, branch, commit — never local paths)</span></span></label>
487
529
  </form>
488
530
  ${V(e)}
489
- `}function We(r){let e=r.lesson,g=r.share,d=e.level==="junior"?"text-green-700 dark:text-green-400 bg-green-50 dark:bg-green-900/30 border-green-300 dark:border-green-700":e.level==="mid"?"text-yellow-700 dark:text-yellow-400 bg-yellow-50 dark:bg-yellow-900/30 border-yellow-300 dark:border-yellow-700":"text-red-700 dark:text-red-400 bg-red-50 dark:bg-red-900/30 border-red-300 dark:border-red-700",t=e.repository_platform==="local",o=null;t&&e.repository?o=`vscode://file/${e.repository}`:e.repository_platform&&K[e.repository_platform]&&e.repository&&(o=`https://${K[e.repository_platform]}/${e.repository}`);let n=null;o&&e.commit_hash&&!t&&(n=e.repository_platform==="gitlab"?`${o}/-/commit/${e.commit_hash}`:e.repository_platform==="bitbucket"?`${o}/commits/${e.commit_hash}`:`${o}/commit/${e.commit_hash}`);let i=!!(e.project||e.repository||e.branch||e.commit_hash||e.folder),s=e.timestamp.slice(0,10),c=e.timestamp.slice(0,16).replace("T"," "),u=`hx-post="/lessons/${encodeURIComponent(e.id)}/feedback" hx-target="#lesson-meta" hx-select="#lesson-meta" hx-swap="outerHTML" hx-select-oob="#lesson-feedback:outerHTML"`,p=html`
531
+ `}function Xe(r){let e=r.lesson,g=r.share,d=e.level==="junior"?"text-green-700 dark:text-green-400 bg-green-50 dark:bg-green-900/30 border-green-300 dark:border-green-700":e.level==="mid"?"text-yellow-700 dark:text-yellow-400 bg-yellow-50 dark:bg-yellow-900/30 border-yellow-300 dark:border-yellow-700":"text-red-700 dark:text-red-400 bg-red-50 dark:bg-red-900/30 border-red-300 dark:border-red-700",t=e.repository_platform==="local",a=null;t&&e.repository?a=`vscode://file/${e.repository}`:e.repository_platform&&K[e.repository_platform]&&e.repository&&(a=`https://${K[e.repository_platform]}/${e.repository}`);let n=null;a&&e.commit_hash&&!t&&(n=e.repository_platform==="gitlab"?`${a}/-/commit/${e.commit_hash}`:e.repository_platform==="bitbucket"?`${a}/commits/${e.commit_hash}`:`${a}/commit/${e.commit_hash}`);let i=!!(e.project||e.repository||e.branch||e.commit_hash||e.folder),s=e.timestamp.slice(0,10),c=e.timestamp.slice(0,16).replace("T"," "),u=`hx-post="/lessons/${encodeURIComponent(e.id)}/feedback" hx-target="#lesson-meta" hx-select="#lesson-meta" hx-swap="outerHTML" hx-select-oob="#lesson-feedback:outerHTML"`,p=html`
490
532
  <div x-data="{ confirmOpen: false }" @keydown.escape.window="confirmOpen = false">
491
533
  <div class="mb-4"><a href="/lessons" class="text-gray-400 dark:text-gray-500 hover:text-gray-700 dark:hover:text-white text-sm transition">← Back to lessons</a></div>
492
534
  ${r.imported?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-700 text-green-700 dark:text-green-300">${r.imported==="new"?html`✓ Imported “${e.title}”${e.shared_by?html`, shared by ${e.shared_by}`:""}. It's in your log now — feedback works as usual and it never counts against your daily limit.`:html`“${e.title}” was already in your log — nothing changed.`}</div>`:""}
@@ -501,7 +543,7 @@ ${r.imported?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm bg-green-
501
543
  <a href="/lessons?level=${e.level}" class="text-xs font-semibold px-2 py-0.5 rounded-full border ${d} shrink-0 hover:ring-2 hover:ring-current hover:ring-offset-1 transition-shadow">${e.level}</a>
502
544
  ${Y(html`<button type="button" @click="confirmOpen = true; open = false" class="w-full text-left px-3 py-2 rounded-lg text-sm transition flex items-center gap-2 text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800 hover:!bg-rose-50 dark:hover:!bg-rose-900/30 hover:text-rose-700 dark:hover:text-rose-300"><span class="w-4 h-4 inline-flex items-center justify-center shrink-0 text-[13px] leading-none" aria-hidden="true">🗑</span>Delete lesson…</button>`,"shrink-0")}
503
545
  <div class="relative shrink-0" x-data="{ open: ${String(g.open)} }" @click.outside="open = false" @keydown.escape="open = false">
504
- <button type="button" @click="open = !open" title="Share this lesson" class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-indigo-400">↗ Share</button>
546
+ <button type="button" @click="open = !open" title="Share this lesson" class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-accent-400">↗ Share</button>
505
547
  <div x-show="open" x-transition:enter="transition ease-out duration-100" x-transition:enter-start="opacity-0 scale-95" x-transition:enter-end="opacity-100 scale-100" class="absolute right-0 top-full mt-1 z-50 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-xl shadow-lg w-80 p-4 space-y-3" style="display:none">
506
548
  ${W(e.id,g)}
507
549
  </div>
@@ -513,33 +555,33 @@ ${r.imported?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm bg-green-
513
555
  </span>
514
556
  <span>🏷 <span class="text-cyan-600 dark:text-cyan-400">${e.topic_id}</span></span>
515
557
  ${e.imported?html`<span title="This lesson was shared with you">🤝 shared by <span class="text-gray-700 dark:text-gray-200">${e.shared_by??"anonymous"}</span></span>`:""}
516
- ${r.course?html`<a href="/courses/${encodeURIComponent(r.course.id)}" title="${r.course.title}" class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-indigo-50 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-300 border border-indigo-200 dark:border-indigo-700 hover:border-indigo-400 transition">🎓 Course · ${r.course.done}/${r.course.total}${r.course.status==="completed"?" \u2713":""}</a>`:""}
517
- ${e.categories.map(I=>html`<a href="/lessons?category=${encodeURIComponent(I)}" class="inline-block bg-gray-100 dark:bg-gray-800 hover:bg-indigo-50 dark:hover:bg-indigo-900/50 text-gray-600 dark:text-gray-300 text-xs rounded px-2 py-0.5 transition border border-gray-200 dark:border-gray-700 hover:border-indigo-400 dark:hover:border-indigo-600">${I}</a>`)}
518
- ${e.feedback?html`${e.feedback==="know"?html`<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400 border border-green-200 dark:border-green-700">✓ I knew this</span>`:e.feedback==="understood"?html`<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-indigo-100 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-300 border border-indigo-200 dark:border-indigo-700">💡 Understood</span>`:html`<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-400 border border-red-200 dark:border-red-700">✗ Couldn't follow</span>`}
558
+ ${r.course?html`<a href="/courses/${encodeURIComponent(r.course.id)}" title="${r.course.title}" class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-accent-50 dark:bg-accent-900/30 text-accent-700 dark:text-accent-300 border border-accent-200 dark:border-accent-700 hover:border-accent-400 transition">🎓 Course · ${r.course.done}/${r.course.total}${r.course.status==="completed"?" \u2713":""}</a>`:""}
559
+ ${e.categories.map(T=>html`<a href="/lessons?category=${encodeURIComponent(T)}" class="inline-block bg-gray-100 dark:bg-gray-800 hover:bg-accent-50 dark:hover:bg-accent-900/50 text-gray-600 dark:text-gray-300 text-xs rounded px-2 py-0.5 transition border border-gray-200 dark:border-gray-700 hover:border-accent-400 dark:hover:border-accent-600">${T}</a>`)}
560
+ ${e.feedback?html`${e.feedback==="know"?html`<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400 border border-green-200 dark:border-green-700">✓ I knew this</span>`:e.feedback==="understood"?html`<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-accent-100 dark:bg-accent-900/30 text-accent-700 dark:text-accent-300 border border-accent-200 dark:border-accent-700">💡 Understood</span>`:html`<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-400 border border-red-200 dark:border-red-700">✗ Couldn't follow</span>`}
519
561
  <form method="post" action="/lessons/${encodeURIComponent(e.id)}/feedback" ${raw(u)}><input type="hidden" name="feedback" value="clear" /><input type="hidden" name="next" value="/lessons/${encodeURIComponent(e.id)}" /><button type="submit" class="text-xs text-gray-400 dark:text-gray-600 hover:text-gray-600 dark:hover:text-gray-400 transition">Clear</button></form>`:""}
520
562
  </div>
521
- <div class="my-5 pl-4 border-l-4 border-indigo-400 dark:border-indigo-500 bg-indigo-50 dark:bg-indigo-950/40 rounded-r-lg py-3 pr-4">
522
- <p class="text-xs font-semibold uppercase tracking-wide text-indigo-500 dark:text-indigo-400 mb-1">TL;DR</p>
523
- <div id="summary-content" class="markdown-body text-sm text-indigo-900 dark:text-indigo-100"></div>
563
+ <div class="my-5 pl-4 border-l-4 border-accent-400 dark:border-accent-500 bg-accent-50 dark:bg-accent-950/40 rounded-r-lg py-3 pr-4">
564
+ <p class="text-xs font-semibold uppercase tracking-wide text-accent-500 dark:text-accent-400 mb-1">TL;DR</p>
565
+ <div id="summary-content" class="markdown-body text-sm text-accent-900 dark:text-accent-100"></div>
524
566
  </div>
525
567
  <div id="body-content" class="markdown-body"></div>
526
568
  ${e.task_context?html`<div class="mt-6 pt-4 border-t border-gray-100 dark:border-gray-800 text-sm text-gray-500 dark:text-gray-400"><span class="text-gray-400 dark:text-gray-500">Context:</span> ${e.task_context}</div>`:""}
527
569
  ${i?html`<div class="mt-4 pt-4 border-t border-gray-100 dark:border-gray-800"><div class="flex flex-wrap gap-x-5 gap-y-1.5 text-xs text-gray-400 dark:text-gray-500 font-mono">
528
- ${e.project?html`<span><span class="text-gray-400 dark:text-gray-600">project</span> ${o?html`<a href="${o}" ${t?"":raw('target="_blank" rel="noopener"')} class="text-gray-600 dark:text-gray-300 hover:text-indigo-600 dark:hover:text-indigo-400 transition">${e.project}</a>`:html`<span class="text-gray-600 dark:text-gray-300">${e.project}</span>`}</span>`:""}
529
- ${e.repository?html`<span class="inline-flex items-center gap-1"><span class="text-gray-400 dark:text-gray-600">repo</span>${e.repository_platform&&K[e.repository_platform]?html`<img src="/static/vendor/icons/${e.repository_platform}.svg" class="w-3 h-3 dark:invert opacity-60 shrink-0" alt="" />`:""}${o?html`<a href="${o}" ${t?"":raw('target="_blank" rel="noopener"')} class="text-gray-600 dark:text-gray-300 hover:text-indigo-600 dark:hover:text-indigo-400 transition">${e.repository}</a>`:html`<span class="text-gray-600 dark:text-gray-300">${e.repository}</span>`}</span>`:""}
530
- ${e.branch?html`<span><span class="text-gray-400 dark:text-gray-600">branch</span> <span class="text-indigo-600 dark:text-indigo-400">${e.branch}</span></span>`:""}
531
- ${e.commit_hash?html`<span><span class="text-gray-400 dark:text-gray-600">commit</span> ${n?html`<a href="${n}" target="_blank" rel="noopener" class="text-cyan-600 dark:text-cyan-400 hover:text-indigo-600 dark:hover:text-indigo-400 transition">${e.commit_hash.slice(0,7)}</a>`:html`<span class="text-cyan-600 dark:text-cyan-400">${e.commit_hash.slice(0,7)}</span>`}</span>`:""}
532
- ${e.folder?html`<span class="inline-flex items-center gap-1"><span class="text-gray-400 dark:text-gray-600">folder</span><a href="vscode://file/${e.folder}" class="inline-flex items-center gap-1 text-gray-600 dark:text-gray-300 hover:text-indigo-600 dark:hover:text-indigo-400 transition"><img src="/static/vendor/icons/vscode.svg" class="w-3.5 h-3.5 shrink-0" alt="" />${e.folder}</a></span>`:""}
570
+ ${e.project?html`<span><span class="text-gray-400 dark:text-gray-600">project</span> ${a?html`<a href="${a}" ${t?"":raw('target="_blank" rel="noopener"')} class="text-gray-600 dark:text-gray-300 hover:text-accent-600 dark:hover:text-accent-400 transition">${e.project}</a>`:html`<span class="text-gray-600 dark:text-gray-300">${e.project}</span>`}</span>`:""}
571
+ ${e.repository?html`<span class="inline-flex items-center gap-1"><span class="text-gray-400 dark:text-gray-600">repo</span>${e.repository_platform&&K[e.repository_platform]?html`<img src="/static/vendor/icons/${e.repository_platform}.svg" class="w-3 h-3 dark:invert opacity-60 shrink-0" alt="" />`:""}${a?html`<a href="${a}" ${t?"":raw('target="_blank" rel="noopener"')} class="text-gray-600 dark:text-gray-300 hover:text-accent-600 dark:hover:text-accent-400 transition">${e.repository}</a>`:html`<span class="text-gray-600 dark:text-gray-300">${e.repository}</span>`}</span>`:""}
572
+ ${e.branch?html`<span><span class="text-gray-400 dark:text-gray-600">branch</span> <span class="text-accent-600 dark:text-accent-400">${e.branch}</span></span>`:""}
573
+ ${e.commit_hash?html`<span><span class="text-gray-400 dark:text-gray-600">commit</span> ${n?html`<a href="${n}" target="_blank" rel="noopener" class="text-cyan-600 dark:text-cyan-400 hover:text-accent-600 dark:hover:text-accent-400 transition">${e.commit_hash.slice(0,7)}</a>`:html`<span class="text-cyan-600 dark:text-cyan-400">${e.commit_hash.slice(0,7)}</span>`}</span>`:""}
574
+ ${e.folder?html`<span class="inline-flex items-center gap-1"><span class="text-gray-400 dark:text-gray-600">folder</span><a href="vscode://file/${e.folder}" class="inline-flex items-center gap-1 text-gray-600 dark:text-gray-300 hover:text-accent-600 dark:hover:text-accent-400 transition"><img src="/static/vendor/icons/vscode.svg" class="w-3.5 h-3.5 shrink-0" alt="" />${e.folder}</a></span>`:""}
533
575
  </div></div>`:""}
534
576
  <div id="lesson-feedback">${e.feedback?"":html`<div class="mt-6 pt-4 border-t border-gray-100 dark:border-gray-800 flex flex-wrap gap-2">
535
577
  <form method="post" action="/lessons/${encodeURIComponent(e.id)}/feedback" ${raw(u)}><input type="hidden" name="feedback" value="know" /><input type="hidden" name="next" value="/lessons/${encodeURIComponent(e.id)}" /><button type="submit" class="px-3 py-1 rounded text-sm font-medium transition bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400 hover:bg-green-100 dark:hover:bg-green-800 hover:text-green-700 dark:hover:text-white">✓ I knew this</button></form>
536
- <form method="post" action="/lessons/${encodeURIComponent(e.id)}/feedback" ${raw(u)}><input type="hidden" name="feedback" value="understood" /><input type="hidden" name="next" value="/lessons/${encodeURIComponent(e.id)}" /><button type="submit" class="px-3 py-1 rounded text-sm font-medium transition bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400 hover:bg-indigo-100 dark:hover:bg-indigo-900 hover:text-indigo-700 dark:hover:text-white">💡 Understood</button></form>
578
+ <form method="post" action="/lessons/${encodeURIComponent(e.id)}/feedback" ${raw(u)}><input type="hidden" name="feedback" value="understood" /><input type="hidden" name="next" value="/lessons/${encodeURIComponent(e.id)}" /><button type="submit" class="px-3 py-1 rounded text-sm font-medium transition bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400 hover:bg-accent-100 dark:hover:bg-accent-900 hover:text-accent-700 dark:hover:text-white">💡 Understood</button></form>
537
579
  <form method="post" action="/lessons/${encodeURIComponent(e.id)}/feedback" ${raw(u)}><input type="hidden" name="feedback" value="dont_know" /><input type="hidden" name="next" value="/lessons/${encodeURIComponent(e.id)}" /><button type="submit" class="px-3 py-1 rounded text-sm font-medium transition bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400 hover:bg-red-100 dark:hover:bg-red-900 hover:text-red-700 dark:hover:text-white">✗ Couldn't follow</button></form>
538
580
  </div>`}</div>
539
581
  <div class="mt-4 pt-4 border-t border-gray-100 dark:border-gray-800 text-xs text-gray-300 dark:text-gray-600">ID: ${e.id}</div>
540
582
  </div>
541
583
  ${J("confirmOpen","confirmOpen = false",html`Delete this lesson?`,html`<p class="text-gray-700 dark:text-gray-200 font-medium">“${e.title}”</p>
542
- <p>${e.imported?html`It was shared with you${e.shared_by?html` by ${e.shared_by}`:""}; you can import it again later. `:""}It will be removed from your log. This cannot be undone.</p>`,html`<button type="button" @click="confirmOpen = false" class="inline-flex items-center px-3 py-1.5 rounded-lg text-sm font-medium border transition bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-indigo-400">Cancel</button>
584
+ <p>${e.imported?html`It was shared with you${e.shared_by?html` by ${e.shared_by}`:""}; you can import it again later. `:""}It will be removed from your log. This cannot be undone.</p>`,html`<button type="button" @click="confirmOpen = false" class="inline-flex items-center px-3 py-1.5 rounded-lg text-sm font-medium border transition bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-accent-400">Cancel</button>
543
585
  <form method="post" action="/lessons/delete" class="contents">
544
586
  <input type="hidden" name="id" value="${e.id}" />
545
587
  <input type="hidden" name="next" value="/lessons" />
@@ -561,12 +603,12 @@ ${J("confirmOpen","confirmOpen = false",html`Delete this lesson?`,html`<p class=
561
603
  });
562
604
  document.getElementById('summary-content').innerHTML = DOMPurify.sanitize(marked.parse(${raw(j(e.summary))}));
563
605
  document.getElementById('body-content').innerHTML = DOMPurify.sanitize(marked.parse(${raw(j(e.body))}));
564
- </script>`;return P({title:`${e.title} \u2014 devcoach`,currentPath:"/lessons",uiTheme:r.uiTheme,head:m,body:p,scripts:v})}var dt={junior:"text-green-700 dark:text-green-400 bg-green-50 dark:bg-green-900/30 border-green-300 dark:border-green-700",mid:"text-yellow-700 dark:text-yellow-400 bg-yellow-50 dark:bg-yellow-900/30 border-yellow-300 dark:border-yellow-700",senior:"text-red-700 dark:text-red-400 bg-red-50 dark:bg-red-900/30 border-red-300 dark:border-red-700"};function Ze(r){let e=r.preview,g=html`<form id="import-form" method="post" action="/lessons/import" enctype="multipart/form-data" class="space-y-3">
606
+ </script>`;return P({title:`${e.title} \u2014 devcoach`,currentPath:"/lessons",uiTheme:r.uiTheme,head:m,body:p,scripts:v})}var gt={junior:"text-green-700 dark:text-green-400 bg-green-50 dark:bg-green-900/30 border-green-300 dark:border-green-700",mid:"text-yellow-700 dark:text-yellow-400 bg-yellow-50 dark:bg-yellow-900/30 border-yellow-300 dark:border-yellow-700",senior:"text-red-700 dark:text-red-400 bg-red-50 dark:bg-red-900/30 border-red-300 dark:border-red-700"};function et(r){let e=r.preview,g=html`<form id="import-form" method="post" action="/lessons/import" enctype="multipart/form-data" class="space-y-3">
565
607
  <input type="hidden" name="from" value="lessons" />
566
- <textarea name="text" rows="5" placeholder="devcoach:lesson:1:… — or the link, a URL, or the whole card" autocomplete="off" class="w-full bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 rounded-lg px-2.5 py-1.5 text-xs focus:outline-none focus:ring-2 focus:ring-indigo-500 font-mono"></textarea>
608
+ <textarea name="text" rows="5" placeholder="devcoach:lesson:1:… — or the link, a URL, or the whole card" autocomplete="off" class="w-full bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 rounded-lg px-2.5 py-1.5 text-xs focus:outline-none focus:ring-2 focus:ring-accent-500 font-mono"></textarea>
567
609
  <div class="flex items-center gap-3">
568
- <label class="cursor-pointer text-xs text-gray-500 dark:text-gray-400 hover:text-indigo-600 dark:hover:text-indigo-400 transition">📄 Choose a .devcoach.md file<input id="import-file" type="file" name="file" accept=".md,.json,.txt" class="hidden" onchange="document.getElementById('import-form').submit()" /></label>
569
- <button type="submit" class="ml-auto bg-indigo-600 hover:bg-indigo-500 text-white font-semibold rounded-lg px-4 py-2 text-sm transition">Import</button>
610
+ <label class="cursor-pointer text-xs text-gray-500 dark:text-gray-400 hover:text-accent-600 dark:hover:text-accent-400 transition">📄 Choose a .devcoach.md file<input id="import-file" type="file" name="file" accept=".md,.json,.txt" class="hidden" onchange="document.getElementById('import-form').submit()" /></label>
611
+ <button type="submit" class="ml-auto bg-accent-600 hover:bg-accent-500 text-white font-semibold rounded-lg px-4 py-2 text-sm transition">Import</button>
570
612
  </div>
571
613
  <p class="text-xs text-gray-400 dark:text-gray-500">You can also drop the file anywhere on this page.</p>
572
614
  </form>`,d=html`
@@ -576,29 +618,29 @@ ${J("confirmOpen","confirmOpen = false",html`Delete this lesson?`,html`<p class=
576
618
  ${r.error?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm bg-rose-50 dark:bg-rose-900/20 border-rose-200 dark:border-rose-700 text-rose-700 dark:text-rose-300">${r.error}</div>`:""}
577
619
  ${e?html`<div class="flex flex-wrap items-center gap-3 mb-2">
578
620
  <h1 class="text-xl font-bold text-gray-900 dark:text-white flex-1 min-w-0">${e.lesson.title}</h1>
579
- <span class="text-xs font-semibold px-2 py-0.5 rounded-full border ${dt[e.lesson.level]??""} shrink-0">${e.lesson.level}</span>
621
+ <span class="text-xs font-semibold px-2 py-0.5 rounded-full border ${gt[e.lesson.level]??""} shrink-0">${e.lesson.level}</span>
580
622
  </div>
581
623
  <div class="flex flex-wrap items-center gap-x-4 gap-y-1 text-sm text-gray-500 dark:text-gray-400 mb-5">
582
624
  <span>🏷 <span class="text-cyan-600 dark:text-cyan-400">${e.lesson.topic_id}</span></span>
583
625
  ${e.lesson.categories.map(n=>html`<span class="inline-block bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-300 text-xs rounded px-2 py-0.5 border border-gray-200 dark:border-gray-700">${n}</span>`)}
584
626
  <span>🤝 shared by <span class="text-gray-700 dark:text-gray-200">${e.shared_by??"anonymous"}</span> · ${e.shared_at.slice(0,10)}</span>
585
627
  </div>
586
- <div class="my-5 pl-4 border-l-4 border-indigo-400 dark:border-indigo-500 bg-indigo-50 dark:bg-indigo-950/40 rounded-r-lg py-3 pr-4">
587
- <p class="text-xs font-semibold uppercase tracking-wide text-indigo-500 dark:text-indigo-400 mb-1">TL;DR</p>
588
- <div id="summary-content" class="markdown-body text-sm text-indigo-900 dark:text-indigo-100"></div>
628
+ <div class="my-5 pl-4 border-l-4 border-accent-400 dark:border-accent-500 bg-accent-50 dark:bg-accent-950/40 rounded-r-lg py-3 pr-4">
629
+ <p class="text-xs font-semibold uppercase tracking-wide text-accent-500 dark:text-accent-400 mb-1">TL;DR</p>
630
+ <div id="summary-content" class="markdown-body text-sm text-accent-900 dark:text-accent-100"></div>
589
631
  </div>
590
632
  <details class="group" open ${e.lesson.body?"":"hidden"}>
591
- <summary class="cursor-pointer text-sm text-indigo-600 dark:text-indigo-400 hover:text-indigo-500 select-none">Full lesson</summary>
633
+ <summary class="cursor-pointer text-sm text-accent-600 dark:text-accent-400 hover:text-accent-500 select-none">Full lesson</summary>
592
634
  <div id="body-content" class="markdown-body mt-3"></div>
593
635
  </details>
594
636
  ${e.lesson.task_context?html`<div class="mt-6 pt-4 border-t border-gray-100 dark:border-gray-800 text-sm text-gray-500 dark:text-gray-400"><span class="text-gray-400 dark:text-gray-500">Context:</span> ${e.lesson.task_context}</div>`:""}
595
637
  <form method="post" action="/lessons/import" class="mt-6 pt-4 border-t border-gray-100 dark:border-gray-800 flex flex-wrap items-center gap-3">
596
638
  <input type="hidden" name="from" value="lessons" />
597
639
  <input type="hidden" name="text" value="${r.code}" />
598
- <button type="submit" class="bg-indigo-600 hover:bg-indigo-500 text-white font-semibold rounded-lg px-4 py-2 text-sm transition">+ Add to my lessons</button>
640
+ <button type="submit" class="bg-accent-600 hover:bg-accent-500 text-white font-semibold rounded-lg px-4 py-2 text-sm transition">+ Add to my lessons</button>
599
641
  <p class="text-xs text-gray-400 dark:text-gray-500">Nothing is saved until you click. It joins your log like your own lessons and never counts against the daily limit.</p>
600
642
  </form>`:g}
601
- </div>`,t=html`<link id="hljs-theme" rel="stylesheet" href="/static/vendor/hljs-dark.min.css" />`,o=html`<script src="/static/vendor/highlight.min.js"></script>
643
+ </div>`,t=html`<link id="hljs-theme" rel="stylesheet" href="/static/vendor/hljs-dark.min.css" />`,a=html`<script src="/static/vendor/highlight.min.js"></script>
602
644
  <script src="/static/vendor/marked.min.js"></script>
603
645
  <script src="/static/vendor/purify.min.js"></script>
604
646
  <script src="/static/share.js"></script>
@@ -613,9 +655,9 @@ ${J("confirmOpen","confirmOpen = false",html`Delete this lesson?`,html`<p class=
613
655
  });
614
656
  ${e?raw(`document.getElementById('summary-content').innerHTML = DOMPurify.sanitize(marked.parse(${j(e.lesson.summary)}));
615
657
  document.getElementById('body-content').innerHTML = DOMPurify.sanitize(marked.parse(${j(e.lesson.body??"")}));`):""}
616
- </script>`;return P({title:e?`${e.lesson.title} \u2014 devcoach`:"Import a lesson \u2014 devcoach",currentPath:"/lessons",uiTheme:r.uiTheme,head:t,body:d,scripts:o})}var lt=[[0,"No cooldown"],[15,"15 minutes"],[30,"30 minutes"],[60,"1 hour"],[90,"1 hour 30 min"],[120,"2 hours"],[180,"3 hours"],[240,"4 hours"],[360,"6 hours"],[480,"8 hours"],[720,"12 hours"],[1440,"24 hours"]],Ke={active:"bg-indigo-50 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-300 border-indigo-200 dark:border-indigo-700",completed:"bg-green-50 dark:bg-green-900/30 text-green-700 dark:text-green-400 border-green-200 dark:border-green-700",abandoned:"bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400 border-gray-200 dark:border-gray-700"},ct={todo:"\u25CB",done:"\u25CF",skipped:"\u25CC"};function Qe(r){return html`<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium border ${Ke[r]??Ke.abandoned}">${r}</span>`}function Xe(r){let e=html`
658
+ </script>`;return P({title:e?`${e.lesson.title} \u2014 devcoach`:"Import a lesson \u2014 devcoach",currentPath:"/lessons",uiTheme:r.uiTheme,head:t,body:d,scripts:a})}var pt=[[0,"No cooldown"],[15,"15 minutes"],[30,"30 minutes"],[60,"1 hour"],[90,"1 hour 30 min"],[120,"2 hours"],[180,"3 hours"],[240,"4 hours"],[360,"6 hours"],[480,"8 hours"],[720,"12 hours"],[1440,"24 hours"]],Ve={active:"bg-accent-50 dark:bg-accent-900/30 text-accent-700 dark:text-accent-300 border-accent-200 dark:border-accent-700",completed:"bg-green-50 dark:bg-green-900/30 text-green-700 dark:text-green-400 border-green-200 dark:border-green-700",abandoned:"bg-gray-100 dark:bg-gray-800 text-gray-500 dark:text-gray-400 border-gray-200 dark:border-gray-700"},ut={todo:"\u25CB",done:"\u25CF",skipped:"\u25CC"};function tt(r){return html`<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium border ${Ve[r]??Ve.abandoned}">${r}</span>`}function rt(r){let e=html`
617
659
  <div class="flex items-center justify-between mb-6">
618
- <h1 class="text-2xl font-bold text-indigo-600 dark:text-indigo-400">Courses</h1>
660
+ <h1 class="text-2xl font-bold text-accent-600 dark:text-accent-400">Courses</h1>
619
661
  <p class="text-sm text-gray-400 dark:text-gray-500">${r.courses.length} course${r.courses.length!==1?"s":""}</p>
620
662
  </div>
621
663
  ${r.courses.length?html`<div class="bg-white dark:bg-gray-900 rounded-xl border border-gray-200 dark:border-gray-800 shadow-sm overflow-hidden">
@@ -627,15 +669,15 @@ ${r.courses.length?html`<div class="bg-white dark:bg-gray-900 rounded-xl border
627
669
  </tr>
628
670
  </thead>
629
671
  <tbody class="divide-y divide-gray-100 dark:divide-gray-800">
630
- ${r.courses.map(d=>{let t=d.steps.filter(n=>n.status!=="todo").length,o=d.steps.length?Math.round(t/d.steps.length*100):0;return html`<tr class="hover:bg-gray-50 dark:hover:bg-gray-800/40 transition-colors cursor-pointer" tabindex="0" role="link" data-href="/courses/${encodeURIComponent(d.id)}" @click="if (!$event.target.closest('a, button')) window.location = $el.dataset.href" @keydown.enter="window.location = $el.dataset.href">
672
+ ${r.courses.map(d=>{let t=d.steps.filter(n=>n.status!=="todo").length,a=d.steps.length?Math.round(t/d.steps.length*100):0;return html`<tr class="hover:bg-gray-50 dark:hover:bg-gray-800/40 transition-colors cursor-pointer" tabindex="0" role="link" data-href="/courses/${encodeURIComponent(d.id)}" @click="if (!$event.target.closest('a, button')) window.location = $el.dataset.href" @keydown.enter="window.location = $el.dataset.href">
631
673
  <td class="px-4 py-3">
632
- <a href="/courses/${encodeURIComponent(d.id)}" class="font-semibold text-gray-800 dark:text-gray-100 hover:text-indigo-600 dark:hover:text-indigo-400 transition line-clamp-2">${d.title}</a>
633
- ${d.lesson_id?html`<span class="block text-[11px] text-gray-400 dark:text-gray-500 truncate">from <a href="/lessons/${encodeURIComponent(d.lesson_id)}" class="hover:text-indigo-500 hover:underline">${d.lesson_title??d.lesson_id}</a></span>`:""}
674
+ <a href="/courses/${encodeURIComponent(d.id)}" class="font-semibold text-gray-800 dark:text-gray-100 hover:text-accent-600 dark:hover:text-accent-400 transition line-clamp-2">${d.title}</a>
675
+ ${d.lesson_id?html`<span class="block text-[11px] text-gray-400 dark:text-gray-500 truncate">from <a href="/lessons/${encodeURIComponent(d.lesson_id)}" class="hover:text-accent-500 hover:underline">${d.lesson_title??d.lesson_id}</a></span>`:""}
634
676
  </td>
635
677
  <td class="px-3 py-3">
636
- <div class="flex items-center gap-2"><div class="flex-1 h-1.5 rounded-full bg-gray-100 dark:bg-gray-800 overflow-hidden"><div class="h-full bg-indigo-500" style="width:${o}%"></div></div><span class="text-xs tabular-nums text-gray-500 dark:text-gray-400 shrink-0">${t}/${d.steps.length}</span></div>
678
+ <div class="flex items-center gap-2"><div class="flex-1 h-1.5 rounded-full bg-gray-100 dark:bg-gray-800 overflow-hidden"><div class="h-full bg-accent-500" style="width:${a}%"></div></div><span class="text-xs tabular-nums text-gray-500 dark:text-gray-400 shrink-0">${t}/${d.steps.length}</span></div>
637
679
  </td>
638
- <td class="px-3 py-3">${Qe(d.status)}</td>
680
+ <td class="px-3 py-3">${tt(d.status)}</td>
639
681
  <td class="px-3 py-3 whitespace-nowrap text-gray-400 dark:text-gray-500 tabular-nums"><span data-ts="${d.updated_at}">${d.updated_at.slice(0,10)}</span></td>
640
682
  </tr>`})}
641
683
  </tbody>
@@ -644,39 +686,39 @@ ${r.courses.length?html`<div class="bg-white dark:bg-gray-900 rounded-xl border
644
686
  <p class="text-3xl mb-3">🎓</p>
645
687
  <p class="text-gray-700 dark:text-gray-200 font-medium">No courses yet.</p>
646
688
  <p class="text-sm text-gray-500 dark:text-gray-400 mt-2 max-w-md mx-auto">A course starts in your agent: answer <span class="font-mono">n</span> (couldn't follow) under a lesson card and say yes to the offer, or run <span class="font-mono">/devcoach:course</span> on any lesson or concept. The AI first asks what you already know, then writes the course here.</p>
647
- </div>`}`,g=html`<script src="/static/relative-time.js"></script>`;return P({title:"Courses \u2014 devcoach",currentPath:"/courses",uiTheme:r.uiTheme,body:e,scripts:g})}function et(r){return html`<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8" /><title>Read-only — devcoach</title>
689
+ </div>`}`,g=html`<script src="/static/relative-time.js"></script>`;return P({title:"Courses \u2014 devcoach",currentPath:"/courses",uiTheme:r.uiTheme,body:e,scripts:g})}function at(r){return html`<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8" /><title>Read-only — devcoach</title>
648
690
  <link rel="stylesheet" href="/static/style.css" /></head>
649
691
  <body style="font-family: ui-monospace, monospace; max-width: 40rem; margin: 4rem auto; padding: 0 1rem; line-height: 1.5">
650
692
  <h1 style="font-size: 1.25rem">This devcoach is older than your database</h1>
651
693
  <p>${r}</p>
652
694
  <p><a href="javascript:history.back()">← Back</a></p>
653
- </body></html>`}function tt(r){let e=r.course,g=e.steps.filter(s=>s.status!=="todo").length,d=e.steps.find(s=>s.position===r.selected)??e.steps[0],t=`/courses/${encodeURIComponent(e.id)}/index.html${d?`#${d.anchor}`:""}`,o=(s,c,u,p)=>html`<form method="post" action="/courses/${encodeURIComponent(e.id)}/steps/${s}" class="inline"><input type="hidden" name="status" value="${c}" /><input type="hidden" name="next" value="/courses/${encodeURIComponent(e.id)}?step=${s}" /><button type="submit" class="${p}">${u}</button></form>`,n="inline-flex items-center px-2.5 py-1 rounded-lg text-xs font-medium border transition bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-indigo-400",i=html`
695
+ </body></html>`}function ot(r){let e=r.course,g=e.steps.filter(s=>s.status!=="todo").length,d=e.steps.find(s=>s.position===r.selected)??e.steps[0],t=`/courses/${encodeURIComponent(e.id)}/index.html${d?`#${d.anchor}`:""}`,a=(s,c,u,p)=>html`<form method="post" action="/courses/${encodeURIComponent(e.id)}/steps/${s}" class="inline"><input type="hidden" name="status" value="${c}" /><input type="hidden" name="next" value="/courses/${encodeURIComponent(e.id)}?step=${s}" /><button type="submit" class="${p}">${u}</button></form>`,n="inline-flex items-center px-2.5 py-1 rounded-lg text-xs font-medium border transition bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-accent-400",i=html`
654
696
  <div x-data="{ confirmOpen: false }" @keydown.escape.window="confirmOpen = false">
655
697
  <div class="sticky top-0 z-20 -mx-4 sm:-mx-6 -mt-8 mb-5 px-4 sm:px-6 h-12 flex items-center gap-3 bg-gray-50/95 dark:bg-gray-950/95 backdrop-blur border-b border-gray-200 dark:border-gray-800">
656
698
  <a href="/courses" title="Back to courses" class="text-gray-400 dark:text-gray-500 hover:text-gray-700 dark:hover:text-white text-sm transition shrink-0">←</a>
657
699
  <span class="text-sm font-semibold text-gray-900 dark:text-white truncate min-w-0">${e.title}</span>
658
- ${Qe(e.status)}
700
+ ${tt(e.status)}
659
701
  <span class="text-xs text-gray-500 dark:text-gray-400 shrink-0">${g}/${e.steps.length} steps</span>
660
702
  ${Y(html`<button type="button" @click="confirmOpen = true; open = false" class="w-full text-left px-3 py-2 rounded-lg text-sm transition flex items-center gap-2 text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800 hover:!bg-rose-50 dark:hover:!bg-rose-900/30 hover:text-rose-700 dark:hover:text-rose-300"><span class="w-4 h-4 inline-flex items-center justify-center shrink-0 text-[13px] leading-none" aria-hidden="true">🗑</span>Delete course…</button>`,"ml-auto shrink-0")}
661
703
  </div>
662
704
  <div class="grid grid-cols-1 lg:grid-cols-[15rem_minmax(0,1fr)] gap-6 lg:gap-8">
663
705
  <aside id="course-steps" class="lg:sticky lg:top-16 self-start">
664
706
  <p class="text-xs font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500 px-2 mb-2">Steps</p>
665
- ${e.steps.length?html`<ol class="space-y-0.5">${e.steps.map(s=>html`<li data-anchor="${s.anchor}" class="rounded-lg px-2 py-1.5 ${d&&s.position===d.position?"bg-indigo-50 dark:bg-indigo-900/30":""}">
707
+ ${e.steps.length?html`<ol class="space-y-0.5">${e.steps.map(s=>html`<li data-anchor="${s.anchor}" class="rounded-lg px-2 py-1.5 ${d&&s.position===d.position?"bg-accent-50 dark:bg-accent-900/30":""}">
666
708
  <a href="/courses/${encodeURIComponent(e.id)}?step=${s.position}" data-anchor="${s.anchor}" class="flex items-start gap-2 text-sm">
667
- <span class="${s.status==="done"?"text-green-500":s.status==="skipped"?"text-gray-400":"text-indigo-400"}">${ct[s.status]??"\u25CB"}</span>
709
+ <span class="${s.status==="done"?"text-green-500":s.status==="skipped"?"text-gray-400":"text-accent-400"}">${ut[s.status]??"\u25CB"}</span>
668
710
  <span class="min-w-0 flex-1"><span class="block text-gray-800 dark:text-gray-100">${s.position}. ${s.title}</span><span class="block text-[11px] text-gray-400 dark:text-gray-500">${s.kind}${s.status!=="todo"?` \xB7 ${s.status}`:""}</span></span>
669
711
  </a>
670
712
  <div data-step-actions class="flex flex-wrap gap-1.5 mt-2 pl-6 ${d&&s.position===d.position?"":"hidden"}">
671
- ${s.status!=="done"?o(s.position,"done","\u2713 Mark done",`${n} hover:border-green-400 hover:text-green-700 dark:hover:text-green-300`):o(s.position,"todo","\u21BA Reopen",n)}
672
- ${s.status==="todo"?o(s.position,"skipped","Skip",n):""}
713
+ ${s.status!=="done"?a(s.position,"done","\u2713 Mark done",`${n} hover:border-green-400 hover:text-green-700 dark:hover:text-green-300`):a(s.position,"todo","\u21BA Reopen",n)}
714
+ ${s.status==="todo"?a(s.position,"skipped","Skip",n):""}
673
715
  </div>
674
716
  </li>`)}</ol>`:html`<p class="text-sm text-gray-500 dark:text-gray-400 px-2">No steps registered yet.</p>`}
675
717
  <details class="mt-4 px-2 text-xs text-gray-500 dark:text-gray-400">
676
718
  <summary class="cursor-pointer font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500 select-none">About this course</summary>
677
719
  <div class="mt-2 space-y-2">
678
720
  <p>🏷 <span class="text-cyan-600 dark:text-cyan-400">${e.topic_id}</span></p>
679
- ${e.lesson_id?html`<p>from <a href="/lessons/${encodeURIComponent(e.lesson_id)}" class="text-gray-700 dark:text-gray-200 hover:text-indigo-500 hover:underline">${r.lessonTitle??e.lesson_id}</a></p>`:""}
721
+ ${e.lesson_id?html`<p>from <a href="/lessons/${encodeURIComponent(e.lesson_id)}" class="text-gray-700 dark:text-gray-200 hover:text-accent-500 hover:underline">${r.lessonTitle??e.lesson_id}</a></p>`:""}
680
722
  ${e.goal?html`<p class="text-gray-600 dark:text-gray-300">${e.goal}</p>`:""}
681
723
  ${e.prerequisites.length?html`<div class="flex flex-wrap items-center gap-1" title="The prerequisite chain explored before writing the course">
682
724
  ${e.prerequisites.map(s=>html`<span class="inline-flex items-center gap-1 px-1.5 py-0.5 rounded-full border ${s.known?"bg-green-50 dark:bg-green-900/20 text-green-700 dark:text-green-300 border-green-200 dark:border-green-800":"bg-rose-50 dark:bg-rose-900/20 text-rose-700 dark:text-rose-300 border-rose-200 dark:border-rose-800"}">${s.known?"\u2713":"\u2717"} ${s.concept}</span>`)}
@@ -689,30 +731,30 @@ ${r.courses.length?html`<div class="bg-white dark:bg-gray-900 rounded-xl border
689
731
  </div>
690
732
  </div>
691
733
  ${J("confirmOpen","confirmOpen = false",html`Delete this course?`,html`<p class="text-gray-700 dark:text-gray-200 font-medium">“${e.title}”</p>
692
- <p>The course, its steps and its document folder will be removed. This cannot be undone.${e.lesson_id?" The lesson it grew from stays.":""}</p>`,html`<button type="button" @click="confirmOpen = false" class="inline-flex items-center px-3 py-1.5 rounded-lg text-sm font-medium border transition bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-indigo-400">Cancel</button>
734
+ <p>The course, its steps and its document folder will be removed. This cannot be undone.${e.lesson_id?" The lesson it grew from stays.":""}</p>`,html`<button type="button" @click="confirmOpen = false" class="inline-flex items-center px-3 py-1.5 rounded-lg text-sm font-medium border transition bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-200 hover:border-accent-400">Cancel</button>
693
735
  <form method="post" action="/courses/delete" class="contents">
694
736
  <input type="hidden" name="id" value="${e.id}" />
695
737
  <button type="submit" class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium border transition bg-rose-600 text-white border-rose-600 hover:bg-rose-500">🗑 Delete course</button>
696
738
  </form>`)}
697
- </div>`;return P({title:`${e.title} \u2014 devcoach`,currentPath:"/courses",uiTheme:r.uiTheme,body:i,scripts:html`<script src="/static/course-viewer.js"></script>`,wide:true})}function rt(r){let e=r.settings.min_gap_minutes,g=[["system","System","\u{1F313}"],["light","Light","\u2600\uFE0F"],["dark","Dark","\u{1F319}"]],d=[["auto","Auto","\u{1F3E0}"],["lessons","Lessons","\u{1F4DA}"],["knowledge","Knowledge map","\u{1F9ED}"]],t=r.flash,o=html`
698
- <h1 class="text-2xl font-bold mb-6 text-indigo-600 dark:text-indigo-400">Settings</h1>
739
+ </div>`;return P({title:`${e.title} \u2014 devcoach`,currentPath:"/courses",uiTheme:r.uiTheme,body:i,scripts:html`<script src="/static/course-viewer.js"></script>`,wide:true})}function st(r){let e=r.settings.min_gap_minutes,g=[["system","System","\u{1F313}"],["light","Light","\u2600\uFE0F"],["dark","Dark","\u{1F319}"]],d=[["auto","Auto","\u{1F3E0}"],["lessons","Lessons","\u{1F4DA}"],["knowledge","Knowledge map","\u{1F9ED}"]],t=r.flash,a=html`
740
+ <h1 class="text-2xl font-bold mb-6 text-accent-600 dark:text-accent-400">Settings</h1>
699
741
  ${t?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm ${t.invalid?"bg-yellow-50 dark:bg-yellow-900/20 border-yellow-200 dark:border-yellow-700 text-yellow-800 dark:text-yellow-300":"bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-700 text-green-700 dark:text-green-300"}">
700
742
  ${t.imported} lesson${t.imported!==1?"s":""} imported.${t.skipped?` ${t.skipped} skipped (already in DB).`:""}${t.invalid?` ${t.invalid} rejected (failed validation).`:""}${t.groups?` ${t.groups} group${t.groups!==1?"s":""} added.`:""}${t.notebook?" Notebook restored.":""}
701
743
  </div>`:""}
702
744
  <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
703
745
  <div class="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-xl p-6">
704
746
  <p class="text-xs font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500 mb-5">Coaching</p>
705
- <form method="post" action="/settings" class="space-y-4">
747
+ <form method="post" action="/settings" class="space-y-4" onsubmit="try { localStorage.removeItem('theme-override') } catch (e) {}">
706
748
  <div class="grid grid-cols-2 gap-4">
707
749
  <div>
708
750
  <label for="max-per-day" class="block text-sm text-gray-600 dark:text-gray-400 mb-1">Max lessons per day</label>
709
- <input id="max-per-day" type="number" name="max_per_day" min="1" max="20" value="${r.settings.max_per_day}" class="w-full bg-gray-50 dark:bg-gray-800 border border-gray-300 dark:border-gray-700 text-gray-900 dark:text-white rounded px-3 py-2 text-sm focus:outline-none focus:border-indigo-500" />
751
+ <input id="max-per-day" type="number" name="max_per_day" min="1" max="20" value="${r.settings.max_per_day}" class="w-full bg-gray-50 dark:bg-gray-800 border border-gray-300 dark:border-gray-700 text-gray-900 dark:text-white rounded px-3 py-2 text-sm focus:outline-none focus:border-accent-500" />
710
752
  <p class="text-xs text-gray-400 dark:text-gray-500 mt-1">Lessons in a 24h window.</p>
711
753
  </div>
712
754
  <div>
713
755
  <label for="min-gap-minutes" class="block text-sm text-gray-600 dark:text-gray-400 mb-1">Minimum gap between lessons</label>
714
- <select id="min-gap-minutes" name="min_gap_minutes" class="w-full bg-gray-50 dark:bg-gray-800 border border-gray-300 dark:border-gray-700 text-gray-900 dark:text-white rounded px-3 py-2 text-sm focus:outline-none focus:border-indigo-500">
715
- ${lt.map(([i,s])=>html`<option value="${i}" ${e===i?"selected":""}>${s}</option>`)}
756
+ <select id="min-gap-minutes" name="min_gap_minutes" class="w-full bg-gray-50 dark:bg-gray-800 border border-gray-300 dark:border-gray-700 text-gray-900 dark:text-white rounded px-3 py-2 text-sm focus:outline-none focus:border-accent-500">
757
+ ${pt.map(([i,s])=>html`<option value="${i}" ${e===i?"selected":""}>${s}</option>`)}
716
758
  </select>
717
759
  <p class="text-xs text-gray-400 dark:text-gray-500 mt-1">Minimum gap between lessons.</p>
718
760
  </div>
@@ -720,12 +762,12 @@ ${t?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm ${t.invalid?"bg-ye
720
762
  <div class="grid grid-cols-2 gap-4">
721
763
  <div>
722
764
  <label for="nudge-every" class="block text-sm text-gray-600 dark:text-gray-400 mb-1">Interactions between lessons</label>
723
- <input id="nudge-every" type="number" name="nudge_every" min="0" max="1000" value="${r.settings.nudge_every}" class="w-full bg-gray-50 dark:bg-gray-800 border border-gray-300 dark:border-gray-700 text-gray-900 dark:text-white rounded px-3 py-2 text-sm focus:outline-none focus:border-indigo-500" />
765
+ <input id="nudge-every" type="number" name="nudge_every" min="0" max="1000" value="${r.settings.nudge_every}" class="w-full bg-gray-50 dark:bg-gray-800 border border-gray-300 dark:border-gray-700 text-gray-900 dark:text-white rounded px-3 py-2 text-sm focus:outline-none focus:border-accent-500" />
724
766
  <p class="text-xs text-gray-400 dark:text-gray-500 mt-1">Cue at most once every N (0 = every turn).</p>
725
767
  </div>
726
768
  <div>
727
769
  <label for="nudge-scope" class="block text-sm text-gray-600 dark:text-gray-400 mb-1">Count interactions</label>
728
- <select id="nudge-scope" name="nudge_scope" class="w-full bg-gray-50 dark:bg-gray-800 border border-gray-300 dark:border-gray-700 text-gray-900 dark:text-white rounded px-3 py-2 text-sm focus:outline-none focus:border-indigo-500">
770
+ <select id="nudge-scope" name="nudge_scope" class="w-full bg-gray-50 dark:bg-gray-800 border border-gray-300 dark:border-gray-700 text-gray-900 dark:text-white rounded px-3 py-2 text-sm focus:outline-none focus:border-accent-500">
729
771
  <option value="session" ${r.settings.nudge_scope==="session"?"selected":""}>Per chat session</option>
730
772
  <option value="global" ${r.settings.nudge_scope==="global"?"selected":""}>Globally</option>
731
773
  </select>
@@ -734,7 +776,7 @@ ${t?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm ${t.invalid?"bg-ye
734
776
  </div>
735
777
  <div>
736
778
  <label for="share-name" class="block text-sm text-gray-600 dark:text-gray-400 mb-1">Your name (for sharing)</label>
737
- <input id="share-name" type="text" name="share_name" maxlength="80" value="${r.settings.share_name??""}" placeholder="git user.name when empty" autocomplete="off" class="w-full bg-gray-50 dark:bg-gray-800 border border-gray-300 dark:border-gray-700 text-gray-900 dark:text-white rounded px-3 py-2 text-sm focus:outline-none focus:border-indigo-500" />
779
+ <input id="share-name" type="text" name="share_name" maxlength="80" value="${r.settings.share_name??""}" placeholder="git user.name when empty" autocomplete="off" class="w-full bg-gray-50 dark:bg-gray-800 border border-gray-300 dark:border-gray-700 text-gray-900 dark:text-white rounded px-3 py-2 text-sm focus:outline-none focus:border-accent-500" />
738
780
  <p class="text-xs text-gray-400 dark:text-gray-500 mt-1">Proposed as the sender when you share a lesson. Empty → your git name.</p>
739
781
  </div>
740
782
  <fieldset class="border-0 p-0 m-0">
@@ -742,7 +784,7 @@ ${t?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm ${t.invalid?"bg-ye
742
784
  <div class="flex gap-2">
743
785
  ${g.map(([i,s,c])=>html`<label class="flex-1 cursor-pointer">
744
786
  <input type="radio" name="ui_theme" value="${i}" ${r.settings.ui_theme===i?"checked":""} class="sr-only peer" />
745
- <span class="flex flex-col items-center justify-center gap-1 px-3 py-2 rounded border border-gray-300 dark:border-gray-700 peer-checked:border-indigo-500 peer-checked:bg-indigo-50 dark:peer-checked:bg-indigo-900/30 peer-checked:text-indigo-700 dark:peer-checked:text-indigo-300 text-gray-500 dark:text-gray-400 text-xs font-medium transition select-none"><span class="text-base leading-none">${c}</span>${s}</span>
787
+ <span class="flex flex-col items-center justify-center gap-1 px-3 py-2 rounded border border-gray-300 dark:border-gray-700 peer-checked:border-accent-500 peer-checked:bg-accent-50 dark:peer-checked:bg-accent-900/30 peer-checked:text-accent-700 dark:peer-checked:text-accent-300 text-gray-500 dark:text-gray-400 text-xs font-medium transition select-none"><span class="text-base leading-none">${c}</span>${s}</span>
746
788
  </label>`)}
747
789
  </div>
748
790
  <p class="text-xs text-gray-400 dark:text-gray-500 mt-1">System follows your OS preference.</p>
@@ -752,12 +794,12 @@ ${t?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm ${t.invalid?"bg-ye
752
794
  <div class="flex gap-2">
753
795
  ${d.map(([i,s,c])=>html`<label class="flex-1 cursor-pointer">
754
796
  <input type="radio" name="ui_home" value="${i}" ${r.settings.ui_home===i?"checked":""} class="sr-only peer" />
755
- <span class="flex flex-col items-center justify-center gap-1 px-3 py-2 rounded border border-gray-300 dark:border-gray-700 peer-checked:border-indigo-500 peer-checked:bg-indigo-50 dark:peer-checked:bg-indigo-900/30 peer-checked:text-indigo-700 dark:peer-checked:text-indigo-300 text-gray-500 dark:text-gray-400 text-xs font-medium transition select-none"><span class="text-base leading-none">${c}</span>${s}</span>
797
+ <span class="flex flex-col items-center justify-center gap-1 px-3 py-2 rounded border border-gray-300 dark:border-gray-700 peer-checked:border-accent-500 peer-checked:bg-accent-50 dark:peer-checked:bg-accent-900/30 peer-checked:text-accent-700 dark:peer-checked:text-accent-300 text-gray-500 dark:text-gray-400 text-xs font-medium transition select-none"><span class="text-base leading-none">${c}</span>${s}</span>
756
798
  </label>`)}
757
799
  </div>
758
800
  <p class="text-xs text-gray-400 dark:text-gray-500 mt-1">Auto opens Lessons once you have one, the knowledge map before.</p>
759
801
  </fieldset>
760
- <button type="submit" class="bg-indigo-600 hover:bg-indigo-500 text-white font-semibold rounded px-4 py-2 text-sm transition">Save settings</button>
802
+ <button type="submit" class="bg-accent-600 hover:bg-accent-500 text-white font-semibold rounded px-4 py-2 text-sm transition">Save settings</button>
761
803
  </form>
762
804
  </div>
763
805
 
@@ -775,7 +817,7 @@ ${t?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm ${t.invalid?"bg-ye
775
817
  <form method="post" action="/settings/import" enctype="multipart/form-data" class="mt-auto flex flex-col gap-2">
776
818
  <input id="file-restore" type="file" name="file" accept=".zip,.json" class="hidden" onchange="updateLabel('file-restore','label-restore','restore-submit')" />
777
819
  <label id="label-restore" for="file-restore" class="cursor-pointer inline-flex items-center justify-center gap-1.5 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-200 font-medium rounded px-4 py-2 text-sm transition truncate">Choose file…</label>
778
- <button id="restore-submit" type="submit" disabled class="bg-indigo-600 hover:bg-indigo-500 disabled:opacity-40 disabled:cursor-not-allowed text-white font-medium rounded px-4 py-2 text-sm transition">Restore</button>
820
+ <button id="restore-submit" type="submit" disabled class="bg-accent-600 hover:bg-accent-500 disabled:opacity-40 disabled:cursor-not-allowed text-white font-medium rounded px-4 py-2 text-sm transition">Restore</button>
779
821
  </form>
780
822
  </div>
781
823
  <div class="flex flex-col gap-1">
@@ -789,7 +831,7 @@ ${t?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm ${t.invalid?"bg-ye
789
831
  <form method="post" action="/lessons/import" enctype="multipart/form-data" class="mt-auto flex flex-col gap-2">
790
832
  <input id="file-import" type="file" name="file" accept=".json" class="hidden" onchange="updateLabel('file-import','label-import','import-submit')" />
791
833
  <label id="label-import" for="file-import" class="cursor-pointer inline-flex items-center justify-center gap-1.5 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-200 font-medium rounded px-4 py-2 text-sm transition truncate">Choose file…</label>
792
- <button id="import-submit" type="submit" disabled class="bg-indigo-600 hover:bg-indigo-500 disabled:opacity-40 disabled:cursor-not-allowed text-white font-medium rounded px-4 py-2 text-sm transition">Import</button>
834
+ <button id="import-submit" type="submit" disabled class="bg-accent-600 hover:bg-accent-500 disabled:opacity-40 disabled:cursor-not-allowed text-white font-medium rounded px-4 py-2 text-sm transition">Import</button>
793
835
  </form>
794
836
  </div>
795
837
  </div>
@@ -800,8 +842,8 @@ ${t?html`<div class="mb-4 px-4 py-2 rounded-lg border text-sm ${t.invalid?"bg-ye
800
842
  <p class="text-xs font-semibold uppercase tracking-wide text-gray-400 dark:text-gray-500">Coaching Notebook</p>
801
843
  <div class="flex items-center gap-2">
802
844
  <div class="flex rounded border border-gray-200 dark:border-gray-700 overflow-hidden text-xs font-medium">
803
- <button type="button" @click="nbMode = 'preview'" :class="nbMode === 'preview' ? 'bg-indigo-600 text-white' : 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700'" class="px-3 py-1.5 transition">Preview</button>
804
- <button type="button" @click="nbMode = 'source'" :class="nbMode === 'source' ? 'bg-indigo-600 text-white' : 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700'" class="px-3 py-1.5 transition border-l border-gray-200 dark:border-gray-700">Source</button>
845
+ <button type="button" @click="nbMode = 'preview'" :class="nbMode === 'preview' ? 'bg-accent-600 text-white' : 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700'" class="px-3 py-1.5 transition">Preview</button>
846
+ <button type="button" @click="nbMode = 'source'" :class="nbMode === 'source' ? 'bg-accent-600 text-white' : 'bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700'" class="px-3 py-1.5 transition border-l border-gray-200 dark:border-gray-700">Source</button>
805
847
  </div>
806
848
  <a href="/settings/notebook/download" class="inline-flex items-center gap-1.5 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-200 font-medium rounded px-3 py-1.5 text-xs transition">↓ Download</a>
807
849
  <a href="vscode://file/${r.notebookPath}" title="Open in VS Code" class="inline-flex items-center gap-1.5 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-200 font-medium rounded px-3 py-1.5 text-xs transition"><img src="/static/vscode.svg" class="w-4 h-4" alt="VS Code" />Open in VS Code</a>
@@ -820,9 +862,10 @@ function updateLabel(inputId, labelId, submitId) {
820
862
  }
821
863
  marked.setOptions({ breaks: true, gfm: true });
822
864
  document.getElementById('notebook-preview-settings').innerHTML = DOMPurify.sanitize(marked.parse(${raw(j(r.notebookContent))}));
823
- </script>`;return P({title:"Settings \u2014 devcoach",currentPath:"/settings",uiTheme:r.uiTheme,body:o,scripts:n})}var Z=25;function B(r,e="/lessons"){return r?.startsWith("/")&&!r.startsWith("//")?r:e}function b(r,e,g=""){let d=r[e];return typeof d=="string"?d:g}var Q=dirname(fileURLToPath(import.meta.url)),X=[join(Q,"../assets/static"),join(Q,"../../assets/static")].find(r=>existsSync(r))??join(Q,"../assets/static"),bt={".css":"text/css; charset=utf-8",".js":"text/javascript; charset=utf-8",".mjs":"text/javascript; charset=utf-8",".json":"application/json",".svg":"image/svg+xml",".png":"image/png",".woff2":"font/woff2",".woff":"font/woff",".ico":"image/x-icon"},ht="https://ultimaphoenix.github.io",ot={"access-control-allow-origin":ht,"access-control-allow-methods":"GET, OPTIONS","access-control-allow-private-network":"true","cache-control":"no-store",vary:"Origin"};function E(r){let e=r.req.header("sec-fetch-site");return e!==void 0&&e!=="same-origin"&&e!=="none"}var ee;function ft(){return ee===void 0&&(ee=n()),ee}function at(r,e,g){let d=pa({explicit:g.name??null,setting:e,gitUserName:ft()}),t=g.includeContext??false,o=oa(r,{includeContext:t,sharedBy:d}),n=qa(o);return {payload:o,state:{id:r.id,open:g.open??false,name:d??"",includeContext:t,text:sa(o,n),link:ta(o,n)}}}function kt(r,e){return r==="lessons"?"/lessons":r==="knowledge"?"/knowledge":e?"/lessons":"/knowledge"}function vt(){try{return A(r=>T(r).ui_theme)}catch{return "system"}}function wt(r={}){let e$1=new Hono;e$1.onError((t,o)=>t instanceof w?o.html(et(t.message),503):(console.error(t),o.text("Internal Server Error",500))),e$1.get("/static/*",t=>{let o=decodeURIComponent(t.req.path.slice(8)),n=join(X,o);if(!n.startsWith(X))return t.notFound();try{let i=readFileSync(n);return new Response(new Uint8Array(i),{headers:{"content-type":bt[extname(n)]??"application/octet-stream","cache-control":"no-cache"}})}catch{return t.notFound()}}),e$1.get("/ping",t=>{let o=null;try{o=A(n=>{let i=x(n);return i?{...i,message:v(i)}:null});}catch{}return t.json({ok:true,version:a,compat:o},200,ot)}),e$1.options("/ping",t=>t.body(null,204,ot)),e$1.post("/shutdown",t=>E(t)?t.text("Forbidden",403):(setTimeout(()=>r.onShutdown?.(),50),t.json({ok:true}))),e$1.get("/shutdown",t=>t.text("Method Not Allowed",405)),e$1.get("/",t=>{let{home:o,hasLessons:n}=A(i=>({home:T(i).ui_home,hasLessons:C(i)>0}));return t.redirect(kt(o,n),302)}),e$1.get("/knowledge",t=>{let{profile:o,stats:n,rateLimit:i,settings:s}=A(p=>({profile:ya(p),stats:Ba(p),rateLimit:xa(p),settings:T(p)})),c=new Map;for(let p of o.groups)for(let m of p.topics)c.set(m,p.name);let u={};for(let p of o.groups)u[p.name]=[];for(let p of o.knowledge){let m=c.get(p.topic)??"Other",v=u[m]??[];v.push(p),u[m]=v;}return t.html(Je({categorised:u,allGroups:o.groups.map(p=>p.name),stats:n,rateLimit:i,maxPerDay:s.max_per_day,uiTheme:s.ui_theme}))}),e$1.post("/knowledge",async t=>{let o=await t.req.parseBody(),n=b(o,"topic").trim();if(n){let i=Number.parseInt(b(o,"confidence","5"),10)||5,s=b(o,"group").trim();A(c=>{N(c,n,i),s&&s!=="Other"&&R(c,n,s);});}return t.redirect("/knowledge",303)}),e$1.post("/knowledge/:topic/delete",t=>(A(o=>O(o,t.req.param("topic"))),t.redirect("/knowledge",303))),e$1.post("/knowledge/:topic/group",async t=>{let o=b(await t.req.parseBody(),"group").trim(),n=t.req.param("topic");return A(i=>{o&&o!=="Other"?R(i,n,o):S(i,n);}),t.redirect("/knowledge",303)}),e$1.post("/knowledge/:topic",async t=>{let o=Number.parseInt(b(await t.req.parseBody(),"delta","0"),10)||0;return A(n=>za(n,t.req.param("topic"),o)),t.redirect("/knowledge",303)}),e$1.post("/groups",async t=>{let o=b(await t.req.parseBody(),"group_name").trim();return o&&o!=="Other"&&A(n=>P$1(n,o)),t.redirect("/knowledge",303)}),e$1.post("/groups/:group_name/delete",t=>(A(o=>Q$1(o,t.req.param("group_name"))),t.redirect("/knowledge",303))),e$1.get("/lessons/export",()=>{let t=A(o=>H(o));return new Response(JSON.stringify(t,null,2),{headers:{"content-type":"application/json","content-disposition":"attachment; filename=devcoach-lessons.json"}})}),e$1.post("/lessons/import",async t=>{let o=await t.req.parseBody(),n=b(o,"from")==="lessons",i=p=>t.redirect(n?`/lessons?import=1&error=${p}`:"/settings?imported=0&skipped=0&invalid=1",303);if(E(t))return i("cross");let s=o.file,u=(s instanceof File&&s.size>0?await s.text():null)??b(o,"text").trim();try{p(u)&&!u.includes("#devcoach:lesson:")&&(u=await q(u));let p$1=A(m=>Ca(m,u));return p$1.kind==="lessons"||!p$1.lesson?t.redirect(`/settings?imported=${p$1.inserted}&skipped=${p$1.duplicated}&invalid=${p$1.invalid}`,303):t.redirect(`/lessons/${encodeURIComponent(p$1.lesson.id)}?imported=${p$1.inserted?"1":"dup"}`,303)}catch(p){if(p instanceof na)return i("invalid");throw p}}),e$1.get("/lessons/import",t=>{let o=(t.req.query("code")??"").trim(),n=null,i=null;if(o)try{n=ra(o);}catch(s){i=s instanceof na?s.message:"That code could not be read.";}return t.html(Ze({code:o,preview:n,error:i,uiTheme:vt()}))}),e$1.get("/lessons",t=>{let o=t.req.query(),n=new Set(["junior","mid","senior"]),i=o.level&&n.has(o.level)?o.level:"",s=o.date_from||"",c=o.date_to||"",u=o.starred==="1",p=o.imported==="1"?"1":o.imported==="0"?"0":"",m=o.shared_by||null,v=o.sort||"timestamp",I$1=o.order==="asc"?"asc":"desc",R=Math.max(1,Number.parseInt(o.page??"1",10)||1),L$1=o.period||"all",l={period:s||c?null:L$1!=="all"?L$1:null,category:o.category||null,level:i||null,project:o.project||null,repository:o.repository||null,branch:o.branch||null,commit:o.commit||null,starred:u?true:null,imported:m||p==="1"?true:p==="0"?false:null,shared_by:m,search:o.search||null,feedback:o.feedback||null,date_from:s||null,date_to:c||null},x=A(k=>{let re=C(k,l),oe=Math.max(1,Math.ceil(re/Z));return R=Math.min(R,oe),{total:re,totalPages:oe,lessons:D(k,{...l,sort:v,order:I$1,page:R,per_page:Z}),allCategories:L(k),allProjects:I(k,"project"),allRepositories:I(k,"repository"),allBranches:I(k,"branch"),allCommits:I(k,"commit_hash"),allSharedBy:J$1(k),theme:T(k).ui_theme}}),$={period:L$1,category:o.category||"",level:i,project:o.project||"",repository:o.repository||"",branch:o.branch||"",commit:o.commit||"",starred:u,imported:m?"1":p,shared_by:m??"",search:o.search||"",feedback:o.feedback||"",date_from:s,date_to:c,sort:v,order:I$1},S=o.error==="invalid"||o.error==="cross"?o.error:null;return t.html(Ve({importOpen:o.import==="1",importError:S,lessons:x.lessons,allCategories:x.allCategories,allProjects:x.allProjects,allRepositories:x.allRepositories,allBranches:x.allBranches,allCommits:x.allCommits,allSharedBy:x.allSharedBy,s:$,page:R,perPage:Z,total:x.total,totalPages:x.totalPages,uiTheme:x.theme}))}),e$1.post("/lessons/:lesson_id/star",async t=>{let o=await t.req.parseBody();return A(n=>G(n,t.req.param("lesson_id"),o.starred==="1")),t.redirect(B(b(o,"next")||void 0),303)}),e$1.post("/lessons/:lesson_id/feedback",async t=>{let o=await t.req.parseBody(),n=b(o,"feedback");if(!["","clear","know","understood","dont_know"].includes(n))return t.text("Unknown feedback value",400);let i=n===""||n==="clear"?null:n;return A(s=>Aa(s,t.req.param("lesson_id"),i)),t.redirect(B(b(o,"next")||void 0),303)}),e$1.post("/lessons/delete",async t=>{if(E(t))return t.text("Forbidden",403);let o=await t.req.parseBody({all:true}),n=o.id,i=(Array.isArray(n)?n:[n]).filter(s=>typeof s=="string"&&s!=="");return A(s=>F(s,i)),t.redirect(B(b(o,"next")||void 0),303)}),e$1.on(["GET","POST"],"/lessons/:lesson_id/share",async t$1=>{if(E(t$1))return t$1.text("Forbidden",403);let o,n,i=false;if(t$1.req.method==="POST"){let m=await t$1.req.parseBody();o=b(m,"name"),n=b(m,"include_context")==="1",i=b(m,"persist")==="1";}else o=t$1.req.query("name"),n=t$1.req.query("include_context")==="1";let s=A(m=>{let v=K$1(m,t$1.req.param("lesson_id")??"");return v?(i&&o!==void 0&&U(m,"share_name",t(o)),{lesson:v,settingName:T(m).share_name}):null});if(!s)return t$1.text("Lesson not found",404);let{state:c,payload:u}=at(s.lesson,s.settingName,{name:o,includeContext:n}),p=t$1.req.query("format");return p==="text"?t$1.text(c.text):p==="link"?t$1.text(c.link):p==="md"?new Response(wa(u),{headers:{"content-type":"text/markdown; charset=utf-8","content-disposition":`attachment; filename="${va(u)}"`}}):p==="panel"?t$1.html(W(s.lesson.id,c,s.lesson.title)):t$1.html(V(c))}),e$1.get("/lessons/:lesson_id",t=>{let o=A(s=>{let c=K$1(s,t.req.param("lesson_id"));if(!c)return null;let u=T(s),p=h$1(s,{lesson_id:c.id})[0],m$1=p?{id:p.id,title:p.title,status:p.status,...m(p)}:null;return {lesson:c,course:m$1,settingName:u.share_name,uiTheme:u.ui_theme}});if(!o)return t.html("<h1>Lesson not found</h1>",404);let n=t.req.query(),i=n.imported==="1"?"new":n.imported==="dup"?"dup":null;return t.html(We({lesson:o.lesson,uiTheme:o.uiTheme,share:at(o.lesson,o.settingName,{open:n.share==="1"}).state,imported:i,course:o.course}))});let g$1={"content-type":"text/html; charset=utf-8","content-security-policy":"sandbox allow-scripts allow-forms; default-src 'none'; script-src 'unsafe-inline' 'unsafe-eval'; style-src 'unsafe-inline'; img-src data:; font-src data:; form-action 'none'; base-uri 'none'; frame-ancestors 'self'","referrer-policy":"no-referrer","x-content-type-options":"nosniff","cache-control":"no-cache"},d=(()=>{try{return `
865
+ </script>`;return P({title:"Settings \u2014 devcoach",currentPath:"/settings",uiTheme:r.uiTheme,body:a,scripts:n})}var Z=25;function B(r,e="/lessons"){return r?.startsWith("/")&&!r.startsWith("//")?r:e}function b(r,e,g=""){let d=r[e];return typeof d=="string"?d:g}var Q=dirname(fileURLToPath(import.meta.url)),X=[join(Q,"../assets/static"),join(Q,"../../assets/static")].find(r=>existsSync(r))??join(Q,"../assets/static"),kt={".css":"text/css; charset=utf-8",".js":"text/javascript; charset=utf-8",".mjs":"text/javascript; charset=utf-8",".json":"application/json",".svg":"image/svg+xml",".png":"image/png",".woff2":"font/woff2",".woff":"font/woff",".ico":"image/x-icon"},vt="https://ultimaphoenix.github.io",nt={"access-control-allow-origin":vt,"access-control-allow-methods":"GET, OPTIONS","access-control-allow-private-network":"true","cache-control":"no-store",vary:"Origin"};function E(r){let e=r.req.header("sec-fetch-site");return e!==void 0&&e!=="same-origin"&&e!=="none"}var ee;function wt(){return ee===void 0&&(ee=n()),ee}function it(r,e,g){let d=ta({explicit:g.name??null,setting:e,gitUserName:wt()}),t=g.includeContext??false,a=sa(r,{includeContext:t,sharedBy:d}),n=ua(a);return {payload:a,state:{id:r.id,open:g.open??false,name:d??"",includeContext:t,text:wa(a,n),link:xa(a,n)}}}function $t(r,e){return r==="lessons"?"/lessons":r==="knowledge"?"/knowledge":e?"/lessons":"/knowledge"}function _t(){try{return E$1(r=>X$1(r).ui_theme)}catch{return "system"}}function St(r={}){let e$1=new Hono;e$1.onError((t,a)=>t instanceof A?a.html(at(t.message),503):(console.error(t),a.text("Internal Server Error",500))),e$1.get("/static/*",t=>{let a=decodeURIComponent(t.req.path.slice(8)),n=join(X,a);if(!n.startsWith(X))return t.notFound();try{let i=readFileSync(n);return new Response(new Uint8Array(i),{headers:{"content-type":kt[extname(n)]??"application/octet-stream","cache-control":"no-cache"}})}catch{return t.notFound()}}),e$1.get("/ping",t=>{let a$1=null;try{a$1=E$1(n=>{let i=B$1(n);return i?{...i,message:z(i)}:null});}catch{}return t.json({ok:true,version:a,compat:a$1},200,nt)}),e$1.options("/ping",t=>t.body(null,204,nt)),e$1.post("/shutdown",t=>E(t)?t.text("Forbidden",403):(setTimeout(()=>r.onShutdown?.(),50),t.json({ok:true}))),e$1.get("/shutdown",t=>t.text("Method Not Allowed",405)),e$1.get("/",t=>{let{home:a,hasLessons:n}=E$1(i=>({home:X$1(i).ui_home,hasLessons:G(i)>0}));return t.redirect($t(a,n),302)}),e$1.get("/knowledge",t=>{let{profile:a,stats:n,rateLimit:i,settings:s}=E$1(p=>({profile:Ca(p),stats:Fa(p),rateLimit:Ba(p),settings:X$1(p)})),c=new Map;for(let p of a.groups)for(let m of p.topics)c.set(m,p.name);let u={};for(let p of a.groups)u[p.name]=[];for(let p of a.knowledge){let m=c.get(p.topic)??"Other",v=u[m]??[];v.push(p),u[m]=v;}return t.html(Ze({categorised:u,allGroups:a.groups.map(p=>p.name),stats:n,rateLimit:i,maxPerDay:s.max_per_day,uiTheme:s.ui_theme}))}),e$1.post("/knowledge",async t=>{let a=await t.req.parseBody(),n=b(a,"topic").trim();if(n){let i=Number.parseInt(b(a,"confidence","5"),10)||5,s=b(a,"group").trim();E$1(c=>{R(c,n,i),s&&s!=="Other"&&V$1(c,n,s);});}return t.redirect("/knowledge",303)}),e$1.post("/knowledge/:topic/delete",t=>(E$1(a=>S(a,t.req.param("topic"))),t.redirect("/knowledge",303))),e$1.post("/knowledge/:topic/group",async t=>{let a=b(await t.req.parseBody(),"group").trim(),n=t.req.param("topic");return E$1(i=>{a&&a!=="Other"?V$1(i,n,a):W$1(i,n);}),t.redirect("/knowledge",303)}),e$1.post("/knowledge/:topic",async t=>{let a=Number.parseInt(b(await t.req.parseBody(),"delta","0"),10)||0;return E$1(n=>Da(n,t.req.param("topic"),a)),t.redirect("/knowledge",303)}),e$1.post("/groups",async t=>{let a=b(await t.req.parseBody(),"group_name").trim();return a&&a!=="Other"&&E$1(n=>T(n,a)),t.redirect("/knowledge",303)}),e$1.post("/groups/:group_name/delete",t=>(E$1(a=>U(a,t.req.param("group_name"))),t.redirect("/knowledge",303))),e$1.get("/lessons/export",()=>{let t=E$1(a=>L(a));return new Response(JSON.stringify(t,null,2),{headers:{"content-type":"application/json","content-disposition":"attachment; filename=devcoach-lessons.json"}})}),e$1.post("/lessons/import",async t=>{let a=await t.req.parseBody(),n=b(a,"from")==="lessons",i=p=>t.redirect(n?`/lessons?import=1&error=${p}`:"/settings?imported=0&skipped=0&invalid=1",303);if(E(t))return i("cross");let s=a.file,u=(s instanceof File&&s.size>0?await s.text():null)??b(a,"text").trim();try{p(u)&&!u.includes("#devcoach:lesson:")&&(u=await q(u));let p$1=E$1(m=>Ga(m,u));return p$1.kind==="lessons"||!p$1.lesson?t.redirect(`/settings?imported=${p$1.inserted}&skipped=${p$1.duplicated}&invalid=${p$1.invalid}`,303):t.redirect(`/lessons/${encodeURIComponent(p$1.lesson.id)}?imported=${p$1.inserted?"1":"dup"}`,303)}catch(p){if(p instanceof ra)return i("invalid");throw p}}),e$1.get("/lessons/import",t=>{let a=(t.req.query("code")??"").trim(),n=null,i=null;if(a)try{n=va(a);}catch(s){i=s instanceof ra?s.message:"That code could not be read.";}return t.html(et({code:a,preview:n,error:i,uiTheme:_t()}))}),e$1.get("/lessons",t=>{let a=t.req.query(),n=new Set(["junior","mid","senior"]),i=a.level&&n.has(a.level)?a.level:"",s=a.date_from||"",c=a.date_to||"",u=a.starred==="1",p=a.imported==="1"?"1":a.imported==="0"?"0":"",m=a.shared_by||null,v=a.sort||"timestamp",T=a.order==="asc"?"asc":"desc",R=Math.max(1,Number.parseInt(a.page??"1",10)||1),L=a.period||"all",l={period:s||c?null:L!=="all"?L:null,category:a.category||null,level:i||null,project:a.project||null,repository:a.repository||null,branch:a.branch||null,commit:a.commit||null,starred:u?true:null,imported:m||p==="1"?true:p==="0"?false:null,shared_by:m,search:a.search||null,feedback:a.feedback||null,date_from:s||null,date_to:c||null},x=E$1(k=>{let re=G(k,l),ae=Math.max(1,Math.ceil(re/Z));return R=Math.min(R,ae),{total:re,totalPages:ae,lessons:H(k,{...l,sort:v,order:T,page:R,per_page:Z}),allCategories:P$1(k),allProjects:M(k,"project"),allRepositories:M(k,"repository"),allBranches:M(k,"branch"),allCommits:M(k,"commit_hash"),allSharedBy:N(k),theme:X$1(k).ui_theme}}),$={period:L,category:a.category||"",level:i,project:a.project||"",repository:a.repository||"",branch:a.branch||"",commit:a.commit||"",starred:u,imported:m?"1":p,shared_by:m??"",search:a.search||"",feedback:a.feedback||"",date_from:s,date_to:c,sort:v,order:T},S=a.error==="invalid"||a.error==="cross"?a.error:null;return t.html(Qe({importOpen:a.import==="1",importError:S,lessons:x.lessons,allCategories:x.allCategories,allProjects:x.allProjects,allRepositories:x.allRepositories,allBranches:x.allBranches,allCommits:x.allCommits,allSharedBy:x.allSharedBy,s:$,page:R,perPage:Z,total:x.total,totalPages:x.totalPages,uiTheme:x.theme}))}),e$1.post("/lessons/:lesson_id/star",async t=>{let a=await t.req.parseBody();return E$1(n=>K$1(n,t.req.param("lesson_id"),a.starred==="1")),t.redirect(B(b(a,"next")||void 0),303)}),e$1.post("/lessons/:lesson_id/feedback",async t=>{let a=await t.req.parseBody(),n=b(a,"feedback");if(!["","clear","know","understood","dont_know"].includes(n))return t.text("Unknown feedback value",400);let i=n===""||n==="clear"?null:n;return E$1(s=>Ea(s,t.req.param("lesson_id"),i)),t.redirect(B(b(a,"next")||void 0),303)}),e$1.post("/lessons/delete",async t=>{if(E(t))return t.text("Forbidden",403);let a=await t.req.parseBody({all:true}),n=a.id,i=(Array.isArray(n)?n:[n]).filter(s=>typeof s=="string"&&s!=="");return E$1(s=>J$1(s,i)),t.redirect(B(b(a,"next")||void 0),303)}),e$1.on(["GET","POST"],"/lessons/:lesson_id/share",async t=>{if(E(t))return t.text("Forbidden",403);let a,n,i=false;if(t.req.method==="POST"){let m=await t.req.parseBody();a=b(m,"name"),n=b(m,"include_context")==="1",i=b(m,"persist")==="1";}else a=t.req.query("name"),n=t.req.query("include_context")==="1";let s=E$1(m=>{let v=O(m,t.req.param("lesson_id")??"");return v?(i&&a!==void 0&&Y$1(m,"share_name",x(a)),{lesson:v,settingName:X$1(m).share_name}):null});if(!s)return t.text("Lesson not found",404);let{state:c,payload:u}=it(s.lesson,s.settingName,{name:a,includeContext:n}),p=t.req.query("format");return p==="text"?t.text(c.text):p==="link"?t.text(c.link):p==="md"?new Response(Aa(u),{headers:{"content-type":"text/markdown; charset=utf-8","content-disposition":`attachment; filename="${za(u)}"`}}):p==="panel"?t.html(W(s.lesson.id,c,s.lesson.title)):t.html(V(c))}),e$1.get("/lessons/:lesson_id",t=>{let a=E$1(s=>{let c=O(s,t.req.param("lesson_id"));if(!c)return null;let u=X$1(s),p=h$1(s,{lesson_id:c.id})[0],m$1=p?{id:p.id,title:p.title,status:p.status,...m(p)}:null;return {lesson:c,course:m$1,settingName:u.share_name,uiTheme:u.ui_theme}});if(!a)return t.html("<h1>Lesson not found</h1>",404);let n=t.req.query(),i=n.imported==="1"?"new":n.imported==="dup"?"dup":null;return t.html(Xe({lesson:a.lesson,uiTheme:a.uiTheme,share:it(a.lesson,a.settingName,{open:n.share==="1"}).state,imported:i,course:a.course}))});let g$1={"content-type":"text/html; charset=utf-8","content-security-policy":"sandbox allow-scripts allow-forms; default-src 'none'; script-src 'unsafe-inline' 'unsafe-eval'; style-src 'unsafe-inline'; img-src data:; font-src data:; form-action 'none'; base-uri 'none'; frame-ancestors 'self'","referrer-policy":"no-referrer","x-content-type-options":"nosniff","cache-control":"no-cache"},d$1=(()=>{try{return `
824
866
  <script>${readFileSync(join(X,"course-frame.js"),"utf8")}</script>
825
- `}catch{return ""}})();return e$1.get("/courses",t=>{let o=A(n=>({courses:h$1(n).map(i=>({...i,lesson_title:i.lesson_id?K$1(n,i.lesson_id)?.title??null:null})),uiTheme:T(n).ui_theme}));return t.html(Xe(o))}),e$1.get("/courses/:course_id/index.html",t=>{let o=t.req.param("course_id");if(!a$1.test(o))return t.notFound();let n=e(o);if(!n)return t.notFound();try{let i=Buffer.concat([readFileSync(n),Buffer.from(d)]);return new Response(new Uint8Array(i),{headers:new Headers(g$1)})}catch{return t.notFound()}}),e$1.get("/courses/:course_id",t=>{let o=t.req.param("course_id"),n=A(c=>{let u=g(c,o);return u?{course:u,lessonTitle:u.lesson_id?K$1(c,u.lesson_id)?.title??null:null,uiTheme:T(c).ui_theme}:null});if(!n)return t.html("<h1>Course not found</h1>",404);let i=n.course.steps.find(c=>c.status==="todo")?.position??1,s=Number.parseInt(t.req.query("step")??"",10)||i;return t.html(tt({...n,selected:s,hasDocument:e(o)!==null}))}),e$1.post("/courses/:course_id/steps/:position",async t=>{if(E(t))return t.text("Forbidden",403);let o=await t.req.parseBody(),n=b(o,"status");if(!["todo","done","skipped"].includes(n))return t.text("Unknown status",400);let i=Number.parseInt(t.req.param("position"),10),s=t.req.param("course_id");return A(u=>Number.isFinite(i)?j$1(u,s,i,n):null)?t.redirect(B(b(o,"next")||void 0,`/courses/${encodeURIComponent(s)}`),303):t.text("Step not found",404)}),e$1.post("/courses/delete",async t=>{if(E(t))return t.text("Forbidden",403);let o=await t.req.parseBody();return A(n=>l(n,b(o,"id"))),t.redirect("/courses",303)}),e$1.get("/settings/export",()=>{let t=A(o=>ka(o));return new Response(new Uint8Array(t),{headers:{"content-type":"application/zip","content-disposition":"attachment; filename=devcoach-backup.zip"}})}),e$1.post("/settings/import",async t=>{let o=(await t.req.parseBody()).file;if(!(o instanceof File))return t.redirect("/settings?imported=0&skipped=0&invalid=1",303);let n=new Uint8Array(await o.arrayBuffer()),i=A(s=>ma(s,n));return t.redirect(`/settings?imported=${i.lessons}&skipped=${i.skipped}&invalid=${i.invalid}&groups=${i.groups}&notebook=${i.learning_state}`,303)}),e$1.get("/settings/notebook/download",()=>{let t=existsSync(o)?readFileSync(o,"utf8"):"";return new Response(t,{headers:{"content-type":"text/markdown","content-disposition":"attachment; filename=devcoach-notebook.md"}})}),e$1.post("/settings/notebook",async t=>{let o$1=await t.req.parseBody();return mkdirSync(dirname(o),{recursive:true}),writeFileSync(o,b(o$1,"content"),"utf8"),t.redirect(B(b(o$1,"next")||void 0,"/settings?notebook_saved=1"),303)}),e$1.post("/settings",async t$1=>{let o=await t$1.req.parseBody(),n=Number.parseInt(b(o,"max_per_day","2"),10)||2,i=Number.parseInt(b(o,"min_gap_minutes","240"),10),s=b(o,"ui_theme","system");["system","dark","light"].includes(s)||(s="system");let c=b(o,"ui_home","auto");["auto","lessons","knowledge"].includes(c)||(c="auto");let u=Math.max(0,Number.parseInt(b(o,"nudge_every","10"),10)||0),p=b(o,"nudge_scope","session");return p!=="session"&&p!=="global"&&(p="session"),A(m=>{U(m,"max_per_day",String(n)),U(m,"min_gap_minutes",String(Number.isNaN(i)?240:i)),U(m,"ui_theme",s),U(m,"ui_home",c),U(m,"nudge_every",String(u)),U(m,"nudge_scope",p),U(m,"share_name",t(b(o,"share_name")));}),t$1.redirect("/settings",303)}),e$1.get("/settings",t=>{let o$1=A(c=>T(c)),n=existsSync(o)?readFileSync(o,"utf8"):"",i=t.req.query(),s=i.imported!==void 0?{imported:Number(i.imported??0),skipped:Number(i.skipped??0),invalid:Number(i.invalid??0),groups:Number(i.groups??0),notebook:Number(i.notebook??0)}:null;return t.html(rt({settings:o$1,notebookContent:n,notebookPath:o,uiTheme:o$1.ui_theme,flash:s}))}),e$1}function $t(r,e=2e3){let g=r;return new Promise(d=>{let t=setTimeout(()=>{g.closeAllConnections?.();},e),o=setInterval(()=>g.closeIdleConnections?.(),100);r.close(()=>{clearTimeout(t),clearInterval(o),d();}),g.closeIdleConnections?.();})}async function _t(r,e=fetch){try{let g=await e(`http://127.0.0.1:${r}/ping`,{signal:AbortSignal.timeout(1500)});if(!g.ok)return null;let d=await g.json();return d.ok===!0?{version:String(d.version??"")}:null}catch{return null}}async function St(r,e,g=_t){if(e.code==="EADDRINUSE"){let d=await g(r);if(d){let t=`http://localhost:${r}`,o=d.version?` (v${d.version})`:"";return {existingUrl:t,exitCode:0,message:`${a$2.yellow("devcoach UI is already running at")} ${a$2.cyan(c(t))}${o}
867
+ `}catch{return ""}})();return e$1.get("/courses",t=>{let a=E$1(n=>({courses:h$1(n).map(i=>({...i,lesson_title:i.lesson_id?O(n,i.lesson_id)?.title??null:null})),uiTheme:X$1(n).ui_theme}));return t.html(rt(a))}),e$1.get("/courses/:course_id/index.html",t=>{let a=t.req.param("course_id");if(!a$1.test(a))return t.notFound();let n=e(a);if(!n)return t.notFound();try{let i=readFileSync(n),s=i.toString("utf8"),c$1=!c(s)&&d(s)?`
868
+ ${b$1()??""}`:"",u=Buffer.concat([i,Buffer.from(d$1+c$1)]);return new Response(new Uint8Array(u),{headers:new Headers(g$1)})}catch{return t.notFound()}}),e$1.get("/courses/:course_id",t=>{let a=t.req.param("course_id"),n=E$1(c=>{let u=g(c,a);return u?{course:u,lessonTitle:u.lesson_id?O(c,u.lesson_id)?.title??null:null,uiTheme:X$1(c).ui_theme}:null});if(!n)return t.html("<h1>Course not found</h1>",404);let i=n.course.steps.find(c=>c.status==="todo")?.position??1,s=Number.parseInt(t.req.query("step")??"",10)||i;return t.html(ot({...n,selected:s,hasDocument:e(a)!==null}))}),e$1.post("/courses/:course_id/steps/:position",async t=>{if(E(t))return t.text("Forbidden",403);let a=await t.req.parseBody(),n=b(a,"status");if(!["todo","done","skipped"].includes(n))return t.text("Unknown status",400);let i=Number.parseInt(t.req.param("position"),10),s=t.req.param("course_id");return E$1(u=>Number.isFinite(i)?j$1(u,s,i,n):null)?t.redirect(B(b(a,"next")||void 0,`/courses/${encodeURIComponent(s)}`),303):t.text("Step not found",404)}),e$1.post("/courses/delete",async t=>{if(E(t))return t.text("Forbidden",403);let a=await t.req.parseBody();return E$1(n=>l(n,b(a,"id"))),t.redirect("/courses",303)}),e$1.get("/settings/export",()=>{let t=E$1(a=>oa(a));return new Response(new Uint8Array(t),{headers:{"content-type":"application/zip","content-disposition":"attachment; filename=devcoach-backup.zip"}})}),e$1.post("/settings/import",async t=>{let a=(await t.req.parseBody()).file;if(!(a instanceof File))return t.redirect("/settings?imported=0&skipped=0&invalid=1",303);let n=new Uint8Array(await a.arrayBuffer()),i=E$1(s=>qa(s,n));return t.redirect(`/settings?imported=${i.lessons}&skipped=${i.skipped}&invalid=${i.invalid}&groups=${i.groups}&notebook=${i.learning_state}`,303)}),e$1.get("/settings/notebook/download",()=>{let t=existsSync(s)?readFileSync(s,"utf8"):"";return new Response(t,{headers:{"content-type":"text/markdown","content-disposition":"attachment; filename=devcoach-notebook.md"}})}),e$1.post("/settings/notebook",async t=>{let a=await t.req.parseBody();return mkdirSync(dirname(s),{recursive:true}),writeFileSync(s,b(a,"content"),"utf8"),t.redirect(B(b(a,"next")||void 0,"/settings?notebook_saved=1"),303)}),e$1.post("/settings",async t=>{let a=await t.req.parseBody(),n=Number.parseInt(b(a,"max_per_day","2"),10)||2,i=Number.parseInt(b(a,"min_gap_minutes","240"),10),s=b(a,"ui_theme","system");["system","dark","light"].includes(s)||(s="system");let c=b(a,"ui_home","auto");["auto","lessons","knowledge"].includes(c)||(c="auto");let u=Math.max(0,Number.parseInt(b(a,"nudge_every","10"),10)||0),p=b(a,"nudge_scope","session");return p!=="session"&&p!=="global"&&(p="session"),E$1(m=>{Y$1(m,"max_per_day",String(n)),Y$1(m,"min_gap_minutes",String(Number.isNaN(i)?240:i)),Y$1(m,"ui_theme",s),Y$1(m,"ui_home",c),Y$1(m,"nudge_every",String(u)),Y$1(m,"nudge_scope",p),Y$1(m,"share_name",x(b(a,"share_name")));}),t.redirect("/settings",303)}),e$1.get("/settings",t=>{let a=E$1(c=>X$1(c)),n=existsSync(s)?readFileSync(s,"utf8"):"",i=t.req.query(),s$1=i.imported!==void 0?{imported:Number(i.imported??0),skipped:Number(i.skipped??0),invalid:Number(i.invalid??0),groups:Number(i.groups??0),notebook:Number(i.notebook??0)}:null;return t.html(st({settings:a,notebookContent:n,notebookPath:s,uiTheme:a.ui_theme,flash:s$1}))}),e$1}function Ct(r,e=2e3){let g=r;return new Promise(d=>{let t=setTimeout(()=>{g.closeAllConnections?.();},e),a=setInterval(()=>g.closeIdleConnections?.(),100);r.close(()=>{clearTimeout(t),clearInterval(a),d();}),g.closeIdleConnections?.();})}async function Tt(r,e=fetch){try{let g=await e(`http://127.0.0.1:${r}/ping`,{signal:AbortSignal.timeout(1500)});if(!g.ok)return null;let d=await g.json();return d.ok===!0?{version:String(d.version??"")}:null}catch{return null}}async function It(r,e,g=Tt){if(e.code==="EADDRINUSE"){let d=await g(r);if(d){let t=`http://localhost:${r}`,a=d.version?` (v${d.version})`:"";return {existingUrl:t,exitCode:0,message:`${a$2.yellow("devcoach UI is already running at")} ${a$2.cyan(c$1(t))}${a}
826
869
  ${a$2.dim("Open it there, start another with --port <n>, or stop it with: devcoach ui --stop")}`}}return {exitCode:1,message:`${a$2.red(`\u2717 Port ${r} is already in use by another process`)}
827
- ${a$2.dim("Pick another port: devcoach ui --port <n>")}`}}return e.code==="EACCES"?{exitCode:1,message:a$2.red(`\u2717 Port ${r} is not allowed (permission denied) \u2014 use a port \u2265 1024`)}:{exitCode:1,message:a$2.red(`\u2717 Could not start the devcoach UI on port ${r}: ${e.message}`)}}function qt(r,e={}){let g=false,d=(s,c)=>{if(g){c&&process.exit(130);return}g=true,process.stdout.isTTY&&console.log(a$2.dim(`devcoach UI stopping (${s})\u2026`)),$t(o).then(()=>{c&&process.exit(0);});},t=wt({onShutdown:()=>d("stop requested",false)}),o=serve({fetch:t.fetch,port:r,hostname:"127.0.0.1"},s=>{let c$1=`http://localhost:${s.port}`;console.log(`devcoach UI running at ${a$2.cyan(c(c$1))}`),process.stdout.isTTY&&console.log(a$2.dim("Press Ctrl+C to stop")),e.onReady?.(c$1);}),n=["SIGINT","SIGTERM","SIGHUP"],i={SIGINT:()=>d("SIGINT",true),SIGTERM:()=>d("SIGTERM",true),SIGHUP:()=>d("SIGHUP",true)};if(e.handleSignals!==false)for(let s of n)process.on(s,i[s]);return o.once("error",s=>{for(let c of n)process.off(c,i[c]);St(r,s).then(c=>{if(e.onListenError){e.onListenError(c);return}console.error(c.message),process.exit(c.exitCode);});}),o}async function Ft(r,e=fetch){try{return (await e(`http://127.0.0.1:${r}/shutdown`,{method:"POST",signal:AbortSignal.timeout(3e3)})).ok}catch{return false}}
828
- export{wt as createApp,St as explainListenError,$t as gracefulShutdown,_t as pingUi,kt as resolveHomePath,qt as startUi,Ft as stopUi};
870
+ ${a$2.dim("Pick another port: devcoach ui --port <n>")}`}}return e.code==="EACCES"?{exitCode:1,message:a$2.red(`\u2717 Port ${r} is not allowed (permission denied) \u2014 use a port \u2265 1024`)}:{exitCode:1,message:a$2.red(`\u2717 Could not start the devcoach UI on port ${r}: ${e.message}`)}}function Kt(r,e={}){let g=false,d=(s,c)=>{if(g){c&&process.exit(130);return}g=true,process.stdout.isTTY&&console.log(a$2.dim(`devcoach UI stopping (${s})\u2026`)),Ct(a).then(()=>{c&&process.exit(0);});},t=St({onShutdown:()=>d("stop requested",false)}),a=serve({fetch:t.fetch,port:r,hostname:"127.0.0.1"},s=>{let c=`http://localhost:${s.port}`;console.log(`devcoach UI running at ${a$2.cyan(c$1(c))}`),process.stdout.isTTY&&console.log(a$2.dim("Press Ctrl+C to stop")),e.onReady?.(c);}),n=["SIGINT","SIGTERM","SIGHUP"],i={SIGINT:()=>d("SIGINT",true),SIGTERM:()=>d("SIGTERM",true),SIGHUP:()=>d("SIGHUP",true)};if(e.handleSignals!==false)for(let s of n)process.on(s,i[s]);return a.once("error",s=>{for(let c of n)process.off(c,i[c]);It(r,s).then(c=>{if(e.onListenError){e.onListenError(c);return}console.error(c.message),process.exit(c.exitCode);});}),a}async function Yt(r,e=fetch){try{return (await e(`http://127.0.0.1:${r}/shutdown`,{method:"POST",signal:AbortSignal.timeout(3e3)})).ok}catch{return false}}
871
+ export{St as createApp,It as explainListenError,Ct as gracefulShutdown,Tt as pingUi,$t as resolveHomePath,Kt as startUi,Yt as stopUi};