devcoach 2.3.1 → 2.4.0-next.575.g9c67058

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/assets/SKILL.md CHANGED
@@ -199,8 +199,9 @@ checkpoints, never touch the notebook.
199
199
  - "Close / stop the dashboard" → `stop_ui` (same `port`) — it stops whichever dashboard listens
200
200
  there, one you opened or one started from a terminal; if the user runs `devcoach ui` in their own
201
201
  terminal, Ctrl+C there stops it just as gracefully (in-flight requests finish)
202
- - "Lessons shared with me" → `get_lessons({imported: true})`
203
- - "Show me the lesson X shared" → `get_lessons({imported: true, search})` → the card,
202
+ - "Lessons shared with me" → `get_lessons({imported: true})`; "what did Ada send me" →
203
+ `get_lessons({shared_by: "Ada"})` (exact sender name)
204
+ - "Show me the lesson X shared" → `get_lessons({shared_by: X, search})` → the card,
204
205
  without `log_lesson` or the feedback line (see `references/sharing.md`)
205
206
 
206
207
  ## Operating notes
@@ -54,7 +54,8 @@ just starred a lesson → The one offer (below)
54
54
  ## Show a shared lesson
55
55
 
56
56
  "Show me what Ada shared", "read me the lesson I imported", "the lesson about X someone sent":
57
- `get_lessons({imported: true, search: …})` (ask if 2+ match), then render it as a normal card —
57
+ `get_lessons({shared_by: "Ada"})` when a sender is named (exact name; `search` also matches the
58
+ sender), else `get_lessons({imported: true, search: …})` — ask if 2+ match — then render it as a normal card —
58
59
  both bands, the title line with `Category · Level`, the body, the 💡 tip — **without**
59
60
  `log_lesson` and **without** the "Did that land?" line. It is already in their log, taught by
60
61
  someone else; it costs nothing on the rate limit. Record feedback only if they volunteer it
@@ -1,26 +1,80 @@
1
- /* Shared relative-time formatter used by lessons.html and lesson_detail.html */
1
+ /* Shared relative-time formatter used by the lessons list and the lesson page. Every [data-ts]
2
+ gets a relative label; when its cell is too narrow for the long form ("17 minutes ago") it
3
+ switches to the compact one ("17m"), and it is re-evaluated after column drags and resizes. */
2
4
  (function () {
3
- var rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });
5
+ /* "yesterday" / "today" are only right at day granularity; for weeks the number is always
6
+ spelled out — "last week" for something 13 days old is a lie — and from two months on the
7
+ relative label stops being useful at all: the date is printed instead ("July 27" within the
8
+ current year, "July 27, 2025" for earlier years). */
9
+ var rtfAuto = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });
10
+ var rtfExact = new Intl.RelativeTimeFormat('en', { numeric: 'always' });
11
+ var COMPACT_BELOW = 120; // cell width (px) under which the compact form is used
12
+
13
+ function parts(iso) {
14
+ var date = new Date(iso);
15
+ var now = new Date();
16
+ var diffMs = now - date;
17
+ var mins = Math.floor(diffMs / 60000);
18
+ var days = Math.floor(diffMs / 86400000);
19
+ /* Calendar months between the two dates by label: a July lesson seen in September is two
20
+ months away, whatever the day of month — old enough to print the date. */
21
+ var months = (now.getFullYear() - date.getFullYear()) * 12 + (now.getMonth() - date.getMonth());
22
+ return { diffMs: diffMs, mins: mins, days: days, months: Math.max(0, months) };
23
+ }
4
24
 
