reladraw 0.28.0 → 0.29.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/dist/page.js CHANGED
@@ -26,7 +26,8 @@ export function playgroundPage(playground) {
26
26
  return page(parts, editing, {
27
27
  title: folder !== undefined ? `reladraw · ${folder}` : 'reladraw playground: write diagrams in the browser',
28
28
  heading: 'reladraw',
29
- drawn: folder !== undefined ? `local playground · ${folder} · not saved to a file` : 'a diagram language where you say where things go',
29
+ // The hosted playground has no line under its name: the page shows what it is.
30
+ drawn: folder !== undefined ? `local playground · ${folder} · not saved to a file` : '',
30
31
  head: playground.head ?? '',
31
32
  library: playground.library ?? '',
32
33
  });
@@ -92,7 +93,8 @@ const PAGE = `<!doctype html>
92
93
  padding: 10px 16px;
93
94
  border-bottom: 1px solid color-mix(in srgb, var(--rule) 45%, transparent);
94
95
  }
95
- .heading { flex: 1 1 16rem; min-width: 0; }
96
+ .heading { flex: 0 1 auto; min-width: 0; }
97
+ header .tools { margin-left: auto; }
96
98
  h1 { margin: 0; font-size: 16px; font-weight: 600; overflow-wrap: anywhere; }
97
99
  .drawn { margin: 0; font-size: 12px; color: var(--muted); overflow-wrap: anywhere; }
98
100
  .menu-list hr { width: 100%; margin: 2px 0; border: 0; border-top: 1px solid color-mix(in srgb, var(--rule) 45%, transparent); }
@@ -271,6 +273,10 @@ const PAGE = `<!doctype html>
271
273
  @media (prefers-reduced-motion: reduce) { #view g[data-edge] :is(line, path, polyline) { animation: none !important; } }
272
274
  aside h2 { margin: 0; padding: 10px 16px 6px; font-size: 13px; font-weight: 600; }
273
275
  aside h2 .what { display: block; font-weight: 400; font-size: 12px; color: var(--muted); }
276
+ /* Close: a small square button, bordered like the buttons beside it. */
277
+ button.close { width: 24px; height: 24px; padding: 0; font-size: 17px; line-height: 1; color: var(--ink);
278
+ display: inline-flex; align-items: center; justify-content: center; }
279
+ aside h2 .close { float: right; margin: -2px -8px 0 8px; }
274
280
  aside ol { list-style: none; margin: 0; padding: 0 8px 12px; overflow: auto; }
275
281
  aside li button {
276
282
  display: block; width: 100%; text-align: left; white-space: normal; border-color: transparent;
@@ -278,6 +284,22 @@ const PAGE = `<!doctype html>
278
284
  }
279
285
  aside li button.chosen { border-color: var(--accent); }
280
286
  aside li.none { padding: 6px 8px; color: var(--muted); font-size: 13px; }
287
+ /* Findings from an older version of the source: dimmed, not cleared. */
288
+ aside li.again, #band .again { padding: 6px 8px; color: var(--muted); font-size: 12px; }
289
+ aside.stale li:not(.again):not(.error), #band.stale .found { opacity: 0.5; }
290
+ /* While the source is open, the findings sit in a band under the picture,
291
+ taking height from it, never width. */
292
+ #picture { flex: 1; display: flex; flex-direction: column; min-width: 0; min-height: 0; }
293
+ #band { flex: none; max-height: 35%; overflow: auto; padding: 4px 12px 8px; font-size: 13px;
294
+ border-top: 1px solid color-mix(in srgb, var(--rule) 45%, transparent); }
295
+ #band[hidden] { display: none; }
296
+ #band .head { display: flex; align-items: center; gap: 8px; padding: 2px 0; }
297
+ #band .head b { font-size: 12px; font-weight: 600; color: var(--muted); }
298
+ #band .head .gap { flex: 1; }
299
+ #band .head button:not(.close) { font-size: 12px; padding: 1px 8px; }
300
+ #band .found button { display: block; width: 100%; text-align: left; white-space: normal; border-color: transparent; padding: 3px 6px; }
301
+ #band .found button.chosen { border-color: var(--accent); }
302
+ #band .again { padding: 2px 0; }
281
303
  aside li.error { padding: 6px 8px; margin: 2px 0; border-left: 3px solid var(--down); font-size: 13px; }
282
304
  aside li.error .kind { color: var(--down); }
283
305
  #live {
@@ -295,12 +317,34 @@ const PAGE = `<!doctype html>
295
317
  background: color-mix(in srgb, var(--ink) 4%, var(--page));
296
318
  }
297
319
  #editor[hidden] { display: none; }
298
- .src-head { display: flex; align-items: center; gap: 6px; padding: 6px 10px; font-size: 12.5px;
320
+ /* The picture's controls float over it: Check at its top left, the zoom
321
+ on a card at its bottom right. They sit beside the view, not in it, so a
322
+ press on them never starts a pan. */
323
+ #canvas { position: relative; flex: 1; display: flex; min-height: 0; }
324
+ .floating { position: absolute; z-index: 4;
325
+ background: color-mix(in srgb, var(--ink) 7%, var(--page));
326
+ box-shadow: 0 4px 14px rgba(0, 0, 0, 0.45); }
327
+ .floating[hidden] { display: none; }
328
+ #check { left: 12px; top: 12px; padding: 5px 14px; }
329
+ #check:hover { background: color-mix(in srgb, var(--ink) 14%, var(--page)); }
330
+ #zoom-card { right: 12px; bottom: 12px; display: flex; align-items: center; gap: 5px; padding: 4px 5px; border-radius: 9px;
331
+ border: 1px solid color-mix(in srgb, var(--rule) 55%, transparent); }
332
+ /* The card a shade darker than its buttons, so each reads as a button. */
333
+ #zoom-card { background: color-mix(in srgb, var(--ink) 4%, var(--page)); }
334
+ #zoom-card button { background: color-mix(in srgb, var(--ink) 13%, var(--page)); }
335
+ #zoom-card button:hover { background: color-mix(in srgb, var(--ink) 20%, var(--page)); }
336
+ #zoom-card button.pm { font-size: 17px; line-height: 1; padding: 2px 9px; min-width: 30px; }
337
+ #zoom-card .divider { width: 1px; height: 18px; margin: 0 3px; background: color-mix(in srgb, var(--rule) 70%, transparent); }
338
+ body:has(.tools.keyed) #check kbd.tag { display: inline; }
339
+ .src-head { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; padding: 6px 10px; font-size: 12.5px;
299
340
  border-bottom: 1px solid color-mix(in srgb, var(--rule) 45%, transparent); }
300
341
  .src-head .file { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
301
342
  #unsaved { color: var(--sel); }
302
343
  #unsaved[hidden] { display: none; }
303
344
  .src-head .gap, #changed .gap { flex: 1; }
345
+ #file-menu[hidden] { display: none; }
346
+ #file-list { left: 0; right: auto; min-width: 12rem; }
347
+ #file-button.open { border-color: var(--sel); background: color-mix(in srgb, var(--sel) 22%, transparent); }
304
348
  .src-head kbd { font: 11px system-ui, sans-serif; color: var(--muted); margin-left: 4px; }
305
349
  #changed { display: flex; align-items: center; gap: 6px; padding: 6px 10px; font-size: 12.5px;
306
350
  background: color-mix(in srgb, var(--sel) 18%, var(--page));
@@ -326,7 +370,17 @@ const PAGE = `<!doctype html>
326
370
  .t-relation, .t-arrow { color: var(--syn-relation); }
327
371
  .t-attribute { color: var(--syn-attribute); }
328
372
  .t-value, .t-color { color: var(--syn-value); }
329
- #edit[aria-pressed="true"] { border-color: var(--sel); color: var(--sel); }
373
+ /* Edit | View: which mode the page is in, the one it is in filled. Edit
374
+ is on the left, the side the source opens on. */
375
+ .mode { display: inline-flex; position: relative; }
376
+ .mode[hidden] { display: none; }
377
+ .mode button { border-radius: 0; color: var(--muted); padding: 4px 12px; }
378
+ .mode button:first-child { border-radius: 6px 0 0 6px; }
379
+ .mode button:last-child { border-radius: 0 6px 6px 0; border-left: 0; }
380
+ .mode button[aria-pressed="true"] { background: color-mix(in srgb, var(--ink) 20%, transparent); color: var(--ink); font-weight: 600; }
381
+ .mode button[aria-pressed="true"]:hover { cursor: default; }
382
+ header:has(.tools.keyed) .mode kbd.tag { display: inline; }
383
+ header:has(.tools.keyed) .mode { outline: 2px solid var(--sel); outline-offset: 4px; border-radius: 6px; }
330
384
  .menu-list.wide { min-width: 340px; }
331
385
  .menu-list .row .when { color: var(--muted); font-variant-numeric: tabular-nums; width: 3.2em; flex: none; }
332
386
  .menu-list .row .who { width: 4.5em; flex: none; }
@@ -342,27 +396,57 @@ const PAGE = `<!doctype html>
342
396
  aside li.error .goto { cursor: pointer; text-decoration: underline dotted; }
343
397
 
344
398
  /* ---- a page with no file: the playground ---- */
345
- .links { display: inline-flex; gap: 12px; margin-left: 10px; font-size: 12px; }
346
- .heading .drawn { display: inline; }
347
- .links a { color: var(--muted); text-decoration: none; border-bottom: 1px solid color-mix(in srgb, var(--rule) 60%, transparent); }
399
+ /* GitHub, as its mark beside the name. */
400
+ .heading h1 { display: inline; vertical-align: middle; }
401
+ .links { display: inline-flex; vertical-align: middle; margin-left: 10px; }
402
+ .links a { display: inline-flex; color: var(--muted); padding: 2px; border-radius: 4px; }
348
403
  .links a:hover { color: var(--ink); }
404
+ .links a:focus-visible { outline: 2px solid var(--accent); }
405
+ /* A link as a row of a menu, like the buttons around it. */
406
+ .menu-list a.row { color: var(--ink); text-decoration: none; font-size: 13px; border: 1px solid transparent; border-radius: 6px; }
407
+ .menu-list a.row:hover { background: color-mix(in srgb, var(--ink) 10%, transparent); }
408
+ #help-menu[hidden] { display: none; }
409
+ .drawn:empty { display: none; }
349
410
  /* The divider between the source and the picture: dragged, or moved with the arrows. */
350
411
  #split { flex: none; width: 6px; margin: 0 -3px; z-index: 2; cursor: col-resize; touch-action: none; }
351
412
  #split[hidden] { display: none; }
352
413
  #split:hover, #split.dragging, #split:focus-visible { background: color-mix(in srgb, var(--accent) 45%, transparent); outline: 0; }
353
414
  body.dragging { cursor: col-resize; user-select: none; }
354
- /* The cheatsheet. Closed it is one line; open it takes height off the
355
- source rather than off the diagram, since it answers what to type. */
356
- #cheatsheet { flex: none; max-height: 46%; overflow: auto; border-top: 1px solid color-mix(in srgb, var(--rule) 45%, transparent); }
357
- #cheatsheet summary { padding: 7px 12px; cursor: pointer; color: var(--muted); font-size: 11px; letter-spacing: .12em; text-transform: uppercase; user-select: none; }
358
- #cheatsheet summary:hover { color: var(--ink); }
359
- #cheatsheet dl { margin: 0; padding: 0 12px 14px; display: grid; grid-template-columns: max-content 1fr; gap: 4px 12px; font-size: 12px; }
360
- #cheatsheet dt { color: var(--muted); white-space: nowrap; }
361
- #cheatsheet dd { margin: 0; line-height: 1.7; }
362
- #cheatsheet code { font: 11.5px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; white-space: nowrap; display: inline-block;
363
- padding: 1px 5px; margin-right: 3px; border-radius: 3px; background: color-mix(in srgb, var(--ink) 9%, transparent); }
364
- #cheatsheet b { color: var(--syn-relation); font-weight: 400; }
365
- #cheatsheet i { color: var(--muted); font-style: normal; }
415
+ /* The cheatsheet: a button in the source's bar opens a large window, a
416
+ card per topic, each row an example in the editor's colors beside what
417
+ it does. The filter narrows the rows as you type. */
418
+ #cheatsheet { width: min(1180px, calc(100vw - 32px)); height: min(820px, calc(100vh - 32px)); max-width: none; max-height: none; }
419
+ #cheatsheet[open] { display: flex; flex-direction: column; }
420
+ #cheatsheet .cs-head { display: flex; align-items: center; gap: 12px; padding: 10px 16px;
421
+ border-bottom: 1px solid color-mix(in srgb, var(--rule) 45%, transparent); }
422
+ #cheatsheet .cs-head h2 { font-size: 14px; }
423
+ #cheat-filter { flex: 0 1 340px; font: inherit; font-size: 13px; color: var(--ink); padding: 4px 10px; border-radius: 6px;
424
+ background: color-mix(in srgb, var(--ink) 4%, var(--page)); border: 1px solid color-mix(in srgb, var(--rule) 70%, transparent); }
425
+ #cheat-filter:focus { outline: 2px solid var(--accent); outline-offset: 1px; }
426
+ #cheatsheet .cs-head .gap { flex: 1; }
427
+ #cheatsheet .cs-grid { flex: 1; min-height: 0; overflow: auto; padding: 14px 16px 18px; }
428
+ /* Cards fill three columns top to bottom; the page fills each column as the
429
+ window opens, so the columns come out about the same height. */
430
+ .cs-cols { display: grid; grid-template-columns: repeat(var(--cols, 3), minmax(0, 1fr)); gap: 14px; align-items: start; }
431
+ .cs-col { display: flex; flex-direction: column; gap: 14px; min-width: 0; }
432
+ .cs-card { border-radius: 8px; overflow: hidden;
433
+ border: 1px solid color-mix(in srgb, var(--hue) 30%, transparent);
434
+ background: color-mix(in srgb, var(--ink) 3%, var(--page)); }
435
+ .cs-card[hidden] { display: none; }
436
+ .cs-card h3 { margin: 0; padding: 7px 12px; font-size: 12px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
437
+ color: var(--hue); background: color-mix(in srgb, var(--hue) 12%, transparent);
438
+ border-bottom: 1px solid color-mix(in srgb, var(--hue) 22%, transparent); }
439
+ .cs-row { display: grid; grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr); gap: 4px 12px; padding: 6px 12px; font-size: 12.5px;
440
+ border-top: 1px solid color-mix(in srgb, var(--rule) 25%, transparent); }
441
+ .cs-card h3 + .cs-row { border-top: 0; }
442
+ .cs-row[hidden] { display: none; }
443
+ .cs-row code { font: 12px/1.5 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; overflow-wrap: anywhere; color: var(--ink); }
444
+ .cs-row > span { color: var(--muted); }
445
+ .cs-note { margin: 0; padding: 7px 12px 9px; font-size: 12px; color: var(--muted);
446
+ border-top: 1px solid color-mix(in srgb, var(--rule) 25%, transparent); }
447
+ .cs-note code { font: 11.5px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; color: var(--ink); }
448
+ .cs-none { margin: 0; padding: 24px 16px; color: var(--muted); }
449
+ .cs-none[hidden] { display: none; }
366
450
  dialog { padding: 0; color: var(--ink); background: var(--page); overflow: hidden;
367
451
  border: 1px solid color-mix(in srgb, var(--rule) 70%, transparent); border-radius: 6px; }
