Oregami
Repositories/oxedyne/daimond

oxedyne/daimond/www/css/terminal.css

22.3 KiB, 1 run

created by r2519314175:1131, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1/* terminal.css — the terminal panel's own look, and the sixteen colours.
2 *
3 * Everything here is either a palette variable or derived from one, so the
4 * terminal wears whatever palette the app is wearing. The one thing that could
5 * not be derived is the ANSI set: a program asking for "red" is asking for a
6 * colour by name, and none of the app's tokens is a full sixteen-colour scheme.
7 * So there are two sets, chosen by `data-ink` — the axis the app already
8 * publishes for exactly this kind of question — and neither of them is
9 * per-palette, because sixteen colours times eleven palettes is eleven schemes
10 * to keep honest and one of them would rot.
11 *
12 * Measured against every palette's own `--bg-primary`, which is the ground the
13 * terminal is drawn on: every one of the sixteen clears 3.0 as LETTERING on
14 * every palette, worst case 3.19, and `dev/verify_terminal.mjs` holds them
15 * there. That is why "black" is a mid grey here — a program printing colour 0
16 * on a dark ground is printing something a reader is entitled to be able to
17 * read, and an unreadable black is not more faithful, it is just unreadable.
18 *
19 * The awkward case is a name that has to mean two things. On paper, colour 7 as
20 * LETTERING must be dark or a prompt written in it is not there at all, while
21 * colour 7 as a BACKGROUND must be pale or a status bar comes out inverted. The
22 * usual light-terminal answer is to pick one and live with the other. Here the
23 * renderer knows which it is resolving, so `--term-ansi-bg-N` overrides the
24 * background value where the two genuinely differ, and both come out right.
25 *
26 * Amber deliberately holds blue out of everything it draws, and the ANSI set is
27 * NOT re-tinted for it. What a program prints is content, in the way a page in
28 * the Web panel is content: the app does not repaint either. A user reading at
29 * night has the palette for the frame around it.
30 */
31
32:root {
33 /* Dark ground, light ink — the default. */
34 --term-ansi-0: #767676;
35 --term-ansi-1: #EE7A6B;
36 --term-ansi-2: #8FCE7A;
37 --term-ansi-3: #DCAE58;
38 --term-ansi-4: #68AEE8;
39 --term-ansi-5: #CE93E2;
40 --term-ansi-6: #5FCBC8;
41 --term-ansi-7: #C9C4BB;
42 --term-ansi-8: #949494;
43 --term-ansi-9: #F7998C;
44 --term-ansi-10: #A9E294;
45 --term-ansi-11: #EDC97A;
46 --term-ansi-12: #8AC8F5;
47 --term-ansi-13: #E0AEF0;
48 --term-ansi-14: #84DFDD;
49 --term-ansi-15: #F3F0E9;
50
51 /* A "black background" must actually be black. Colour 0 as LETTERING is
52 lifted to a grey so it can be read at all on a dark ground -- it clears
53 3.19 on the palest of the dark palettes, which is the floor everything
54 else in the app is held to -- while as a GROUND it is the black the
55 program asked for. */
56 --term-ansi-bg-0: #1A1A1A;
57
58 --term-fg: var(--text-primary);
59 --term-bg: var(--bg-primary);
60 --term-cursor: var(--accent);
61 /* A translucent wash rather than a palette colour: it is painted OVER the
62 cell's own background, so it has to survive being laid on any of the
63 sixteen, and an opaque accent would hide the text it is selecting.
64 Written literally because nothing else in the app mixes colours, and one
65 `color-mix` here would be the only one. */
66 --term-selection: rgba(255, 255, 255, .26);
67 --term-font: var(--font-mono);
68 /* One rung below body text, which is what dense monospace furniture uses
69 elsewhere in the app, and it moves with the user's text-size choice. */
70 --term-font-size: var(--fs-sm);
71}
72
73:root[data-ink="dark"] {
74 /* Light ground, dark ink. */
75 --term-ansi-0: #242424;
76 --term-ansi-1: #A32A1E;
77 --term-ansi-2: #256D2A;
78 --term-ansi-3: #7A5A00;
79 --term-ansi-4: #1A5A96;
80 --term-ansi-5: #8A2E9C;
81 --term-ansi-6: #0F6E6C;
82 --term-ansi-7: #4E4E4E;
83 --term-ansi-8: #6E6E6E;
84 --term-ansi-9: #B93A29;
85 --term-ansi-10: #33843A;
86 --term-ansi-11: #8F6D08;
87 --term-ansi-12: #2668AC;
88 --term-ansi-13: #9C43AC;
89 --term-ansi-14: #177E7C;
90 --term-ansi-15: #1C1C1C;
91 /* The other side of the same problem. On paper, colours 7 and 15 have to be
92 dark to be readable AS TEXT — a prompt in bright white is otherwise not
93 there at all — and pale to be right AS A BACKGROUND, which is what a
94 status bar or a selected row uses them for. Both, then. */
95 --term-ansi-bg-7: #D6D3CC;
96 --term-ansi-bg-15: #FFFFFF;
97 --term-ansi-bg-8: #9C9C9C;
98
99 --term-selection: rgba(0, 0, 0, .20);
100}
101
102/* ── The panel ──────────────────────────────────────────────── */
103
104.term {
105 position: relative;
106 min-width: 0;
107 /* The grid is sized to the box, so the box may never be sized to the grid:
108 a canvas that pushed its container wider would grow itself on the next
109 measurement and never settle. */
110 overflow: hidden;
111 background: var(--term-bg);
112 border: 1px solid var(--border-strong);
113 border-radius: var(--radius);
114 padding: 4px;
115 /* A terminal is a fixed grid, so it takes whatever room it is given and
116 computes columns from it rather than scrolling sideways. */
117 width: 100%;
118 height: 100%;
119 box-sizing: border-box;
120}
121
122/* Focus is drawn OUTSIDE the control, standing off from its edge against the
123 surface behind it, which is the rule the rest of the app follows: a ring made
124 out of the control's own border is only legible while the resting border is
125 nearly invisible, and this control has a strong resting border by design. */
126.term:focus-within {
127 outline: 2px solid var(--accent);
128 outline-offset: 2px;
129}
130
131.term-canvas {
132 display: block;
133 max-width: 100%;
134}
135
136/* The real control. It carries no text — anything typed or pasted into it is
137 sent and cleared — so it is placed over the canvas and made invisible rather
138 than hidden: a display:none field takes no keystrokes and gets no soft
139 keyboard on a phone. */
140.term-input {
141 position: absolute;
142 inset: 0;
143 width: 100%;
144 height: 100%;
145 margin: 0;
146 padding: 0;
147 border: 0;
148 resize: none;
149 overflow: hidden;
150 background: transparent;
151 color: transparent;
152 caret-color: transparent;
153 outline: none;
154 font: inherit;
155 /* Behind the paste question, which must be clickable. */
156 z-index: 1;
157 /* AND IT MUST NOT TAKE THE MOUSE. Stretched over the canvas at `inset: 0`, this
158 field was the topmost thing under the pointer everywhere on the screen, so a
159 real drag hit the textarea and the canvas `mousedown` -- the whole of
160 selection -- never ran. Reported as "dragging doesn't select at all" on
161 2026-08-26; selection had been built, painted and tested since the panel
162 shipped. `dev/verify_terminal.mjs` could not see it, because dispatching a
163 MouseEvent AT the canvas bypasses hit-testing: the check that catches this
164 asks `elementFromPoint` what is actually on top.
165
166 The keyboard is unaffected -- focus is given by the canvas's own handlers,
167 which call `input.focus()` on mousedown and on touchstart, so a soft keyboard
168 still comes up on a phone. */
169 pointer-events: none;
170}
171.term-input::selection { background: transparent; }
172
173/* The size, while it is changing. A person dragging a panel edge wants to know
174 what they are choosing; the rest of the time it is in the way. */
175.term-size {
176 position: absolute;
177 right: 10px;
178 bottom: 8px;
179 z-index: 2;
180 padding: 2px 7px;
181 border-radius: var(--radius-sm);
182 background: var(--bg-secondary);
183 border: 1px solid var(--border-2);
184 color: var(--text-secondary);
185 font: var(--fs-2xs) / 1.4 var(--font-mono);
186 opacity: 0;
187 transition: opacity .18s ease;
188 pointer-events: none;
189}
190.term-size.on { opacity: 1; }
191
192/* A bell is a flash of the panel's own edge, not a sound: a page that makes a
193 noise is a page people mute. */
194.term.bell { box-shadow: inset 0 0 0 2px var(--accent); }
195
196/* ── The paste question ─────────────────────────────────────── */
197
198.term-paste {
199 position: absolute;
200 left: 8px;
201 right: 8px;
202 bottom: 8px;
203 z-index: 3;
204 padding: 10px 12px;
205 background: var(--bg-secondary);
206 border: 1px solid var(--border-strong);
207 border-radius: var(--radius);
208 box-shadow: 0 6px 18px rgba(0, 0, 0, .28);
209}
210.term-paste[hidden] { display: none; }
211
212.term-paste-say {
213 margin: 0 0 8px;
214 color: var(--text-primary);
215 font-size: var(--fs-sm);
216 line-height: 1.45;
217}
218
219.term-paste-row { display: flex; flex-wrap: wrap; gap: 8px; }
220
221.term-paste-btn {
222 padding: 5px 12px;
223 border-radius: var(--radius-sm);
224 border: 1px solid var(--border-strong);
225 background: var(--bg-tertiary);
226 color: var(--text-primary);
227 font-size: var(--fs-sm);
228 cursor: pointer;
229}
230.term-paste-btn:hover { background: var(--bg-hover); }
231.term-paste-btn.primary {
232 background: var(--accent);
233 border-color: var(--accent);
234 color: var(--on-fill);
235}
236.term-paste-btn.primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
237.term-paste-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
238
239/* ── What a screen reader is given ──────────────────────────────
240 Clipped rather than hidden. `display: none` and `visibility: hidden` both
241 take an element out of the accessibility tree, which would leave the canvas
242 as the only thing there — and a canvas says nothing at all. */
243
244.term-sr {
245 position: absolute;
246 width: 1px;
247 height: 1px;
248 margin: -1px;
249 padding: 0;
250 overflow: hidden;
251 clip: rect(0 0 0 0);
252 clip-path: inset(50%);
253 white-space: nowrap;
254 border: 0;
255}
256
257/* The transcript is many lines, so it may not be one nowrap line. */
258.term-mirror { white-space: normal; }
259
260/* ── The Terminal panel ─────────────────────────────────────────
261 The furniture AROUND the terminal: the head, the sentence saying where the
262 session is, the notices that ride beside the stream, and the line that says
263 how to get the keyboard back out. The terminal itself is everything above;
264 nothing here reaches inside it. */
265
266/* The panel is a stage card and a flex column, like the AI, the Web page and a
267 document. app.css names the stage's cards one by one (`.ai, .web, .doc, .msg,
268 .compose, .tools`) and graph.css gives the Graph its own copy of that card for
269 exactly this reason; this is the Terminal's, declared here because this is the
270 file the terminal owns.
271
272 Both halves of the rule earn their place. Left off the CARD list the panel
273 would be a bare box on the stage's ground with no edge between it and the
274 conversation beside it. Left off the FLEX list it would stay a block: its head
275 and its body would then share the WIDTH instead of the height, and nothing
276 inside it could be told to fill what is left. Spending was left off the dock's
277 equivalent once and came out as a stranded column of letters; the Terminal
278 comes out as a canvas frozen at its minimum grid however tall the panel is.
279
280 No `flex` of its own: a stage occupant's flex and width are set inline by the
281 engine's applySeat, which is the only thing that knows whether it is sharing
282 the stage or holding it alone. */
283.panel.termpanel {
284 min-width: 0;
285 background: var(--bg-secondary);
286 border: 1px solid var(--border);
287 border-radius: var(--radius-lg);
288 display: flex;
289 flex-direction: column;
290 overflow: hidden;
291}
292
293/* The head is the stage's card head, so the panel's own name is set in the same
294 type as the AI's and the page's rather than in the dock's small capitals. The
295 name is fixed and the program's title takes the slack beside it, so the three
296 buttons stay against the right however long a title runs. */
297.termpanel .chead { gap: 8px; }
298.termpanel .chead .ctitle { flex: none; }
299
300.termpanel .termp-card {
301 display: flex;
302 flex-direction: column;
303 gap: 6px;
304 min-height: 0;
305 /* The card takes the panel's height so the host below can be told to fill
306 what is left -- a terminal computes its columns from the box it is given,
307 so the box has to be a real one. */
308 flex: 1 1 auto;
309 padding: 8px;
310 box-sizing: border-box;
311}
312
313/* The host is the only thing that grows. `min-height: 0` because a flex item
314 defaults to its content's size, and a canvas that had already grown would
315 then keep the box at that size for ever. */
316.termpanel .termp-host {
317 flex: 1 1 auto;
318 min-height: 120px;
319 display: flex;
320 /* The containing block for the notices, which lie over the SCREEN and not
321 over the state line above it. */
322 position: relative;
323}
324.termpanel .termp-host > .term { flex: 1 1 auto; }
325
326/* A terminal that was refused before it ever drew anything is an empty black
327 box under a paragraph explaining why it is empty. The paragraph is the whole
328 of what there is to read, so it gets the panel. The moment a session starts —
329 or once anything has been drawn — the box comes back. */
330.termpanel.no-term .termp-host { display: none; }
331
332/* What the program calls itself, beside the panel's own name. It takes the head's
333 slack, so the buttons keep their place. Truncated rather than wrapped: a title
334 is a label, and a two-line head moves the terminal. */
335.termp-title {
336 flex: 1 1 auto;
337 min-width: 0;
338 overflow: hidden;
339 text-overflow: ellipsis;
340 white-space: nowrap;
341 color: var(--text-secondary);
342 font: var(--fs-xs) / 1.4 var(--font-mono);
343}
344
345/* The three buttons sit closer together than the stage head's default 8px: they
346 are icons in a row and read as one control group, not as three. */
347.termpanel .chead-right { gap: 2px; }
348
349/* Where the session is, in a sentence. Refusals arrive here verbatim, so it has
350 to be able to hold a paragraph without pushing the terminal off the panel. */
351.termp-state {
352 margin: 0;
353 color: var(--text-secondary);
354 font-size: var(--fs-xs);
355 line-height: 1.5;
356 /* Two lines, whatever it says, and it scrolls rather than grows. A state line
357 that changed height would take that height out of the terminal below it,
358 the grid would be recomputed smaller, and the model would push the top of
359 the screen into scrollback — so "the program exited" or "output is missing"
360 would sweep away the output it is about. Fixed, nothing moves. */
361 flex: none;
362 height: 3em;
363 overflow-y: auto;
364}
365.termp-state:empty { display: none; }
366/* With no screen to protect, the sentence is the whole panel and takes the room:
367 this is the refusal case, where there is nothing else to read. */
368.termpanel.no-term .termp-state { height: auto; max-height: 14em; }
369.termpanel.no-term .termp-state.refused {
370 padding: 8px 10px 8px 10px;
371 border: 1px solid var(--border-strong);
372 border-left: 3px solid var(--warn);
373 border-radius: var(--radius-sm);
374 background: var(--bg-secondary);
375}
376/* A refusal is not a status line. It is the one thing on the panel worth
377 reading, so it is marked rather than merely coloured -- a coloured sentence is
378 invisible to a reader who cannot see the colour.
379
380 A rule down its left and nothing else, because the mark must not change the
381 BOX: padding and a border would make the state line taller the moment a
382 session was refused or a hole appeared, and the terminal underneath would lose
383 the top of its screen to the reflow. The full frame belongs to the case where
384 there is no screen to protect, below. */
385.termp-state.refused {
386 border-left: 3px solid var(--warn);
387 padding-left: 8px;
388 color: var(--text-primary);
389}
390
391/* A hole in the output, said beside the stream. Each notice stays until the
392 session ends: a gap is a fact about the transcript above it, and a message
393 that faded would leave a screen nobody can account for.
394
395 OVER the terminal rather than above it, and that is not a decorative choice.
396 A notice in the flow takes its height out of the panel, the grid is recomputed
397 smaller, and the model pushes the lines at the top of the screen into
398 scrollback — so the arrival of "output is missing here" would itself sweep the
399 output around the hole out of view, at the one moment a reader needs to see
400 it. Out of flow, the grid does not move and nothing is swept anywhere. */
401.termp-gaps {
402 position: absolute;
403 left: 6px;
404 right: 6px;
405 top: 6px;
406 z-index: 4;
407 display: flex;
408 flex-direction: column;
409 gap: 4px;
410 max-height: 45%;
411 overflow-y: auto;
412}
413.termp-gaps:empty { display: none; }
414.termp-gap {
415 display: flex;
416 align-items: flex-start;
417 gap: 6px;
418 padding: 6px 8px;
419 border: 1px solid var(--border-strong);
420 border-left: 3px solid var(--warn);
421 border-radius: var(--radius-sm);
422 background: var(--bg-secondary);
423 color: var(--text-primary);
424 font-size: var(--fs-xs);
425 line-height: 1.45;
426}
427
428/* Take one notice off the screen it is lying over. The fact it reports does not
429 go with it — the count stays in the state line — so this is a "move it out of
430 my way" and not a "forget it". A real button with a real name: the glyph on it
431 is a decoration and says nothing to a screen reader. */
432.termp-gap-x {
433 flex: none;
434 margin: -4px -4px 0 auto;
435 /* A hit area a finger can find, around a glyph that is four pixels wide. */
436 min-width: 26px;
437 min-height: 26px;
438 display: flex;
439 align-items: center;
440 justify-content: center;
441 border: 1px solid transparent;
442 border-radius: var(--radius-sm);
443 background: transparent;
444 color: var(--text-secondary);
445 font-size: var(--fs-xs);
446 line-height: 1.4;
447 cursor: pointer;
448}
449.termp-gap-x:hover { color: var(--text-primary); border-color: var(--border-strong); }
450.termp-gap-x:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
451
452/* The way out, in writing. Quiet, and always there: a hint that appeared only
453 while the terminal had focus would be unreadable at the moment it is wanted,
454 because the reader is looking at the cursor. */
455.termp-foot {
456 margin: 0;
457 color: var(--text-secondary);
458 font-size: var(--fs-2xs);
459 line-height: 1.4;
460}
461/* With no terminal on the panel there is nothing to move the keyboard out of,
462 and a hint about a control that is not there is one more thing to read past. */
463.termpanel.no-term .termp-foot { display: none; }
464
465/* The head's own bell mark, for a bell that rings while the eye is elsewhere.
466 The terminal flashes its own edge; this is the same event at panel scale, and
467 it is aria-hidden because a bell is not something to announce twice. */
468.termp-bell {
469 width: 8px;
470 height: 8px;
471 border-radius: 50%;
472 background: var(--accent);
473 opacity: 0;
474 transition: opacity .2s ease;
475}
476.termpanel.bell .termp-bell { opacity: 1; }
477
478/* The heading is a focus target: F6 out of the terminal lands here, and a
479 programmatic focus with no ring is a keyboard user who has lost their place. */
480.termpanel .chead [role="heading"]:focus-visible {
481 outline: 2px solid var(--accent);
482 outline-offset: 2px;
483 border-radius: var(--radius-sm);
484}
485
486@media (prefers-reduced-motion: reduce) {
487 .termp-bell { transition: none; }
488}
489
490/* ── Narrow screens ─────────────────────────────────────────── */
491
492@media (max-width: 560px) {
493 .term {
494 padding: 2px;
495 /* Below about twenty columns a terminal stops being a terminal, so the
496 floor is held and the panel scrolls rather than the type shrinking
497 until nothing can be read. The scroll is INSIDE the panel: the page
498 itself never scrolls sideways. */
499 overflow-x: auto;
500 }
501 .term-size { right: 6px; bottom: 6px; }
502}
503
504/* A reader who has asked for less motion gets a steady cursor; the blink is
505 turned off in terminal.js, and this stops the bell flashing too. */
506@media (prefers-reduced-motion: reduce) {
507 .term-size { transition: none; }
508 .term.bell { box-shadow: inset 0 0 0 2px var(--border-strong); }
509}
510
511/* ── What a right-click offers ──────────────────────────────── */
512
513/* A canvas's own context menu is the browser's, and it offers to save the
514 terminal as an image. This replaces it with the three things a person
515 right-clicks a terminal for. */
516
517.term-menu {
518 position: absolute;
519 z-index: 4;
520 min-width: 148px;
521 padding: 4px;
522 background: var(--bg-secondary);
523 border: 1px solid var(--border-strong);
524 border-radius: var(--radius);
525 box-shadow: 0 6px 18px rgba(0, 0, 0, .28);
526 display: flex;
527 flex-direction: column;
528}
529.term-menu[hidden] { display: none; }
530
531.term-menu-item {
532 padding: 6px 10px;
533 border: 0;
534 border-radius: var(--radius-sm);
535 background: transparent;
536 color: var(--text-primary);
537 font-size: var(--fs-sm);
538 text-align: left;
539 cursor: pointer;
540}
541.term-menu-item:hover:not(:disabled) { background: var(--bg-hover); }
542.term-menu-item:disabled { color: var(--text-secondary); cursor: default; }
543
544/* ── The folder rows in Settings ─────────────────────────────── */
545
546/* The app's own control box -- the one `.settings-select` wears -- rather than
547 `.admin-item`, which is a full-width borderless list row and read as a
548 misplaced menu entry beside a pulldown. */
549
550.termroot-btn {
551 background: var(--bg-tertiary);
552 border: 1px solid var(--border-strong);
553 border-radius: var(--radius-sm);
554 color: var(--text-primary);
555 padding: 9px 12px;
556 font-size: var(--fs-base);
557 cursor: pointer;
558 text-align: left;
559}
560.termroot-btn:hover { background: var(--bg-hover); }
561.termroot-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; border-color: var(--accent); }
562
563.termroot-acts { display: flex; flex-wrap: wrap; gap: 8px; }
564
565/* The folder a terminal will open in, stated rather than offered in a list: a
566 pulldown of somebody's particular directories is a guess, not a choice. */
567.termroot-now {
568 display: block;
569 margin: 0 0 8px;
570 font-family: var(--font-mono);
571 font-size: var(--fs-2xs);
572 color: var(--text-primary);
573 word-break: break-all;
574}
575.termroot-now[data-default="1"] { color: var(--text-secondary); }
576
577/* ── The folder walk ─────────────────────────────────────────── */
578
579/* Real folders with real paths, listed by the hand. The browser's own picker
580 cannot serve this: it answers with a handle carrying a name and no path, and
581 a fence needs the path. */
582
583.termroot-walk {
584 display: flex;
585 flex-direction: column;
586 gap: 2px;
587 max-height: 320px;
588 overflow-y: auto;
589 margin-top: 8px;
590 padding: 6px;
591 border: 1px solid var(--border-strong);
592 border-radius: var(--radius-sm);
593 background: var(--bg-tertiary);
594}
595.termroot-walk .admin-note {
596 position: sticky;
597 top: -6px;
598 padding: 4px 6px;
599 background: var(--bg-tertiary);
600 color: var(--text-secondary);
601 font-family: var(--font-mono);
602 font-size: var(--fs-2xs);
603 word-break: break-all;
604}
605/* The rows ARE a list, so they keep the list idiom; only the buttons beside a
606 field wear the field's box. */
607.termroot-walk .admin-item { font-family: var(--font-mono); font-size: var(--fs-xs); }
608.termroot-walk .termroot-btn { margin: 4px 0 6px; }