k0-board 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/.claude/skills/k0-changelog/SKILL.md +2 -2
  2. package/.claude/skills/k0-discuss/SKILL.md +105 -0
  3. package/.claude/skills/k0-discuss/references/rounds.md +280 -0
  4. package/.claude/skills/k0-epic/SKILL.md +196 -0
  5. package/.claude/skills/k0-links/.claude-plugin/plugin.json +6 -0
  6. package/.claude/skills/k0-links/hooks/hooks.json +1 -0
  7. package/.claude/skills/k0-links/hooks/links.js +128 -0
  8. package/.claude/skills/k0-links/hooks/register.js +204 -0
  9. package/.claude/skills/k0-next/SKILL.md +82 -0
  10. package/.claude/skills/k0-order/SKILL.md +123 -0
  11. package/.claude/skills/k0-plan/SKILL.md +156 -0
  12. package/.claude/skills/k0-split/SKILL.md +169 -0
  13. package/.claude/skills/k0-story/SKILL.md +108 -0
  14. package/.claude/skills/k0-ultracode/SKILL.md +251 -0
  15. package/.claude/skills/k0-ultracode/references/agent-brief.md +71 -0
  16. package/.claude/skills/k0-ultracode/references/modes.md +49 -0
  17. package/.claude/skills/k0-verify/SKILL.md +219 -0
  18. package/.claude/skills/k0-whatsnew/SKILL.md +95 -0
  19. package/.claude/skills/k0-work/SKILL.md +212 -0
  20. package/CHANGELOG.md +361 -1
  21. package/README.md +931 -111
  22. package/bin/k0-board.js +79 -11
  23. package/menubar/K0MenuBar.swift +35 -5
  24. package/package.json +13 -1
  25. package/platform/contract.js +30 -5
  26. package/platform/darwin/terminal.js +230 -56
  27. package/platform/index.js +2 -1
  28. package/platform/linux/index.js +3 -0
  29. package/platform/linux/tray.py +10 -10
  30. package/platform/shared/command.js +19 -3
  31. package/platform/shared/tmux.js +32 -14
  32. package/platform/win32/index.js +5 -2
  33. package/platform/win32/terminal.js +24 -8
  34. package/platform/win32/tray.ps1 +13 -13
  35. package/server/backlog.js +618 -0
  36. package/server/changelog.js +13 -13
  37. package/server/db.js +1800 -146
  38. package/server/files.js +60 -21
  39. package/server/git.js +53 -14
  40. package/server/idle.js +15 -13
  41. package/server/index.js +1251 -186
  42. package/server/launcher.js +122 -14
  43. package/server/machine.js +10 -1
  44. package/server/mirror.js +1048 -0
  45. package/server/prices.js +59 -0
  46. package/server/projects.js +57 -12
  47. package/server/servers.js +22 -1
  48. package/server/sessions.js +5 -5
  49. package/server/settings.js +25 -2
  50. package/server/update.js +324 -0
  51. package/server/usage.js +642 -0
  52. package/server/watcher.js +67 -13
  53. package/server/worktree.js +600 -0
  54. package/server/writer.js +98 -26
  55. package/web/awake.js +62 -0
  56. package/web/base.css +4 -0
  57. package/web/board.css +396 -16
  58. package/web/board.js +1182 -152
  59. package/web/changelog.js +3 -3
  60. package/web/files.js +73 -16
  61. package/web/index.html +109 -12
  62. package/web/list.css +536 -0
  63. package/web/list.js +1015 -0
  64. package/web/mentions.js +27 -9
  65. package/web/recency.js +10 -10
  66. package/web/units.js +1 -1
  67. package/web/usage.css +169 -0
  68. package/web/usage.html +78 -0
  69. package/web/usage.js +447 -0
  70. package/web/view.js +22 -3
  71. package/web/whatsnew.css +211 -0
  72. package/web/whatsnew.html +49 -0
  73. package/web/whatsnew.js +339 -0