368
452
  dialog::backdrop { background: rgba(0, 0, 0, .6); }
@@ -416,20 +500,14 @@ const PAGE = `<!doctype html>
416
500
  <body>
417
501
  <header>
418
502
  <div class="heading">
419
- <h1>{{heading}}</h1>
420
- <p class="drawn">{{drawn}}</p>{{links}}
503
+ <h1>{{heading}}</h1>{{links}}
504
+ <p class="drawn">{{drawn}}</p>
421
505
  </div>
506
+ <span id="mode" class="mode" role="group" aria-label="Mode" hidden>
507
+ <button id="edit" type="button" title="The source beside the picture (e)" aria-pressed="false">Edit<kbd class="tag">e</kbd></button>
508
+ <button id="mode-view" type="button" title="Only the picture (e)" aria-pressed="true">View</button>
509
+ </span>
422
510
  <div class="tools">
423
- <button id="browse" type="button" title="Open one of the examples in the editor" aria-haspopup="dialog" hidden>Examples</button>
424
- <button id="edit"type="button" title="Show or hide the source (e)" aria-pressed="false" hidden>Edit<kbd class="tag">e</kbd></button>
425
- <span class="gap" id="edit-gap" hidden></span>
426
- <button id="out" type="button" title="Zoom out (-)" aria-label="Zoom out">−</button>
427
- <span id="scale" aria-live="polite">100%</span>
428
- <button id="in" type="button" title="Zoom in (+)" aria-label="Zoom in">+</button>
429
- <button id="fit" type="button" title="The whole diagram in view (0)">Fit</button>
430
- <button id="actual" type="button" title="The drawing at its own scale (1)">100%</button>
431
- <span class="gap"></span>
432
- <button id="full" type="button" title="Full screen (f)"><span class="label">Full screen</span><kbd class="tag">f</kbd></button>
433
511
  <span class="menu" id="versions-menu" hidden>
434
512
  <button id="versions-button" type="button" aria-haspopup="true" aria-expanded="false" aria-controls="versions-list">Versions ▾<kbd class="tag">o</kbd></button>
435
513
  <span id="versions-list" class="menu-list wide" role="menu" hidden></span>
@@ -446,7 +524,7 @@ const PAGE = `<!doctype html>
446
524
  </span>
447
525
  </span>
448
526
  <span class="menu">
449
- <button id="view-button" type="button" aria-haspopup="true" aria-expanded="false" aria-controls="view-list">View ▾<kbd class="tag">v</kbd></button>
527
+ <button id="view-button" type="button" aria-haspopup="true" aria-expanded="false" aria-controls="view-list">Settings ▾<kbd class="tag">s</kbd></button>
450
528
  <span id="view-list" class="menu-list" role="menu" hidden>
451
529
  <button id="theme-row" class="row" type="button" role="menuitem" data-key="t" aria-haspopup="true" title="Draw the diagram in another theme"><span class="name">Theme <span class="value"></span> ▸</span><kbd>t</kbd></button>
452
530
  <hr>
