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.
- package/.claude/skills/k0-import/SKILL.md +83 -0
- package/CHANGELOG.md +57 -0
- package/LICENSE +21 -0
- package/README.md +688 -0
- package/bin/k0-board.js +456 -0
- package/bin/prompt.js +41 -0
- package/menubar/Info.plist +25 -0
- package/menubar/K0MenuBar.swift +561 -0
- package/menubar/build.sh +28 -0
- package/package.json +45 -0
- package/platform/contract.js +134 -0
- package/platform/darwin/extras.js +254 -0
- package/platform/darwin/index.js +24 -0
- package/platform/darwin/metrics.js +59 -0
- package/platform/darwin/power.js +133 -0
- package/platform/darwin/service.js +143 -0
- package/platform/darwin/shell.js +59 -0
- package/platform/darwin/terminal.js +324 -0
- package/platform/index.js +97 -0
- package/platform/linux/index.js +60 -0
- package/platform/linux/metrics.js +88 -0
- package/platform/linux/power.js +137 -0
- package/platform/linux/service.js +111 -0
- package/platform/linux/shell.js +97 -0
- package/platform/linux/tray.py +229 -0
- package/platform/shared/command.js +26 -0
- package/platform/shared/ps.js +40 -0
- package/platform/shared/run.js +99 -0
- package/platform/shared/tmux.js +242 -0
- package/platform/win32/index.js +53 -0
- package/platform/win32/metrics.js +72 -0
- package/platform/win32/power.js +109 -0
- package/platform/win32/powershell.js +44 -0
- package/platform/win32/service.js +111 -0
- package/platform/win32/shell.js +64 -0
- package/platform/win32/terminal.js +209 -0
- package/platform/win32/tray.ps1 +159 -0
- package/server/db.js +262 -0
- package/server/files.js +488 -0
- package/server/git.js +157 -0
- package/server/index.js +636 -0
- package/server/launcher.js +121 -0
- package/server/machine.js +213 -0
- package/server/mode.js +182 -0
- package/server/paths.js +67 -0
- package/server/pdf.js +110 -0
- package/server/projects.js +152 -0
- package/server/sessions.js +341 -0
- package/server/watcher.js +200 -0
- package/web/base.css +134 -0
- package/web/board.css +561 -0
- package/web/board.js +777 -0
- package/web/favicon.js +41 -0
- package/web/favicon.svg +8 -0
- package/web/files.css +535 -0
- package/web/files.html +63 -0
- package/web/files.js +877 -0
- package/web/fonts/caveat-latin-ext.woff2 +0 -0
- package/web/fonts/caveat-latin.woff2 +0 -0
- package/web/fuzzy.js +57 -0
- package/web/index.html +112 -0
- package/web/md.js +238 -0
- package/web/mentions.js +188 -0
- package/web/refs.js +176 -0
- package/web/title.js +26 -0
- package/web/units.js +33 -0
- 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
|
+
}
|
package/web/favicon.svg
ADDED
|
@@ -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>
|