package/web/board.css CHANGED
@@ -1,6 +1,10 @@
1
1
  /* The house variables — colours, fonts, scale — live in base.css, which belongs to both
2
2
  pages. From here down there is only the board. */
3
3
  @import url('/base.css');
4
+ /* And the list, which is the other half of the dashboard: the same stories as rows. It is a file
5
+ of its own because it is a view of its own — the board reaches it through one line in `render` —
6
+ and because it carries the whole of the panel that opens beside a row. */
7
+ @import url('/list.css');
4
8
 
5
9
  /* The page no longer scrolls: it is the board that moves underneath the view. */
6
10
  body {
@@ -32,24 +36,32 @@ body {
32
36
  letter-spacing: 1px;
33
37
  }
34
38
 
39
+ /* The pills, the shape of the view and the repository: one group on the bar, because they are one
40
+ question. Two of them say what you are looking at and the third says what shape it comes in, and
41
+ a switch that changed the shape from the other end of the bar would not look like it belonged to
42
+ the same answer. */
43
+ .filterbar { display: flex; align-items: center; gap: 12px; flex: 1; flex-wrap: wrap; }
44
+
35
45
  /* The legend IS the filter: click a status and it leaves the board. */
36
- .filters { display: flex; gap: 8px; flex: 1; flex-wrap: wrap; }
46
+ /* Seven pills is the widest thing on the bar by a long way, and the bar has to end up on one row:
47
+ what is trimmed here is the space around the words and never the words, which are the legend. */
48
+ .filters { display: flex; gap: 6px; flex-wrap: wrap; }
37
49
  .filters button {
38
50
  display: flex;
39
51
  align-items: center;
40
- gap: 7px;
52
+ gap: 6px;
41
53
  border: 1px solid var(--line);
42
54
  background: #fffdf7;
43
55
  border-radius: 999px;
44
- padding: 5px 13px 5px 9px;
45
- font-size: .82rem;
56
+ padding: 5px 11px 5px 8px;
57
+ font-size: .8rem;
46
58
  font-weight: 600;
47
59
  color: var(--ink);
48
60
  cursor: pointer;
49
61
  }
50
62
  .filters button i {
51
- width: 13px;
52
- height: 13px;
63
+ width: 12px;
64
+ height: 12px;
53
65
  border-radius: 4px;
54
66
  flex: none;
55
67
  border: 1px solid rgba(0, 0, 0, .12);
@@ -81,6 +93,11 @@ body {
81
93
  /* `display: flex` beats the browser's `[hidden]`: until there is a sample, it hides here. */
82
94
  .gauge[hidden] { display: none; }
83
95
  .gauge:hover { border-color: #bdb6a5; background: #fff; }
96
+ /* RAM over CPU rather than side by side, with the closing time under them. The bar is one row again
97
+ and the four mode buttons have gone two by two: the gauge gives back the width it was taking by
98
+ going up instead of along, and written across it was over two hundred pixels of a bar that had
99
+ none to give. The dot stays where it was — the box is what became three lines, not the chip. */
100
+ .gauge .nums { display: flex; flex-direction: column; line-height: 1.15; }
84
101
  .gauge i {
85
102
  width: 9px;
86
103
  height: 9px;
@@ -89,9 +106,9 @@ body {
89
106
  background: var(--calm);
90
107
  }
91
108
  /* That k0 is looking after the memory for you is worth knowing once and then forgetting: it sits
92
- on the same chip, lighter than the numbers beside it, because the numbers are what you came to
93
- read and this is only there so the closing is never a surprise. */
94
- .gauge .idle { font-weight: 500; opacity: .6; margin-left: -2px; white-space: nowrap; }
109
+ on the same chip, under the numbers and lighter than them, because the numbers are what you came
110
+ to read and this is only there so the closing is never a surprise. */
111
+ .gauge .idle { font-weight: 500; opacity: .6; white-space: nowrap; }
95
112
  .gauge.warn i { background: var(--warn); }
96
113
  .gauge.bad i { background: var(--alert); }
97
114
  /* Solid red only when it is critical: the one moment it is allowed to interrupt. */
@@ -113,6 +130,50 @@ body {
113
130
  .new svg,
114
131
  .log svg { width: 17px; height: 17px; display: block; }
115
132
 
133
+ /* The plus opens a little menu, so it has to have something to hang off. */
134
+ .new-wrap { position: relative; flex: none; display: flex; }
135
+
136
+ /* Two entries and no more: the two things k0 can make. It is a menu and not a second dialog
137
+ because the choice is one word long — and the sentence under each word is the whole difference
138
+ between them, since one is typed here and the other is talked through in a terminal. */
139
+ .new-wrap .menu {
140
+ position: absolute;
141
+ z-index: 6;
142
+ top: calc(100% + 6px);
143
+ left: 0;
144
+ min-width: 230px;
145
+ padding: 5px;
146
+ background: var(--paper);
147
+ border: 1px solid var(--line);
148
+ border-radius: 11px;
149
+ box-shadow: 0 12px 30px rgba(0, 0, 0, .18);
150
+ display: flex;
151
+ flex-direction: column;
152
+ gap: 2px;
153
+ }
154
+ .new-wrap .menu[hidden] { display: none; }
155
+ .new-wrap .menu button {
156
+ display: block;
157
+ width: 100%;
158
+ border: none;
159
+ background: none;
160
+ border-radius: 7px;
161
+ padding: 8px 11px;
162
+ text-align: left;
163
+ font-size: .95rem;
164
+ font-weight: 600;
165
+ color: var(--ink);
166
+ }
167
+ .new-wrap .menu button:hover { background: #f0ede3; border: none; }
168
+ .new-wrap .menu button em {
169
+ display: block;
170
+ margin-top: 2px;
171
+ font-style: normal;
172
+ font-size: .76rem;
173
+ font-weight: 400;
174
+ color: var(--muted);
175
+ }
176
+
116
177
  /* The way out to the ChangeLog. It is a link, not a button, because it goes somewhere — and it
117
178
  is drawn quieter than the plus, because starting work matters more than reading about it. */
118
179
  .log {
@@ -121,15 +182,127 @@ body {
121
182
  color: var(--muted);
122
183
  }
123
184
  .log:hover { background: #fff; border-color: #c9c3b4; color: var(--ink); }
185
+ /* `display: flex` beats the browser's `[hidden]`, the same trap the gauge and the What's New mark
186
+ fall into. The door to the backlog is drawn hidden and put on the bar only when the server says
187
+ there is a backlog behind the board; without this it would sit there whatever the answer was,
188
+ leading to a page whose whole content is the sentence "The k0 backlog is switched off." */
189
+ .log[hidden] { display: none; }
190
+
191
+ /* ── The mark that says k0 moved ──────────────────────────────────────
192
+ It sits against the name and it is a dot, not a badge with a number on it: there is nothing to
193
+ count. It only exists at all when the server says so — after a version jump, or when npm has
194
+ something newer — so it is never one more thing on the bar that means nothing today. */
195
+ .whatsnew {
196
+ flex: none;
197
+ margin-left: -6px;
198
+ align-self: flex-start;
199
+ padding: 4px;
200
+ display: flex;
201
+ border-radius: 999px;
202
+ }
203
+ /* `display: flex` beats the browser's `[hidden]`, the same trap the gauge falls into: the mark
204
+ ships hidden and is shown only when the server has something to say. Without this the dot sits
205
+ against the name for ever on a board where nothing is new, and with no sentence on it either —
206
+ the title is only written when there is one. */
207
+ .whatsnew[hidden] { display: none; }
208
+ .whatsnew span {
209
+ width: 7px;
210
+ height: 7px;
211
+ border-radius: 999px;
212
+ background: var(--warn);
213
+ display: block;
214
+ }
215
+ .whatsnew:hover span { transform: scale(1.3); }
216
+
217
+ /* ── Kanban or List ───────────────────────────────────────────────────
218
+ One control with two halves, not two buttons: the same stories as a board, or the same stories as
219
+ rows. It stands inside the filter group because it is the same kind of control as the pills next
220
+ to it — how you are looking at what you have already chosen to see — and it is on the bar at all
221
+ only where there is a backlog behind the board. */
222
+ .columns {
223
+ flex: none;
224
+ display: flex;
225
+ gap: 2px;
226
+ padding: 2px;
227
+ border: 1px solid var(--line);
228
+ border-radius: 999px;
229
+ background: var(--paper);
230
+ }
231
+ /* `display: flex` beats the browser's `[hidden]`, the same trap the gauge and the What's New mark
232
+ fall into: without this the switch stands on the bar of a board that has no backlog behind it. */
233
+ .columns[hidden] { display: none; }
234
+ .columns button {
235
+ border: none;
236
+ background: none;
237
+ border-radius: 999px;
238
+ padding: 4px 10px;
239
+ font-size: .8rem;
240
+ font-weight: 600;
241
+ color: var(--muted);
242
+ }
243
+ .columns button:hover { background: rgba(0, 0, 0, .05); border: none; }
244
+ /* The lit half is dark, the same ink as the primary button in a dialog. Two greys would read as
245
+ two buttons waiting to be pressed rather than as one answer already given. */
246
+ .columns button[aria-checked="true"] { background: var(--ink); color: var(--paper); }
247
+ .columns button[aria-checked="true"]:hover { background: var(--ink); }
248
+
249
+ /* And which repository, which now serves both views: the list has no columns to be repositories,
250
+ and on the board it narrows what is already there.
251
+
252
+ Drawn here rather than left to the browser. It was a `<select>`, which on macOS opens the
253
+ system's own list in the system's own type in the middle of the screen — a panel from another
254
+ application landing on top of a board made of paper — and which cannot hold the search that
255
+ makes a list of twenty repositories usable at all. It is the same picker the new-story dialog
256
+ has, so there is one way of choosing a repository in k0 and not two. */
257
+ .repo-filter { flex: none; }
258
+ .repo-filter > button {
259
+ font: inherit;
260
+ font-size: .82rem;
261
+ font-weight: 600;
262
+ color: var(--ink);
263
+ background: var(--paper);
264
+ border: 1px solid var(--line);
265
+ border-radius: 999px;
266
+ padding: 5px 12px;
267
+ /* A repository with a long name must not be what pushes the bar onto a second row: past this it
268
+ is cut with an ellipsis, and the whole name is still in the list. */
269
+ max-width: 160px;
270
+ overflow: hidden;
271
+ text-overflow: ellipsis;
272
+ white-space: nowrap;
273
+ cursor: pointer;
274
+ }
275
+ .repo-filter > button:hover { border-color: #bdb6a5; }
276
+ .repo-filter > button[aria-expanded="true"] { border-color: #bdb6a5; background: var(--bg); }
277
+ /* Wider than the button it hangs off: the button is cut short on purpose and the list is where the
278
+ whole name is read. */
279
+ .repo-filter .picker-list { left: 0; right: auto; min-width: 260px; }
280
+ .repo-filter .picker-list input {
281
+ display: block;
282
+ width: 100%;
283
+ font: inherit;
284
+ font-size: .9rem;
285
+ padding: 7px 10px;
286
+ margin-bottom: 5px;
287
+ border: 1px solid var(--line);
288
+ border-radius: 7px;
289
+ background: var(--paper);
290
+ }
291
+ .repo-filter .picker-list input:focus { outline: none; border-color: #bdb6a5; }
124
292
 
125
293
  /* ── The four modes ───────────────────────────────────────────────────
126
294
  At the end of the bar, a row of four: how awake the machine has to stay. Switched off they are
127
295
  pale outlines like the statuses filtered away; the lit one takes the colour of its mode. The
128
296
  colour is not decoration: from across the room — which is what driving mode is for — you do not
129
297
  focus on the icon, but you do see the colour. */
298
+ /* Two by two rather than four across: the bar was going to a second row, and of everything on it
299
+ these four are the ones that lose the least by being stacked — they are a scale you glance at,
300
+ not a sentence you read. The columns are `auto` and not a width, because driving mode makes each
301
+ button 46px and a fixed column would clip them. */
130
302
  .modes {
131
303
  flex: none;
132
- display: flex;
304
+ display: grid;
305
+ grid-template-columns: repeat(2, auto);
133
306
  gap: 3px;
134
307
  }
135
308
  .modes button {
@@ -177,6 +350,58 @@ body {
177
350
  not to make: an amber ring around it, and the title says the rest. */
178
351
  .modes button[data-partial] { box-shadow: 0 0 0 2px #e0a72e; }
179
352
 
353
+ /* ── Inside an epic ───────────────────────────────────────────────────
354
+ A strip under the bar, on the page only once you have gone into a lane. It says the four things
355
+ there are to say about standing in one: which epic, where it lives, how far it has got, and the
356
+ way back out. The colour is the epic's own — the hue is worked out from its key, the same one
357
+ the label on every note below is drawn in — so the strip and the notes under it are visibly one
358
+ thing rather than two that happen to be on screen together. */
359
+ .lane {
360
+ flex: none;
361
+ display: flex;
362
+ align-items: center;
363
+ gap: 12px;
364
+ padding: 9px 22px;
365
+ background: hsl(var(--h) 48% 95%);
366
+ border-bottom: 1px solid hsl(var(--h) 34% 84%);
367
+ }
368
+ .lane[hidden] { display: none; }
369
+ .lane .key {
370
+ flex: none;
371
+ font: 700 .8rem/1 var(--mono);
372
+ color: hsl(var(--h) 52% 30%);
373
+ }
374
+ .lane .name {
375
+ font: 600 1.4rem/1 var(--hand);
376
+ color: var(--ink);
377
+ min-width: 0;
378
+ overflow: hidden;
379
+ text-overflow: ellipsis;
380
+ white-space: nowrap;
381
+ }
382
+ .lane .repo { flex: none; font-size: .8rem; color: var(--muted); }
383
+ /* Done over total and no weighting: eleven stories with one finished is a bar one eleventh along,
384
+ however big that one was. Anything else is an estimate with a progress bar drawn round it. */
385
+ .lane .bar {
386
+ flex: none;
387
+ width: 160px;
388
+ height: 6px;
389
+ border-radius: 999px;
390
+ background: hsl(var(--h) 28% 87%);
391
+ overflow: hidden;
392
+ }
393
+ .lane .bar i {
394
+ display: block;
395
+ height: 100%;
396
+ background: hsl(var(--h) 52% 46%);
397
+ transition: width .3s ease;
398
+ }
399
+ .lane .count { flex: none; font-size: .8rem; color: var(--muted); font-variant-numeric: tabular-nums; }
400
+ .lane .spacer { flex: 1; }
401
+ /* Words and not an ✕: coming back to the whole board is not closing something, and there is
402
+ nothing here to close. */
403
+ .lane button { flex: none; padding: 5px 13px; font-size: .82rem; font-weight: 600; }
404
+
180
405
  /* ── The board ───────────────────────────────────────────────────────── */
181
406
  /* The viewport is the fixed window, the board is the content sliding behind it: one transform on
182
407
  .board, and no scrollbar anywhere. */
@@ -279,6 +504,8 @@ body {
279
504
  .column > h2 .fold[disabled] { opacity: .12; cursor: default; }
280
505
  .column > h2 .fold[disabled]:hover { opacity: .12; background: none; }
281
506
 
507
+ /* Where the state columns used to be there is now a view of its own: see list.css. */
508
+
282
509
  .empty {
283
510
  position: absolute;
284
511
  inset: 0;
@@ -376,9 +603,26 @@ body {
376
603
  .postit.BACKLOG { background: var(--BACKLOG); }
377
604
  .postit.COMPLETED { background: var(--COMPLETED); opacity: .6; }
378
605
  .postit.COMPLETED .title { text-decoration: line-through; }
379
- /* Grinding away: the faintest breath, no blinking dots. */
380
- .postit.WORKING, .postit.PLANNING { animation: breathe 2.6s ease-in-out infinite; }
381
- @keyframes breathe { 50% { box-shadow: 2px 3px 8px rgba(0, 0, 0, .17), 0 0 0 3px rgba(59, 130, 246, .22); } }
606
+ /* Grinding away: the faintest breath, no blinking dots.
607
+ The breath is a ring on a layer of its own, and the only thing moving is that layer's opacity,
608
+ which the browser composites without redrawing anything. It used to animate `box-shadow` on the
609
+ post-it itself — a blurred shadow, recomputed and repainted sixty times a second, for every
610
+ working post-it, in every open tab, for as long as the board was on screen. On a machine already
611
+ under pressure that was the most expensive thing k0 did, and none of it was in k0's own process:
612
+ the bill arrived under the browser's name. The ring sits just inside the edge rather than just
613
+ outside it, because the post-it clips what overflows it. */
614
+ .postit.WORKING::after, .postit.PLANNING::after {
615
+ content: '';
616
+ position: absolute;
617
+ inset: 0;
618
+ border-radius: inherit;
619
+ box-shadow: inset 0 0 0 3px rgba(59, 130, 246, .22);
620
+ pointer-events: none;
621
+ opacity: 0;
622
+ will-change: opacity;
623
+ animation: breathe 2.6s ease-in-out infinite;
624
+ }
625
+ @keyframes breathe { 50% { opacity: 1; } }
382
626
 
383
627
  /* The repository's name and, beside it, how it is doing: the two read together. */
384
628
  .postit .head {
@@ -423,8 +667,11 @@ body {
423
667
  font-style: italic;
424
668
  color: rgba(0, 0, 0, .56);
425
669
  margin-top: 6px;
670
+ /* The notes are written one per line and have to read that way: `- OMI` under `- Locato`, not
671
+ `- OMI - Locato` run together. Three lines, like the prompt, so two notes that wrap both show. */
672
+ white-space: pre-wrap;
426
673
  display: -webkit-box;
427
- -webkit-line-clamp: 2;
674
+ -webkit-line-clamp: 3;
428
675
  -webkit-box-orient: vertical;
429
676
  overflow: hidden;
430
677
  }
@@ -451,6 +698,104 @@ body {
451
698
  .postit .since { margin-left: auto; }
452
699
  .postit .dead { font-style: italic; }
453
700
 
701
+ /* ── What the backlog adds to a note ──────────────────────────────────
702
+ Four marks and no more: what the story is called, the epic it belongs to, the star, and whether
703
+ it is waiting on something. Everything else a story has — its decisions, its rounds, its plan —
704
+ is a page and not a note, and a post-it that tried to carry it would stop being a post-it. With
705
+ the backlog off none of them is drawn and the note is the note it has always been. */
706
+
707
+ /* `K42` is the name the story keeps for ever; `1.12` is where it is sitting in the tree today and
708
+ moves the moment the tree does. Two different facts, so two weights — said in one breath they
709
+ would read as one long name. */
710
+ .postit .key {
711
+ flex: none;
712
+ font-family: var(--mono);
713
+ font-size: .72rem;
714
+ font-weight: 700;
715
+ line-height: 1;
716
+ color: rgba(0, 0, 0, .6);
717
+ cursor: default;
718
+ }
719
+ .postit .key em {
720
+ font-style: normal;
721
+ font-weight: 500;
722
+ color: rgba(0, 0, 0, .36);
723
+ margin-left: 4px;
724
+ }
725
+
726
+ /* The epic, in the epic's colour, and the way into it. The hue is on the element itself, worked
727
+ out from the key: nothing stores a colour, so there is nothing to keep in step with an epic
728
+ that gets renamed or moved. */
729
+ .postit .epic {
730
+ flex: 0 1 auto;
731
+ min-width: 0;
732
+ padding: 2px 8px;
733
+ border: none;
734
+ border-radius: 999px;
735
+ background: hsl(var(--h) 58% 88%);
736
+ color: hsl(var(--h) 55% 26%);
737
+ font-size: .72rem;
738
+ font-weight: 700;
739
+ line-height: 1.4;
740
+ overflow: hidden;
741
+ text-overflow: ellipsis;
742
+ white-space: nowrap;
743
+ }
744
+ .postit .epic:hover { background: hsl(var(--h) 58% 81%); border: none; }
745
+
746
+ /* Waiting on something that is not done. It warns and it never stops: what it is waiting for is
747
+ in the title, and the button that starts the story asks once and then goes ahead. */
748
+ .postit .waits {
749
+ flex: none;
750
+ display: inline-flex;
751
+ align-items: center;
752
+ gap: 2px;
753
+ color: var(--alert);
754
+ font-weight: 700;
755
+ font-variant-numeric: tabular-nums;
756
+ cursor: default;
757
+ }
758
+ .postit .waits svg { width: 13px; height: 13px; display: block; }
759
+
760
+ /* The flag: the mark and the target both, at the bottom right, mirroring the pencil at the top.
761
+
762
+ It is invisible until the pointer is on the note, and then it is a faint outline — an outline
763
+ sitting on every note all the time is a mark that is everywhere and therefore marks nothing. Set,
764
+ it stays on whether or not anybody is looking, and the note takes a border and its key and name
765
+ take the same colour: the point of flagging something is to find it again from across the room,
766
+ and a fifteen-pixel glyph cannot be seen from there. */
767
+ .postit .flag {
768
+ position: absolute;
769
+ right: 6px;
770
+ bottom: 6px;
771
+ width: 24px;
772
+ height: 24px;
773
+ padding: 0;
774
+ display: flex;
775
+ align-items: center;
776
+ justify-content: center;
777
+ border-radius: 6px;
778
+ background: none;
779
+ border-color: transparent;
780
+ box-shadow: none;
781
+ color: rgba(0, 0, 0, .35);
782
+ opacity: 0;
783
+ cursor: pointer;
784
+ transition: opacity .12s ease;
785
+ }
786
+ .postit .flag svg { width: 15px; height: 15px; display: block; }
787
+ .postit:hover .flag, .postit .flag:focus-visible { opacity: 1; }
788
+ .postit .flag:hover { background: rgba(255, 255, 255, .72); border-color: rgba(0, 0, 0, .14); }
789
+ /* On: the colour, and no longer waiting to be hovered. */
790
+ .postit .flag.on { opacity: 1; color: var(--warn); }
791
+ .postit.flagged { border: 2px solid var(--warn); }
792
+ /* And the two pieces of writing at the top take it with them, so a flagged note is told apart from
793
+ the ones beside it by more than a hairline of border. */
794
+ .postit.flagged .key, .postit.flagged .title { color: var(--warn); }
795
+ /* The room the flag needs. It sits over the row of buttons, which is empty space until you hover:
796
+ this is what stops the last of them running underneath it once they come out. */
797
+ .postit .actions { padding-right: 22px; }
798
+
454
799
  /* ── The last column: every repository ────────────────────────────────
455
800
  Headings only, one per repository, in the order you last used them. No cards and no counts:
456
801
  this is how you get into the files of a directory you are not working on — which without this
@@ -566,7 +911,7 @@ body {
566
911
  .postit .load.hot { color: var(--alert); }
567
912
 
568
913
  /* The corner icon: the pencil opens the editor, and on a finished card — where there is nothing
569
- left to edit — the bin takes its place. */
914
+ left to edit — the bin takes its place. The flag at the bottom right is hung the same way. */
570
915
  .postit .corner {
571
916
  position: absolute;
572
917
  top: 6px;
@@ -591,9 +936,12 @@ body {
591
936
  git mark stays next to the repository name instead of next to the pencil. */
592
937
  .postit .head, .postit .title { padding-right: 26px; }
593
938
 
594
- /* The buttons stay hidden until you hover: the dashboard stays clean. */
939
+ /* The buttons stay hidden until you hover: the dashboard stays clean. The row wraps and the words
940
+ in it do not — a note is 268px wide and three buttons can outgrow that, but a label broken over
941
+ two lines reads as two buttons and made the post-it grow a row taller than the ones beside it. */
595
942
  .postit .actions {
596
943
  display: flex;
944
+ flex-wrap: wrap;
597
945
  gap: 7px;
598
946
  margin-top: 11px;
599
947
  opacity: 0;
@@ -603,6 +951,7 @@ body {
603
951
  .postit .actions button {
604
952
  font-size: .84rem;
605
953
  font-weight: 600;
954
+ white-space: nowrap;
606
955
  padding: 6px 13px;
607
956
  background: rgba(255, 255, 255, .72);
608
957
  border-color: rgba(0, 0, 0, .14);
@@ -625,6 +974,28 @@ body {
625
974
  The dialog and its buttons live in base.css, which belongs to both pages: the file viewer opens
626
975
  one too. What is left here is only what belongs to a card's editor and to nothing else. */
627
976
 
977
+ /* An aside on the label's own line rather than under it. base.css makes a dialog's `small` a block,
978
+ which is right where it explains a field you have already filled in and wrong where it is a
979
+ parenthesis after the field's name — there it costs a whole line to say four words. */
980
+ dialog label small.aside { display: inline; margin: 0; font-weight: 400; }
981
+
982
+ /* The epic dialog says what pressing the button will actually do, because what it does is open a
983
+ terminal — and a dialog whose one button starts a conversation somewhere else has to say so. */
984
+ #epic-new select {
985
+ display: block;
986
+ width: 100%;
987
+ margin-top: 6px;
988
+ font: inherit;
989
+ font-size: 1rem;
990
+ color: var(--ink);
991
+ padding: 10px 12px;
992
+ border: 1px solid var(--line);
993
+ border-radius: 8px;
994
+ background: #fff;
995
+ }
996
+ #epic-new .note { margin: 0 0 18px; font-size: .88rem; line-height: 1.5; color: var(--muted); }
997
+ #epic-new .note code { font: .85em var(--mono); background: var(--bg); border-radius: 4px; padding: 1px 5px; }
998
+
628
999
  /* Choosing the repository: a searchable field, most used at the top. */
629
1000
  .picker { position: relative; }
630
1001
  .picker-list {
@@ -673,6 +1044,9 @@ body {
673
1044
  transition: opacity .2s, transform .2s;
674
1045
  max-width: 80vw;
675
1046
  font-size: .92rem;
1047
+ /* The one refusal in k0 is a paragraph and then the decisions it is about, one to a line. The
1048
+ line breaks are in the text, and without this they collapse into a wall. */
1049
+ white-space: pre-line;
676
1050
  }
677
1051
  .toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
678
1052
 
@@ -718,6 +1092,12 @@ html[data-driving] .postit .corner svg { width: 20px; height: 20px; }
718
1092
  html[data-driving] .zoom button { width: 40px; height: 40px; }
719
1093
  html[data-driving] .zoom button svg { width: 22px; height: 22px; }
720
1094
 
1095
+ /* The flag. It is exactly what you are meant to
1096
+ pick out from a distance, so it grows with everything else. The border round the note does the
1097
+ work at that range; this is what says which mark it is once you are closer. */
1098
+ html[data-driving] .postit .flag { width: 28px; height: 28px; }
1099
+ html[data-driving] .postit .flag svg { width: 21px; height: 21px; }
1100
+
721
1101
  /* The card is in pixels too: with the text at 22 it would choke inside 268. And since the card is
722
1102
  never taller than it is wide, raising this raises the ceiling with it. */
723
1103
  html[data-driving] { --card-w: 340px; }