@@ -454,11 +532,13 @@ const PAGE = `<!doctype html>
454
532
  <button id="auto-row" class="row switch" type="button" role="menuitemcheckbox" data-key="a" title="Show the side panel only while something is selected or the findings are open"><span class="sw"></span><span class="name">Auto-hide panel</span><span class="word"></span><kbd>a</kbd></button>
455
533
  <hr>
456
534
  <button id="findings-row" class="row switch" type="button" role="menuitemcheckbox" data-key="f" title="Show or hide the findings in the side panel"><span class="sw"></span><span class="name">Findings</span><span class="word"></span><kbd>f</kbd></button>
535
+ <button id="autocheck-row" class="row switch" type="button" role="menuitemcheckbox" data-key="c" title="Look for findings every time the picture is drawn, without pressing Check" hidden><span class="sw"></span><span class="name">Check as I type</span><span class="word"></span><kbd>c</kbd></button>
457
536
  <hr>
458
537
  <button id="guides-row" class="row switch" type="button" role="menuitemcheckbox" data-key="g" title="Draw the guides: everything written with guide in front"><span class="sw"></span><span class="name">Guides</span><span class="word"></span><kbd>g</kbd></button>
459
538
  <span id="theme-list" class="menu-list sub" role="menu" hidden></span>
460
539
  </span>
461
540
  </span>
541
+ <button id="full" type="button" title="Full screen (f)"><span class="label">Full screen</span><kbd class="tag">f</kbd></button>
462
542
  </div>
463
543
  </header>
464
544
  <div id="strip" hidden><span class="crumbs"></span><span class="hint"></span></div>
@@ -468,11 +548,26 @@ const PAGE = `<!doctype html>
468
548
  <div id="changed" role="alert" hidden><span class="what"></span><span class="gap"></span><button id="load-disk" type="button">Load it</button><button id="keep-mine" type="button">Keep mine</button></div>
469
549
  <div class="src-head">
470
550
  <span class="file"></span><span id="unsaved" title="Not saved to the file" hidden>●</span><span class="gap"></span>
471
- <button id="open-file" type="button" title="Open a .reladraw file into the source" hidden>Open…</button>
551
+ <span class="menu" id="file-menu" hidden>
552
+ <button id="file-button" type="button" aria-haspopup="true" aria-expanded="false" aria-controls="file-list"><span class="name">File ▾</span></button>
553
+ <span id="file-list" class="menu-list" role="menu" hidden>
554
+ <button id="browse" class="row" type="button" role="menuitem" title="Open one of the examples in the editor" aria-haspopup="dialog"><span class="name">Examples…</span></button>
555
+ <button id="open-file" class="row" type="button" role="menuitem" title="Open a .reladraw file into the source"><span class="name">Open…</span></button>
556
+ <hr>
557
+ <button id="write-row" class="row" type="button" role="menuitem" title="Save the source as a .reladraw file"><span class="name">Save…</span><kbd>⌘S</kbd></button>
558
+ </span>
559
+ </span>
472
560
  <input id="file-input" type="file" accept=".reladraw,text/plain" hidden>
473
561
  <button id="format" type="button" title="Lay the source out again, as reladraw format does, with the nearest .reladraw-format (Shift+Alt+F)"><span class="name">Format</span></button>
474
562
  <button id="format-open" type="button" aria-haspopup="dialog" title="How Format lays the source out" hidden>Format settings…</button>
475
563
  <button id="write" type="button" title="Write the source to the file (⌘S or Ctrl+S)"><span class="name">Save</span><kbd>⌘S</kbd></button>
564
+ <span class="menu" id="help-menu" hidden>
565
+ <button id="help-button" type="button" aria-haspopup="true" aria-expanded="false" aria-controls="help-list"><span class="name">Help ▾</span></button>
566
+ <span id="help-list" class="menu-list" role="menu" hidden>
567
+ <button id="cheat-open" class="row" type="button" role="menuitem" title="The language on one screen"><span class="name">Cheatsheet</span><kbd>?</kbd></button>
568
+ <a id="syntax-link" class="row" role="menuitem" href="https://github.com/reladraw/reladraw/blob/main/SYNTAX.md" target="_blank" rel="noopener" title="The full syntax reference, on GitHub"><span class="name">Syntax reference ↗</span></a>
569
+ </span>
570
+ </span>
476
571
  </div>
477
572
  <div class="code">
478
573
  <pre id="paint" aria-hidden="true"></pre>
@@ -480,18 +575,32 @@ const PAGE = `<!doctype html>
480
575
  </div>{{cheatsheet}}
481
576
  </section>
482
577
  <div id="split" role="separator" aria-orientation="vertical" aria-label="Resize the source and the diagram" tabindex="0" title="Drag to resize; double-click to reset" hidden></div>
578
+ <div id="picture">
579
+ <div id="canvas">
580
+ <button id="check" class="floating" type="button" title="Look for overlaps and other places the picture may not read as meant (c)" hidden>Check<kbd class="tag">c</kbd></button>
581
+ <div id="zoom-card" class="floating">
582
+ <button id="out" class="pm" type="button" title="Zoom out (-)" aria-label="Zoom out">−</button>
583
+ <span id="scale" aria-live="polite">100%</span>
584
+ <button id="in" class="pm" type="button" title="Zoom in (+)" aria-label="Zoom in">+</button>
585
+ <span class="divider"></span>
586
+ <button id="fit" type="button" title="The whole diagram in view (0)">Fit</button>
587
+ <button id="actual" type="button" title="The drawing at its own scale (1)">100%</button>
588
+ </div>
483
589
  <div id="view">
484
590
  <div id="stage">
485
591
  {{drawing}}
486
592
  </div>
487
593
  <div id="tip"></div>
488
594
  </div>
595
+ </div>
596
+ <div id="band" aria-label="Findings" aria-live="polite" hidden></div>
597
+ </div>
489
598
  <div id="side">
490
599
  <section id="selected" aria-label="Selected" aria-live="polite">
491
600
  <p class="empty">Click a box or a line to see its links and what it connects to.</p>
492
601
  </section>
493
602
  <aside id="findings" aria-label="Findings">
494
- <h2>Findings <span class="what">where the picture, drawn as written, may not read as meant</span></h2>
603
+ <h2><button id="findings-close" class="close" type="button" title="Close the findings" aria-label="Close the findings">×</button>Findings <span class="what">where the picture, drawn as written, may not read as meant</span></h2>
495
604
  <ol id="finding-list"></ol>
496
605
  </aside>
497
606
  </div>
@@ -516,7 +625,7 @@ const PAGE = `<!doctype html>
516
625
  // Another theme is carried as the colors that differ from the first, put
517
626
  // back in place the first time it is shown.
518
627
  var VARIES = new RegExp(DATA.varies, 'gi');
519
- // Whether guides are drawn, which the View menu's switch changes. The
628
+ // Whether guides are drawn, which the Settings menu's switch changes. The
520
629
  // drawings with guides the other way are a second set, packed the same way
521
630
  // against its own first theme.
522
631
  var guidesShown = DATA.guidesShown;
@@ -730,9 +839,22 @@ const PAGE = `<!doctype html>
730
839
 
731
840
  var panel = document.getElementById('findings');
732
841
  var list = document.getElementById('finding-list');
733
- var count = DATA.findings.length;
734
-
735
- // A row of the View menu that is on or off: its switch, and the word beside it.
842
+ var band = document.getElementById('band');
843
+ // A page with an editor looks for findings only when Check is pressed,
844
+ // unless "Check as I type" is on: on a large diagram the checks would hold
845
+ // up the typing. A page without one carries the findings it was made with.
846
+ var AUTO_CHECK = 'reladraw-page-autocheck';
847
+ var autoCheck = false;
848
+ try { autoCheck = localStorage.getItem(AUTO_CHECK) === 'on'; } catch (e) {}
849
+ // The text last checked and what was found in it. Findings outlive edits,
850
+ // dimmed, since they describe the source as it was.
851
+ var checked = !EDIT || autoCheck ? { text: DATA.source, findings: DATA.findings } : null;
852
+ var shownText = DATA.source;
853
+ function found() { return checked ? checked.findings : []; }
854
+ function stale() { return !!checked && checked.text !== shownText; }
855
+ var count = found().length;
856
+
857
+ // A row of the Settings menu that is on or off: its switch, and the word beside it.
736
858
  function setSwitch(row, on) {
737
859
  row.setAttribute('aria-checked', String(on));
738
860
  row.querySelector('.word').textContent = on ? 'on' : 'off';
@@ -741,7 +863,6 @@ const PAGE = `<!doctype html>
741
863
  var autoRow = document.getElementById('auto-row');
742
864
  var findingsRow = document.getElementById('findings-row');
743
865
  var guidesRow = document.getElementById('guides-row');
744
- findingsRow.querySelector('.name').textContent = 'Findings (' + count + ')';
745
866
  guidesRow.querySelector('.name').textContent = 'Guides (' + DATA.guideCount + ')';
746
867
 
747
868
  /* ---- the side panel: shown, hidden, or shown only when wanted ---- */
@@ -760,8 +881,15 @@ const PAGE = `<!doctype html>
760
881
  function keepPanel() {
761
882
  try { localStorage.setItem(KEPT, JSON.stringify({ on: panelOn, autoHide: autoHide })); } catch (e) {}
762
883
  }
884
+ // While the source is open the panel would squeeze the picture between two
885
+ // columns, so it behaves as if auto-hidden: it comes up for a selection or
886
+ // an error, and for the findings only when asked.
887
+ function editing() {
888
+ var pane = document.getElementById('editor');
889
+ return !!pane && !pane.hidden;
890
+ }
763
891
  function layoutSide() {
764
- side.hidden = !(panelOn && (!autoHide || selection || !panel.hidden));
892
+ side.hidden = !(panelOn && (!(autoHide || editing()) || selection || !panel.hidden));
765
893
  // A finding lights what it is about only while the finding can be seen.
766
894
  if ((panel.hidden || side.hidden) && lit && lit.length) unlight();
767
895
  setSwitch(panelRow, panelOn);
@@ -793,6 +921,7 @@ const PAGE = `<!doctype html>
793
921
  else showPanel(panel.hidden);
794
922
  }
