k0-board 0.2.0 → 0.4.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/CHANGELOG.md +116 -1
- package/README.md +179 -18
- package/bin/k0-board.js +16 -0
- package/package.json +1 -1
- package/platform/contract.js +7 -1
- package/platform/darwin/index.js +1 -0
- package/platform/darwin/shell.js +4 -1
- package/platform/index.js +1 -0
- package/platform/linux/index.js +11 -0
- package/platform/linux/shell.js +10 -1
- package/platform/win32/index.js +1 -0
- package/platform/win32/shell.js +4 -1
- package/server/db.js +11 -0
- package/server/files.js +105 -14
- package/server/idle.js +56 -0
- package/server/index.js +90 -4
- package/server/launcher.js +5 -1
- package/server/paths.js +5 -4
- package/server/settings.js +121 -0
- package/web/board.css +12 -0
- package/web/board.js +22 -4
- package/web/conf.js +118 -0
- package/web/files.css +206 -1
- package/web/files.html +10 -0
- package/web/files.js +463 -62
- package/web/fuzzy.js +73 -21
- package/web/json.js +145 -0
package/web/board.js
CHANGED
|
@@ -111,7 +111,13 @@ function postit(card, now) {
|
|
|
111
111
|
: ''
|
|
112
112
|
}
|
|
113
113
|
<div class="foot">
|
|
114
|
-
${
|
|
114
|
+
${
|
|
115
|
+
// Whoever shut the window, said in the same breath and the same weight — this line is
|
|
116
|
+
// already only an italic, and it stays one. Which of the two it is does not need its own
|
|
117
|
+
// place in the redraw signature: `auto_closed` never moves without `session_alive`
|
|
118
|
+
// moving with it, and that one is in there.
|
|
119
|
+
dead ? `<span class="dead">${card.auto_closed ? 'closed automatically' : 'session closed'}</span>` : ''
|
|
120
|
+
}
|
|
115
121
|
<span class="since" title="${LABEL[card.status]} for ${since(card.status_since, now)}">${since(card.status_since, now)}</span>
|
|
116
122
|
</div>
|
|
117
123
|
<div class="actions"></div>`
|
|
@@ -559,7 +565,7 @@ function repoColumn(projects, shown) {
|
|
|
559
565
|
* is nearly always. The colour is the kernel's judgement, not a threshold invented here: green
|
|
560
566
|
* while it says all is well, amber at the first warning, red when it is critical.
|
|
561
567
|
*/
|
|
562
|
-
function renderMachine(m, cards) {
|
|
568
|
+
function renderMachine(m, cards, idleHours = 0) {
|
|
563
569
|
const el = $('#gauge')
|
|
564
570
|
if (!m?.mem) return el.setAttribute('hidden', '') // the first sample is not in yet
|
|
565
571
|
el.removeAttribute('hidden')
|
|
@@ -567,7 +573,12 @@ function renderMachine(m, cards) {
|
|
|
567
573
|
|
|
568
574
|
const ram = Math.round((m.mem.used / m.mem.total) * 100)
|
|
569
575
|
const cpu = m.cpu === null ? '—' : `${Math.round(m.cpu * 100)}%`
|
|
570
|
-
|
|
576
|
+
// Whether anybody is looking after the memory for you, and after how long. It belongs on this
|
|
577
|
+
// chip and nowhere else: this is where the memory is already being talked about, and a thing
|
|
578
|
+
// that quietly closes your windows should not be something you have to know about beforehand.
|
|
579
|
+
// Its presence is the whole message — switched off, there is nothing here rather than a nought.
|
|
580
|
+
const closes = idleHours > 0 ? `<span class="idle">· closes at ${idleHours}h</span>` : ''
|
|
581
|
+
el.innerHTML = `<i></i>RAM ${ram}% · CPU ${cpu}${closes}`
|
|
571
582
|
|
|
572
583
|
const top = cards.find((c) => c.id === heaviest)
|
|
573
584
|
const lines = [
|
|
@@ -577,6 +588,13 @@ function renderMachine(m, cards) {
|
|
|
577
588
|
if (top?.load) lines.push(`heaviest: ${top.title} — ${weight(top.load.rss)}`)
|
|
578
589
|
// What is heavy and is not k0: without it, the board would take blame that belongs to Chrome.
|
|
579
590
|
if (m.others?.length) lines.push(`outside k0: ${m.others.map((o) => `${o.name} ${gb(o.rss)}`).join(' · ')}`)
|
|
591
|
+
// The chip has room for three words; the sentence that explains them goes here, and it is also
|
|
592
|
+
// the only place that can say the closing is switched off — the chip says that by staying quiet.
|
|
593
|
+
lines.push(
|
|
594
|
+
idleHours > 0
|
|
595
|
+
? `a terminal nobody touches for ${idleHours}h is closed and its memory given back — the card stays, Resume picks it up`
|
|
596
|
+
: 'closing forgotten terminals is switched off'
|
|
597
|
+
)
|
|
580
598
|
lines.push(showLoad ? 'click to hide the weight on each card' : 'click to show the weight on each card')
|
|
581
599
|
el.title = lines.join('\n')
|
|
582
600
|
}
|
|
@@ -857,7 +875,7 @@ async function refresh() {
|
|
|
857
875
|
// What is eating most right now: it is what tints that one red and nobody else.
|
|
858
876
|
heaviest =
|
|
859
877
|
data.cards.filter((c) => c.load).sort((a, b) => b.load.rss - a.load.rss)[0]?.id ?? null
|
|
860
|
-
renderMachine(machine, data.cards)
|
|
878
|
+
renderMachine(machine, data.cards, data.closeIdleAfterHours)
|
|
861
879
|
|
|
862
880
|
// Redraw only if something really changed, so the editor and the focus stay where they are.
|
|
863
881
|
// The git state is inside the signature: without it, the mark would stay the one from the
|
package/web/conf.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// ── Configuration files, laid out ─────────────────────────────────────────────
|
|
2
|
+
// An `.env` is a table pretending to be a text file: two columns, one per line, and the reason
|
|
3
|
+
// you opened it is always to compare a name with a value. Shown as text, the values start in a
|
|
4
|
+
// different place on every line and the eye has to walk each one to the `=`. Shown as a table,
|
|
5
|
+
// you read the column.
|
|
6
|
+
//
|
|
7
|
+
// The rest — YAML, TOML, INI — is not a table: nesting matters, and a table would flatten the one
|
|
8
|
+
// thing that carries the meaning. Those keep their shape and only get their keys, their values
|
|
9
|
+
// and their comments told apart by colour.
|
|
10
|
+
//
|
|
11
|
+
// Like the tree next door, this knows nothing about the page: text in, a string of HTML out.
|
|
12
|
+
|
|
13
|
+
/** Written again here for the same reason as in `json.js`: the tests run in Node. */
|
|
14
|
+
const esc = (s) =>
|
|
15
|
+
String(s ?? '').replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' })[c])
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* An `.env` read line by line, keeping everything — the comments and the blank lines included.
|
|
19
|
+
*
|
|
20
|
+
* They are kept because they are the file: a comment above a key says what the key is for, and a
|
|
21
|
+
* view that dropped it would be a different file that happens to have the same values. Anything
|
|
22
|
+
* this does not understand comes back as it was written rather than being guessed at.
|
|
23
|
+
*
|
|
24
|
+
* Returns rows of `{ kind: 'pair' | 'comment' | 'blank' | 'raw' }`.
|
|
25
|
+
*/
|
|
26
|
+
export function envRows(text) {
|
|
27
|
+
const rows = []
|
|
28
|
+
for (const raw of String(text ?? '').split('\n')) {
|
|
29
|
+
const line = raw.replace(/\r$/, '')
|
|
30
|
+
const trimmed = line.trim()
|
|
31
|
+
if (!trimmed) {
|
|
32
|
+
rows.push({ kind: 'blank' })
|
|
33
|
+
continue
|
|
34
|
+
}
|
|
35
|
+
if (trimmed.startsWith('#')) {
|
|
36
|
+
rows.push({ kind: 'comment', text: trimmed.replace(/^#\s?/, '') })
|
|
37
|
+
continue
|
|
38
|
+
}
|
|
39
|
+
// `export FOO=bar` is the same line with a word in front of it: shell files and `.env` files
|
|
40
|
+
// are copied back and forth, and both spellings turn up in the same repository.
|
|
41
|
+
const body = trimmed.replace(/^export\s+/, '')
|
|
42
|
+
const at = body.indexOf('=')
|
|
43
|
+
if (at <= 0) {
|
|
44
|
+
rows.push({ kind: 'raw', text: line })
|
|
45
|
+
continue
|
|
46
|
+
}
|
|
47
|
+
const key = body.slice(0, at).trim()
|
|
48
|
+
rows.push({ kind: 'pair', key, value: unquote(body.slice(at + 1).trim()) })
|
|
49
|
+
}
|
|
50
|
+
// A file ends with a newline, and that last empty line is not a blank line in the file: it is
|
|
51
|
+
// the end of it. Showing it would put a hole at the bottom of every table.
|
|
52
|
+
if (rows.length && rows[rows.length - 1].kind === 'blank') rows.pop()
|
|
53
|
+
return rows
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The value without the quotes around it, and without the comment after it.
|
|
58
|
+
*
|
|
59
|
+
* The comment is only cut off an unquoted value: inside quotes a `#` is a character like any
|
|
60
|
+
* other, and a password with a hash in it must not lose half of itself on the way to the screen.
|
|
61
|
+
*/
|
|
62
|
+
function unquote(value) {
|
|
63
|
+
const quoted = /^(['"])([\s\S]*)\1$/.exec(value)
|
|
64
|
+
if (quoted) return quoted[2]
|
|
65
|
+
const hash = value.search(/\s#/)
|
|
66
|
+
return hash === -1 ? value : value.slice(0, hash).trimEnd()
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** The two columns. An empty value is said in words, or the row would look like a mistake. */
|
|
70
|
+
export function envTable(text) {
|
|
71
|
+
const body = envRows(text)
|
|
72
|
+
.map((r) => {
|
|
73
|
+
if (r.kind === 'pair') {
|
|
74
|
+
const value = r.value
|
|
75
|
+
? `<td class="val">${esc(r.value)}</td>`
|
|
76
|
+
: '<td class="val none">empty</td>'
|
|
77
|
+
return `<tr><th>${esc(r.key)}</th>${value}</tr>`
|
|
78
|
+
}
|
|
79
|
+
if (r.kind === 'comment') return `<tr class="note"><td colspan="2">${esc(r.text)}</td></tr>`
|
|
80
|
+
if (r.kind === 'raw') return `<tr class="odd"><td colspan="2">${esc(r.text)}</td></tr>`
|
|
81
|
+
return '<tr class="gap"><td colspan="2"></td></tr>'
|
|
82
|
+
})
|
|
83
|
+
.join('')
|
|
84
|
+
return `<table class="env">${body}</table>`
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* YAML, TOML and INI: the same file, with the parts told apart.
|
|
89
|
+
*
|
|
90
|
+
* No parsing. A YAML parser written here would be a second thing to keep correct, and every one
|
|
91
|
+
* of its mistakes would show you a file that is not the file. Reading a line at a time never
|
|
92
|
+
* shows you anything that is not there — at worst it colours something in as though it were
|
|
93
|
+
* ordinary text, which is what it does with everything it does not recognise.
|
|
94
|
+
*/
|
|
95
|
+
export function highlight(text) {
|
|
96
|
+
const out = String(text ?? '')
|
|
97
|
+
.split('\n')
|
|
98
|
+
.map((raw) => {
|
|
99
|
+
const line = raw.replace(/\r$/, '')
|
|
100
|
+
const lead = line.length - line.trimStart().length
|
|
101
|
+
const indent = esc(line.slice(0, lead))
|
|
102
|
+
const rest = line.slice(lead)
|
|
103
|
+
if (!rest) return ''
|
|
104
|
+
if (rest.startsWith('#') || rest.startsWith(';')) return `${indent}<span class="c">${esc(rest)}</span>`
|
|
105
|
+
// A TOML or INI heading: the one line that says where you are in the file.
|
|
106
|
+
if (/^\[.*]$/.test(rest)) return `${indent}<span class="sec">${esc(rest)}</span>`
|
|
107
|
+
// `- ` is a YAML list, and what follows it may itself be a pair.
|
|
108
|
+
const dash = /^-\s+/.exec(rest)
|
|
109
|
+
const item = dash ? rest.slice(dash[0].length) : rest
|
|
110
|
+
const bullet = dash ? `<span class="dash">${esc(dash[0])}</span>` : ''
|
|
111
|
+
const pair = /^([^:=#]+?)\s*([:=])\s*([\s\S]*)$/.exec(item)
|
|
112
|
+
if (!pair) return `${indent}${bullet}<span class="v">${esc(item)}</span>`
|
|
113
|
+
const value = pair[3] ? `<span class="v">${esc(pair[3])}</span>` : ''
|
|
114
|
+
return `${indent}${bullet}<span class="key">${esc(pair[1])}</span><span class="sep">${pair[2]}</span> ${value}`
|
|
115
|
+
})
|
|
116
|
+
.join('\n')
|
|
117
|
+
return `<pre class="conf">${out}</pre>`
|
|
118
|
+
}
|
package/web/files.css
CHANGED
|
@@ -74,6 +74,28 @@ body {
|
|
|
74
74
|
width: 100%;
|
|
75
75
|
color: var(--ink);
|
|
76
76
|
}
|
|
77
|
+
/* The switch that lets the configuration through. It was a bare glyph inside the search pill and
|
|
78
|
+
nobody found it: at that size, in that place, it read as part of the field rather than as
|
|
79
|
+
something you could press. So it is a button of its own, next to the search, outlined like the
|
|
80
|
+
ones over a document, and it says what it does in a word. Switched on it fills in, so from
|
|
81
|
+
across the room you can tell whether you are looking at everything or not. */
|
|
82
|
+
.topbar .nerd {
|
|
83
|
+
flex: none;
|
|
84
|
+
display: flex;
|
|
85
|
+
align-items: center;
|
|
86
|
+
gap: 5px;
|
|
87
|
+
padding: 5px 11px;
|
|
88
|
+
border: 1px solid var(--line);
|
|
89
|
+
background: #fff;
|
|
90
|
+
border-radius: 999px;
|
|
91
|
+
font: inherit;
|
|
92
|
+
font-size: .74rem;
|
|
93
|
+
color: var(--muted);
|
|
94
|
+
cursor: pointer;
|
|
95
|
+
}
|
|
96
|
+
.topbar .nerd svg { width: 14px; height: 14px; display: block; flex: none; }
|
|
97
|
+
.topbar .nerd:hover { color: var(--ink); border-color: #bdb6a5; background: #fff; }
|
|
98
|
+
.topbar .nerd.on, .topbar .nerd.on:hover { background: var(--ink); border-color: var(--ink); color: #fff; }
|
|
77
99
|
|
|
78
100
|
/* ── The two panes ───────────────────────────────────────────────────── */
|
|
79
101
|
/* The divider is a column of the grid: drag it and the size stays. */
|
|
@@ -160,6 +182,16 @@ body {
|
|
|
160
182
|
overflow: hidden;
|
|
161
183
|
}
|
|
162
184
|
.row q mark { background: #fde68a; color: var(--ink); border-radius: 3px; padding: 0 1px; }
|
|
185
|
+
/* The letters that made this row match, in its name. Underlined and not highlighted, so the two
|
|
186
|
+
answers stay apart at a glance: yellow means the word was found inside the file, a line under
|
|
187
|
+
the letters means they are in its name. */
|
|
188
|
+
.row b u {
|
|
189
|
+
color: var(--ink);
|
|
190
|
+
text-decoration: underline;
|
|
191
|
+
text-decoration-thickness: 2px;
|
|
192
|
+
text-underline-offset: 2px;
|
|
193
|
+
text-decoration-color: var(--alert);
|
|
194
|
+
}
|
|
163
195
|
.row:hover { background: rgba(0, 0, 0, .035); }
|
|
164
196
|
.row.on { background: var(--WORKING); }
|
|
165
197
|
.row.active { box-shadow: inset 3px 0 0 var(--ink); }
|
|
@@ -215,6 +247,26 @@ body {
|
|
|
215
247
|
.from-text .x:hover { opacity: 1; text-decoration: none; }
|
|
216
248
|
.from-text .x svg { width: 15px; height: 15px; display: block; }
|
|
217
249
|
|
|
250
|
+
/* The line that says the switch is hiding what you asked for. It sits above everything, in the
|
|
251
|
+
same green as the other things that say "the listing is not showing you all of it", and it is a
|
|
252
|
+
button: the answer to "where is my .env" should be one click, not an instruction. */
|
|
253
|
+
.nudge {
|
|
254
|
+
display: block;
|
|
255
|
+
width: 100%;
|
|
256
|
+
border: 0;
|
|
257
|
+
border-bottom: 1px solid rgba(0, 0, 0, .08);
|
|
258
|
+
border-radius: 0;
|
|
259
|
+
background: var(--IDLE);
|
|
260
|
+
padding: 9px 16px;
|
|
261
|
+
font: inherit;
|
|
262
|
+
font-size: .78rem;
|
|
263
|
+
font-weight: 600;
|
|
264
|
+
color: var(--ink);
|
|
265
|
+
text-align: left;
|
|
266
|
+
cursor: pointer;
|
|
267
|
+
}
|
|
268
|
+
.nudge:hover { background: var(--IDLE); text-decoration: underline; }
|
|
269
|
+
|
|
218
270
|
/* The heading of a group that opens and closes: the guesses arrive closed. */
|
|
219
271
|
.list h2.fold { cursor: pointer; user-select: none; }
|
|
220
272
|
.list h2.fold::after {
|
|
@@ -239,6 +291,45 @@ body {
|
|
|
239
291
|
.row.fold b::after { content: ' ▸'; opacity: .6; font-weight: 400; }
|
|
240
292
|
.row.fold.open b::after { content: ' ▾'; }
|
|
241
293
|
|
|
294
|
+
/* ── Folders ──────────────────────────────────────────────────────────
|
|
295
|
+
A folder is a row like any other, and deliberately so: the arrow keys walk it, ⌘-click opens
|
|
296
|
+
it in another tab, and nothing about it has to be learnt separately. The only difference is the
|
|
297
|
+
mark in front of the name and the slash after it, which is how a folder has always been told
|
|
298
|
+
apart from a file. */
|
|
299
|
+
.row.folder b::before {
|
|
300
|
+
content: '';
|
|
301
|
+
display: inline-block;
|
|
302
|
+
width: 12px;
|
|
303
|
+
height: 9px;
|
|
304
|
+
margin-right: 7px;
|
|
305
|
+
border: 1px solid var(--muted);
|
|
306
|
+
border-radius: 1px 2px 2px 2px;
|
|
307
|
+
border-top-width: 3px;
|
|
308
|
+
vertical-align: middle;
|
|
309
|
+
opacity: .75;
|
|
310
|
+
}
|
|
311
|
+
.row.folder b::after { content: '/'; opacity: .45; }
|
|
312
|
+
|
|
313
|
+
/* Where you are, one piece of the path at a time. It sits in the same green strip as the label of
|
|
314
|
+
a pasted text: both say the listing is a slice, and they must look like the same statement. */
|
|
315
|
+
.from-text.trail { gap: 3px; overflow-x: auto; scrollbar-width: none; }
|
|
316
|
+
.from-text.trail::-webkit-scrollbar { display: none; }
|
|
317
|
+
.from-text a, .from-text b {
|
|
318
|
+
font: inherit;
|
|
319
|
+
font-size: .78rem;
|
|
320
|
+
font-weight: 600;
|
|
321
|
+
color: var(--ink);
|
|
322
|
+
text-decoration: none;
|
|
323
|
+
white-space: nowrap;
|
|
324
|
+
cursor: pointer;
|
|
325
|
+
}
|
|
326
|
+
.from-text a:hover { text-decoration: underline; }
|
|
327
|
+
/* The last piece is where you are: it does not lead anywhere, and it does not pretend to. */
|
|
328
|
+
.from-text b { cursor: default; opacity: .95; }
|
|
329
|
+
.from-text i { font-style: normal; font-size: .72rem; opacity: .4; }
|
|
330
|
+
.from-text .act { margin-left: auto; padding: 2px 7px; background: rgba(255, 255, 255, .7); }
|
|
331
|
+
.from-text .act + .x { margin-left: 4px; }
|
|
332
|
+
|
|
242
333
|
/* "7 more": it sits under the row it belongs to, off to one side. */
|
|
243
334
|
.more {
|
|
244
335
|
display: block;
|
|
@@ -328,6 +419,10 @@ body {
|
|
|
328
419
|
.crumb b { font: 600 .82rem var(--mono); color: var(--ink); }
|
|
329
420
|
.crumb i { font-style: normal; opacity: .5; }
|
|
330
421
|
.crumb em { margin-left: auto; font-style: normal; }
|
|
422
|
+
/* The path above a document is the way to its neighbours: each piece opens the folder it names,
|
|
423
|
+
in the list on the left. It stays grey — it is still the path, not a row of buttons. */
|
|
424
|
+
.crumb a.up { color: inherit; text-decoration: none; border-bottom: 1px dotted transparent; }
|
|
425
|
+
.crumb a.up:hover { color: var(--ink); border-bottom-color: var(--muted); }
|
|
331
426
|
|
|
332
427
|
/* ── What to do with it ───────────────────────────────────────────────
|
|
333
428
|
Print the document, take it away as it is, or have it laid out into a PDF. They sit here at
|
|
@@ -350,8 +445,115 @@ body {
|
|
|
350
445
|
}
|
|
351
446
|
.act:hover { color: var(--ink); border-color: #bdb6a5; }
|
|
352
447
|
.act svg { width: 14px; height: 14px; display: block; flex: none; }
|
|
448
|
+
/* A button this machine cannot honour stays where it is, faint, with the reason in its title. One
|
|
449
|
+
that vanished would leave you looking for it; one that did nothing would leave you wondering. */
|
|
450
|
+
.act[disabled] { opacity: .3; cursor: default; }
|
|
451
|
+
.act[disabled]:hover { color: var(--muted); border-color: var(--line); }
|
|
452
|
+
/* Save is the one that changes something, so it is the one that is filled in. */
|
|
453
|
+
.act.keep { background: var(--ink); border-color: var(--ink); color: #fff; font-weight: 600; }
|
|
454
|
+
.act.keep:hover { color: #fff; border-color: var(--ink); }
|
|
353
455
|
.cut { padding: 12px 32px; color: var(--muted); font-size: .82rem; font-style: italic; }
|
|
354
456
|
|
|
457
|
+
/* ── Searching inside one file ────────────────────────────────────────
|
|
458
|
+
Only where the browser's own find cannot go: a tree is mostly folded, and ⌘F says "not found"
|
|
459
|
+
for something that is three lines away behind a closed branch. */
|
|
460
|
+
.findin { display: flex; align-items: center; gap: 6px; margin-left: 12px; }
|
|
461
|
+
.findin input {
|
|
462
|
+
border: 1px solid var(--line);
|
|
463
|
+
background: #fff;
|
|
464
|
+
border-radius: 999px;
|
|
465
|
+
padding: 4px 11px;
|
|
466
|
+
font: inherit;
|
|
467
|
+
font-size: .72rem;
|
|
468
|
+
color: var(--ink);
|
|
469
|
+
outline: 0;
|
|
470
|
+
width: 170px;
|
|
471
|
+
}
|
|
472
|
+
.findin input:focus { border-color: #bdb6a5; }
|
|
473
|
+
.findin em {
|
|
474
|
+
font-style: normal;
|
|
475
|
+
font-size: .72rem;
|
|
476
|
+
color: var(--muted);
|
|
477
|
+
font-variant-numeric: tabular-nums;
|
|
478
|
+
min-width: 1.4em;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/* ── A JSON file, as a tree ───────────────────────────────────────────
|
|
482
|
+
The folding is `<details>`: the browser already knows how to do it, so there is no state here
|
|
483
|
+
and it works before a single line of script has run. The colours are the types — you learn them
|
|
484
|
+
in one glance and then never read them again, you just see them. */
|
|
485
|
+
.tree { padding: 18px 32px; font: .8rem/1.55 var(--mono); }
|
|
486
|
+
.json .kids { margin-left: 1.05rem; padding-left: .7rem; border-left: 1px solid var(--line); }
|
|
487
|
+
.json summary { cursor: pointer; list-style: none; padding: 1px 0; border-radius: 4px; }
|
|
488
|
+
.json summary::-webkit-details-marker { display: none; }
|
|
489
|
+
.json summary::before {
|
|
490
|
+
content: '▸';
|
|
491
|
+
display: inline-block;
|
|
492
|
+
width: .9em;
|
|
493
|
+
opacity: .5;
|
|
494
|
+
transition: transform .12s;
|
|
495
|
+
}
|
|
496
|
+
.json details[open] > summary::before { transform: rotate(90deg); }
|
|
497
|
+
.json summary:hover { background: rgba(0, 0, 0, .04); }
|
|
498
|
+
.json .leaf { padding: 1px 0 1px .9em; }
|
|
499
|
+
.json .key { font-weight: 600; color: #1e40af; }
|
|
500
|
+
.json .key::after { content: ':'; opacity: .45; font-weight: 400; margin-right: .35em; }
|
|
501
|
+
.json .brace { opacity: .45; margin-left: .35em; }
|
|
502
|
+
.json em { font-style: normal; margin-left: .6em; font-size: .9em; color: var(--muted); }
|
|
503
|
+
.json .v.str { color: #15803d; }
|
|
504
|
+
.json .v.str::before, .json .v.str::after { content: '"'; opacity: .4; }
|
|
505
|
+
.json .v.num { color: #b45309; }
|
|
506
|
+
.json .v.bool { color: #7c3aed; }
|
|
507
|
+
.json .v.nil, .json .v.empty { color: var(--muted); font-style: italic; }
|
|
508
|
+
.json mark { background: #fde68a; color: var(--ink); border-radius: 3px; padding: 0 1px; }
|
|
509
|
+
|
|
510
|
+
/* ── An .env file, as the table it always was ─────────────────────────
|
|
511
|
+
Two columns and one line each: the reason you open one is to compare a name with a value, and
|
|
512
|
+
as text the values start in a different place on every line. The comments stay — a comment
|
|
513
|
+
above a key is what the key is for, and a view that dropped it would be a different file. */
|
|
514
|
+
.env { width: calc(100% - 64px); margin: 20px 32px; border-collapse: collapse; font: .8rem var(--mono); }
|
|
515
|
+
.env th, .env td { padding: 6px 10px; border-bottom: 1px solid var(--line); text-align: left; vertical-align: top; }
|
|
516
|
+
.env th { width: 34%; font-weight: 600; color: #1e40af; overflow-wrap: anywhere; }
|
|
517
|
+
.env .val { color: var(--ink); overflow-wrap: anywhere; }
|
|
518
|
+
.env .val.none { color: var(--muted); font-style: italic; }
|
|
519
|
+
.env .note td { color: var(--muted); font-style: italic; border-bottom: 0; padding-top: 14px; }
|
|
520
|
+
.env .odd td { color: var(--muted); }
|
|
521
|
+
.env .gap td { padding: 0; height: 8px; border-bottom: 0; }
|
|
522
|
+
|
|
523
|
+
/* ── YAML, TOML, INI ──────────────────────────────────────────────────
|
|
524
|
+
Not a table: the nesting is the meaning, and flattening it would throw away the one thing you
|
|
525
|
+
opened the file to see. So the file keeps its shape and only gets its parts told apart. */
|
|
526
|
+
.conf {
|
|
527
|
+
margin: 0;
|
|
528
|
+
padding: 20px 32px;
|
|
529
|
+
font: .82rem/1.6 var(--mono);
|
|
530
|
+
white-space: pre;
|
|
531
|
+
overflow-x: auto;
|
|
532
|
+
}
|
|
533
|
+
.conf .key { color: #1e40af; font-weight: 600; }
|
|
534
|
+
.conf .sep, .conf .dash { opacity: .45; }
|
|
535
|
+
.conf .sec { color: #7c3aed; font-weight: 600; }
|
|
536
|
+
.conf .c { color: var(--muted); font-style: italic; }
|
|
537
|
+
|
|
538
|
+
/* ── Writing a file ───────────────────────────────────────────────────
|
|
539
|
+
In the pane where the document was, not in a window over it: a dialog is the right size for a
|
|
540
|
+
title and the wrong size for a README, and here the pane is already as tall as the file. */
|
|
541
|
+
.crumb.editing { background: var(--IDLE); }
|
|
542
|
+
.crumb.editing em { font-style: italic; }
|
|
543
|
+
.edit {
|
|
544
|
+
display: block;
|
|
545
|
+
width: 100%;
|
|
546
|
+
height: calc(100vh - 96px);
|
|
547
|
+
border: 0;
|
|
548
|
+
outline: 0;
|
|
549
|
+
resize: none;
|
|
550
|
+
padding: 20px 32px;
|
|
551
|
+
background: #fff;
|
|
552
|
+
color: var(--ink);
|
|
553
|
+
font: .82rem/1.6 var(--mono);
|
|
554
|
+
tab-size: 2;
|
|
555
|
+
}
|
|
556
|
+
|
|
355
557
|
/* ── The label ────────────────────────────────────────────────────────
|
|
356
558
|
What is written between the two rules at the top of the file. It is not text of the document —
|
|
357
559
|
it is what the document says it is — so it reads as a label: set apart, in a different colour,
|
|
@@ -511,7 +713,10 @@ body {
|
|
|
511
713
|
.split { display: block; }
|
|
512
714
|
.doc { overflow: visible; padding: 0 14mm; background: #fff; }
|
|
513
715
|
|
|
514
|
-
.md, .plain { padding: 0; font-size: 10.5pt; line-height: 1.5; }
|
|
716
|
+
.md, .plain, .tree, .conf { padding: 0; font-size: 10.5pt; line-height: 1.5; }
|
|
717
|
+
.env { width: 100%; margin: 0; }
|
|
718
|
+
/* A file half-written is not a document: there is nothing to put on paper. */
|
|
719
|
+
.edit { display: none !important; }
|
|
515
720
|
/* The label is hidden, but it is still there: the space that held it down is not. */
|
|
516
721
|
.plate + .md { padding-top: 0; }
|
|
517
722
|
.md a { color: #000; }
|
package/web/files.html
CHANGED
|
@@ -20,6 +20,16 @@
|
|
|
20
20
|
</button>
|
|
21
21
|
<input id="q" autocomplete="off" spellcheck="false" placeholder="search a file…">
|
|
22
22
|
</div>
|
|
23
|
+
<!-- The switch that lets the configuration through. Off by default: in a repository of prose
|
|
24
|
+
an `.env` is not what you came for, and on the day it is, it is the only thing you came
|
|
25
|
+
for. It sits beside the search and not inside it, and it says what it is in a word: put
|
|
26
|
+
in the pill as a bare glyph it read as part of the field's decoration, and nobody found
|
|
27
|
+
it. -->
|
|
28
|
+
<button type="button" class="nerd" id="nerd" aria-pressed="false"
|
|
29
|
+
title="Show configuration files too — .env, JSON, YAML, TOML, INI" aria-label="Show configuration files too">
|
|
30
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 6l-5 6 5 6"/><path d="M16 6l5 6-5 6"/></svg>
|
|
31
|
+
config
|
|
32
|
+
</button>
|
|
23
33
|
<span class="count" id="count"></span>
|
|
24
34
|
</header>
|
|
25
35
|
|