reladraw 0.25.0 → 0.26.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/.claude/skills/reladraw/SKILL.md +3 -3
- package/.claude/skills/reladraw/reference/syntax.md +17 -4
- package/README.md +1 -1
- package/SYNTAX.md +17 -4
- package/dist/page.js +261 -49
- package/dist/viewer.d.ts +2 -1
- package/dist/viewer.js +51 -2
- package/package.json +1 -1
|
@@ -15,7 +15,7 @@ That matters here specifically. You can't see the SVG you produced. With auto-la
|
|
|
15
15
|
|
|
16
16
|
## Check it is installed, and the same version as this skill
|
|
17
17
|
|
|
18
|
-
This skill teaches reladraw 0.
|
|
18
|
+
This skill teaches reladraw 0.26.0. Once per session, before the first render, run:
|
|
19
19
|
|
|
20
20
|
```
|
|
21
21
|
reladraw --version
|
|
@@ -86,7 +86,7 @@ even with X top even with X bottom even with X left even with X right ev
|
|
|
86
86
|
- **A gap is exact**: `none`, `tight`, `normal` (the default) or `wide`, in the placement's bracket, `right of a (gap: tight)`, or a number of the file's unit in front, `2 right of a`. A normal box is about 4 units tall.
|
|
87
87
|
- **A lone direction also centers on its target.** `right of a` alone puts the box level with `a`. Another placement on that axis replaces the centering: `right of a even with b`.
|
|
88
88
|
- **Each axis is stated once.** Two placements on one axis are refused. The one exception: `right of a left of b` centers the box in the space between them, and then something must say how high.
|
|
89
|
-
- **Several targets are one region**: `below a and b` sits under the pair, centered between them.
|
|
89
|
+
- **Several targets are one region**: `below a and b` sits under the pair, centered between them. A distance and a direction after `and` start a second placement instead: `right of a and 2 above c top` is two placements. To use a point as a target, name it first with `point p 2 above c top-center`.
|
|
90
90
|
- **Nothing moves a box.** Two boxes placed `right of a` both land on one spot and are reported. Chain them (`b right of a`, `c right of b`) or put them in a container with a layout.
|
|
91
91
|
|
|
92
92
|
### Containers
|
|
@@ -151,7 +151,7 @@ style store fill: theme-primary-subtle border: theme-primary badge: database
|
|
|
151
151
|
node records "Records" style: store
|
|
152
152
|
```
|
|
153
153
|
|
|
154
|
-
`fill` is a box's area, `border` its outline, `line` an edge's line, and a text's color goes in its bracket. `corners: sharp` squares a box's rounded corners, and `border: (color: theme-primary, thickness: thick)` thickens its outline, the box growing to keep its padding. Prefer theme colors, which read in light and dark themes alike: `theme-primary` and `theme-secondary` are the accents, `-subtle` is an accent softened toward the page (the one to fill a box with), `theme-muted` is quieter text. A hex or CSS color stays fixed in every theme.
|
|
154
|
+
`fill` is a box's area, `border` its outline, `line` an edge's line, and a text's color goes in its bracket. `corners: sharp` squares a box's rounded corners, and `border: (color: theme-primary, thickness: thick)` thickens its outline, the box growing to keep its padding. Prefer theme colors, which read in light and dark themes alike: `theme-primary` and `theme-secondary` are the accents, `-subtle` is an accent softened toward the page (the one to fill a box with), `theme-muted` is quieter text. A hex or CSS color stays fixed in every theme. Leave out `diagram theme:` unless the user asks for a theme; the page lets each reader pick their own.
|
|
155
155
|
|
|
156
156
|
### Links
|
|
157
157
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Syntax reference — 0.
|
|
1
|
+
# Syntax reference — 0.26.0
|
|
2
2
|
|
|
3
3
|
What the language accepts. The changelog at the end lists what changed in each published version; 0.17.0's changes are large, and its entry has a table of every old spelling and what to write instead.
|
|
4
4
|
|
|
@@ -1288,11 +1288,13 @@ The output is a standalone SVG: every picture is drawn into it as path data, nev
|
|
|
1288
1288
|
reladraw render arch.reladraw --page
|
|
1289
1289
|
```
|
|
1290
1290
|
|
|
1291
|
-
`--page` writes `arch.html`: one self-contained file, for opening in a browser, attaching or committing, that needs nothing running and fetches nothing. The page is in the diagram's own colors, so a dark diagram doesn't sit on a white page, and it is headed with `diagram title:`, or the file's name when there is none. Under the title it says which file it was drawn from and when, such as "drawn from arch.reladraw, 2026-11-05 14:32": the page is a snapshot of the file, and is behind it once the file is edited, until it is rendered again. Drag to pan; scroll, pinch or double-click to zoom; Fit shows the whole diagram and 100% returns it to its own scale. The zoom is always shown, with "Fit" beside it while the whole diagram is in view. There is a full-screen button. The Save menu saves the page itself, as it was written rather than at its current zoom; the `.reladraw` source it was drawn from; or the picture as an SVG or a PNG, as `render` would draw it; or copies the SVG. Each is named after the source file: `arch.html`, `arch.reladraw`, `arch.svg`, `arch.png`. In Chrome and Edge it asks where to save; other browsers put the file with their downloads. The findings are listed beside it in a panel that folds away, and clicking one lights up what its line drew. A box's or a line's name shows under the pointer.
|
|
1291
|
+
`--page` writes `arch.html`: one self-contained file, for opening in a browser, attaching or committing, that needs nothing running and fetches nothing. The page is in the diagram's own colors, so a dark diagram doesn't sit on a white page, and it is headed with `diagram title:`, or the file's name when there is none. Under the title it says which file it was drawn from and when, such as "drawn from arch.reladraw, 2026-11-05 14:32": the page is a snapshot of the file, and is behind it once the file is edited, until it is rendered again. Drag to pan, or use `h` `j` `k` `l`, or press `g` to grab the diagram and pan it with the arrow keys (Shift for bigger steps either way, `g` or Escape to let go); scroll, pinch or double-click to zoom; Fit shows the whole diagram and 100% returns it to its own scale. The zoom is always shown, with "Fit" beside it while the whole diagram is in view. There is a full-screen button. The Save menu saves the page itself, as it was written rather than at its current zoom; the `.reladraw` source it was drawn from; or the picture as an SVG or a PNG, as `render` would draw it; or copies the SVG. Each is named after the source file: `arch.html`, `arch.reladraw`, `arch.svg`, `arch.png`. In Chrome and Edge it asks where to save; other browsers put the file with their downloads. The findings are listed beside it in a panel that folds away, and clicking one lights up what its line drew. A box's or a line's name shows under the pointer. The theme picker draws the diagram in another theme, as `--theme` would, keeping the view and the selection; a page or text color the file states holds in every theme, and Save SVG, PNG and Copy SVG take the theme shown. It starts on "Diagram's theme", the one the file names (or dark), which follows the file when it is rendered again. A theme picked by name is the reader's: it holds on every reladraw page in that browser, whatever the file says, until "Diagram's theme" is picked again.
|
|
1292
|
+
|
|
1293
|
+
The side panel can be hidden with the Panel button or `p`, and then stays hidden whatever is selected. With Auto-hide ticked it shows only while something is selected or the findings are open, and the findings start folded. Both choices are remembered by the browser for every page.
|
|
1292
1294
|
|
|
1293
1295
|
**A click on a box or a line selects it**, and never follows a link. The selection is amber; what feeds it is green and what it feeds is red, the lines dashed and moving the way they run; the rest dims. A line runs from the end without an arrowhead when only one end has one, and otherwise from the end written first. The panel beside the drawing names the selection and lists its links, each with its badge, and the boxes it comes from and goes to; a link opens in a new tab, and a box selects it. One line out lights by default, and the panel's button lights everything upstream and downstream. Its description, if it has one, comes last ([Descriptions](#descriptions)). A box with links underlines its text under the pointer. A click on the background or Escape clears the selection.
|
|
1294
1296
|
|
|
1295
|
-
Keys: `+` and `-` zoom, `0` fits, `1` is actual size, `f` is full screen, Escape clears the selection.
|
|
1297
|
+
Keys: `+` and `-` zoom, `0` fits, `1` is actual size, `f` is full screen, `p` shows or hides the side panel, Escape clears the selection.
|
|
1296
1298
|
|
|
1297
1299
|
### Formatting
|
|
1298
1300
|
|
|
@@ -1414,6 +1416,17 @@ Designed and absent from the code.
|
|
|
1414
1416
|
|
|
1415
1417
|
Pre-1.0, so the minor number is where a breaking change goes. Every removal is refused by name with the replacement quoted, rather than dropped in silence: an older file stops with an error saying what to write instead.
|
|
1416
1418
|
|
|
1419
|
+
**0.26.0**
|
|
1420
|
+
|
|
1421
|
+
- **The arrow keys pan a diagram**, on the `--page` page and in the playground. Press `g` to grab the diagram: a border and a hint show it is held, the arrows pan it a step at a time, and Shift makes the steps bigger. `g` again or Escape lets go, and the arrows go back to whatever else uses them. Clicking into a text field lets go too. `h` `j` `k` `l` pan the same way without a grab.
|
|
1422
|
+
- **The `--page` page has a theme picker**, which draws the diagram in any theme without moving anything, and Save SVG and PNG save the theme shown. It starts on the diagram's own theme, which follows the file; a theme the reader picks holds on every page in that browser until they pick the diagram's theme again.
|
|
1423
|
+
- **The `--page` page's side panel can be hidden**, with the Panel button or `p`, or set to Auto-hide, showing only while something is selected or the findings are open. A long description now fills the panel's height instead of scrolling in the top part of it. Closing the findings clears the highlight a finding lit.
|
|
1424
|
+
- The agent skill now leaves out `diagram theme:` unless the user asks for a theme.
|
|
1425
|
+
|
|
1426
|
+
**0.25.1**
|
|
1427
|
+
|
|
1428
|
+
- The agent skill now says that a distance and a direction after `and` start a second placement, and how to name a point to use as a target. No change to what the tool accepts or draws.
|
|
1429
|
+
|
|
1417
1430
|
**0.25.0**
|
|
1418
1431
|
|
|
1419
1432
|
- **A link can be declared once and used by name**: `url orders-docs "https://…" (name: "Docs", icon: "D")`, then `url: orders-docs`, so long addresses stay out of the statements that link to them. A declaration may list several links, declared names among them, and a `url:` mixes names and quoted addresses. See [Destinations](#destinations).
|
|
@@ -1422,7 +1435,7 @@ Pre-1.0, so the minor number is where a breaking change goes. Every removal is r
|
|
|
1422
1435
|
|
|
1423
1436
|
- **`2 right of and 3 below a`** is accepted, the same as `2 right and 3 below a`. It used to be refused with a message that took `and` for the name of a box.
|
|
1424
1437
|
|
|
1425
|
-
-
|
|
1438
|
+
- **A distance and a direction after `and` or a comma start a second placement**: `node b right of a and 2 above c top` puts `b` right of `a`, with its bottom 2 above `c`'s top. It used to take `2 above c top` as a point added to the targets, so `b` was placed right of the region covering `a` and that point, with no warning, and the second half looked ignored. To get that reading, name the point first: `point p 2 above c top-center`, then `node b right of a and p`.
|
|
1426
1439
|
|
|
1427
1440
|
- **A box or a line can have a description**, shown in the panel when it is selected on a `--page` page, and as hover text in an SVG. It is written on the thing, `description: "Forwards each request by its path."`; at a distance, `description for orders and to-orders """…"""`; or named, `description replica """…"""`, and taken with `description: replica`. The text is a small part of Markdown: paragraphs, lists, bold, italic, code and links.
|
|
1428
1441
|
|
package/README.md
CHANGED
|
@@ -94,7 +94,7 @@ To install the skill from the main branch instead, which can run ahead of the re
|
|
|
94
94
|
|
|
95
95
|
## Status
|
|
96
96
|
|
|
97
|
-
Version 0.
|
|
97
|
+
Version 0.26.0. Early stage, but works. The parser, layout engine, and SVG renderer are written in TypeScript, with zero runtime dependencies. There is a command-line tool that turns a .reladraw text file into an SVG, and an element that does the same inside a web page.
|
|
98
98
|
|
|
99
99
|
## License
|
|
100
100
|
|
package/SYNTAX.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Syntax reference — 0.
|
|
1
|
+
# Syntax reference — 0.26.0
|
|
2
2
|
|
|
3
3
|
What the language accepts. The changelog at the end lists what changed in each published version; 0.17.0's changes are large, and its entry has a table of every old spelling and what to write instead.
|
|
4
4
|
|
|
@@ -1288,11 +1288,13 @@ The output is a standalone SVG: every picture is drawn into it as path data, nev
|
|
|
1288
1288
|
reladraw render arch.reladraw --page
|
|
1289
1289
|
```
|
|
1290
1290
|
|
|
1291
|
-
`--page` writes `arch.html`: one self-contained file, for opening in a browser, attaching or committing, that needs nothing running and fetches nothing. The page is in the diagram's own colors, so a dark diagram doesn't sit on a white page, and it is headed with `diagram title:`, or the file's name when there is none. Under the title it says which file it was drawn from and when, such as "drawn from arch.reladraw, 2026-11-05 14:32": the page is a snapshot of the file, and is behind it once the file is edited, until it is rendered again. Drag to pan; scroll, pinch or double-click to zoom; Fit shows the whole diagram and 100% returns it to its own scale. The zoom is always shown, with "Fit" beside it while the whole diagram is in view. There is a full-screen button. The Save menu saves the page itself, as it was written rather than at its current zoom; the `.reladraw` source it was drawn from; or the picture as an SVG or a PNG, as `render` would draw it; or copies the SVG. Each is named after the source file: `arch.html`, `arch.reladraw`, `arch.svg`, `arch.png`. In Chrome and Edge it asks where to save; other browsers put the file with their downloads. The findings are listed beside it in a panel that folds away, and clicking one lights up what its line drew. A box's or a line's name shows under the pointer.
|
|
1291
|
+
`--page` writes `arch.html`: one self-contained file, for opening in a browser, attaching or committing, that needs nothing running and fetches nothing. The page is in the diagram's own colors, so a dark diagram doesn't sit on a white page, and it is headed with `diagram title:`, or the file's name when there is none. Under the title it says which file it was drawn from and when, such as "drawn from arch.reladraw, 2026-11-05 14:32": the page is a snapshot of the file, and is behind it once the file is edited, until it is rendered again. Drag to pan, or use `h` `j` `k` `l`, or press `g` to grab the diagram and pan it with the arrow keys (Shift for bigger steps either way, `g` or Escape to let go); scroll, pinch or double-click to zoom; Fit shows the whole diagram and 100% returns it to its own scale. The zoom is always shown, with "Fit" beside it while the whole diagram is in view. There is a full-screen button. The Save menu saves the page itself, as it was written rather than at its current zoom; the `.reladraw` source it was drawn from; or the picture as an SVG or a PNG, as `render` would draw it; or copies the SVG. Each is named after the source file: `arch.html`, `arch.reladraw`, `arch.svg`, `arch.png`. In Chrome and Edge it asks where to save; other browsers put the file with their downloads. The findings are listed beside it in a panel that folds away, and clicking one lights up what its line drew. A box's or a line's name shows under the pointer. The theme picker draws the diagram in another theme, as `--theme` would, keeping the view and the selection; a page or text color the file states holds in every theme, and Save SVG, PNG and Copy SVG take the theme shown. It starts on "Diagram's theme", the one the file names (or dark), which follows the file when it is rendered again. A theme picked by name is the reader's: it holds on every reladraw page in that browser, whatever the file says, until "Diagram's theme" is picked again.
|
|
1292
|
+
|
|
1293
|
+
The side panel can be hidden with the Panel button or `p`, and then stays hidden whatever is selected. With Auto-hide ticked it shows only while something is selected or the findings are open, and the findings start folded. Both choices are remembered by the browser for every page.
|
|
1292
1294
|
|
|
1293
1295
|
**A click on a box or a line selects it**, and never follows a link. The selection is amber; what feeds it is green and what it feeds is red, the lines dashed and moving the way they run; the rest dims. A line runs from the end without an arrowhead when only one end has one, and otherwise from the end written first. The panel beside the drawing names the selection and lists its links, each with its badge, and the boxes it comes from and goes to; a link opens in a new tab, and a box selects it. One line out lights by default, and the panel's button lights everything upstream and downstream. Its description, if it has one, comes last ([Descriptions](#descriptions)). A box with links underlines its text under the pointer. A click on the background or Escape clears the selection.
|
|
1294
1296
|
|
|
1295
|
-
Keys: `+` and `-` zoom, `0` fits, `1` is actual size, `f` is full screen, Escape clears the selection.
|
|
1297
|
+
Keys: `+` and `-` zoom, `0` fits, `1` is actual size, `f` is full screen, `p` shows or hides the side panel, Escape clears the selection.
|
|
1296
1298
|
|
|
1297
1299
|
### Formatting
|
|
1298
1300
|
|
|
@@ -1414,6 +1416,17 @@ Designed and absent from the code.
|
|
|
1414
1416
|
|
|
1415
1417
|
Pre-1.0, so the minor number is where a breaking change goes. Every removal is refused by name with the replacement quoted, rather than dropped in silence: an older file stops with an error saying what to write instead.
|
|
1416
1418
|
|
|
1419
|
+
**0.26.0**
|
|
1420
|
+
|
|
1421
|
+
- **The arrow keys pan a diagram**, on the `--page` page and in the playground. Press `g` to grab the diagram: a border and a hint show it is held, the arrows pan it a step at a time, and Shift makes the steps bigger. `g` again or Escape lets go, and the arrows go back to whatever else uses them. Clicking into a text field lets go too. `h` `j` `k` `l` pan the same way without a grab.
|
|
1422
|
+
- **The `--page` page has a theme picker**, which draws the diagram in any theme without moving anything, and Save SVG and PNG save the theme shown. It starts on the diagram's own theme, which follows the file; a theme the reader picks holds on every page in that browser until they pick the diagram's theme again.
|
|
1423
|
+
- **The `--page` page's side panel can be hidden**, with the Panel button or `p`, or set to Auto-hide, showing only while something is selected or the findings are open. A long description now fills the panel's height instead of scrolling in the top part of it. Closing the findings clears the highlight a finding lit.
|
|
1424
|
+
- The agent skill now leaves out `diagram theme:` unless the user asks for a theme.
|
|
1425
|
+
|
|
1426
|
+
**0.25.1**
|
|
1427
|
+
|
|
1428
|
+
- The agent skill now says that a distance and a direction after `and` start a second placement, and how to name a point to use as a target. No change to what the tool accepts or draws.
|
|
1429
|
+
|
|
1417
1430
|
**0.25.0**
|
|
1418
1431
|
|
|
1419
1432
|
- **A link can be declared once and used by name**: `url orders-docs "https://…" (name: "Docs", icon: "D")`, then `url: orders-docs`, so long addresses stay out of the statements that link to them. A declaration may list several links, declared names among them, and a `url:` mixes names and quoted addresses. See [Destinations](#destinations).
|
|
@@ -1422,7 +1435,7 @@ Pre-1.0, so the minor number is where a breaking change goes. Every removal is r
|
|
|
1422
1435
|
|
|
1423
1436
|
- **`2 right of and 3 below a`** is accepted, the same as `2 right and 3 below a`. It used to be refused with a message that took `and` for the name of a box.
|
|
1424
1437
|
|
|
1425
|
-
-
|
|
1438
|
+
- **A distance and a direction after `and` or a comma start a second placement**: `node b right of a and 2 above c top` puts `b` right of `a`, with its bottom 2 above `c`'s top. It used to take `2 above c top` as a point added to the targets, so `b` was placed right of the region covering `a` and that point, with no warning, and the second half looked ignored. To get that reading, name the point first: `point p 2 above c top-center`, then `node b right of a and p`.
|
|
1426
1439
|
|
|
1427
1440
|
- **A box or a line can have a description**, shown in the panel when it is selected on a `--page` page, and as hover text in an SVG. It is written on the thing, `description: "Forwards each request by its path."`; at a distance, `description for orders and to-orders """…"""`; or named, `description replica """…"""`, and taken with `description: replica`. The text is a small part of Markdown: paragraphs, lists, bold, italic, code and links.
|
|
1428
1441
|
|
package/dist/page.js
CHANGED
|
@@ -5,7 +5,7 @@ import { linkLabel, linksOf } from './links.js';
|
|
|
5
5
|
import { markdownHtml } from './markdown.js';
|
|
6
6
|
import { iconSvg, renderTagged, themeOf } from './render.js';
|
|
7
7
|
import { resolve } from './resolve.js';
|
|
8
|
-
import { luminance } from './themes.js';
|
|
8
|
+
import { DEFAULT_THEME, luminance, THEME_NAMES, THEMES } from './themes.js';
|
|
9
9
|
import { PAN_ZOOM } from './viewer.js';
|
|
10
10
|
/**
|
|
11
11
|
* `reladraw render --page`: one diagram in one self-contained HTML page, made
|
|
@@ -23,12 +23,28 @@ export function drawPage(source, file, options = {}, at = new Date()) {
|
|
|
23
23
|
const drawn = `drawn from ${file}, ${stamp(at)}`;
|
|
24
24
|
const run = (text) => {
|
|
25
25
|
const layout = resolve(parse(text), options);
|
|
26
|
-
const { svg, tagged } = renderTagged(layout, options);
|
|
27
26
|
const found = options.checks === false ? [] : findings(layout, options.checks ? options.checks(fileChecks(layout)) : fileChecks(layout));
|
|
28
27
|
const title = layout.diagram['title'] ?? name;
|
|
29
|
-
|
|
28
|
+
// The theme it was drawn in: the one --theme named, else the file's.
|
|
29
|
+
const named = layout.diagram['theme'];
|
|
30
|
+
const current = (options.theme && THEME_NAMES.find((one) => THEMES[one] === options.theme)) ??
|
|
31
|
+
(named !== undefined && THEMES[named] !== undefined ? named : DEFAULT_THEME);
|
|
32
|
+
// The same diagram drawn in every theme, for the page's theme picker. A
|
|
33
|
+
// theme moves nothing, so only the colors differ between them. A page or
|
|
34
|
+
// text color the file states still holds in each, as in the playground.
|
|
35
|
+
const themes = {};
|
|
36
|
+
for (const one of THEME_NAMES) {
|
|
37
|
+
const drawnIn = { ...options, theme: options.theme && one === current ? options.theme : THEMES[one] };
|
|
38
|
+
const theme = themeOf(layout, drawnIn);
|
|
39
|
+
themes[one] = { ...renderTagged(layout, drawnIn), theme, colors: colorsOf(theme), scheme: schemeOf(theme) };
|
|
40
|
+
}
|
|
41
|
+
// A --theme that is not one of the named ones is drawn as given, and offered as such.
|
|
42
|
+
if (themes[current] === undefined) {
|
|
43
|
+
const theme = themeOf(layout, options);
|
|
44
|
+
themes[current] = { ...renderTagged(layout, options), theme, colors: colorsOf(theme), scheme: schemeOf(theme) };
|
|
45
|
+
}
|
|
30
46
|
return {
|
|
31
|
-
html: page({ title, name, drawn, source,
|
|
47
|
+
html: page({ title, name, drawn, source, findings: found, themes, current, graph: graphOf(layout, themes) }),
|
|
32
48
|
findings: found,
|
|
33
49
|
};
|
|
34
50
|
};
|
|
@@ -42,7 +58,7 @@ export function drawPage(source, file, options = {}, at = new Date()) {
|
|
|
42
58
|
/** A description as the panel shows it: its Markdown as HTML, or nothing. */
|
|
43
59
|
const describe = (attrs) => attrs['description'] === undefined ? {} : { description: markdownHtml(attrs['description']) };
|
|
44
60
|
const BADGE_SIDE = 20;
|
|
45
|
-
function graphOf(layout,
|
|
61
|
+
function graphOf(layout, themes) {
|
|
46
62
|
const views = (links) => (links ?? []).map((link) => {
|
|
47
63
|
const label = linkLabel(link, links);
|
|
48
64
|
const icon = link.icon === undefined
|
|
@@ -50,7 +66,10 @@ function graphOf(layout, theme) {
|
|
|
50
66
|
: 'picture' in link.icon
|
|
51
67
|
? layout.linkIcons[link.icon.picture]
|
|
52
68
|
: link.icon;
|
|
53
|
-
|
|
69
|
+
const badges = {};
|
|
70
|
+
for (const [one, view] of Object.entries(themes))
|
|
71
|
+
badges[one] = iconSvg(icon, BADGE_SIDE, view.theme);
|
|
72
|
+
return { href: link.href, label, badges };
|
|
54
73
|
});
|
|
55
74
|
// A node with no links of its own has its container's, as a click on it in
|
|
56
75
|
// the SVG follows its container's.
|
|
@@ -87,36 +106,82 @@ function stamp(at) {
|
|
|
87
106
|
const two = (n) => String(n).padStart(2, '0');
|
|
88
107
|
return `${at.getFullYear()}-${two(at.getMonth() + 1)}-${two(at.getDate())} ${two(at.getHours())}:${two(at.getMinutes())}`;
|
|
89
108
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
109
|
+
const hex = (color) => /^#[0-9a-f]{6}$/i.test(color);
|
|
110
|
+
/**
|
|
111
|
+
* What may differ between one theme's drawing and another's: colors,
|
|
112
|
+
* opacities, and the ids made from a drawing's hash and an arrowhead's color.
|
|
113
|
+
* Split on, its matches are the odd pieces. Matching too much costs nothing;
|
|
114
|
+
* a drawing whose other pieces differ is carried whole.
|
|
115
|
+
*/
|
|
116
|
+
const VARIES = /(#[0-9a-f]{3,8}\b|rgba?\([^)]*\)|opacity="[^"]*"|opacity:[^;"]*|\br[0-9a-f]{8}\b|\b[0-9a-f]{6}\b)/gi;
|
|
117
|
+
// The browser's own controls (scroll bars, focus rings) follow the page, so
|
|
118
|
+
// a dark diagram doesn't get a light one's. A color written by hand in a
|
|
119
|
+
// form that can't be measured leaves the choice to the browser.
|
|
120
|
+
function schemeOf(theme) {
|
|
121
|
+
return hex(theme.text) && hex(theme.background)
|
|
122
|
+
? luminance(theme.text) > luminance(theme.background)
|
|
123
|
+
? 'dark'
|
|
124
|
+
: 'light'
|
|
125
|
+
: 'light dark';
|
|
126
|
+
}
|
|
127
|
+
function colorsOf(theme) {
|
|
128
|
+
return {
|
|
129
|
+
'--page': theme.background,
|
|
130
|
+
'--ink': theme.text,
|
|
131
|
+
'--muted': theme.mutedText,
|
|
132
|
+
'--rule': theme.boxStroke,
|
|
133
|
+
'--accent': theme.primary.color,
|
|
134
|
+
'--on-accent': theme.primary.on,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
function page({ title, name, drawn, source, findings, themes, current, graph }) {
|
|
138
|
+
const shown = themes[current];
|
|
139
|
+
// First the diagram's own theme, which follows the file; then every theme
|
|
140
|
+
// as the reader's choice, dark ones together and light together, as the
|
|
141
|
+
// playground's picker has them, so stepping through the list doesn't flash
|
|
142
|
+
// between black and white.
|
|
143
|
+
const group = (light) => Object.keys(themes)
|
|
144
|
+
.filter((one) => (schemeOf(themes[one].theme) === 'light') === light)
|
|
145
|
+
.map((one) => `<option value="${escapeHtml(one)}">${escapeHtml(one)}</option>`)
|
|
146
|
+
.join('');
|
|
147
|
+
const picker = `<option value="" selected>Diagram's theme (${escapeHtml(current)})</option>` +
|
|
148
|
+
`<optgroup label="Dark">${group(false)}</optgroup><optgroup label="Light">${group(true)}</optgroup>`;
|
|
149
|
+
// The shown theme goes in whole; another is only the colors that differ
|
|
150
|
+
// from it, when the rest of the drawing is the same, as it is since a theme
|
|
151
|
+
// moves nothing. That keeps the page near its size with one theme.
|
|
152
|
+
const pack = (other, base) => {
|
|
153
|
+
const theirs = other.split(VARIES);
|
|
154
|
+
const ours = base.split(VARIES);
|
|
155
|
+
const same = theirs.length === ours.length && theirs.every((piece, i) => i % 2 === 1 || piece === ours[i]);
|
|
156
|
+
return same ? theirs.filter((_, i) => i % 2 === 1) : other;
|
|
157
|
+
};
|
|
96
158
|
const data = {
|
|
97
|
-
svg,
|
|
98
159
|
name,
|
|
99
160
|
source,
|
|
161
|
+
current,
|
|
162
|
+
varies: VARIES.source,
|
|
163
|
+
themes: Object.fromEntries(Object.entries(themes).map(([one, view]) => [
|
|
164
|
+
one,
|
|
165
|
+
one === current
|
|
166
|
+
? { svg: view.svg, tagged: view.tagged, colors: view.colors, scheme: view.scheme }
|
|
167
|
+
: { svg: pack(view.svg, shown.svg), tagged: pack(view.tagged, shown.tagged), colors: view.colors, scheme: view.scheme },
|
|
168
|
+
])),
|
|
100
169
|
findings: findings.map((finding) => ({ kind: finding.kind, message: finding.message, line: finding.line })),
|
|
101
170
|
graph,
|
|
102
171
|
};
|
|
103
172
|
// Replaced by functions, so a `$` in a title or a drawing is taken as written.
|
|
104
173
|
// The zoom's code goes in first, so nothing a diagram says can be taken for its marker.
|
|
105
174
|
return PAGE.replace('{{panzoom}}', () => PAN_ZOOM)
|
|
106
|
-
.replace('{{scheme}}', () => scheme)
|
|
175
|
+
.replace('{{scheme}}', () => shown.scheme)
|
|
107
176
|
.replace('{{title}}', () => escapeHtml(title))
|
|
108
177
|
.replace('{{heading}}', () => escapeHtml(title))
|
|
109
178
|
.replace('{{drawn}}', () => escapeHtml(drawn))
|
|
110
179
|
.replace('{{colors}}', () => [
|
|
111
|
-
|
|
112
|
-
`--ink: ${theme.text};`,
|
|
113
|
-
`--muted: ${theme.mutedText};`,
|
|
114
|
-
`--rule: ${theme.boxStroke};`,
|
|
115
|
-
`--accent: ${theme.primary.color};`,
|
|
116
|
-
`--on-accent: ${theme.primary.on};`,
|
|
180
|
+
...Object.entries(shown.colors).map(([key, value]) => `${key}: ${value};`),
|
|
117
181
|
'--sel: #e0b25a; --up: #22b98c; --down: #e5484d;',
|
|
118
182
|
].join(' '))
|
|
119
|
-
.replace('{{
|
|
183
|
+
.replace('{{themes}}', () => picker)
|
|
184
|
+
.replace('{{drawing}}', () => shown.tagged)
|
|
120
185
|
.replace('{{data}}', () => scriptJson(data));
|
|
121
186
|
}
|
|
122
187
|
function escapeHtml(text) {
|
|
@@ -177,6 +242,16 @@ const PAGE = `<!doctype html>
|
|
|
177
242
|
main { position: relative; flex: 1; display: flex; min-height: 0; }
|
|
178
243
|
#view { position: relative; flex: 1; overflow: hidden; touch-action: none; cursor: grab; }
|
|
179
244
|
#view.dragging { cursor: grabbing; }
|
|
245
|
+
#view.grabbed::after {
|
|
246
|
+
content: ''; position: absolute; inset: 0; z-index: 3; pointer-events: none;
|
|
247
|
+
border: 2px solid var(--accent);
|
|
248
|
+
}
|
|
249
|
+
.grab-hint {
|
|
250
|
+
position: absolute; left: 50%; bottom: 12px; transform: translateX(-50%); z-index: 3;
|
|
251
|
+
padding: 3px 10px; border-radius: 4px; font-size: 12px; white-space: nowrap; pointer-events: none;
|
|
252
|
+
background: var(--accent); color: var(--on-accent);
|
|
253
|
+
}
|
|
254
|
+
.grab-hint[hidden] { display: none; }
|
|
180
255
|
#stage { position: absolute; left: 0; top: 0; transform-origin: 0 0; }
|
|
181
256
|
#stage svg { display: block; }
|
|
182
257
|
#view g[data-name].hover { filter: drop-shadow(0 0 3px var(--accent)); }
|
|
@@ -191,12 +266,24 @@ const PAGE = `<!doctype html>
|
|
|
191
266
|
width: min(24rem, 36vw); display: flex; flex-direction: column; min-height: 0;
|
|
192
267
|
border-left: 1px solid color-mix(in srgb, var(--rule) 45%, transparent);
|
|
193
268
|
}
|
|
194
|
-
|
|
269
|
+
#side[hidden] { display: none; }
|
|
270
|
+
/* What is selected takes the room it needs and the findings the rest; when
|
|
271
|
+
both want more than there is, each gives up room and scrolls. */
|
|
272
|
+
aside { display: flex; flex-direction: column; min-height: min(8rem, 40%); flex: 1 1 auto;
|
|
195
273
|
border-top: 1px solid color-mix(in srgb, var(--rule) 45%, transparent); }
|
|
196
274
|
aside[hidden] { display: none; }
|
|
275
|
+
select {
|
|
276
|
+
font: inherit; font-size: 13px; color: var(--ink); background: var(--page);
|
|
277
|
+
border: 1px solid color-mix(in srgb, var(--rule) 70%, transparent); border-radius: 6px;
|
|
278
|
+
padding: 3px 6px; cursor: pointer;
|
|
279
|
+
}
|
|
280
|
+
select:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
|
|
281
|
+
.check { display: inline-flex; align-items: center; gap: 4px; font-size: 13px; cursor: pointer; white-space: nowrap; }
|
|
282
|
+
.check input { margin: 0; accent-color: var(--accent); cursor: inherit; }
|
|
283
|
+
.check:has(input:disabled) { opacity: 0.45; cursor: default; }
|
|
197
284
|
|
|
198
285
|
/* ---- what is selected ---- */
|
|
199
|
-
#selected { padding: 10px 16px 12px; overflow: auto; flex:
|
|
286
|
+
#selected { padding: 10px 16px 12px; overflow: auto; flex: 0 1 auto; min-height: 0; }
|
|
200
287
|
#selected .empty { margin: 0; color: var(--muted); font-size: 13px; }
|
|
201
288
|
#selected h2 { margin: 0 0 8px; font-size: 15px; font-weight: 600; color: var(--sel); overflow-wrap: anywhere; }
|
|
202
289
|
#selected h3 {
|
|
@@ -285,6 +372,10 @@ const PAGE = `<!doctype html>
|
|
|
285
372
|
<button id="copy-svg" type="button" title="Copy the picture's SVG to the clipboard">Copy SVG</button>
|
|
286
373
|
</span>
|
|
287
374
|
</span>
|
|
375
|
+
<select id="theme" aria-label="Theme" title="Draw the diagram in another theme">{{themes}}</select>
|
|
376
|
+
<span class="gap"></span>
|
|
377
|
+
<button id="toggle-panel" type="button" aria-controls="side" title="Show or hide the side panel (p)">Panel</button>
|
|
378
|
+
<label class="check" title="Show the side panel only while something is selected or the findings are open"><input id="auto-hide" type="checkbox"> Auto-hide</label>
|
|
288
379
|
<button id="toggle-findings" type="button" aria-controls="findings"></button>
|
|
289
380
|
</div>
|
|
290
381
|
</header>
|
|
@@ -314,6 +405,29 @@ const PAGE = `<!doctype html>
|
|
|
314
405
|
var view = document.getElementById('view');
|
|
315
406
|
var stage = document.getElementById('stage');
|
|
316
407
|
var svg = stage.querySelector('svg');
|
|
408
|
+
// The theme shown, which the picker changes; Save SVG, PNG and Copy SVG
|
|
409
|
+
// save the drawing in it.
|
|
410
|
+
var shownTheme = DATA.current;
|
|
411
|
+
// Another theme is carried as the colors that differ from the first, put
|
|
412
|
+
// back in place the first time it is shown.
|
|
413
|
+
var VARIES = new RegExp(DATA.varies, 'gi');
|
|
414
|
+
var first = DATA.themes[DATA.current];
|
|
415
|
+
function unpack(packed, base) {
|
|
416
|
+
if (typeof packed === 'string') return packed;
|
|
417
|
+
var pieces = base.split(VARIES);
|
|
418
|
+
for (var i = 1, j = 0; i < pieces.length; i += 2) pieces[i] = packed[j++];
|
|
419
|
+
return pieces.join('');
|
|
420
|
+
}
|
|
421
|
+
function themed(name) {
|
|
422
|
+
var view = DATA.themes[name];
|
|
423
|
+
if (!view.unpacked) {
|
|
424
|
+
view.svg = unpack(view.svg, first.svg);
|
|
425
|
+
view.tagged = unpack(view.tagged, first.tagged);
|
|
426
|
+
view.unpacked = true;
|
|
427
|
+
}
|
|
428
|
+
return view;
|
|
429
|
+
}
|
|
430
|
+
function shown() { return themed(shownTheme); }
|
|
317
431
|
var tip = document.getElementById('tip');
|
|
318
432
|
var scaleLabel = document.getElementById('scale');
|
|
319
433
|
{{panzoom}}
|
|
@@ -425,7 +539,7 @@ const PAGE = `<!doctype html>
|
|
|
425
539
|
copySvg.onclick = function () {
|
|
426
540
|
function fallback() {
|
|
427
541
|
var area = document.createElement('textarea');
|
|
428
|
-
area.value =
|
|
542
|
+
area.value = shown().svg;
|
|
429
543
|
document.body.appendChild(area);
|
|
430
544
|
area.select();
|
|
431
545
|
var done = false;
|
|
@@ -434,14 +548,14 @@ const PAGE = `<!doctype html>
|
|
|
434
548
|
flash(copySvg, done ? 'Copied' : 'Blocked');
|
|
435
549
|
}
|
|
436
550
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
437
|
-
navigator.clipboard.writeText(
|
|
551
|
+
navigator.clipboard.writeText(shown().svg).then(function () { flash(copySvg, 'Copied'); }, fallback);
|
|
438
552
|
} else fallback();
|
|
439
553
|
};
|
|
440
554
|
|
|
441
555
|
var saveSvg = document.getElementById('save-svg');
|
|
442
556
|
saveSvg.onclick = function () {
|
|
443
557
|
saveFile(saveSvg, DATA.name + '.svg', 'SVG image', 'image/svg+xml', function () {
|
|
444
|
-
return ready(new Blob([
|
|
558
|
+
return ready(new Blob([shown().svg], { type: 'image/svg+xml' }));
|
|
445
559
|
});
|
|
446
560
|
};
|
|
447
561
|
|
|
@@ -462,7 +576,7 @@ const PAGE = `<!doctype html>
|
|
|
462
576
|
canvas.toBlob(function (blob) { blob ? done(blob) : fail(); }, 'image/png');
|
|
463
577
|
};
|
|
464
578
|
image.onerror = fail;
|
|
465
|
-
image.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(
|
|
579
|
+
image.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(shown().svg);
|
|
466
580
|
});
|
|
467
581
|
}
|
|
468
582
|
savePng.onclick = function () {
|
|
@@ -509,9 +623,49 @@ const PAGE = `<!doctype html>
|
|
|
509
623
|
var toggle = document.getElementById('toggle-findings');
|
|
510
624
|
var count = DATA.findings.length;
|
|
511
625
|
|
|
626
|
+
/* ---- the side panel: shown, hidden, or shown only when wanted ---- */
|
|
627
|
+
|
|
628
|
+
// Hidden, it stays hidden whatever is selected. Auto-hide shows it only
|
|
629
|
+
// while something is selected or the findings are open. Both choices are
|
|
630
|
+
// remembered in this browser for every reladraw page.
|
|
631
|
+
var side = document.getElementById('side');
|
|
632
|
+
var panelButton = document.getElementById('toggle-panel');
|
|
633
|
+
var autoHideBox = document.getElementById('auto-hide');
|
|
634
|
+
var KEPT = 'reladraw-page-panel';
|
|
635
|
+
var panelOn = true;
|
|
636
|
+
var autoHide = false;
|
|
637
|
+
try {
|
|
638
|
+
var kept = JSON.parse(localStorage.getItem(KEPT) || 'null');
|
|
639
|
+
if (kept) { panelOn = kept.on !== false; autoHide = kept.autoHide === true; }
|
|
640
|
+
} catch (e) {}
|
|
641
|
+
function keepPanel() {
|
|
642
|
+
try { localStorage.setItem(KEPT, JSON.stringify({ on: panelOn, autoHide: autoHide })); } catch (e) {}
|
|
643
|
+
}
|
|
644
|
+
function layoutSide() {
|
|
645
|
+
side.hidden = !(panelOn && (!autoHide || selection || !panel.hidden));
|
|
646
|
+
// A finding lights what it is about only while the finding can be seen.
|
|
647
|
+
if ((panel.hidden || side.hidden) && lit && lit.length) unlight();
|
|
648
|
+
panelButton.setAttribute('aria-pressed', String(panelOn));
|
|
649
|
+
if (!toggle.disabled) toggle.setAttribute('aria-pressed', String(panelOn && !panel.hidden));
|
|
650
|
+
autoHideBox.checked = autoHide;
|
|
651
|
+
autoHideBox.disabled = !panelOn;
|
|
652
|
+
}
|
|
653
|
+
function setPanel(on) {
|
|
654
|
+
panelOn = on;
|
|
655
|
+
keepPanel();
|
|
656
|
+
layoutSide();
|
|
657
|
+
}
|
|
658
|
+
panelButton.onclick = function () { setPanel(!panelOn); };
|
|
659
|
+
autoHideBox.onchange = function () {
|
|
660
|
+
autoHide = autoHideBox.checked;
|
|
661
|
+
keepPanel();
|
|
662
|
+
layoutSide();
|
|
663
|
+
};
|
|
664
|
+
|
|
512
665
|
function showPanel(open) {
|
|
513
666
|
panel.hidden = !open;
|
|
514
667
|
toggle.setAttribute('aria-pressed', String(open));
|
|
668
|
+
layoutSide();
|
|
515
669
|
}
|
|
516
670
|
if (count === 0) {
|
|
517
671
|
toggle.textContent = 'No findings';
|
|
@@ -519,8 +673,14 @@ const PAGE = `<!doctype html>
|
|
|
519
673
|
panel.hidden = true;
|
|
520
674
|
} else {
|
|
521
675
|
toggle.textContent = count === 1 ? '1 finding' : count + ' findings';
|
|
522
|
-
|
|
523
|
-
|
|
676
|
+
// Asking for the findings shows the panel they are in.
|
|
677
|
+
toggle.onclick = function () {
|
|
678
|
+
if (!panelOn) { panelOn = true; keepPanel(); showPanel(true); }
|
|
679
|
+
else showPanel(panel.hidden);
|
|
680
|
+
};
|
|
681
|
+
// Open from the start, unless the panel is to stay out of the way.
|
|
682
|
+
panel.hidden = autoHide;
|
|
683
|
+
toggle.setAttribute('aria-pressed', String(!autoHide));
|
|
524
684
|
}
|
|
525
685
|
|
|
526
686
|
var lit = [];
|
|
@@ -593,24 +753,28 @@ const PAGE = `<!doctype html>
|
|
|
593
753
|
function nodeGroups(name) { return svg.querySelectorAll('g[data-node="' + CSS.escape(name) + '"]'); }
|
|
594
754
|
function edgeGroups(index) { return svg.querySelectorAll('g[data-edge="' + index + '"]'); }
|
|
595
755
|
|
|
596
|
-
//
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
756
|
+
// Run on each drawing the stage is given: at the start, and after a theme change.
|
|
757
|
+
function prepare() {
|
|
758
|
+
// A line is too thin to hit, so each gets an unseen wider copy along it.
|
|
759
|
+
each(svg.querySelectorAll('g[data-edge]'), function (group) {
|
|
760
|
+
each(group.querySelectorAll('line, path[fill="none"], polyline'), function (stroke) {
|
|
761
|
+
if (stroke.getAttribute('stroke-opacity') === '0') { stroke.classList.add('target'); return; }
|
|
762
|
+
var wide = stroke.cloneNode(false);
|
|
763
|
+
['marker-start', 'marker-end', 'mask', 'stroke-dasharray'].forEach(function (name) { wide.removeAttribute(name); });
|
|
764
|
+
wide.setAttribute('stroke-width', '12');
|
|
765
|
+
wide.setAttribute('pointer-events', 'stroke');
|
|
766
|
+
wide.classList.add('target');
|
|
767
|
+
stroke.parentNode.insertBefore(wide, stroke.nextSibling);
|
|
768
|
+
});
|
|
606
769
|
});
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
}
|
|
770
|
+
Object.keys(graph.nodes).forEach(function (name) {
|
|
771
|
+
if (graph.nodes[name].links.length > 0) each(nodeGroups(name), function (group) { group.classList.add('has-links'); });
|
|
772
|
+
});
|
|
773
|
+
graph.edges.forEach(function (edge, index) {
|
|
774
|
+
if (edge.links.length > 0) each(edgeGroups(index), function (group) { group.classList.add('has-links'); });
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
prepare();
|
|
614
778
|
|
|
615
779
|
function clearMarks() {
|
|
616
780
|
svg.classList.remove('selecting');
|
|
@@ -692,6 +856,7 @@ const PAGE = `<!doctype html>
|
|
|
692
856
|
}
|
|
693
857
|
|
|
694
858
|
function showSelected() {
|
|
859
|
+
layoutSide();
|
|
695
860
|
selectedPanel.textContent = '';
|
|
696
861
|
if (!selection) {
|
|
697
862
|
selectedPanel.append(element('p', 'empty', 'Click a box or a line to see its links and what it connects to.'));
|
|
@@ -725,7 +890,7 @@ const PAGE = `<!doctype html>
|
|
|
725
890
|
row.target = '_blank';
|
|
726
891
|
row.rel = 'noopener';
|
|
727
892
|
row.title = link.href;
|
|
728
|
-
row.innerHTML = link.
|
|
893
|
+
row.innerHTML = link.badges[shownTheme];
|
|
729
894
|
row.append(element('span', 'label', link.label));
|
|
730
895
|
selectedPanel.append(row);
|
|
731
896
|
});
|
|
@@ -773,17 +938,64 @@ const PAGE = `<!doctype html>
|
|
|
773
938
|
if (link && svg.contains(link)) event.preventDefault();
|
|
774
939
|
});
|
|
775
940
|
|
|
941
|
+
/* ---- the theme picker ---- */
|
|
942
|
+
|
|
943
|
+
// "Diagram's theme" follows the file, so an edit to its theme shows on the
|
|
944
|
+
// next render. Any other choice is the reader's, and holds on every reladraw
|
|
945
|
+
// page in this browser, whatever its file says, until the diagram's theme
|
|
946
|
+
// is chosen again.
|
|
947
|
+
var picker = document.getElementById('theme');
|
|
948
|
+
var schemeMeta = document.querySelector('meta[name="color-scheme"]');
|
|
949
|
+
var CHOSEN_THEME = 'reladraw-page-theme';
|
|
950
|
+
|
|
951
|
+
// The same drawing in other colors: nothing moves, so the view, the
|
|
952
|
+
// selection and the panel stay as they were.
|
|
953
|
+
function showTheme(name) {
|
|
954
|
+
if (!DATA.themes[name] || name === shownTheme) return;
|
|
955
|
+
shownTheme = name;
|
|
956
|
+
var next = shown();
|
|
957
|
+
Object.keys(next.colors).forEach(function (key) { document.documentElement.style.setProperty(key, next.colors[key]); });
|
|
958
|
+
schemeMeta.setAttribute('content', next.scheme);
|
|
959
|
+
unlight();
|
|
960
|
+
hovered = null;
|
|
961
|
+
tip.style.display = 'none';
|
|
962
|
+
stage.innerHTML = next.tagged;
|
|
963
|
+
svg = stage.querySelector('svg');
|
|
964
|
+
prepare();
|
|
965
|
+
zoom.show();
|
|
966
|
+
select(selection);
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
picker.onchange = function () {
|
|
970
|
+
try {
|
|
971
|
+
if (picker.value) localStorage.setItem(CHOSEN_THEME, picker.value);
|
|
972
|
+
else localStorage.removeItem(CHOSEN_THEME);
|
|
973
|
+
} catch (e) {}
|
|
974
|
+
showTheme(picker.value || DATA.current);
|
|
975
|
+
};
|
|
976
|
+
|
|
977
|
+
// A theme chosen on an earlier page, if this page has it.
|
|
978
|
+
try {
|
|
979
|
+
var chosenTheme = localStorage.getItem(CHOSEN_THEME);
|
|
980
|
+
if (chosenTheme && DATA.themes[chosenTheme]) {
|
|
981
|
+
picker.value = chosenTheme;
|
|
982
|
+
showTheme(chosenTheme);
|
|
983
|
+
}
|
|
984
|
+
} catch (e) {}
|
|
985
|
+
|
|
776
986
|
/* ---- keys, beyond the zoom's own ---- */
|
|
777
987
|
|
|
778
988
|
document.addEventListener('keydown', function (event) {
|
|
779
989
|
if (event.ctrlKey || event.metaKey || event.altKey) return;
|
|
780
|
-
if (event.target.closest && event.target.closest('input, textarea')) return;
|
|
781
|
-
if (event.key === '
|
|
990
|
+
if (event.target.closest && event.target.closest('input, textarea, select')) return;
|
|
991
|
+
if (event.key === 'p') setPanel(!panelOn);
|
|
992
|
+
else if (event.key === 'f' && !full.hidden) toggleFull();
|
|
782
993
|
else if (event.key === 'Escape') { showSave(false); unlight(); select(null); }
|
|
783
994
|
else return;
|
|
784
995
|
event.preventDefault();
|
|
785
996
|
});
|
|
786
997
|
|
|
998
|
+
layoutSide();
|
|
787
999
|
zoom.show();
|
|
788
1000
|
})();
|
|
789
1001
|
</script>
|
package/dist/viewer.d.ts
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* The pan and zoom shared by the page `render --page` writes and the
|
|
3
3
|
* playground, so the two look at a diagram the same way: − and +, Fit, 100%,
|
|
4
4
|
* the zoom always shown and saying "Fit" while fitted, the wheel, a pinch, a
|
|
5
|
-
* drag, a double-click,
|
|
5
|
+
* drag, a double-click, the keys + - 0 1, h j k l to pan, and g to grab the
|
|
6
|
+
* diagram so the arrows pan it too.
|
|
6
7
|
*
|
|
7
8
|
* It is browser code kept as a string, since it runs in a page, not here: the
|
|
8
9
|
* page writes it into its script, and the playground's build writes it into
|
package/dist/viewer.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* The pan and zoom shared by the page `render --page` writes and the
|
|
3
3
|
* playground, so the two look at a diagram the same way: − and +, Fit, 100%,
|
|
4
4
|
* the zoom always shown and saying "Fit" while fitted, the wheel, a pinch, a
|
|
5
|
-
* drag, a double-click,
|
|
5
|
+
* drag, a double-click, the keys + - 0 1, h j k l to pan, and g to grab the
|
|
6
|
+
* diagram so the arrows pan it too.
|
|
6
7
|
*
|
|
7
8
|
* It is browser code kept as a string, since it runs in a page, not here: the
|
|
8
9
|
* page writes it into its script, and the playground's build writes it into
|
|
@@ -169,9 +170,57 @@ function panZoom(view, stage, label, buttons) {
|
|
|
169
170
|
// The view's own size, not the window's: a dragged divider changes it too.
|
|
170
171
|
new ResizeObserver(refit).observe(view);
|
|
171
172
|
|
|
173
|
+
function typing(target) {
|
|
174
|
+
return !!(target && target.closest && target.closest('input, textarea, select, [contenteditable]'));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// g grabs the diagram, and while it is held the arrows pan it; g again or
|
|
178
|
+
// Escape lets go, and the arrows go back to whatever else uses them. The
|
|
179
|
+
// keys are taken before the page's own handlers see them, so a held
|
|
180
|
+
// diagram's arrows don't also move a list or a divider, and its Escape lets
|
|
181
|
+
// go without also clearing what is selected.
|
|
182
|
+
var held = false;
|
|
183
|
+
var hint = document.createElement('div');
|
|
184
|
+
hint.className = 'grab-hint';
|
|
185
|
+
hint.textContent = 'Arrows or h j k l pan · Shift for bigger steps · g or Esc to let go';
|
|
186
|
+
hint.hidden = true;
|
|
187
|
+
view.appendChild(hint);
|
|
188
|
+
|
|
189
|
+
function grab(on) {
|
|
190
|
+
held = on;
|
|
191
|
+
hint.hidden = !on;
|
|
192
|
+
view.classList.toggle('grabbed', on);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
var ARROWS = { ArrowLeft: [1, 0], ArrowRight: [-1, 0], ArrowUp: [0, 1], ArrowDown: [0, -1] };
|
|
196
|
+
// h j k l pan as the arrows do, without a grab: nothing else uses them.
|
|
197
|
+
var LETTERS = { h: [1, 0], l: [-1, 0], k: [0, 1], j: [0, -1] };
|
|
198
|
+
|
|
199
|
+
document.addEventListener('keydown', function (event) {
|
|
200
|
+
if (event.ctrlKey || event.metaKey || event.altKey) return;
|
|
201
|
+
if (typing(event.target)) return;
|
|
202
|
+
var arrow = (held && ARROWS[event.key]) || LETTERS[event.key.toLowerCase()];
|
|
203
|
+
if (event.key === 'g' || event.key === 'G') grab(!held);
|
|
204
|
+
else if (held && event.key === 'Escape') grab(false);
|
|
205
|
+
else if (arrow) {
|
|
206
|
+
// A tenth of the view a press, half of it with Shift; held down, the
|
|
207
|
+
// key repeats.
|
|
208
|
+
var room = size();
|
|
209
|
+
var part = event.shiftKey ? 0.5 : 0.1;
|
|
210
|
+
pan(arrow[0] * room.w * part, arrow[1] * room.h * part);
|
|
211
|
+
} else return;
|
|
212
|
+
event.preventDefault();
|
|
213
|
+
event.stopImmediatePropagation();
|
|
214
|
+
}, true);
|
|
215
|
+
|
|
216
|
+
// Starting to type lets go, so the arrows move the cursor again.
|
|
217
|
+
document.addEventListener('focusin', function (event) {
|
|
218
|
+
if (held && typing(event.target)) grab(false);
|
|
219
|
+
});
|
|
220
|
+
|
|
172
221
|
document.addEventListener('keydown', function (event) {
|
|
173
222
|
if (event.ctrlKey || event.metaKey || event.altKey) return;
|
|
174
|
-
if (event.target
|
|
223
|
+
if (typing(event.target)) return;
|
|
175
224
|
if (event.key === '+' || event.key === '=') zoomMiddle(1.25);
|
|
176
225
|
else if (event.key === '-' || event.key === '_') zoomMiddle(0.8);
|
|
177
226
|
else if (event.key === '0') fit();
|