795
923
  findingsRow.onclick = flipFindings;
924
+ document.getElementById('findings-close').onclick = function () { showPanel(false); };
796
925
  if (count === 0) {
797
926
  panel.hidden = true;
798
927
  } else {
@@ -834,14 +963,22 @@ const PAGE = `<!doctype html>
834
963
  zoom.pan((room.left + room.width / 2) - (r.l + r.r) / 2, (room.top + room.height / 2) - (r.t + r.b) / 2);
835
964
  }
836
965
 
966
+ // Whether the source has stopped parsing, and whether that is what opened
967
+ // the findings, so they close again once it parses.
968
+ var failing = false;
969
+ var openedByError = false;
837
970
  // The list, made again when a served page redraws. An error the file has
838
971
  // stopped parsing with heads it, over the findings of the last drawing that
839
972
  // parsed, which is still the one shown.
973
+ var lastError = null;
840
974
  function listFindings(error) {
975
+ failing = !!error;
976
+ lastError = error;
841
977
  list.textContent = '';
842
- count = DATA.findings.length + (error ? 1 : 0);
843
- findingsRow.querySelector('.name').textContent = 'Findings (' + count + ')';
978
+ count = found().length + (error ? 1 : 0);
979
+ findingsRow.querySelector('.name').textContent = checked || error ? 'Findings (' + count + ')' : 'Findings';
844
980
  guidesRow.querySelector('.name').textContent = 'Guides (' + DATA.guideCount + ')';
981
+ panel.classList.toggle('stale', stale());
845
982
  if (error) {
846
983
  var wrong = element('li', 'error');
847
984
  var head = element('span', 'where', error.line ? 'line ' + error.line + ' ' : '');
@@ -865,11 +1002,88 @@ const PAGE = `<!doctype html>
865
1002
  document.createTextNode(error.message), element('span', 'note', 'The picture is the last version that drew.'));
866
1003
  list.append(wrong);
867
1004
  }
