k0-board 0.1.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 (67) hide show
  1. package/.claude/skills/k0-import/SKILL.md +83 -0
  2. package/CHANGELOG.md +57 -0
  3. package/LICENSE +21 -0
  4. package/README.md +688 -0
  5. package/bin/k0-board.js +456 -0
  6. package/bin/prompt.js +41 -0
  7. package/menubar/Info.plist +25 -0
  8. package/menubar/K0MenuBar.swift +561 -0
  9. package/menubar/build.sh +28 -0
  10. package/package.json +45 -0
  11. package/platform/contract.js +134 -0
  12. package/platform/darwin/extras.js +254 -0
  13. package/platform/darwin/index.js +24 -0
  14. package/platform/darwin/metrics.js +59 -0
  15. package/platform/darwin/power.js +133 -0
  16. package/platform/darwin/service.js +143 -0
  17. package/platform/darwin/shell.js +59 -0
  18. package/platform/darwin/terminal.js +324 -0
  19. package/platform/index.js +97 -0
  20. package/platform/linux/index.js +60 -0
  21. package/platform/linux/metrics.js +88 -0
  22. package/platform/linux/power.js +137 -0
  23. package/platform/linux/service.js +111 -0
  24. package/platform/linux/shell.js +97 -0
  25. package/platform/linux/tray.py +229 -0
  26. package/platform/shared/command.js +26 -0
  27. package/platform/shared/ps.js +40 -0
  28. package/platform/shared/run.js +99 -0
  29. package/platform/shared/tmux.js +242 -0
  30. package/platform/win32/index.js +53 -0
  31. package/platform/win32/metrics.js +72 -0
  32. package/platform/win32/power.js +109 -0
  33. package/platform/win32/powershell.js +44 -0
  34. package/platform/win32/service.js +111 -0
  35. package/platform/win32/shell.js +64 -0
  36. package/platform/win32/terminal.js +209 -0
  37. package/platform/win32/tray.ps1 +159 -0
  38. package/server/db.js +262 -0
  39. package/server/files.js +488 -0
  40. package/server/git.js +157 -0
  41. package/server/index.js +636 -0
  42. package/server/launcher.js +121 -0
  43. package/server/machine.js +213 -0
  44. package/server/mode.js +182 -0
  45. package/server/paths.js +67 -0
  46. package/server/pdf.js +110 -0
  47. package/server/projects.js +152 -0
  48. package/server/sessions.js +341 -0
  49. package/server/watcher.js +200 -0
  50. package/web/base.css +134 -0
  51. package/web/board.css +561 -0
  52. package/web/board.js +777 -0
  53. package/web/favicon.js +41 -0
  54. package/web/favicon.svg +8 -0
  55. package/web/files.css +535 -0
  56. package/web/files.html +63 -0
  57. package/web/files.js +877 -0
  58. package/web/fonts/caveat-latin-ext.woff2 +0 -0
  59. package/web/fonts/caveat-latin.woff2 +0 -0
  60. package/web/fuzzy.js +57 -0
  61. package/web/index.html +112 -0
  62. package/web/md.js +238 -0
  63. package/web/mentions.js +188 -0
  64. package/web/refs.js +176 -0
  65. package/web/title.js +26 -0
  66. package/web/units.js +33 -0
  67. package/web/view.js +248 -0