5
25
  function relativeTime(iso) {
26
+ var p = parts(iso);
27
+ if (p.diffMs < 0 || p.mins < 1) return 'just now';
28
+ if (p.mins < 60) return rtfAuto.format(-p.mins, 'minute');
29
+ if (p.days === 0) return 'today';
30
+ if (p.days === 1) return 'yesterday';
31
+ if (p.days < 7) return rtfAuto.format(-p.days, 'day');
32
+ if (p.months < 1) return rtfExact.format(-Math.floor(p.days / 7), 'week');
33
+ if (p.months < 2) return rtfExact.format(-p.months, 'month');
34
+ return absoluteDate(iso, 'long');
35
+ }
36
+
37
+ var monthDayLong = new Intl.DateTimeFormat('en', { month: 'long', day: 'numeric' });
38
+ var monthDayShort = new Intl.DateTimeFormat('en', { month: 'short', day: 'numeric' });
39
+ var monthDayYear = new Intl.DateTimeFormat('en', { month: 'long', day: 'numeric', year: 'numeric' });
40
+ var monthYear = new Intl.DateTimeFormat('en', { month: 'short', year: 'numeric' });
41
+
42
+ /* Same year → "July 27" (compact: "Jul 27"); an earlier year → "July 27, 2025" (compact:
43
+ "Jul 2025" — at that distance the year matters more than the day). */
44
+ function absoluteDate(iso, style) {
6
45
  var date = new Date(iso);
7
- var diffMs = Date.now() - date;
8
- if (diffMs < 0) return 'just now';
9
- var diffMins = Math.floor(diffMs / 60000);
10
- if (diffMins < 1) return 'just now';
11
- if (diffMins < 60) return rtf.format(-diffMins, 'minute');
12
- var diffDays = Math.floor(diffMs / 86400000);
13
- if (diffDays === 0) return 'today';
14
- if (diffDays === 1) return 'yesterday';
15
- if (diffDays < 7) return rtf.format(-diffDays, 'day');
16
- if (diffDays < 30) return rtf.format(-Math.floor(diffDays / 7), 'week');
17
- if (diffDays < 365) return rtf.format(-Math.floor(diffDays / 30), 'month');
18
- return rtf.format(-Math.floor(diffDays / 365), 'year');
46
+ var sameYear = date.getFullYear() === new Date().getFullYear();
47
+ if (style === 'long') return (sameYear ? monthDayLong : monthDayYear).format(date);
48
+ return (sameYear ? monthDayShort : monthYear).format(date);
49
+ }
50
+
51
+ var weekdayShort = new Intl.DateTimeFormat('en', { weekday: 'short' });
52
+
53
+ function weekdayDay(date) {
54
+ return weekdayShort.format(date) + ' ' + date.getDate();
55
+ }
56
+
57
+ /* Compact: "17m" · "today" · "Mon 2" within the last week · "Sep 10" from a week on
58
+ (the full date for earlier years). */
59
+ function compactTime(iso) {
60
+ var p = parts(iso);
61
+ if (p.diffMs < 0 || p.mins < 1) return 'now';
62
+ if (p.mins < 60) return p.mins + 'm';
63
+ if (p.days === 0) return 'today';
64
+ if (p.days < 7) return weekdayDay(new Date(iso));
65
+ return absoluteDate(iso, 'short');
66
+ }
67
+
68
+ function isNarrow(el) {
69
+ var cell = el.closest('td, th');
70
+ if (!cell) return false;
71
+ var w = cell.getBoundingClientRect().width;
72
+ return w > 0 && w < COMPACT_BELOW;
19
73
  }
20
74
 
21
75
  function applyRelativeTimes(root) {
22
76
  root.querySelectorAll('[data-ts]').forEach(function (el) {
23
- el.textContent = relativeTime(el.dataset.ts);
77
+ el.textContent = isNarrow(el) ? compactTime(el.dataset.ts) : relativeTime(el.dataset.ts);
24
78
  });
25
79
  }
26
80
 
@@ -29,4 +83,13 @@
29
83
  document.body.addEventListener('htmx:afterSettle', function () {
30
84
  applyRelativeTimes(document);
31
85
  });
86
+ /* Column drags / resets (table-resize.js) and window resizes change the available width. */
87
+ document.addEventListener('dc:columns-resized', function () {
88
+ applyRelativeTimes(document);
89
+ });
90
+ var timer;
91
+ globalThis.addEventListener('resize', function () {
92
+ clearTimeout(timer);
93
+ timer = setTimeout(function () { applyRelativeTimes(document); }, 150);
94
+ });
32
95
  })();
@@ -28,6 +28,9 @@
28
28
  --md-table-even-bg: #f9fafb;
29
29
  --md-strong: #111827;
30
30
  --md-em: #7c3aed;
31
+
32
+ --check-border: #cbd5e1;
33
+ --check-bg: #ffffff;
31
34
  }
32
35
 
33
36
  .dark {
@@ -56,6 +59,9 @@
56
59
  --md-table-even-bg: #111827;
57
60
  --md-strong: #f3f4f6;
58
61
  --md-em: #c4b5fd;
62
+
63
+ --check-border: #4b5563;
64
+ --check-bg: #111827;
59
65
  }
60
66
 
61
67
  /* ── Scrollbar ──────────────────────────────────────────────────────────── */
@@ -162,3 +168,67 @@
162
168
  color: var(--md-table-td-color);
163
169
  }
164
170
  .markdown-body tr:nth-child(even) td { background: var(--md-table-even-bg); }