868
- if (count === 0) list.append(element('li', 'none', 'No findings.'));
869
- DATA.findings.forEach(addFinding);
870
- }
871
- function addFinding(finding) {
872
- var item = document.createElement('li');
1005
+ if (!checked) {
1006
+ list.append(element('li', 'none', 'Automatic checking is off. Press Check in the bar above to look for overlaps and other places the picture may not read as meant.'));
1007
+ } else {
1008
+ if (stale()) list.append(element('li', 'again', againText()));
1009
+ if (found().length === 0) list.append(element('li', 'none', 'No findings.'));
1010
+ found().forEach(function (finding) {
1011
+ var item = document.createElement('li');
1012
+ item.append(findingButton(finding));
1013
+ list.append(item);
1014
+ });
1015
+ }
1016
+ showBand();
1017
+ }
1018
+ function againText() {
1019
+ return autoCheck ? 'From the last version that drew; the source has changed since.'
1020
+ : 'The source has changed since this check. Press Check again.';
1021
+ }
1022
+
1023
+ // The band under the picture while the source is open: the first few
1024
+ // findings, and View all for the panel with every one.
1025
+ var BAND_ROWS = 3;
1026
+ var bandClosed = false;
1027
+ function showBand() {
1028
+ var list = found();
1029
+ band.textContent = '';
1030
+ band.hidden = !(EDIT && editing() && checked && !bandClosed && (list.length > 0 || !autoCheck));
1031
+ if (band.hidden) return;
1032
+ band.classList.toggle('stale', stale());
1033
+ var head = element('div', 'head');
1034
+ head.append(element('b', '', list.length ? 'Findings \\u00B7 ' + list.length : 'No findings'));
1035
+ head.append(element('span', 'gap'));
1036
+ if (list.length) {
1037
+ var all = element('button', '', list.length > BAND_ROWS ? 'View all ' + list.length : 'View all');
1038
+ all.type = 'button';
1039
+ all.title = 'Open the findings in the side panel';
1040
+ all.onclick = openFindings;
1041
+ head.append(all);
1042
+ }
1043
+ var close = element('button', 'close', '\\u00D7');
1044
+ close.type = 'button';
1045
+ close.title = 'Close until the next check';
1046
+ close.setAttribute('aria-label', 'Close the findings');
1047
+ close.onclick = function () { bandClosed = true; showBand(); };
1048
+ head.append(close);
1049
+ band.append(head);
1050
+ if (stale()) band.append(element('div', 'again', againText()));
1051
+ var rows = element('div', 'found');
1052
+ list.slice(0, BAND_ROWS).forEach(function (finding) { rows.append(findingButton(finding)); });
1053
+ band.append(rows);
1054
+ }
1055
+ // The panel takes the findings over: the band closes until the next check,
1056
+ // and closing the panel doesn't bring it back.
1057
+ function openFindings() {
1058
+ if (!panelOn) { panelOn = true; keepPanel(); }
1059
+ bandClosed = true;
1060
+ showBand();
1061
+ showPanel(true);
1062
+ }
1063
+
1064
+ // Looks for findings in the source as it stands, drawing it again to do so.
1065
+ var checkButton = document.getElementById('check');
1066
+ function runCheck() {
1067
+ if (!EDIT || !lib) return;
1068
+ bandClosed = false;
1069
+ drawnText = null;
1070
+ draw(sourceBox.value, true);
1071
+ // In View mode there is no band, so what was found shows in the panel.
1072
+ if (!editing()) openFindings();
1073
+ }
1074
+ checkButton.onclick = runCheck;
1075
+ var autoCheckRow = document.getElementById('autocheck-row');
1076
+ function setAutoCheck(on) {
1077
+ autoCheck = on;
1078
+ try { localStorage.setItem(AUTO_CHECK, on ? 'on' : 'off'); } catch (e) {}
1079
+ setSwitch(autoCheckRow, on);
1080
+ checkButton.hidden = !EDIT || on;
1081
+ if (on) runCheck();
1082
+ else listFindings(lastError);
1083
+ }
1084
+ autoCheckRow.onclick = function () { setAutoCheck(!autoCheck); };
1085
+
1086
+ function findingButton(finding) {
873
1087
  var button = document.createElement('button');
874
1088
  button.type = 'button';
875
1089
  var where = document.createElement('span');
@@ -890,8 +1104,7 @@ const PAGE = `<!doctype html>
890
1104
  light(finding, button);
891
1105
  if (EDIT && !editorPane.hidden) goToLine(finding.line);
892
1106
  };
893
- item.append(button);
894
- list.append(item);
1107
+ return button;
895
1108
  }
896
1109
  listFindings(null);
897
1110
 
@@ -1213,14 +1426,14 @@ const PAGE = `<!doctype html>
1213
1426
 
1214
1427
  var STRIP = {
1215
1428
  bar: ['<b>Menu</b>', 'letter to open \\u00B7 \\u232B back \\u00B7 Esc out'],
1216
- view: ['Menu \\u203A <b>View</b>', 'letter to flip \\u00B7 \\u2191\\u2193 Enter \\u00B7 \\u232B back \\u00B7 Esc out'],
1429
+ view: ['Menu \\u203A <b>Settings</b>', 'letter to flip \\u00B7 \\u2191\\u2193 Enter \\u00B7 \\u232B back \\u00B7 Esc out'],
1217
1430
  save: ['Menu \\u203A <b>Export</b>', 'letter to save \\u00B7 \\u2191\\u2193 Enter \\u00B7 \\u232B back \\u00B7 Esc out'],
1218
1431
  versions: ['Menu \\u203A <b>Versions</b>', '\\u2191\\u2193 Enter to put one in the editor \\u00B7 \\u232B back \\u00B7 Esc out'],
1219
- theme: ['Menu \\u203A View \\u203A <b>Theme</b>', '\\u2191\\u2193 to try \\u00B7 Enter keep \\u00B7 \\u232B back \\u00B7 Esc out'],
1432
+ theme: ['Menu \\u203A Settings \\u203A <b>Theme</b>', '\\u2191\\u2193 to try \\u00B7 Enter keep \\u00B7 \\u232B back \\u00B7 Esc out'],
1220
1433
  };
1221
1434
 
1222
1435
  function rows() {
1223
- if (level === 'view') return Array.prototype.slice.call(viewList.querySelectorAll(':scope > .row'));
1436
+ if (level === 'view') return Array.prototype.slice.call(viewList.querySelectorAll(':scope > .row:not([hidden])'));
1224
1437
  if (level === 'save') return Array.prototype.slice.call(saveList.querySelectorAll(':scope > .row'));
1225
1438
  if (level === 'versions') return Array.prototype.slice.call(versionsList.querySelectorAll(':scope > .row'));
1226
1439
  if (level === 'theme') return themeItems.map(function (item) { return item.row; });
@@ -1717,10 +1930,12 @@ const PAGE = `<!doctype html>
1717
1930
  var ZOOM_KEYS = { '+': 1, '=': 1, '-': 1, '_': 1, '0': 1, '1': 1 };
1718
1931
 
1719
1932
  // On the window, before the zoom's own keys, so a menu's letters reach the
1720
- // menu and not the diagram underneath: g flips guides in View, not a grab.
1933
+ // menu and not the diagram underneath: g flips guides in Settings, not a grab.
1721
1934
  window.addEventListener('keydown', function (event) {
1722
1935
  if (event.ctrlKey || event.metaKey || event.altKey) return;
1723
1936
  if (event.target.closest && event.target.closest('input, textarea, select, [contenteditable]')) return;
1937
+ // A window open over the page has the keys, Escape included.
1938
+ if (document.querySelector('dialog[open]')) return;
1724
1939
  var key = event.key;
1725
1940
  if (key === 'Escape') plain();
1726
1941
  else if (level === 'pick' || level === 'filter') {
@@ -1728,6 +1943,8 @@ const PAGE = `<!doctype html>
1728
1943
  } else if (level === null) {
1729
1944
  if (key === 'm' || key === 'M') setLevel('bar');
1730
1945
  else if (key === 'e' && EDIT) toggleEditor();
1946
+ else if (key === 'c' && !checkButton.hidden) runCheck();
1947
+ else if (key === '?' && cheatBox) openCheatsheet();
1731
1948
  else if (key === 's') startHints('all');
1732
1949
  else if (key === '/') startHints('all', true);
1733
1950
  else if (key === 'n') startHints('neighbors');
@@ -1749,10 +1966,11 @@ const PAGE = `<!doctype html>
1749
1966
  else if (key.length === 1) {
1750
1967
  var letter = key.toLowerCase();
1751
1968
  if (level === 'bar') {
1752
- if (letter === 'v') setLevel('view');
1969
+ if (letter === 's') setLevel('view');
1753
1970
  else if (letter === 'x') setLevel('save');
1754
1971
  else if (letter === 'o' && tiedFile()) setLevel('versions');
1755
1972
  else if (letter === 'e' && EDIT) { setLevel(null); toggleEditor(); }
1973
+ else if (letter === 'c' && !checkButton.hidden) { setLevel(null); runCheck(); }
1756
1974
  else if (letter === 'f' && !full.hidden) { setLevel(null); toggleFull(); }
1757
1975
  } else if (level === 'view' || level === 'save' || level === 'versions') {
1758
1976
  rows().forEach(function (row, i) {
@@ -1812,6 +2030,7 @@ const PAGE = `<!doctype html>
1812
2030
  if (selection && !(selection.kind === 'node' ? graph.nodes[selection.key] : graph.edges[selection.key])) selection = null;
1813
2031
  if (level === 'pick' || level === 'filter') setLevel(null);
1814
2032
  listFindings(null);
2033
+ if (openedByError) { openedByError = false; if (editing()) showPanel(false); }
1815
2034
  redraw();
1816
2035
  }
1817
2036
 
@@ -1837,6 +2056,40 @@ const PAGE = `<!doctype html>
1837
2056
  var paintBox = document.getElementById('paint');
1838
2057
  var unsavedMark = document.getElementById('unsaved');
1839
2058
  var writeButton = document.getElementById('write');
2059
+ // A menu in the source's bar: its button opens and closes its list, and a
2060
+ // row chosen, a click elsewhere or Escape closes it. Returns its closer.
2061
+ function sourceMenu(menu, button, list) {
2062
+ function close() {
2063
+ list.hidden = true;
2064
+ button.classList.remove('open');
2065
+ button.setAttribute('aria-expanded', 'false');
2066
+ }
2067
+ button.onclick = function () {
2068
+ var open = list.hidden;
2069
+ list.hidden = !open;
2070
+ button.classList.toggle('open', open);
2071
+ button.setAttribute('aria-expanded', String(open));
2072
+ };
2073
+ // Choosing a row closes the menu before the row does its work.
2074
+ list.addEventListener('click', function () { close(); }, true);
2075
+ document.addEventListener('click', function (event) {
2076
+ if (!list.hidden && !menu.contains(event.target)) close();
2077
+ });
2078
+ document.addEventListener('keydown', function (event) {
2079
+ if (event.key === 'Escape' && !list.hidden) close();
2080
+ }, true);
2081
+ return close;
2082
+ }
2083
+ // A page with no file keeps Examples, Open and Save in a File menu.
2084
+ var fileMenu = document.getElementById('file-menu');
2085
+ var fileButton = document.getElementById('file-button');
2086
+ var closeFileMenu = sourceMenu(fileMenu, fileButton, document.getElementById('file-list'));
2087
+ // Help: the cheatsheet, and the full reference.
2088
+ var helpMenu = document.getElementById('help-menu');
2089
+ sourceMenu(helpMenu, document.getElementById('help-button'), document.getElementById('help-list'));
2090
+ document.getElementById('write-row').onclick = function () { writeButton.click(); };
2091
+ // Where "Saved" flashes: the Save button, or the File menu it is in.
2092
+ function saveFlash(text) { flash(writeButton.hidden ? fileButton : writeButton, text); }
1840
2093
  var formatButton = document.getElementById('format');
1841
2094
  var openButton = document.getElementById('open-file');
1842
2095
  var formatOpen = document.getElementById('format-open');
@@ -1846,30 +2099,45 @@ const PAGE = `<!doctype html>
1846
2099
  function toggleEditor(show, quiet) {
1847
2100
  if (!EDIT) return;
1848
2101
  var on = show === undefined ? editorPane.hidden : show;
2102
+ var changed = editorPane.hidden === on;
1849
2103
  editorPane.hidden = !on;
1850
2104
  splitBar.hidden = !on;
1851
2105
  editButton.setAttribute('aria-pressed', String(on));
2106
+ lookButton.setAttribute('aria-pressed', String(!on));
2107
+ // The findings close with the source open, and come back as the page
2108
+ // first shows them when it closes.
2109
+ if (changed) {
2110
+ openedByError = openedByError && on;
2111
+ showPanel(on ? failing : count > 0 && !autoHide);
2112
+ showBand();
2113
+ }
1852
2114
  if (on) { paintSource(); if (!quiet) sourceBox.focus(); }
1853
2115
  else if (document.activeElement === sourceBox) sourceBox.blur();
1854
2116
  }
1855
- editButton.onclick = function () { toggleEditor(); };
2117
+ // The View half of the Edit | View switch, apart from the Settings menu.
2118
+ var lookButton = document.getElementById('mode-view');
2119
+ editButton.onclick = function () { toggleEditor(true); };
2120
+ lookButton.onclick = function () { toggleEditor(false); };
1856
2121
 
1857
2122
  function escapeCode(text) { return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;'); }
1858
2123
  // The scanner covers each line end to end, so this rebuilds the text exactly
1859
2124
  // and only wraps some of it in spans; a lost character would slide the rest
1860
2125
  // of the line out from under the cursor.
2126
+ // One line of source as colored HTML; the library has to have loaded.
2127
+ function colorLine(line) {
2128
+ var out = '';
2129
+ lib.highlightLine(line).forEach(function (span) {
2130
+ var piece = escapeCode(line.slice(span.start, span.end));
2131
+ out += span.kind === 'plain' ? piece : '<span class="t-' + span.kind + '">' + piece + '</span>';
2132
+ });
2133
+ return out;
2134
+ }
1861
2135
  function paintSource() {
1862
2136
  var text = sourceBox.value;
1863
2137
  var out = '';
1864
2138
  try {
1865
2139
  if (!lib) throw new Error('not loaded');
1866
- text.split(/\\r?\\n/).forEach(function (line) {
1867
- lib.highlightLine(line).forEach(function (span) {
1868
- var piece = escapeCode(line.slice(span.start, span.end));
1869
- out += span.kind === 'plain' ? piece : '<span class="t-' + span.kind + '">' + piece + '</span>';
1870
- });
1871
- out += '\\n';
1872
- });
2140
+ text.split(/\\r?\\n/).forEach(function (line) { out += colorLine(line) + '\\n'; });
1873
2141
  } catch (e) {
1874
2142
  // Coloring is decoration; the editor has to keep working without it.
1875
2143
  out = escapeCode(text) + '\\n';
@@ -1907,17 +2175,21 @@ const PAGE = `<!doctype html>
1907
2175
  var cost = 0;
1908
2176
  var typing = null;
1909
2177
 
1910
- function draw(text) {
2178
+ // \`check\` looks for findings this once, as Check does.
2179
+ function draw(text, check) {
1911
2180
  if (text === drawnText || !lib) return;
1912
2181
  drawnText = text;
2182
+ shownText = text;
2183
+ var checking = autoCheck || !!check;
1913
2184
  var started = performance.now();
1914
2185
  var made;
1915
2186
  try {
1916
- made = lib.pageData(text, fileName, {});
2187
+ made = lib.pageData(text, fileName, checking ? {} : { checks: false });
2188
+ if (checking) checked = { text: text, findings: made.data.findings };
1917
2189
  } catch (error) {
1918
2190
  cost = performance.now() - started;
1919
2191
  listFindings({ message: error.message, line: error.line });
1920
- if (panelOn && panel.hidden) showPanel(true);
2192
+ if (panelOn && panel.hidden) { openedByError = true; showPanel(true); }
1921
2193
  return;
1922
2194
  }
1923
2195
  cost = performance.now() - started;
@@ -2106,8 +2378,10 @@ const PAGE = `<!doctype html>
2106
2378
  fileName = file.split('/').pop();
2107
2379
  disk = text;
2108
2380
  base = text;
2109
- browseButton.hidden = true;
2110
- openButton.hidden = true;
2381
+ // A page tied to a file has only Save, so it is a button, not a File menu.
2382
+ closeFileMenu();
2383
+ fileMenu.hidden = true;
2384
+ writeButton.hidden = false;
2111
2385
  formatOpen.hidden = true;
2112
2386
  document.getElementById('versions-menu').hidden = false;
2113
2387
  writeButton.querySelector('.name').textContent = 'Save';
@@ -2179,9 +2453,9 @@ const PAGE = `<!doctype html>
2179
2453
  base = text;
2180
2454
  changedStrip.hidden = true;
2181
2455
  markUnsaved();
2182
- flash(writeButton, 'Saved');
2456
+ saveFlash('Saved');
2183
2457
  }).catch(function (error) {
2184
- flash(writeButton, 'Not saved');
2458
+ saveFlash('Not saved');
2185
2459
  liveNote.textContent = 'Couldn\\u2019t save ' + file + ': ' + error.message;
2186
2460
  liveNote.hidden = false;
2187
2461
  });
@@ -2319,7 +2593,7 @@ const PAGE = `<!doctype html>
2319
2593
  return handle.createWritable().then(function (out) {
2320
2594
  return out.write(text).then(function () { return out.close(); }).then(function () {
2321
2595
  keep(text, handle.name);
2322
- flash(writeButton, 'Saved');
2596
+ saveFlash('Saved');
2323
2597
  });
2324
2598
  });
2325
2599
  })
@@ -2357,7 +2631,7 @@ const PAGE = `<!doctype html>
2357
2631
  keep(text, told.file.split('/').pop());
2358
2632
  history.replaceState(null, '', '/' + told.file.split('/').map(encodeURIComponent).join('/'));
2359
2633
  tie(told.file, text);
2360
- flash(writeButton, 'Saved');
2634
+ saveFlash('Saved');
2361
2635
  });
2362
2636
  }).catch(function (error) {
2363
2637
  problem.textContent = 'Couldn\\u2019t save: ' + error.message + '. Is reladraw serve still running?';
@@ -2523,11 +2797,90 @@ const PAGE = `<!doctype html>
2523
2797
  };
2524
2798
  }
