prolog-notebook 0.1.2 → 0.3.1

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/src/notebook.css CHANGED
@@ -51,6 +51,9 @@ code { font: .92em ui-monospace, "SF Mono", Menlo, monospace; background: var(--
51
51
  .cell .bar {
52
52
  display: flex;
53
53
  align-items: center;
54
+ /* Wraps rather than overflows: the tick is a sentence when something is wrong
55
+ ("program changed since this ran") and a narrow column must not clip it. */
56
+ flex-wrap: wrap;
54
57
  gap: .6rem;
55
58
  padding: .45rem .7rem;
56
59
  background: var(--code-bg);
@@ -96,7 +99,21 @@ button:disabled { opacity: .38; cursor: default; }
96
99
  button.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
97
100
  button.primary:hover { background: #98380f; }
98
101
 
99
- .status { font: 400 .74rem/1 system-ui, sans-serif; text-transform: none; letter-spacing: 0; }
102
+ /* A standing property of the cell, not a passing state so it sits apart from
103
+ the tick, which is about what just happened. Marks the one cell whose state a
104
+ re-consult does not undo (format §8). */
105
+ .badge {
106
+ font: 700 .62rem/1 system-ui, sans-serif;
107
+ letter-spacing: .1em;
108
+ text-transform: uppercase;
109
+ padding: .22rem .5rem;
110
+ border-radius: 999px;
111
+ border: 1px solid currentColor;
112
+ opacity: .75;
113
+ }
114
+ .badge.stateful { color: #a8730f; }
115
+
116
+ .status { font: 400 .74rem/1.3 system-ui, sans-serif; text-transform: none; letter-spacing: 0; text-align: right; }
100
117
  .status.ok { color: var(--ok); }
101
118
  .status.err { color: var(--err); }
102
119
  .status.busy { color: #8a8371; }
@@ -114,10 +131,295 @@ button.primary:hover { background: #98380f; }
114
131
  }
115
132
  .out:empty { display: none; }
116
133
  .line.echo { color: #8a8371; }
134
+ /* Attribution. An output is never shown without saying whose it is
135
+ (docs/modes.md §3), so this line is structural rather than decorative. */
136
+ .line.from {
137
+ color: #8a8371;
138
+ font: 600 .74rem/1.6 system-ui, sans-serif;
139
+ letter-spacing: .06em;
140
+ text-transform: uppercase;
141
+ }
142
+ /* Saved answers that no longer follow from the program above them. Marked, never
143
+ silently dropped and never silently trusted. */
144
+ .line.warn { color: #a8730f; }
145
+ /* Put away, never discarded: a chapter shows its answers (docs/modes.md §2), but
146
+ prose that says "press Run, then ; next to walk through the rest" is arguing
147
+ with a page that has already printed all six. The attribution line stays
148
+ visible while they are hidden, so the reader can always see that they exist. */
149
+ .out.answers-hidden .line:not(.from) { display: none; }
150
+ .line.from button.peek {
151
+ display: inline-flex;
152
+ align-items: center;
153
+ gap: .3rem;
154
+ vertical-align: -.15em;
155
+ margin-left: .6rem;
156
+ padding: .16rem .55rem .16rem .45rem;
157
+ border-radius: 999px;
158
+ font: 600 .68rem/1.4 system-ui, sans-serif;
159
+ letter-spacing: .06em;
160
+ text-transform: uppercase;
161
+ vertical-align: baseline;
162
+ }
117
163
  .line.sol { color: var(--ok); }
118
164
  .line.done { color: #8a8371; font-style: italic; }
165
+ /* "more solutions may follow." must never be mistaken for "no more solutions." at
166
+ a glance, so the sequence is marked as continuing rather than closed. This is
167
+ also where the cold-page `; next` affordance will attach (869ectt0y). */
168
+ .line.done.partial::before { content: '\22EE '; opacity: .7; }
119
169
  .line.err { color: var(--err); }
120
170
 
171
+ /* The page's own controls. Chrome, not content: built by notebook.js at runtime,
172
+ so a printed or GitHub-rendered chapter never shows a button that cannot work.
173
+
174
+ A LOZENGE, AND A CARD THAT RISES OUT OF IT. Closed, it is a dot and a word in
175
+ the corner of the viewport. Clicking it raises a card immediately above it —
176
+ right edges aligned, so the thing the reader pressed is visibly the thing that
177
+ opened — with one row per thing the page controls.
178
+
179
+ IT MOVES; IT DOES NOT MEASURE. The first version was one pill that WIDENED,
180
+ which meant JavaScript had to measure its contents on every state change, since
181
+ CSS cannot transition to `auto`. Any re-render landing mid-animation then
182
+ snapped it to its final width — and export's "has anything changed?" listener
183
+ did exactly that on the next frame after every click, including the click that
184
+ opened it. A transform needs no measuring, cannot be cancelled by a re-render,
185
+ and stays on the compositor. The bug class goes away rather than being patched.
186
+
187
+ Rows also mean the next unit costs vertical space, which nobody is short of.
188
+ The widening pill had reached the edge of the viewport with three.
189
+
190
+ Fixed to the VIEWPORT rather than sticky inside the column, because the column
191
+ is where the prose is, and a control floating over the sentence being read is
192
+ worse than one that is hard to find. */
193
+ .page-controls {
194
+ position: fixed;
195
+ right: 1rem;
196
+ bottom: 1rem;
197
+ z-index: 5;
198
+ display: flex;
199
+ justify-content: flex-end;
200
+ }
201
+
202
+ .page-controls .panel {
203
+ position: absolute;
204
+ right: 0;
205
+ bottom: 100%;
206
+ margin-bottom: .5rem;
207
+ /* TWO COLUMNS, AND A WIDTH THAT DOES NOT MOVE. Every button sits in the second
208
+ column, so they are all as wide as the widest of them — and because the card
209
+ itself is a fixed width, nothing shifts when a label changes under the
210
+ reader's cursor. Sized by content, it twitched on every click: "Hide saved
211
+ answers" and "Show saved answers" are not the same length, and neither are
212
+ "Start engine" and "Restart engine". */
213
+ display: grid;
214
+ grid-template-columns: 1fr auto;
215
+ align-items: center;
216
+ width: 25rem;
217
+ max-width: calc(100vw - 2rem);
218
+ padding: .1rem .9rem;
219
+ border: 1px solid var(--rule);
220
+ border-radius: 14px;
221
+ background: var(--paper);
222
+ box-shadow: 0 6px 24px rgba(0, 0, 0, .14);
223
+ /* HIDDEN, not merely transparent. An opacity of 0 leaves a button clickable,
224
+ and this card lies over the page: invisible things must not be touchable, and
225
+ visibility says so in a way opacity does not. */
226
+ visibility: hidden;
227
+ opacity: 0;
228
+ transform: translateY(.7rem);
229
+ /* The fade is quicker than the rise, so it is already legible while it is still
230
+ arriving rather than sliding into place as a blank rectangle. */
231
+ transition: opacity .18s ease, transform .28s cubic-bezier(.32, .72, 0, 1), visibility .28s;
232
+ }
233
+ .page-controls[data-open="true"] .panel {
234
+ visibility: visible;
235
+ opacity: 1;
236
+ transform: none;
237
+ }
238
+
239
+ /* One row per thing the page controls: what is true, then the button that changes
240
+ it. Ruled rather than merely spaced, because two state phrases in a column
241
+ otherwise read as one paragraph. */
242
+ /* The unit is a grouping, not a box: its two children join the card's own grid so
243
+ that every state phrase and every button line up down the card. */
244
+ .page-controls .unit { display: contents; }
245
+ /* HIDDEN MEANS HIDDEN. The `hidden` attribute is only a UA rule — `[hidden] {
246
+ display: none }` — so ANY display declaration of ours outranks it and the
247
+ element stays on screen. That is how the version picker came to sit next to
248
+ the phrase it replaces, both visible at once, and it is the same principle
249
+ already written into the panel's own fade: invisible things must not be
250
+ touchable, and something told to hide must actually go. */
251
+ .page-controls [hidden] { display: none !important; }
252
+
253
+ /* The one row whose state is a CHOICE. A native select, because it works on a
254
+ touch screen and from a keyboard without anything being written here — but
255
+ stripped of its chrome, so a card of quiet phrases does not suddenly grow a
256
+ form control. The chevron is the same glyph the lozenge uses. */
257
+ .page-controls .picker {
258
+ display: inline-flex;
259
+ align-items: center;
260
+ gap: .1rem;
261
+ }
262
+ .page-controls .picker select {
263
+ appearance: none;
264
+ padding: 0;
265
+ border: 0;
266
+ background: none;
267
+ font: inherit;
268
+ color: inherit;
269
+ cursor: pointer;
270
+ }
271
+ .page-controls .picker svg {
272
+ width: .7rem;
273
+ height: .7rem;
274
+ /* The glyph is a "<": 270° points it down, which is the direction this one
275
+ opens. The lozenge's own chevron uses 90° because its panel rises. */
276
+ transform: rotate(270deg);
277
+ opacity: .7;
278
+ }
279
+ .page-controls .picker:hover { color: var(--ink); }
280
+ .page-controls .unit > .state { padding: .6rem 1.4rem .6rem 0; }
281
+ .page-controls .unit + .unit > * { border-top: 1px solid var(--rule); }
282
+
283
+ /* WHAT THIS IS, not what it is doing. It spans both columns because it has no
284
+ button to line up with: every row above is a state and its verb, and this is
285
+ neither. Quieter than the states above it, and not selectable-looking — it is
286
+ a colophon, and a reader should be able to ignore it at a glance. */
287
+ .page-controls .about {
288
+ grid-column: 1 / -1;
289
+ margin: 0;
290
+ padding: .55rem 0 .6rem;
291
+ border-top: 1px solid var(--rule);
292
+ font: 400 .66rem/1.5 system-ui, sans-serif;
293
+ color: #a49c88;
294
+ /* The banner is one long sentence in a narrow card. Balanced rather than left
295
+ to strand "License." on a line of its own; ignored where unsupported. */
296
+ text-wrap: pretty;
297
+ }
298
+ /* Two lines: what is running, then who owns it. Each is short enough not to wrap
299
+ at this width, which is why the split is by MEANING rather than by shrinking
300
+ the type until the sentence fits. */
301
+ .page-controls .about > span { display: block; }
302
+ /* Inline, because it belongs to the sentence above it rather than to a line of
303
+ its own — and absent until the engine has been asked, which cannot happen
304
+ until it is running. */
305
+ .page-controls .about .engine-version { display: inline; }
306
+
307
+ /* Free to wrap, now that there is a column to wrap in. The widening pill had to
308
+ ellipsise this sentence to keep the buttons on screen; a row has the width it
309
+ needs and the card grows downward instead. */
310
+ .page-controls .state {
311
+ min-width: 0;
312
+ font: 400 .74rem/1.4 system-ui, sans-serif;
313
+ color: #8a8371;
314
+ }
315
+
316
+ .page-controls .handle {
317
+ display: flex;
318
+ align-items: center;
319
+ gap: .45rem;
320
+ flex: 0 0 auto;
321
+ padding: .42rem .75rem;
322
+ border: 1px solid var(--rule);
323
+ border-radius: 999px;
324
+ background: var(--paper);
325
+ box-shadow: 0 2px 12px rgba(0, 0, 0, .09);
326
+ font: 600 .7rem/1 system-ui, sans-serif;
327
+ letter-spacing: .04em;
328
+ color: #8a8371;
329
+ white-space: nowrap;
330
+ transition: box-shadow .3s ease;
331
+ }
332
+ /* Lifted a little while open, so the pair reads as being in front of the page
333
+ rather than printed on it. */
334
+ .page-controls[data-open="true"] .handle { box-shadow: 0 5px 22px rgba(0, 0, 0, .14); }
335
+ .page-controls .handle:hover:not(:disabled) { background: var(--code-bg); }
336
+ /* A LIGHT, NOT AN ICON — and the distinction is the whole vocabulary. Every glyph
337
+ in a button here depicts that button's verb; this one thing depicts a state, so
338
+ it is deliberately not drawn as a symbol. Grey for no engine, amber while one is
339
+ arriving, green once it is running: readable without being read. */
340
+ .page-controls .handle .dot {
341
+ width: .48rem;
342
+ height: .48rem;
343
+ flex: 0 0 auto;
344
+ border-radius: 50%;
345
+ background: #c8c1ae;
346
+ transition: background .3s ease;
347
+ }
348
+ .page-controls.live .handle .dot { background: var(--ok); }
349
+ .page-controls.busy .handle .dot {
350
+ background: #d09a2c;
351
+ animation: engine-pulse 1.1s ease-in-out infinite;
352
+ }
353
+ @keyframes engine-pulse {
354
+ 50% { opacity: .3; }
355
+ }
356
+
357
+ /* The lozenge's own verb: it opens the panel, and it points the way the panel
358
+ goes — up to raise it, down to put it away. Rotated, not swapped for a
359
+ different glyph, so the turn itself reads as the state changing. The drawn
360
+ chevron points left, so up is a quarter turn from where it starts. */
361
+ .page-controls .chev {
362
+ display: inline-flex;
363
+ flex: 0 0 auto;
364
+ margin-left: .1rem;
365
+ opacity: .55;
366
+ transform: rotate(90deg);
367
+ transition: transform .3s cubic-bezier(.32, .72, 0, 1);
368
+ }
369
+ .page-controls[data-open="true"] .chev { transform: rotate(270deg); }
370
+
371
+ .page-controls .panel button {
372
+ display: flex;
373
+ align-items: center;
374
+ justify-content: center;
375
+ gap: .38rem;
376
+ /* Fills its column, so all three are one width. */
377
+ width: 100%;
378
+ margin: .6rem 0;
379
+ padding: .4rem .8rem;
380
+ border-radius: 999px;
381
+ border-color: transparent;
382
+ background: var(--code-bg);
383
+ }
384
+ /* THREE PEERS, ONE WEIGHT. Download was filled with the accent, on the argument
385
+ that the reader's copy is the point of export. That held while the panel was a
386
+ row of units; as three rows it reads as though the other two were lesser
387
+ versions of it — and hiding the answers or restarting the engine is what a
388
+ reader reaches for far more often.
389
+
390
+ The accent belongs to Run and Consult, which are the actions the CHAPTER is
391
+ about. A control in the corner shouting at the same volume is a tool insisting
392
+ on itself, which is the thing this panel was shaped to avoid. */
393
+ /* Line icons, inherited colour, sized in ems so they follow the label rather than
394
+ the page. Decoration for the eye and a second reading for everyone else — the
395
+ words are always there too. */
396
+ .page-controls .icon {
397
+ display: inline-flex;
398
+ flex: 0 0 auto;
399
+ opacity: .75;
400
+ }
401
+ .page-controls svg,
402
+ .line.from .icon svg {
403
+ width: 1.05em;
404
+ height: 1.05em;
405
+ }
406
+ .line.from .icon { display: inline-flex; opacity: .8; }
407
+ .page-controls .panel button:hover:not(:disabled) { background: #ebe5d5; }
408
+
409
+ /* A phone has no corner to spare: the card takes the width of the screen rather
410
+ than a column of it, and the lozenge stays where it was. */
411
+ @media (max-width: 30rem) {
412
+ .page-controls { left: 1rem; }
413
+ .page-controls .panel { left: 0; width: auto; max-width: none; }
414
+ }
415
+
416
+ @media (prefers-reduced-motion: reduce) {
417
+ .page-controls .panel, .page-controls .handle, .page-controls .chev { transition: none; }
418
+ .page-controls .panel { transform: none; }
419
+ /* The amber still says "busy"; only the blinking goes. */
420
+ .page-controls.busy .handle .dot { animation: none; }
421
+ }
422
+
121
423
  /* --- teaching devices --- */
122
424
 
123
425
  .predict {
@@ -156,6 +458,12 @@ button.primary:hover { background: #98380f; }
156
458
  }
157
459
  .aside strong { display: block; margin-bottom: .3rem; }
158
460
 
461
+ /* Generated containers wrap their prose in <p>, which the hand-written markup did
462
+ not. Without this the last paragraph adds a trailing 1rem inside every box. */
463
+ .aside p:last-child,
464
+ .predict > p:last-child,
465
+ .bullets p:last-child { margin-bottom: 0; }
466
+
159
467
  .note {
160
468
  font-family: var(--hand);
161
469
  font-size: 1.12rem;
@@ -176,11 +484,21 @@ button.primary:hover { background: #98380f; }
176
484
  }
177
485
  .cell, textarea, input { background: #1c1b22; }
178
486
  .out { background: #1a191f; }
487
+ .line.warn, .status.warn, .badge.stateful { color: #d9a441; }
488
+ /* A prediction box is where the reader writes, so it must not be the one white
489
+ rectangle on a dark page. */
490
+ .predict textarea { background: #1c1b22; }
179
491
  .predict { background: #201e19; border-color: #5a4f36; }
180
492
  .aside { background: #201d1c; }
181
493
  button { background: #24232b; border-color: #454150; color: var(--ink); }
182
494
  button:hover:not(:disabled) { background: #2d2b35; }
183
495
  button.primary { background: var(--accent); color: #16151a; }
496
+ /* A shadow is invisible on a dark page, so the pill leans on its edge instead. */
497
+ .page-controls .panel,
498
+ .page-controls .handle { background: #1c1b22; border-color: #4b4656; box-shadow: none; }
499
+ .page-controls .handle:hover:not(:disabled) { background: #2d2b35; }
500
+ .page-controls .panel button { background: #2a2833; }
501
+ .page-controls .panel button:hover:not(:disabled) { background: #35323f; }
184
502
  }
185
503
 
186
504
  /* Shown until mount() runs; see notebook.js. Its presence means the page is inert. */