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/CHANGELOG.md +211 -0
- package/README.md +130 -27
- package/bin/prolog-notebook.mjs +206 -0
- package/package.json +19 -4
- package/src/browser.js +252 -16
- package/src/build-info.js +103 -0
- package/src/build-info.json +4 -0
- package/src/clauses.js +236 -0
- package/src/engine.js +227 -16
- package/src/export.js +126 -0
- package/src/format.js +649 -0
- package/src/node.js +14 -4
- package/src/notebook.css +319 -1
- package/src/notebook.js +1261 -60
- package/src/page.js +85 -0
- package/src/render.js +367 -0
- package/src/run.js +128 -0
- package/src/session.js +227 -0
- package/src/version.js +51 -0
- package/src/worker.js +77 -0
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
|
-
|
|
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. */
|