171
+
172
+ /* ── Lessons table: Select-mode checkboxes + drag-to-resize handles ─────────── */
173
+
174
+ .dc-check {
175
+ appearance: none;
176
+ -webkit-appearance: none;
177
+ width: 18px;
178
+ height: 18px;
179
+ border-radius: 5px;
180
+ border: 1.5px solid var(--check-border);
181
+ background: var(--check-bg);
182
+ vertical-align: middle;
183
+ cursor: pointer;
184
+ transition: background-color 0.12s, border-color 0.12s;
185
+ }
186
+ .dc-check:checked {
187
+ background-color: #4f46e5;
188
+ border-color: #4f46e5;
189
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='none' stroke='white' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 10.5l3.5 3.5L15 7'/%3E%3C/svg%3E");
190
+ background-size: 72%;
191
+ background-position: center;
192
+ background-repeat: no-repeat;
193
+ }
194
+ .dc-check:focus-visible {
195
+ outline: 2px solid #6366f1;
196
+ outline-offset: 2px;
197
+ }
198
+
199
+ /* The hit area is the last 10px of the header (inside its padding, so it never covers the label);
200
+ what you see is a 2px line drawn exactly on the column boundary. */
201
+ .dc-resize-handle {
202
+ position: absolute;
203
+ top: 0;
204
+ right: 0;
205
+ width: 10px;
206
+ height: 100%;
207
+ cursor: col-resize;
208
+ z-index: 1;
209
+ }
210
+ .dc-resize-handle::after {
211
+ content: "";
212
+ position: absolute;
213
+ top: 18%;
214
+ bottom: 18%;
215
+ right: 0;
216
+ width: 2px;
217
+ border-radius: 1px;
218
+ background-color: transparent;
219
+ transition: background-color 0.12s;
220
+ }
221
+ .dc-resize-handle:hover::after,
222
+ .dc-resizing .dc-resize-handle:active::after {
223
+ background-color: rgba(99, 102, 241, 0.85);
224
+ }
225
+ .dc-resizing {
226
+ user-select: none;
227
+ cursor: col-resize;
228
+ }
229
+ /* In Select mode every click on a row means "toggle it": links and forms inside stop reacting. */
230
+ .dc-selecting tbody a,
231
+ .dc-selecting tbody form,
232
+ .dc-selecting tbody button {
233
+ pointer-events: none;
234
+ }
@@ -0,0 +1,178 @@
1
+ // Drag-to-resize table columns (the lessons table). Widths live on the <col> elements and are
2
+ // remembered per column in localStorage; double-click a handle to reset the two columns beside it,
3
+ // or call window.resetLessonColumns() (the ⋯ menu) to reset them all. Desktop widths only — on
4
+ // narrow screens columns are already hidden and a drag would fight scrolling.
5
+ //
6
+ // A drag moves the boundary between two neighbouring columns: one grows exactly as much as the
7
+ // other shrinks, so the table never changes width. One column (`<col data-flex>`, the title) never
8
+ // gets an explicit width: it absorbs whatever its neighbours give or take, clamped to MIN_FLEX so
9
+ // it stays readable. Stored widths that no longer fit (a narrower window) are dropped on load.
10
+ (function () {
11
+ var MIN = 48;
12
+ var MIN_FLEX = 220;
13
+ var STORAGE = "lessons-col-widths";
14
+ if (!globalThis.matchMedia || !globalThis.matchMedia("(min-width: 1024px)").matches) {
15
+ globalThis.resetLessonColumns = function () {};
16
+ return;
17
+ }
18
+
19
+ function load() {
20
+ try {
21
+ return JSON.parse(localStorage.getItem(STORAGE) || "{}") || {};
22
+ } catch {
23
+ return {};
24
+ }
25
+ }
26
+ function save(widths) {
27
+ try {
28
+ localStorage.setItem(STORAGE, JSON.stringify(widths));
29
+ } catch {
30
+ /* storage unavailable: widths last for this page only */
31
+ }
32
+ }
33
+ function clamp(v, lo, hi) {
34
+ return Math.min(hi, Math.max(lo, v));
35
+ }
36
+ // Each column declares the narrowest width its content survives (`data-min`); the flex column
37
+ // falls back to MIN_FLEX, the others to MIN.
38
+ function minOf(col) {
39
+ var m = Number(col.dataset.min);
40
+ if (m > 0) return m;
41
+ return col.hasAttribute("data-flex") ? MIN_FLEX : MIN;
42
+ }
43
+ function setWidth(col, px) {
44
+ col.style.width = Math.round(px) + "px";
45
+ }
46
+ // The defaults are inline widths on the <col>s: remember them so resets restore, not wipe.
47
+ function restore(col) {
48
+ col.style.width = col.dataset.defaultWidth || "";
49
+ }
50
+ // Let listeners (relative-time.js) react to the new widths.
51
+ function announce() {
52
+ document.dispatchEvent(new CustomEvent("dc:columns-resized"));
53
+ }
54
+
55
+ var tables = document.querySelectorAll("table[data-resizable]");
56
+ tables.forEach(function (table) {
57
+ var cols = Array.prototype.slice.call(table.querySelectorAll("colgroup > col"));
58
+ var ths = Array.prototype.slice.call(table.querySelectorAll("thead th"));
59
+ if (cols.length !== ths.length) return;
60
+ var widths = load();
61
+ cols.forEach(function (col) {
62
+ col.dataset.defaultWidth = col.style.width || "";
63
+ });
64
+ var flexIndex = cols.findIndex(function (col) {
65
+ return col.hasAttribute("data-flex");
66
+ });
67
+ var flexTh = flexIndex >= 0 ? ths[flexIndex] : null;
68
+ function flexWidth() {
69
+ return flexTh ? flexTh.getBoundingClientRect().width : Infinity;
70
+ }
71
+ function apply() {
72
+ cols.forEach(function (col) {
73
+ var px = widths[col.dataset.col];
74
+ if (typeof px === "number" && px >= minOf(col)) col.style.width = px + "px";
75
+ else restore(col);
76
+ });
77
+ if (flexWidth() < MIN_FLEX) {
78
+ // The remembered layout does not fit this window: fall back to the defaults.
79
+ widths = {};
80
+ cols.forEach(restore);
81
+ }
82
+ announce();
83
+ }
84
+ apply();
85
+ var resizeTimer;
86
+ globalThis.addEventListener("resize", function () {
87
+ clearTimeout(resizeTimer);
88
+ resizeTimer = setTimeout(apply, 150);
89
+ });
90
+
91
+ function nextVisible(i) {
92
+ for (var j = i + 1; j < ths.length; j++) {
93
+ if (ths[j].getBoundingClientRect().width > 0) return j;
94
+ }
95
+ return -1;
96
+ }
97
+
98
+ ths.forEach(function (th, i) {
99
+ // The star column stays fixed, and the share column (last) is never a drag partner.
100
+ if (i === 0 || i >= ths.length - 2) return;
101
+ var handle = document.createElement("span");
102
+ handle.className = "dc-resize-handle";
103
+ handle.setAttribute("aria-hidden", "true");
104
+ th.appendChild(handle);
105
+
106
+ handle.addEventListener("click", function (e) {
107
+ e.stopPropagation();
108
+ });
109
+ handle.addEventListener("dblclick", function (e) {
110
+ e.stopPropagation();
111
+ var j = nextVisible(i);
112
+ [cols[i], j >= 0 ? cols[j] : null].forEach(function (col) {
113
+ if (!col || col.hasAttribute("data-flex")) return;
114
+ restore(col);
115
+ delete widths[col.dataset.col];
116
+ });
117
+ save(widths);
118
+ announce();
119
+ });
120
+ handle.addEventListener("pointerdown", function (e) {
121
+ if (e.button !== 0) return;
122
+ var j = nextVisible(i);
123
+ if (j < 0) return;
124
+ e.preventDefault();
125
+ e.stopPropagation();
126
+ var left = cols[i];
127
+ var right = cols[j];
128
+ var leftFlex = left.hasAttribute("data-flex");
129
+ var rightFlex = right.hasAttribute("data-flex");
130
+ var startX = e.clientX;
131
+ var startL = th.getBoundingClientRect().width;
132
+ var startR = ths[j].getBoundingClientRect().width;
133
+ var total = startL + startR;
134
+ var minL = minOf(left);
135
+ var minR = minOf(right);
136
+ if (total < minL + minR) return; // nothing to give either way
137
+ handle.setPointerCapture(e.pointerId);
138
+ table.classList.add("dc-resizing");
139
+ function move(ev) {
140
+ var dx = ev.clientX - startX;
141
+ if (leftFlex) {
142
+ setWidth(right, clamp(startR - dx, minR, total - minL));
143
+ } else if (rightFlex) {
144
+ setWidth(left, clamp(startL + dx, minL, total - minR));
145
+ } else {
146
+ var l = clamp(startL + dx, minL, total - minR);
147
+ setWidth(left, l);
148
+ setWidth(right, total - l);
149
+ }
150
+ announce();
151
+ }
152
+ function up() {
153
+ handle.removeEventListener("pointermove", move);
154
+ handle.removeEventListener("pointerup", up);
155
+ handle.removeEventListener("pointercancel", up);
156
+ table.classList.remove("dc-resizing");
157
+ if (!leftFlex) widths[left.dataset.col] = Math.round(th.getBoundingClientRect().width);
158
+ if (!rightFlex) widths[right.dataset.col] = Math.round(ths[j].getBoundingClientRect().width);
159
+ save(widths);
160
+ announce();
161
+ }
162
+ handle.addEventListener("pointermove", move);
163
+ handle.addEventListener("pointerup", up);
164
+ handle.addEventListener("pointercancel", up);
165
+ });
166
+ });
167
+ });
168
+
169
+ globalThis.resetLessonColumns = function () {
170
+ try {
171
+ localStorage.removeItem(STORAGE);
172
+ } catch {
173
+ /* ignore */
174
+ }
175
+ document.querySelectorAll("table[data-resizable] colgroup > col").forEach(restore);
176
+ announce();
177
+ };
178
+ })();