package/web/favicon.js ADDED
@@ -0,0 +1,41 @@
1
+ // ── The icon in the browser tab ───────────────────────────────────────────────
2
+ // The same as the one in the menu bar: a small rounded square like a sticky note, tinted with
3
+ // the most urgent status. With nothing going on, only the outline is left.
4
+ //
5
+ // The measurements come from `icon()` in menubar/K0MenuBar.swift: side 14, one point of margin,
6
+ // radius 2.5, outline 1.5. Here they are scaled to 32.
7
+ //
8
+ // The colours are its colours, not the board's: in a menu bar pastels disappear, and a browser
9
+ // tab is just as small. If you change a colour over there, change it here too — they are the
10
+ // two faces of the same icon.
11
+ const COLOR = {
12
+ ASK: '#f04545',
13
+ PLANNED: '#fa7317',
14
+ IDLE: '#ebb308',
15
+ WORKING: '#3b82f5',
16
+ PLANNING: '#3b82f5',
17
+ }
18
+ const QUIET = '#9ca3b0'
19
+
20
+ /** The drawing, for a status or for nothing at all. A pure function: it can be tested. */
21
+ export function icon(state) {
22
+ const body = COLOR[state]
23
+ ? `<rect x="2.3" y="2.3" width="27.4" height="27.4" rx="5.7" fill="${COLOR[state]}"/>`
24
+ : `<rect x="4" y="4" width="24" height="24" rx="5.7" fill="none" stroke="${QUIET}" stroke-width="3.4"/>`
25
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">${body}</svg>`
26
+ }
27
+
28
+ let last
29
+ /** Changes the icon only when it really changes: this runs once a second. */
30
+ export function setFavicon(state) {
31
+ if (state === last) return
32
+ last = state
33
+ let link = document.querySelector('link[rel="icon"]')
34
+ if (!link) {
35
+ link = document.createElement('link')
36
+ link.rel = 'icon'
37
+ document.head.append(link)
38
+ }
39
+ link.type = 'image/svg+xml'
40
+ link.href = `data:image/svg+xml,${encodeURIComponent(icon(state))}`
41
+ }
@@ -0,0 +1,8 @@
1
+ <!-- The rounded square of the menu bar icon, in its unlit form: outline only, as when there is
2
+ nothing going on. This is what the browser shows before the javascript starts; the moment it
3
+ does, `favicon.js` fills it with the colour of the most urgent status, exactly as the menu
4
+ bar icon does.
5
+ The measurements come from `icon()` in menubar/K0MenuBar.swift, scaled to 32. -->
6
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
7
+ <rect x="4" y="4" width="24" height="24" rx="5.7" fill="none" stroke="#9ca3b0" stroke-width="3.4"/>
8
+ </svg>
package/web/files.css ADDED
@@ -0,0 +1,535 @@
1
+ /* The file viewer: k0's second tab. Colours and fonts live in base.css. */
2
+ @import url('/base.css');
3
+
4
+ body {
5
+ margin: 0;
6
+ background: var(--bg);
7
+ color: var(--ink);
8
+ font-family: var(--sans);
9
+ font-size: 1rem;
10
+ line-height: 1.5;
11
+ -webkit-font-smoothing: antialiased;
12
+ display: flex;
13
+ flex-direction: column;
14
+ overflow: hidden;
15
+ }
16
+
17
+ /* ── The top bar ─────────────────────────────────────────────────────── */
18
+ .topbar {
19
+ display: flex;
20
+ align-items: center;
21
+ gap: 14px;
22
+ padding: 10px 18px;
23
+ background: var(--paper);
24
+ border-bottom: 1px solid var(--line);
25
+ flex: none;
26
+ }
27
+ .topbar .home {
28
+ font: 700 1.2rem/1 var(--mono);
29
+ letter-spacing: 1px;
30
+ color: var(--ink);
31
+ text-decoration: none;
32
+ opacity: .65;
33
+ }
34
+ .topbar .home:hover { opacity: 1; }
35
+ /* The repository's name is handwritten, as at the top of the board's columns. */
36
+ .topbar h1 { margin: 0; font: 600 1.6rem/1 var(--hand); }
37
+ .topbar .ctx { color: var(--muted); font-size: .85rem; }
38
+ .topbar .count { color: var(--muted); font-size: .8rem; font-variant-numeric: tabular-nums; }
39
+
40
+ .find {
41
+ display: flex;
42
+ align-items: center;
43
+ gap: 7px;
44
+ margin-left: auto;
45
+ border: 1px solid var(--line);
46
+ background: #fff;
47
+ border-radius: 999px;
48
+ padding: 5px 14px;
49
+ width: min(340px, 38vw);
50
+ }
51
+ .find svg { width: 15px; height: 15px; color: var(--muted); flex: none; display: block; }
52
+ /* The lens is no longer a drawing next to the field: it is the button for the other search, the
53
+ one you do by pasting. It stays grey and small as before — it only lights up on hover, or it
54
+ would look like a button you were meant to press straight away. */
55
+ .find .lens {
56
+ flex: none;
57
+ padding: 0;
58
+ margin: 0;
59
+ border: 0;
60
+ background: none;
61
+ border-radius: 999px;
62
+ color: var(--muted);
63
+ display: flex;
64
+ align-items: center;
65
+ cursor: pointer;
66
+ }
67
+ .find .lens:hover { background: none; color: var(--ink); }
68
+ .find input {
69
+ border: 0;
70
+ outline: 0;
71
+ background: none;
72
+ font: inherit;
73
+ font-size: .9rem;
74
+ width: 100%;
75
+ color: var(--ink);
76
+ }
77
+
78
+ /* ── The two panes ───────────────────────────────────────────────────── */
79
+ /* The divider is a column of the grid: drag it and the size stays. */
80
+ .split {
81
+ flex: 1;
82
+ min-height: 0;
83
+ display: grid;
84
+ grid-template-columns: var(--left, 28%) 7px 1fr;
85
+ }
86
+ .grip {
87
+ cursor: col-resize;
88
+ background: var(--line);
89
+ border-left: 1px solid var(--bg);
90
+ border-right: 1px solid var(--bg);
91
+ touch-action: none;
92
+ }
93
+ .grip:hover, .grip:active { background: var(--muted); }
94
+
95
+ /* ── The listing ─────────────────────────────────────────────────────── */
96
+ .list {
97
+ overflow-y: auto;
98
+ overflow-x: hidden;
99
+ background: var(--paper);
100
+ padding-bottom: 30px;
101
+ outline: 0;
102
+ }
103
+ .list h2 {
104
+ position: sticky;
105
+ top: 0;
106
+ z-index: 1;
107
+ margin: 0;
108
+ padding: 9px 16px 5px;
109
+ background: var(--paper);
110
+ border-bottom: 1px solid var(--line);
111
+ font-size: .68rem;
112
+ font-weight: 700;
113
+ letter-spacing: .09em;
114
+ text-transform: uppercase;
115
+ color: var(--muted);
116
+ }
117
+ .list h2 small { margin-left: 7px; font-weight: 600; opacity: .7; }
118
+
119
+ .row {
120
+ display: grid;
121
+ grid-template-columns: 1fr auto;
122
+ gap: 0 10px;
123
+ padding: 7px 16px;
124
+ border-bottom: 1px solid rgba(0, 0, 0, .045);
125
+ text-decoration: none;
126
+ color: inherit;
127
+ cursor: pointer;
128
+ }
129
+ .row b {
130
+ font: 600 .86rem/1.3 var(--mono);
131
+ overflow-wrap: anywhere;
132
+ }
133
+ /* The path is relative to the repository, never absolute: nobody cares about /home. */
134
+ .row i {
135
+ grid-column: 1;
136
+ font-style: normal;
137
+ font-size: .72rem;
138
+ color: var(--muted);
139
+ overflow-wrap: anywhere;
140
+ }
141
+ .row em {
142
+ grid-row: 1;
143
+ grid-column: 2;
144
+ font-style: normal;
145
+ font-size: .72rem;
146
+ color: var(--muted);
147
+ white-space: nowrap;
148
+ }
149
+ /* The line found inside the file: it is what makes you say "yes, this is the one". */
150
+ .row q {
151
+ grid-column: 1 / -1;
152
+ margin-top: 3px;
153
+ font-size: .74rem;
154
+ line-height: 1.4;
155
+ color: var(--muted);
156
+ quotes: none;
157
+ display: -webkit-box;
158
+ -webkit-line-clamp: 2;
159
+ -webkit-box-orient: vertical;
160
+ overflow: hidden;
161
+ }
162
+ .row q mark { background: #fde68a; color: var(--ink); border-radius: 3px; padding: 0 1px; }
163
+ .row:hover { background: rgba(0, 0, 0, .035); }
164
+ .row.on { background: var(--WORKING); }
165
+ .row.active { box-shadow: inset 3px 0 0 var(--ink); }
166
+ /* The red dot: the same mark that on a card says "there is uncommitted work here". */
167
+ .row.hot b::before {
168
+ content: '';
169
+ display: inline-block;
170
+ width: 6px;
171
+ height: 6px;
172
+ margin-right: 6px;
173
+ border-radius: 50%;
174
+ background: var(--alert);
175
+ vertical-align: middle;
176
+ }
177
+
178
+ /* ── When a pasted text is in charge ──────────────────────────────────
179
+ The listing is no longer the whole repository but a slice, and that has to be visible without
180
+ having to be remembered: at the top it says how many files and where they come from, with the
181
+ way out next to it. The same label reopens the text, because the thing you most often want to
182
+ do after looking is correct it. */
183
+ .from-text {
184
+ position: sticky;
185
+ top: 0;
186
+ z-index: 2;
187
+ display: flex;
188
+ align-items: center;
189
+ gap: 6px;
190
+ padding: 7px 10px 7px 16px;
191
+ background: var(--IDLE);
192
+ border-bottom: 1px solid rgba(0, 0, 0, .08);
193
+ }
194
+ .from-text button, .from-text span {
195
+ border: 0;
196
+ background: none;
197
+ padding: 0;
198
+ font: inherit;
199
+ font-size: .78rem;
200
+ font-weight: 600;
201
+ color: var(--ink);
202
+ cursor: pointer;
203
+ text-align: left;
204
+ }
205
+ /* A directory's name is not clickable: there is nothing to correct, it just says so. */
206
+ .from-text span { cursor: default; }
207
+ .from-text button:hover { background: none; text-decoration: underline; }
208
+ .from-text .x {
209
+ margin-left: auto;
210
+ flex: none;
211
+ display: flex;
212
+ align-items: center;
213
+ opacity: .55;
214
+ }
215
+ .from-text .x:hover { opacity: 1; text-decoration: none; }
216
+ .from-text .x svg { width: 15px; height: 15px; display: block; }
217
+
218
+ /* The heading of a group that opens and closes: the guesses arrive closed. */
219
+ .list h2.fold { cursor: pointer; user-select: none; }
220
+ .list h2.fold::after {
221
+ content: '▸';
222
+ margin-left: 7px;
223
+ font-size: .8em;
224
+ opacity: .6;
225
+ display: inline-block;
226
+ }
227
+ .list h2.fold.open::after { transform: rotate(90deg); }
228
+
229
+ /* A row that opens a directory. It reads like the others — same grid — but it is a button, not a
230
+ link: it leads nowhere, it opens underneath. */
231
+ .row.fold {
232
+ width: 100%;
233
+ border: 0;
234
+ border-bottom: 1px solid rgba(0, 0, 0, .045);
235
+ border-radius: 0;
236
+ background: none;
237
+ text-align: left;
238
+ }
239
+ .row.fold b::after { content: ' ▸'; opacity: .6; font-weight: 400; }
240
+ .row.fold.open b::after { content: ' ▾'; }
241
+
242
+ /* "7 more": it sits under the row it belongs to, off to one side. */
243
+ .more {
244
+ display: block;
245
+ width: 100%;
246
+ border: 0;
247
+ border-bottom: 1px solid rgba(0, 0, 0, .045);
248
+ border-radius: 0;
249
+ background: none;
250
+ text-align: left;
251
+ padding: 4px 16px 5px 16px;
252
+ font: inherit;
253
+ font-size: .72rem;
254
+ color: var(--muted);
255
+ cursor: pointer;
256
+ }
257
+ .more:hover { background: rgba(0, 0, 0, .035); border-color: rgba(0, 0, 0, .045); color: var(--ink); }
258
+
259
+ /* What has been opened sits further in: at a glance you can see it depends on the row above and
260
+ is not a result in its own right. */
261
+ .kids { box-shadow: inset 3px 0 0 var(--line); }
262
+ .kids .row { padding-left: 30px; }
263
+
264
+ /* The names the text writes that are not here. Said quietly and at the bottom: it is there to
265
+ tell you whether you picked the wrong repository, not as a result. */
266
+ .missing {
267
+ margin: 0;
268
+ padding: 10px 16px 4px;
269
+ font-size: .72rem;
270
+ line-height: 1.5;
271
+ color: var(--muted);
272
+ font-style: italic;
273
+ overflow-wrap: anywhere;
274
+ }
275
+
276
+ /* The paste dialog. Its bones live in base.css, along with the board editor's: what is here is
277
+ only what this one has on top. */
278
+ #paste textarea { font-size: .92rem; }
279
+ #paste .note {
280
+ margin: -6px 0 16px;
281
+ padding: 9px 12px;
282
+ border-radius: 8px;
283
+ background: #fff6f6;
284
+ border: 1px solid #f1c9c9;
285
+ font-size: .82rem;
286
+ line-height: 1.45;
287
+ color: var(--ink);
288
+ }
289
+
290
+ /* ── The document ────────────────────────────────────────────────────── */
291
+ .doc {
292
+ overflow: auto;
293
+ background: #fff;
294
+ padding: 0 0 60px;
295
+ }
296
+ .blank {
297
+ color: var(--muted);
298
+ padding: 40px;
299
+ text-align: center;
300
+ }
301
+ .blank button {
302
+ margin-top: 14px;
303
+ border: 1px solid var(--line);
304
+ background: var(--paper);
305
+ border-radius: 8px;
306
+ padding: 7px 16px;
307
+ font: inherit;
308
+ font-size: .85rem;
309
+ cursor: pointer;
310
+ }
311
+ .blank button:hover { background: #fff; }
312
+
313
+ .crumb {
314
+ position: sticky;
315
+ top: 0;
316
+ z-index: 2;
317
+ display: flex;
318
+ align-items: center;
319
+ flex-wrap: wrap;
320
+ gap: 4px;
321
+ padding: 7px 32px;
322
+ background: rgba(255, 255, 255, .94);
323
+ backdrop-filter: blur(6px);
324
+ border-bottom: 1px solid var(--line);
325
+ font-size: .78rem;
326
+ color: var(--muted);
327
+ }
328
+ .crumb b { font: 600 .82rem var(--mono); color: var(--ink); }
329
+ .crumb i { font-style: normal; opacity: .5; }
330
+ .crumb em { margin-left: auto; font-style: normal; }
331
+
332
+ /* ── What to do with it ───────────────────────────────────────────────
333
+ Print the document, take it away as it is, or have it laid out into a PDF. They sit here at
334
+ the end of the path row, and not in the bar at the top, because they are about this document:
335
+ on the search row they had nothing to do with anything. */
336
+ .acts { display: flex; align-items: center; gap: 6px; margin-left: 12px; }
337
+ .act {
338
+ display: flex;
339
+ align-items: center;
340
+ gap: 4px;
341
+ border: 1px solid var(--line);
342
+ background: #fff;
343
+ border-radius: 999px;
344
+ padding: 4px 9px;
345
+ font: inherit;
346
+ font-size: .72rem;
347
+ color: var(--muted);
348
+ text-decoration: none;
349
+ cursor: pointer;
350
+ }
351
+ .act:hover { color: var(--ink); border-color: #bdb6a5; }
352
+ .act svg { width: 14px; height: 14px; display: block; flex: none; }
353
+ .cut { padding: 12px 32px; color: var(--muted); font-size: .82rem; font-style: italic; }
354
+
355
+ /* ── The label ────────────────────────────────────────────────────────
356
+ What is written between the two rules at the top of the file. It is not text of the document —
357
+ it is what the document says it is — so it reads as a label: set apart, in a different colour,
358
+ with the title picked out. It used to end up in the text as just another paragraph, and nobody
359
+ could tell what it was. */
360
+ .plate {
361
+ margin: 22px 32px 0;
362
+ padding: 12px 16px;
363
+ border-left: 3px solid var(--WORKING);
364
+ background: var(--paper);
365
+ border-radius: 0 8px 8px 0;
366
+ }
367
+ .plate b {
368
+ display: block;
369
+ font-size: 1.05rem;
370
+ font-weight: 700;
371
+ color: #1e40af;
372
+ }
373
+ .plate span {
374
+ display: inline-block;
375
+ margin: 4px 14px 0 0;
376
+ font-size: .78rem;
377
+ color: var(--muted);
378
+ }
379
+ .plate i { font-style: normal; opacity: .65; }
380
+ .plate + .md { padding-top: 18px; }
381
+
382
+ .plain {
383
+ margin: 0;
384
+ padding: 20px 32px;
385
+ font: .82rem/1.6 var(--mono);
386
+ white-space: pre;
387
+ overflow-x: auto;
388
+ }
389
+ .media { padding: 28px 32px; }
390
+ .media img { max-width: 100%; height: auto; border: 1px solid var(--line); border-radius: 6px; }
391
+ /* A PDF and a page are looked at inside their own frame, as tall as the pane. */
392
+ .pdf, .page { width: 100%; height: calc(100vh - 96px); border: 0; display: block; background: #fff; }
393
+
394
+ /* ── The laid-out markdown ───────────────────────────────────────────── */
395
+ /* No maximum width: the text runs to the end of the pane and wraps where the pane sends it. How
396
+ wide to keep it is the divider's decision, not this stylesheet's. */
397
+ .md {
398
+ padding: 26px 32px;
399
+ font-size: .96rem;
400
+ line-height: 1.62;
401
+ }
402
+ .md > :first-child { margin-top: 0; }
403
+ .md h1, .md h2, .md h3, .md h4 { line-height: 1.25; margin: 1.7em 0 .55em; }
404
+ .md h1 { font-size: 1.7rem; }
405
+ .md h2 { font-size: 1.32rem; padding-bottom: .25em; border-bottom: 1px solid var(--line); }
406
+ .md h3 { font-size: 1.08rem; }
407
+ .md h4 { font-size: .96rem; }
408
+ .md p, .md ul, .md ol, .md table, .md pre, .md blockquote { margin: 0 0 1em; }
409
+ .md ul, .md ol { padding-left: 1.5em; }
410
+ .md li { margin: .25em 0; }
411
+ .md li > ul, .md li > ol { margin: .25em 0; }
412
+ /* A link has to look like a link, even when its text is a piece of code. */
413
+ .md a { color: #1d4ed8; text-decoration-color: rgba(29, 78, 216, .4); }
414
+ .md a:hover { text-decoration-color: currentColor; }
415
+ .md a code { color: inherit; background: rgba(29, 78, 216, .09); }
416
+
417
+ /* ── File names found in the text ─────────────────────────────────────
418
+ They are links, but they must not look like links until you are over them: in a table listing
419
+ eight files, eight blue words would BE the table. They stay written as they were — inside the
420
+ little grey box or in the middle of the sentence — and they light up only under the pointer,
421
+ which is when you are actually asking yourself whether you can click. */
422
+ .md a.ref {
423
+ color: inherit;
424
+ text-decoration: none;
425
+ cursor: pointer;
426
+ border-bottom: 1px solid transparent;
427
+ }
428
+ .md a.ref:hover {
429
+ color: #1d4ed8;
430
+ border-bottom-color: rgba(29, 78, 216, .45);
431
+ }
432
+ .md code:has(> a.ref):hover { background: rgba(29, 78, 216, .09); }
433
+ .md hr { border: 0; border-top: 1px solid var(--line); margin: 2em 0; }
434
+ .md blockquote {
435
+ border-left: 3px solid var(--line);
436
+ padding-left: 1em;
437
+ color: var(--muted);
438
+ }
439
+ .md code {
440
+ font: .84em var(--mono);
441
+ background: var(--bg);
442
+ border-radius: 4px;
443
+ padding: .12em .35em;
444
+ }
445
+ .md pre {
446
+ background: var(--bg);
447
+ border: 1px solid var(--line);
448
+ border-radius: 8px;
449
+ padding: 12px 14px;
450
+ overflow-x: auto;
451
+ }
452
+ .md pre code { background: none; padding: 0; font-size: .8rem; line-height: 1.55; }
453
+ .md img { max-width: 100%; height: auto; border-radius: 6px; }
454
+ .md table { border-collapse: collapse; width: 100%; font-size: .88rem; }
455
+ .md th, .md td { border: 1px solid var(--line); padding: 6px 10px; text-align: left; vertical-align: top; }
456
+ .md th { background: var(--paper); font-weight: 700; }
457
+ /* A plan's checkboxes: they show because they are part of the text, but they are not tickable
458
+ here. The box and the text are two columns, so a long line wraps under itself rather than under
459
+ the box. The list bullet goes: the box is already the mark. */
460
+ .md li:has(> .task) { list-style: none; margin-left: -1.15em; }
461
+ .md .task { display: flex; gap: .5em; align-items: baseline; }
462
+ .md .task input { margin: 0; flex: none; }
463
+
464
+ /* ── The message at the bottom ───────────────────────────────────────── */
465
+ .toast {
466
+ position: fixed;
467
+ left: 50%;
468
+ bottom: 26px;
469
+ transform: translateX(-50%) translateY(14px);
470
+ background: var(--ink);
471
+ color: #fff;
472
+ padding: 10px 18px;
473
+ border-radius: 10px;
474
+ font-size: .86rem;
475
+ opacity: 0;
476
+ pointer-events: none;
477
+ transition: opacity .18s, transform .18s;
478
+ }
479
+ .toast.show { opacity: 1; transform: translateX(-50%); }
480
+
481
+ /* ── On paper ─────────────────────────────────────────────────────────
482
+ The sheet gets the document and nothing else. The file listing, the bar and the divider are of
483
+ no use on paper: without this section it came out as twenty-nine pages with half an interface
484
+ inside them.
485
+
486
+ Nor does anything that on screen says WHERE you are — the file path, the size, the date — or
487
+ the label: whoever is holding the sheet has the document, and the title is already written
488
+ below it, large. The sheet starts at the document.
489
+
490
+ This margin only holds if "Margins" in the browser's print dialog is set to "Default": on
491
+ "None" the browser overrides it and the text runs to the edge of the paper. The PDF made by
492
+ the button does not go through that dialog, and always has its margin. */
493
+ @media print {
494
+ /* The two measurements come from two different places, and that is not an oversight.
495
+ At the sides it is the padding of the pane below: nobody can take that away, and it holds on
496
+ every page. Top and bottom it is the `@page` margin, and the browser only keeps that if
497
+ "Margins" in its print dialog is set to "Default" — on "None" it zeroes it and the text runs
498
+ to the edge of the paper. Padding cannot stand in for it: top and bottom it would only apply
499
+ to the first page and the last. Repeating table header groups were tried too
500
+ (`display: table-header-group`): an empty pseudo-element does not keep its height, and the
501
+ space is not reserved. The PDF made by the button does not go through the print dialog, and
502
+ accordingly always gets it right.
503
+
504
+ Wide top and bottom, narrow at the sides, on purpose: at the top and bottom the white space
505
+ lifts the text off the edge and leaves room for a staple, while taking it away at the sides
506
+ gains line length — which on a dense document means fewer pages. */
507
+ @page { margin: 25mm 0; }
508
+
509
+ html, body { height: auto; overflow: visible; background: #fff; }
510
+ .topbar, .list, .grip, .toast, .crumb, .plate { display: none !important; }
511
+ .split { display: block; }
512
+ .doc { overflow: visible; padding: 0 14mm; background: #fff; }
513
+
514
+ .md, .plain { padding: 0; font-size: 10.5pt; line-height: 1.5; }
515
+ /* The label is hidden, but it is still there: the space that held it down is not. */
516
+ .plate + .md { padding-top: 0; }
517
+ .md a { color: #000; }
518
+ .md a code, .md code { background: #f2f2f2; }
519
+ /* On paper a file name is a file name: no underlines. */
520
+ .md a.ref { color: inherit; border-bottom: 0; }
521
+ .md pre { background: #f7f7f7; border-color: #ddd; }
522
+
523
+ /* You cannot scroll a sheet of paper. On screen a long line of code runs out of the pane and
524
+ you drag to reach it; on paper that piece is simply lost — a `curl` command cut in half
525
+ cannot even be copied out. So here it wraps, and it is allowed to break inside an address
526
+ too, which has no spaces to break at. */
527
+ .md pre, .plain { white-space: pre-wrap; overflow-wrap: anywhere; overflow: visible; }
528
+
529
+ /* Nothing cut in half across two sheets. */
530
+ pre, table, blockquote, img, tr, li { break-inside: avoid; }
531
+ h1, h2, h3, h4 { break-after: avoid; }
532
+ .md h2 { border-bottom-color: #bbb; }
533
+ .md th { background: #f2f2f2; }
534
+ .media img { border: 0; }
535
+ }
package/web/files.html ADDED
@@ -0,0 +1,63 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>k0 — Files</title>
7
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg">
8
+ <link rel="stylesheet" href="/files.css">
9
+ </head>
10
+ <body>
11
+ <header class="topbar">
12
+ <a class="home" href="/" title="Back to the dashboard">k0</a>
13
+ <h1 id="repo"></h1>
14
+ <span class="ctx" id="ctx"></span>
15
+ <div class="find">
16
+ <!-- The lens used to be a drawing; now it is the door to the other search, the one you
17
+ do by pasting a piece of conversation instead of typing. -->
18
+ <button type="button" class="lens" id="paste-open" title="Paste a chunk of chat and find the files it names" aria-label="Paste a chunk of chat">
19
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="11" cy="11" r="7"/><path d="M20 20l-4.2-4.2"/></svg>
20
+ </button>
21
+ <input id="q" autocomplete="off" spellcheck="false" placeholder="search a file…">
22
+ </div>
23
+ <span class="count" id="count"></span>
24
+ </header>
25
+
26
+ <main class="split" id="split">
27
+ <nav class="list" id="list" tabindex="-1"></nav>
28
+ <div class="grip" id="grip" title="Drag to resize" role="separator" aria-orientation="vertical"></div>
29
+ <section class="doc" id="doc">
30
+ <p class="blank">Pick a file on the left.</p>
31
+ </section>
32
+ </main>
33
+
34
+ <!-- The text is pasted in here, into a big box: a piece of chat does not fit on one line, and
35
+ seeing all of it is how you notice whether you grabbed the right piece. -->
36
+ <dialog id="paste">
37
+ <form id="paste-form">
38
+ <div class="head">
39
+ <h2>Paste a chunk of chat</h2>
40
+ <button type="button" id="p-close" class="x" title="Close" aria-label="Close">
41
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M6 6l12 12M18 6L6 18"/></svg>
42
+ </button>
43
+ </div>
44
+
45
+ <label>The text
46
+ <textarea id="p-text" rows="10" placeholder="paste the piece of conversation that names the files…"></textarea>
47
+ <small>it looks for the file names written in the text, not for what they are about</small>
48
+ </label>
49
+
50
+ <p class="note" id="p-note" hidden></p>
51
+
52
+ <footer>
53
+ <span class="spacer"></span>
54
+ <button type="button" id="p-cancel">Cancel</button>
55
+ <button type="submit" class="primary">Find the files</button>
56
+ </footer>
57
+ </form>
58
+ </dialog>
59
+
60
+ <div id="toast" class="toast"></div>
61
+ <script type="module" src="/files.js"></script>
62
+ </body>
63
+ </html>