2525
2799
 
2800
+ /* ---- the cheatsheet ---- */
2801
+
2802
+ // A window of cards, each row an example colored as the editor colors it.
2803
+ // The filter keeps the rows holding every word typed, matching the card's
2804
+ // name too, so "placement" keeps a whole card.
2805
+ var cheatBox = document.getElementById('cheatsheet');
2806
+ var openCheatsheet = function () {};
2807
+ if (cheatBox) {
2808
+ var cheatFilter = document.getElementById('cheat-filter');
2809
+ var cheatNone = cheatBox.querySelector('.cs-none');
2810
+ var cheatColored = false;
2811
+ var cheatGrid = cheatBox.querySelector('.cs-grid');
2812
+ var cheatCards = Array.prototype.slice.call(cheatGrid.querySelectorAll('.cs-card'));
2813
+ var cheatCols = element('div', 'cs-cols');
2814
+ cheatGrid.append(cheatCols);
2815
+ // Each card, in order, goes to the shortest column so far.
2816
+ var layoutCheat = function () {
2817
+ var width = cheatGrid.clientWidth;
2818
+ var count = width >= 1000 ? 3 : width >= 640 ? 2 : 1;
2819
+ cheatCols.style.setProperty('--cols', String(count));
2820
+ cheatCols.textContent = '';
2821
+ var cols = [];
2822
+ var heights = [];
2823
+ for (var i = 0; i < count; i++) {
2824
+ cols.push(element('div', 'cs-col'));
2825
+ heights.push(0);
2826
+ cheatCols.append(cols[i]);
2827
+ }
2828
+ cheatCards.forEach(function (card) {
2829
+ var at = heights.indexOf(Math.min.apply(null, heights));
2830
+ cols[at].append(card);
2831
+ if (!card.hidden) heights[at] += card.offsetHeight + 14;
2832
+ });
2833
+ };
2834
+ window.addEventListener('resize', function () { if (cheatBox.open) layoutCheat(); });
2835
+ openCheatsheet = function () {
2836
+ if (!cheatColored && lib) {
2837
+ cheatColored = true;
2838
+ each(cheatBox.querySelectorAll('.cs-row code'), function (code) {
2839
+ try { code.innerHTML = colorLine(code.textContent); } catch (e) {}
2840
+ });
2841
+ }
2842
+ cheatBox.showModal();
2843
+ layoutCheat();
2844
+ cheatFilter.focus();
2845
+ cheatFilter.select();
2846
+ };
2847
+ helpMenu.hidden = false;
2848
+ document.getElementById('cheat-open').onclick = openCheatsheet;
2849
+ closeOnBackdrop(cheatBox);
2850
+ cheatFilter.addEventListener('input', function () {
2851
+ var words = cheatFilter.value.toLowerCase().split(/\\s+/).filter(Boolean);
2852
+ var matches = function (text) {
2853
+ text = text.toLowerCase();
2854
+ return words.every(function (word) { return text.indexOf(word) >= 0; });
2855
+ };
2856
+ var any = false;
2857
+ each(cheatBox.querySelectorAll('.cs-card'), function (card) {
2858
+ var title = card.querySelector('h3').textContent;
2859
+ var shown = false;
2860
+ each(card.querySelectorAll('.cs-row'), function (row) {
2861
+ row.hidden = !matches(title + ' ' + row.textContent);
2862
+ if (!row.hidden) shown = true;
2863
+ });
2864
+ var note = card.querySelector('.cs-note');
2865
+ if (note) {
2866
+ note.hidden = !matches(title + ' ' + note.textContent);
2867
+ if (!note.hidden) shown = true;
2868
+ }
2869
+ card.hidden = !shown;
2870
+ if (shown) any = true;
2871
+ });
2872
+ cheatNone.hidden = any;
2873
+ layoutCheat();
2874
+ });
2875
+ }
2876
+
2526
2877
  /* ---- the start ---- */
2527
2878
 
2528
2879
  if (EDIT) {
2529
- editButton.hidden = false;
2530
- document.getElementById('edit-gap').hidden = false;
2880
+ document.getElementById('mode').hidden = false;
2881
+ checkButton.hidden = autoCheck;
2882
+ autoCheckRow.hidden = false;
2883
+ setSwitch(autoCheckRow, autoCheck);
2531
2884
  try {
2532
2885
  var share = parseFloat(localStorage.getItem(SPLIT));
2533
2886
  if (share > 0 && share < 1) divide(share, false);
@@ -2536,8 +2889,8 @@ const PAGE = `<!doctype html>
2536
2889
  sourceBox.value = DATA.source;
2537
2890
  tie(EDIT.file, DATA.source);
2538
2891
  } else {
2539
- browseButton.hidden = false;
2540
- openButton.hidden = false;
2892
+ fileMenu.hidden = false;
2893
+ writeButton.hidden = true;
2541
2894
  formatOpen.hidden = false;
2542
2895
  writeButton.querySelector('.name').textContent = 'Save\\u2026';
2543
2896
  writeButton.title = EDIT.kind === 'local'
@@ -2574,145 +2927,124 @@ const PAGE = `<!doctype html>
2574
2927
  `;
2575
2928
  /** What a page with an editor shows under its source: the language on one screen. */
2576
2929
  const CHEATSHEET = `
2577
- <details id="cheatsheet">
2578
- <summary>Cheatsheet</summary>
2579
- <dl>
2580
- <dt>Statements</dt>
2581
- <dd>
2582
- <code>node <i>name</i> <i>["text"]</i> <i>[placements]</i> <i>[key: value …]</i></code><br />
2583
- <code>edge <i>a</i> -&gt; <i>b</i> <i>["text"]</i> <i>[key: value …]</i></code>
2584
- &nbsp;<i>(<code>&lt;-</code> to write the target first, <code>&lt;-&gt;</code> for both ends)</i><br />
2585
- <code>point <i>name</i> <i>where</i></code> &nbsp;<code>region <i>name</i> <i>a</i> top-left to <i>b</i> bottom-right</code><br />
2586
- <code>bundle <i>name</i> via: <i>…</i></code> &nbsp;<i>(a path lines share)</i>
2587
- &nbsp;<code><b>guide</b> node|edge|point|region …</code> <i>(reference only; turn on Guides in View to see them)</i><br />
2588
- <code>style <i>name</i> <i>key: value …</i></code><br />
2589
- <code>diagram theme: nord background: #1e2229 text: (color: #e0e0e0)</code><br />
2590
- <code>default node|leaf|container|edge <i>key: value …</i></code>
2591
- &nbsp;<i>(a style every one of that kind wears; the more specific wins)</i><br />
2592
- <i>The picture is drawn exactly as written: nothing is moved to make room, and lines go only
2593
- where they are told. Anything that overlaps is drawn as written; the findings beside the diagram report it, as <code>reladraw check</code> does.</i>
2594
- </dd>
2595
-
2596
- <dt>Labels</dt>
2597
- <dd>
2598
- <code>node parser</code> — no text, so the node takes its name as its text.<br />
2599
- <code>node parser ""</code> — deliberately blank.<br />
2600
- <code>"two / lines"</code> — <code> / </code> with spaces either side breaks a line.
2601
- </dd>
2602
-
2603
- <dt>Containment</dt>
2604
- <dd>
2605
- <code>node server.docker "Docker"</code> — a dotted name is a child. Declare the parent first. With no text of its own, it reads "docker".<br />
2606
- <code>layout: column</code> or <code>row</code> — the children in written order, with
2607
- <code>(gap: tight, widths: match, align-children: left)</code>.
2608
- Without a layout, one child is unplaced and the rest are placed against it.<br />
2609
- <code>layout: deck</code> — a stack of cards, the first child in front.<br />
2610
- <code>gather-children: left</code> — where the children sit in spare room.
2611
- <code>shape: none</code> on a container draws no box.
2612
- </dd>
2613
-
2614
- <dt>Placement</dt>
2615
- <dd>
2616
- <code><b>above</b> X</code> <code><b>below</b> X</code>
2617
- <code><b>left of</b> X</code> <code><b>right of</b> X</code>
2618
- <code><b>above-left of</b> X</code> … <code><b>below-right of</b> X</code><br />
2619
- <code><b>even with</b> X</code> — share a center line;
2620
- <code><b>top</b>|<b>bottom</b>|<b>left</b>|<b>right</b> <b>even with</b> X</code> shares that edge.<br />
2621
- <code>right of A <b>and</b> B</code> — against the box that just bounds them both.<br />
2622
- <code><b>inside</b> X top-right</code> <code><b>outside</b> X right</code>
2623
- <code><b>on</b> X bottom-left</code> — against a part of X. The parts are
2624
- <code>text</code>, the sides <code>top</code> <code>bottom</code> <code>left</code> <code>right</code>,
2625
- and <code>top-left</code> <code>top-center</code> <code>top-right</code>
2626
- <code>left-center</code> <code>center</code> <code>right-center</code>
2627
- <code>bottom-left</code> <code>bottom-center</code> <code>bottom-right</code>.<br />
2628
- <code><b>on</b> between A and B</code> <code>below down from A and left from B</code> — at a point.<br />
2629
- <i>One node is unplaced; every other is placed against something. One direction on its own
2630
- also centers on its target. Each axis is said once.</i>
2631
- </dd>
2632
-
2633
- <dt>Gaps</dt>
2634
- <dd>
2635
- <code>right of X <b>(gap: wide)</b></code> or <code><b>2</b> right of X</code>, on the placement.
2636
- One of <code>none</code> <code>tight</code> <code>normal</code> <code>wide</code> (0, 2, 4, 8 units),
2637
- or a number of units; a box is about 4 units tall. A gap is <i>exact</i>.
2638
- </dd>
2639
-
2640
- <dt>Sizes</dt>
2641
- <dd>
2642
- <code>width: 30</code> <code>height: menu</code> <code>height: 0.5 menu height</code>
2643
- <code>height: a top to c bottom</code> <code>min-width: 10</code>
2644
- </dd>
2645
-
2646
- <dt>Appearance</dt>
2647
- <dd>
2648
- <code>style: <i>name</i></code>
2649
- <code>fill: #191728</code>
2650
- <code>border: none</code>
2651
- <code>corners: sharp</code> <i>(a box; rounded unless told)</i>
2652
- <code>line: #d2904e</code> <i>(an edge)</i>
2653
- <code>overlap: allow</code>
2654
- <code>url: "https://example.com"</code>
2655
- <code>description: "What it does"</code> <i>(shown on the page; never drawn)</i>
2656
- </dd>
2657
-
2658
- <dt>Lines</dt>
2659
- <dd>
2660
- <code>line: (path: square, corners: rounded)</code> — everything about an edge's line.
2661
- <code>path:</code> takes <code>straight</code> (the default) <code>square</code> <code>curved</code>;
2662
- <code>corners:</code> <code>sharp</code> <code>rounded</code>;
2663
- <code>crossing:</code> <code>gap</code> <code>arc</code> <code>square</code> <code>none</code>;
2664
- <code>pattern:</code> <code>solid</code> <code>dashed</code> <code>dotted</code> <code>dash-dot</code>;
2665
- <code>thickness:</code> <code>thin</code> <code>normal</code> <code>thick</code>, a multiple, or <code>3px</code>;
2666
- <code>turn: 1/3</code> <code>step-out: tight</code> <code>curve: subtle</code>.
2667
- <i><code>default edge line: (path: square)</code> sets every edge.</i>
2668
- </dd>
2669
-
2670
- <dt>Body</dt>
2671
- <dd>
2672
- <code>shape: rectangle|document|circle|none</code> — the outline the node is drawn with;
2673
- <code>none</code> is text with no box, which is what an annotation is.<br />
2674
- <code>icon: <i>name</i></code> — the node <i>is</i> that picture, with no box.<br />
2675
- <code>badge: <i>name</i></code> — the picture is part of the label, beside the text, and the node keeps its box.<br />
2676
- <code>layout: deck cards: "text" <i>["text" …]</i></code> — a stack of cards, front first, one per text.<br />
2677
- Pictures: <code>disk</code> <code>desktop</code> <code>laptop</code>
2678
- <code>package</code> <code>cubes</code> <code>cube</code> <code>database</code>.<br />
2679
- Your own: <code>icon <i>name</i> """&lt;svg …&gt;…&lt;/svg&gt;"""</code> declares one,
2680
- and <code>icon: <i>name</i></code> or <code>badge: <i>name</i></code> uses it.
2681
- </dd>
2682
-
2683
- <dt>Text</dt>
2684
- <dd>
2685
- Everything a text says about itself goes in brackets after it:
2686
- <code>(color: theme-muted)</code>
2687
- <code>(size: small|normal|large)</code>
2688
- <code>(wrap: 24)</code> <i>(characters, folds the text)</i>.
2689
- A title says which side it sits against: <code>"Server" (inside top, align: left)</code>,
2690
- <code>(on top)</code>, <code>(outside top)</code>, <code>(center)</code>.
2691
- In a style, which has no text of its own, the bracket hangs off the key:
2692
- <code>text: (color: theme-muted)</code>.<br />
2693
- <code>" / "</code> in any text is a line break; <code>\\/</code> is a literal slash.<br />
2694
- <code>"name / <b>[dim]</b>quieter<b>[/dim]</b>"</code> draws those words in the
2695
- text color of <code>style dim</code>; <code>\\[</code> is a literal bracket.
2696
- </dd>
2697
-
2698
- <dt>Links</dt>
2699
- <dd>
2700
- A line is straight between its two boxes, through anything in between, unless told otherwise.<br />
2701
- <code>from: right</code> <code>to: left</code> name a side;
2702
- <code>to: bottom-center</code> or <code>to: left 3/4 down</code> a point on it.<br />
2703
- <code>via: c top, between a and b</code> — the places the line passes, in order:
2704
- just outside a side, a point, a box's center, <code>through <i>region</i> horizontal</code>.<br />
2705
- <code>"text" (at: start)</code> <code>(at: 1/4)</code> <code>(above edge)</code>
2706
- <code>(on: longest level)</code> — where a line's text sits; the middle by default.
2707
- </dd>
2708
-
2709
- <dt>Comments</dt>
2710
- <dd><code>// to the end of the line</code>, anywhere, including after a statement.</dd>
2711
- </dl>
2712
- </details>
2930
+ <dialog id="cheatsheet" aria-label="Cheatsheet">
2931
+ <div class="cs-head">
2932
+ <h2>Cheatsheet</h2>
2933
+ <input id="cheat-filter" type="search" placeholder="Filter: above, gap, color…" autocomplete="off" spellcheck="false" aria-label="Filter the cheatsheet">
2934
+ <span class="gap"></span>
2935
+ <button class="close" type="button" data-close title="Close (Esc)" aria-label="Close the cheatsheet">×</button>
2936
+ </div>
2937
+ <div class="cs-grid">
2938
+ <section class="cs-card" style="--hue: var(--syn-keyword)"><h3>Statements</h3>
2939
+ <div class="cs-row"><code>node api "API"</code><span>a box, with its text</span></div>
2940
+ <div class="cs-row"><code>edge a -&gt; b</code><span>a line from a to b</span></div>
2941
+ <div class="cs-row"><code>edge a &lt;- b</code><span>the same line, written target first</span></div>
2942
+ <div class="cs-row"><code>edge a &lt;-&gt; b</code><span>arrows at both ends</span></div>
2943
+ <div class="cs-row"><code>point p 2 above a top-center</code><span>a named spot, to place against or pass through</span></div>
2944
+ <div class="cs-row"><code>region r a top-left to c bottom-right</code><span>a named area, from one corner to another</span></div>
2945
+ <div class="cs-row"><code>bundle trunk via: through r horizontal</code><span>a path that lines share</span></div>
2946
+ <div class="cs-row"><code>guide node g right of a</code><span>for reference only; Guides in Settings shows it</span></div>
2947
+ <div class="cs-row"><code>style hot fill: red</code><span>a named look, worn with style: hot</span></div>
2948
+ <div class="cs-row"><code>default edge line: (path: square)</code><span>a look every edge wears; also node, leaf, container; the more specific wins</span></div>
2949
+ <div class="cs-row"><code>diagram theme: nord background: #1e2229</code><span>the whole diagram's theme and colors</span></div>
2950
+ <p class="cs-note">The picture is drawn exactly as written: nothing moves to make room, and lines go only where they are told. Check reports anything that overlaps.</p>
2951
+ </section>
2952
+ <section class="cs-card" style="--hue: var(--syn-relation)"><h3>Placement</h3>
2953
+ <div class="cs-row"><code>node b right of a</code><span>beside a, centered on it; also above, below, left of</span></div>
2954
+ <div class="cs-row"><code>node b above-left of a</code><span>diagonally, corner to corner; also above-right, below-left, below-right</span></div>
2955
+ <div class="cs-row"><code>node b 3 left of a and 2 above a</code><span>3 units left of a, and 2 above it</span></div>
2956
+ <div class="cs-row"><code>node c right of a and b</code><span>against the box that just bounds a and b</span></div>
2957
+ <div class="cs-row"><code>node b even with a</code><span>on a's center line</span></div>
2958
+ <div class="cs-row"><code>node b top even with a</code><span>tops in line; also bottom, left, right</span></div>
2959
+ <div class="cs-row"><code>node f inside box top-right</code><span>in a corner of box</span></div>
2960
+ <div class="cs-row"><code>node g outside box right</code><span>just outside a side of box</span></div>
2961
+ <div class="cs-row"><code>node h on box bottom-left</code><span>centered on a part of box</span></div>
2962
+ <div class="cs-row"><code>node i on between a and c</code><span>midway between a and c</span></div>
2963
+ <div class="cs-row"><code>node j below down from b and left from a</code><span>at the point straight down from b and left from a</span></div>
2964
+ <p class="cs-note">The parts of a box: <code>text</code>, the sides <code>top</code> <code>bottom</code> <code>left</code> <code>right</code>, and <code>top-left</code> <code>top-center</code> <code>top-right</code> <code>left-center</code> <code>center</code> <code>right-center</code> <code>bottom-left</code> <code>bottom-center</code> <code>bottom-right</code>. One node is unplaced; every other is placed against something. Each axis is said once.</p>
2965
+ </section>
2966
+ <section class="cs-card" style="--hue: var(--syn-value)"><h3>Gaps</h3>
2967
+ <div class="cs-row"><code>node b 2 right of a</code><span>exactly 2 units apart</span></div>
2968
+ <div class="cs-row"><code>node b right of a (gap: wide)</code><span>none, tight, normal or wide: 0, 2, 4 or 8 units</span></div>
2969
+ <p class="cs-note">A box is about 4 units tall. A gap is exact.</p>
2970
+ </section>
2971
+ <section class="cs-card" style="--hue: var(--syn-string)"><h3>Labels</h3>
2972
+ <div class="cs-row"><code>node parser</code><span>no text, so its name is its text</span></div>
2973
+ <div class="cs-row"><code>node parser ""</code><span>deliberately blank</span></div>
2974
+ <div class="cs-row"><code>node a "two / lines"</code><span>a space, slash and space breaks the line</span></div>
2975
+ </section>
2976
+ <section class="cs-card" style="--hue: var(--syn-name)"><h3>Containment</h3>
2977
+ <div class="cs-row"><code>node server.docker "Docker"</code><span>a dotted name is a child; declare the parent first</span></div>
2978
+ <div class="cs-row"><code>node server layout: column</code><span>children top to bottom, in written order; or row</span></div>
2979
+ <div class="cs-row"><code>layout: column (gap: tight, widths: match, align-children: left)</code><span>how the children line up</span></div>
2980
+ <div class="cs-row"><code>layout: deck</code><span>a stack of cards, the first child in front</span></div>
2981
+ <div class="cs-row"><code>gather-children: left</code><span>where the children sit in spare room</span></div>
2982
+ <div class="cs-row"><code>shape: none</code><span>on a container, draws no box</span></div>
2983
+ <p class="cs-note">Without a layout, one child is unplaced and the rest are placed against it.</p>
2984
+ </section>
2985
+ <section class="cs-card" style="--hue: var(--syn-attribute)"><h3>Sizes</h3>
2986
+ <div class="cs-row"><code>width: 30</code><span>30 units wide</span></div>
2987
+ <div class="cs-row"><code>height: menu</code><span>as tall as menu</span></div>
2988
+ <div class="cs-row"><code>height: 0.5 menu height</code><span>half as tall as menu</span></div>
2989
+ <div class="cs-row"><code>height: a top to c bottom</code><span>from a's top to c's bottom</span></div>
2990
+ <div class="cs-row"><code>min-width: 10</code><span>at least 10 units wide</span></div>
2991
+ </section>
2992
+ <section class="cs-card" style="--hue: var(--syn-keyword)"><h3>Appearance</h3>
2993
+ <div class="cs-row"><code>style: hot</code><span>wear a named style</span></div>
2994
+ <div class="cs-row"><code>fill: #191728</code><span>a box's color</span></div>
2995
+ <div class="cs-row"><code>border: none</code><span>no outline</span></div>
2996
+ <div class="cs-row"><code>corners: sharp</code><span>square corners; boxes are rounded unless told</span></div>
2997
+ <div class="cs-row"><code>line: #d2904e</code><span>a line's color</span></div>
2998
+ <div class="cs-row"><code>overlap: allow</code><span>this one may overlap without a finding</span></div>
2999
+ <div class="cs-row"><code>url: "https://example.com"</code><span>a link, shown on the page</span></div>
3000
+ <div class="cs-row"><code>description: "What it does"</code><span>shown on the page; never drawn</span></div>
3001
+ </section>
3002
+ <section class="cs-card" style="--hue: var(--syn-string)"><h3>Lines</h3>
3003
+ <div class="cs-row"><code>line: (path: square, corners: rounded)</code><span>everything about a line goes in line:</span></div>
3004
+ <div class="cs-row"><code>line: (path: curved, curve: subtle)</code><span>straight (the default), square or curved</span></div>
3005
+ <div class="cs-row"><code>line: (path: square, turn: 1/3, step-out: tight)</code><span>where a square line turns, and how far it steps out</span></div>
3006
+ <div class="cs-row"><code>line: (crossing: arc)</code><span>where lines cross: gap, arc, square or none</span></div>
3007
+ <div class="cs-row"><code>line: (pattern: dashed)</code><span>solid, dashed, dotted or dash-dot</span></div>
3008
+ <div class="cs-row"><code>line: (thickness: thick)</code><span>thin, normal, thick, a multiple, or 3px</span></div>
3009
+ </section>
3010
+ <section class="cs-card" style="--hue: var(--syn-relation)"><h3>Where lines go</h3>
3011
+ <div class="cs-row"><code>from: right to: left</code><span>leave and arrive at a side</span></div>
3012
+ <div class="cs-row"><code>to: bottom-center</code><span>arrive at a point on a side</span></div>
3013
+ <div class="cs-row"><code>to: left 3/4 down</code><span>a fraction of the way along a side</span></div>
3014
+ <div class="cs-row"><code>via: c top, between a and b</code><span>the places a line passes, in order</span></div>
3015
+ <div class="cs-row"><code>via: through r horizontal</code><span>straight through a region</span></div>
3016
+ <div class="cs-row"><code>edge a -&gt; b "calls" (at: start)</code><span>where a line's text sits: start, 1/4, the middle by default</span></div>
3017
+ <div class="cs-row"><code>edge a -&gt; b "calls" (above edge)</code><span>text beside the line rather than on it</span></div>
3018
+ <div class="cs-row"><code>edge a -&gt; b "calls" (on: longest level)</code><span>text on the longest level stretch</span></div>
3019
+ <p class="cs-note">A line runs straight between its two boxes, through anything in between, unless told otherwise.</p>
3020
+ </section>
3021
+ <section class="cs-card" style="--hue: var(--syn-name)"><h3>Body</h3>
3022
+ <div class="cs-row"><code>shape: document</code><span>rectangle, document, circle or none</span></div>
3023
+ <div class="cs-row"><code>node note "a note" right of a shape: none</code><span>just text: an annotation</span></div>
3024
+ <div class="cs-row"><code>icon: database</code><span>the node is the picture, with no box</span></div>
3025
+ <div class="cs-row"><code>badge: database</code><span>the picture beside the text, in the box</span></div>
3026
+ <div class="cs-row"><code>layout: deck cards: "v1" "v2"</code><span>a stack of cards, one per text, front first</span></div>
3027
+ <div class="cs-row"><code>icon logo """&lt;svg …&gt;…&lt;/svg&gt;"""</code><span>your own picture, used as icon: logo</span></div>
3028
+ <p class="cs-note">Pictures: <code>disk</code> <code>desktop</code> <code>laptop</code> <code>package</code> <code>cubes</code> <code>cube</code> <code>database</code>.</p>
3029
+ </section>
3030
+ <section class="cs-card" style="--hue: var(--syn-value)"><h3>Text</h3>
3031
+ <div class="cs-row"><code>node a "Server" (size: large)</code><span>small, normal or large</span></div>
3032
+ <div class="cs-row"><code>node a "Server" (color: theme-muted)</code><span>the text's color</span></div>
3033
+ <div class="cs-row"><code>node a "A long label" (wrap: 24)</code><span>folds at 24 characters</span></div>
3034
+ <div class="cs-row"><code>node a "Server" (inside top, align: left)</code><span>where a container's title sits: on top, outside top, center</span></div>
3035
+ <div class="cs-row"><code>style dim text: (color: theme-muted)</code><span>in a style, the bracket hangs off text:</span></div>
3036
+ <div class="cs-row"><code>node a "name / [dim]quieter[/dim]"</code><span>those words in style dim's text color</span></div>
3037
+ <div class="cs-row"><code>node a "a \\/ b"</code><span>a slash that doesn't break the line; \\[ for a bracket</span></div>
3038
+ </section>
3039
+ <section class="cs-card" style="--hue: var(--syn-comment)"><h3>Comments</h3>
3040
+ <div class="cs-row"><code>// to the end of the line</code><span>anywhere, including after a statement</span></div>
3041
+ </section>
3042
+ </div>
3043
+ <p class="cs-none" hidden>Nothing matches. Try one word, such as above, gap or color.</p>
3044
+ </dialog>
2713
3045
  `;
2714
3046
  /** Where a page with no file sends someone who wants more than the cheatsheet. */
2715
- const LINKS = `<nav class="links"><a href="https://github.com/reladraw/reladraw">GitHub</a><a href="https://github.com/reladraw/reladraw/blob/main/SYNTAX.md">Syntax</a></nav>`;
3047
+ const LINKS = `<nav class="links"><a href="https://github.com/reladraw/reladraw" title="reladraw on GitHub" aria-label="reladraw on GitHub"><svg viewBox="0 0 16 16" width="18" height="18" aria-hidden="true"><path fill="currentColor" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/></svg></a></nav>`;
2716
3048
  /**
2717
3049
  * A page with no file's dialogs: the examples, the format settings, which a
2718
3050
  * file's page takes from its nearest .reladraw-format instead, and, served,