@caddis/cli 0.4.47 → 0.4.49
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/bundles/antigravity-plugin/plugin.json +1 -1
- package/bundles/antigravity-plugin/skills/flow-studio/SKILL.md +2 -2
- package/bundles/antigravity-plugin/skills/flow-studio/model.schema.json +1 -1
- package/bundles/antigravity-plugin/skills/flow-studio/references/design-language.md +5 -5
- package/bundles/antigravity-plugin/skills/flow-studio/references/model-schema.md +1 -0
- package/bundles/antigravity-plugin/skills/flow-studio/scripts/lib/styles.test.mjs +13 -4
- package/bundles/antigravity-plugin/skills/flow-studio/src/chrome.ts +9 -6
- package/bundles/antigravity-plugin/skills/flow-studio/src/controls.ts +1 -1
- package/bundles/antigravity-plugin/skills/flow-studio/src/model.test.ts +8 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/model.ts +2 -1
- package/bundles/antigravity-plugin/skills/flow-studio/src/notes.ts +1 -1
- package/bundles/antigravity-plugin/skills/flow-studio/src/playbackui.ts +1 -1
- package/bundles/antigravity-plugin/skills/flow-studio/src/rules.test.ts +10 -1
- package/bundles/antigravity-plugin/skills/flow-studio/src/rules.ts +6 -0
- package/bundles/antigravity-plugin/skills/flow-studio/src/styles.css +1 -1
- package/bundles/antigravity-plugin/skills/flow-studio/templates/engine.html +20 -10
- package/bundles/antigravity-plugin/skills/flow-studio/templates/flow-studio.css +1 -1
- package/bundles/antigravity-plugin/skills/flow-studio/templates/flow-studio.lib.mjs +21 -9
- package/bundles/antigravity-plugin/skills/flow-studio/templates/shell-body.html +1 -1
- package/bundles/codex/.codex/skills/frontend/flow-studio/SKILL.md +2 -2
- package/bundles/codex/.codex/skills/frontend/flow-studio/model.schema.json +1 -1
- package/bundles/codex/.codex/skills/frontend/flow-studio/references/design-language.md +5 -5
- package/bundles/codex/.codex/skills/frontend/flow-studio/references/model-schema.md +1 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/scripts/lib/styles.test.mjs +13 -4
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/chrome.ts +9 -6
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/controls.ts +1 -1
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/model.test.ts +8 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/model.ts +2 -1
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/notes.ts +1 -1
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/playbackui.ts +1 -1
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/rules.test.ts +10 -1
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/rules.ts +6 -0
- package/bundles/codex/.codex/skills/frontend/flow-studio/src/styles.css +1 -1
- package/bundles/codex/.codex/skills/frontend/flow-studio/templates/engine.html +20 -10
- package/bundles/codex/.codex/skills/frontend/flow-studio/templates/flow-studio.css +1 -1
- package/bundles/codex/.codex/skills/frontend/flow-studio/templates/flow-studio.lib.mjs +21 -9
- package/bundles/codex/.codex/skills/frontend/flow-studio/templates/shell-body.html +1 -1
- package/bundles/manifest.json +2 -2
- package/package.json +1 -1
|
@@ -70,13 +70,13 @@ In either route, derive node and link IDs from immutable business keys. Preserve
|
|
|
70
70
|
8. **Short titles**: titles fit one line at column width. Long descriptions belong in subtitles or member lists.
|
|
71
71
|
9. **Fixed node dimensions**: node kinds use fixed box sizes so font rendering does not shift layout.
|
|
72
72
|
10. **Reduced motion**: removes all animations while preserving static borders and status glows.
|
|
73
|
-
11. **Thin links, dotted canvas**: links are hairlines (about
|
|
73
|
+
11. **Thin links, dotted canvas**: links are hairlines (about 0.8 px, with a 6 px arrowhead) and the canvas background is faint dots, not a grid. Both come from tokens (`--edge-w`, `--dot`, ...), so an app can change them with one CSS line; see `references/design-language.md`.
|
|
74
74
|
|
|
75
75
|
## Model summary
|
|
76
76
|
|
|
77
77
|
```
|
|
78
78
|
version 1, meta {title, help, asOf, source, labels?}, layout "columns-lanes" | "free" | "auto"
|
|
79
|
-
columns [{id, title, short?, width, phase?}] stages, left to right
|
|
79
|
+
columns [{id, title, short?, width, phase?, continues?}] stages, left to right
|
|
80
80
|
lanes [{id, title, badge?, badgeTone?, note?, gap?}] rows; gap true = dashed amber lane
|
|
81
81
|
states {id: {word, tone, border, edge, motion, bucket}} tone: ok|warn|crit|info|muted|accent; bucket: good|neutral|gap
|
|
82
82
|
nodes [{id, kind, lane?, col?, colSpan?, row?, x?, y?, w?, h?, state, title, subtitle?, members?, weight?,
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"type": "object", "required": ["id", "title", "width"], "additionalProperties": false,
|
|
27
27
|
"properties": {
|
|
28
28
|
"id": { "type": "string" }, "title": { "type": "string" }, "short": { "type": "string" },
|
|
29
|
-
"width": { "type": "number", "minimum": 60 }, "phase": { "type": "string" }
|
|
29
|
+
"width": { "type": "number", "minimum": 60 }, "phase": { "type": "string" }, "continues": { "type": "boolean" }
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
},
|
|
@@ -28,11 +28,11 @@ Tokens are defined on `:root` and adapted for dark mode via `@media (prefers-col
|
|
|
28
28
|
| `--dot` | `rgba(31,35,40,.18)` | `rgba(255,255,255,.16)` | Colour of the canvas background dots |
|
|
29
29
|
| `--dot-size` | `1.2px` | Same | Radius of a background dot |
|
|
30
30
|
| `--dot-gap` | `24px` | Same | Spacing of the background dots |
|
|
31
|
-
| `--edge-w` | `
|
|
32
|
-
| `--edge-w-hot` | `
|
|
33
|
-
| `--edge-w-play` | `1.
|
|
34
|
-
| `--trail-w` | `1.
|
|
35
|
-
| `--ripple-w` | `
|
|
31
|
+
| `--edge-w` | `0.8px` | Same | Width of a normal link (thin, like a hairline; motion carries the state) |
|
|
32
|
+
| `--edge-w-hot` | `1px` | Same | Width of a traced or selected link |
|
|
33
|
+
| `--edge-w-play` | `1.2px` | Same | Width of a link while playback runs along it |
|
|
34
|
+
| `--trail-w` | `1.3px` | Same | Width of the draw-in trail on a selected path (divided by the zoom) |
|
|
35
|
+
| `--ripple-w` | `1.5px` | Same | Width of the what-if ripple (divided by the zoom) |
|
|
36
36
|
| `--ok-bg` | `rgba(26,127,55,.10)` | `rgba(63,185,80,.12)` | Subtle tinted background for healthy states |
|
|
37
37
|
| `--warn-bg` | `rgba(154,103,0,.10)` | `rgba(210,153,34,.14)` | Subtle tinted background for warning states |
|
|
38
38
|
| `--crit-bg` | `rgba(207,34,46,.08)` | `rgba(248,81,73,.12)` | Subtle tinted background for critical states |
|
|
@@ -48,6 +48,7 @@ Columns provide vertical partitioning for time, stages, or architecture tiers.
|
|
|
48
48
|
| `short` | `string` | Optional | - | Compact abbreviated title for tight viewports. |
|
|
49
49
|
| `width` | `number` | Required | - | Column width in pixels (minimum 60). |
|
|
50
50
|
| `phase` | `string` | Optional | - | Super-category phase grouping shown in top banner. |
|
|
51
|
+
| `continues` | `boolean` | Optional | `false` | This column is a wrapped continuation of the column before it (for example one long list laid out in three side-by-side columns). It shows no header label and no strip cell of its own: its nodes count into the cell of the column it continues. Not allowed on the first column. |
|
|
51
52
|
|
|
52
53
|
---
|
|
53
54
|
|
|
@@ -22,14 +22,23 @@ test("dots are set by tokens: size, spacing and a colour for light and dark", ()
|
|
|
22
22
|
});
|
|
23
23
|
|
|
24
24
|
test("links are thin by default and every width comes from a token", () => {
|
|
25
|
-
assert.match(css, /--edge-w:\s*
|
|
25
|
+
assert.match(css, /--edge-w:\s*0\.8px/);
|
|
26
26
|
assert.match(css, /path\.edge \{ fill:none; stroke-width:var\(--edge-w\)/);
|
|
27
27
|
assert.match(css, /path\.edge\.hot \{[^}]*stroke-width:var\(--edge-w-hot\)/);
|
|
28
28
|
assert.match(css, /path\.trail \{[^}]*stroke-width:calc\(var\(--trail-w\)/);
|
|
29
29
|
assert.match(css, /path\.ripple \{[^}]*stroke-width:calc\(var\(--ripple-w\)/);
|
|
30
|
-
// Thin means at most 1
|
|
31
|
-
const w = (name) => Number(css.match(new RegExp(`${name}
|
|
32
|
-
assert.ok(w("--edge-w") <= 1
|
|
30
|
+
// Thin means a hairline: at most 1 px for a plain link, 1.2 px for a highlighted or playing one.
|
|
31
|
+
const w = (name) => Number(css.match(new RegExp(`${name}: *([0-9.]+)px`))[1]);
|
|
32
|
+
assert.ok(w("--edge-w") <= 1 && w("--edge-w-hot") <= 1.2 && w("--edge-w-play") <= 1.2 && w("--ripple-w") <= 1.5 && w("--trail-w") <= 1.5);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("the arrowhead is small: at most 7 px, and the playback dot at most 5 px in radius", () => {
|
|
36
|
+
const shell = readFileSync(join(root, "templates/shell-body.html"), "utf8");
|
|
37
|
+
const marker = shell.match(/<marker id="arr"[^>]*markerWidth="(\d+)" markerHeight="(\d+)"/);
|
|
38
|
+
assert.ok(marker, "the arrow marker exists");
|
|
39
|
+
assert.ok(Number(marker[1]) <= 7 && Number(marker[2]) <= 7, `arrowhead is ${marker[1]} x ${marker[2]}`);
|
|
40
|
+
const playback = readFileSync(join(root, "src/playbackui.ts"), "utf8");
|
|
41
|
+
assert.ok(Number(playback.match(/class: "token", r: "(\d+(?:\.\d+)?)"/)[1]) <= 5);
|
|
33
42
|
});
|
|
34
43
|
|
|
35
44
|
test("forced-colours mode drops the dot pattern", () => {
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { el, on, type Ctx } from "./ctx.ts";
|
|
5
5
|
import { esc } from "./model.ts";
|
|
6
6
|
import { DASH, toneVar, cssId } from "./stateCss.ts";
|
|
7
|
-
import { bucketOf, columnTitle, effectiveLinks, nodeStateId, nodeVisible } from "./rules.ts";
|
|
7
|
+
import { bucketOf, columnTitle, effectiveLinks, nodeStateId, nodeVisible, stripHeads } from "./rules.ts";
|
|
8
8
|
import { matches } from "./interact.ts";
|
|
9
9
|
|
|
10
10
|
const DEFAULT_LABELS: Record<string, string> = {
|
|
@@ -34,16 +34,19 @@ export function install(ctx: Ctx): void {
|
|
|
34
34
|
|
|
35
35
|
function buildStrip(): void {
|
|
36
36
|
const box = els.strip;
|
|
37
|
+
// One cell per head column: a column that `continues` another counts into the cell before it.
|
|
38
|
+
const heads = stripHeads(model), cells = model.columns.map((c, i) => ({ c, i })).filter(({ i }) => heads[i] === i);
|
|
37
39
|
box.style.display = model.columns.length ? "" : "none";
|
|
38
|
-
box.style.gridTemplateColumns = `repeat(${Math.max(1,
|
|
40
|
+
box.style.gridTemplateColumns = `repeat(${Math.max(1, cells.length)}, minmax(0, 1fr))`;
|
|
39
41
|
if (!box.firstChild) {
|
|
40
|
-
box.innerHTML =
|
|
42
|
+
box.innerHTML = cells.map(({ c }) => `<div class="lcell"><div class="ln">${esc(c.title)}</div><div class="lb"><i class="good"></i><i class="neutral"></i><i class="gap"></i></div><div class="lv"></div></div>`).join("");
|
|
41
43
|
}
|
|
42
|
-
box.querySelectorAll<HTMLElement>(".lcell").forEach((cell,
|
|
43
|
-
|
|
44
|
+
box.querySelectorAll<HTMLElement>(".lcell").forEach((cell, cellIndex) => {
|
|
45
|
+
const i = cells[cellIndex]?.i ?? cellIndex;
|
|
46
|
+
cell.style.setProperty("--i", String(cellIndex));
|
|
44
47
|
let tot = 0, good = 0, neutral = 0, gap = 0;
|
|
45
48
|
for (const n of model.nodes) {
|
|
46
|
-
if (n.col !== i || n.kind === "chip") continue;
|
|
49
|
+
if (heads[n.col ?? -1] !== i || n.kind === "chip") continue;
|
|
47
50
|
if (!visible(n.id)) continue;
|
|
48
51
|
const w = Math.max(1, n.weight ?? 1);
|
|
49
52
|
tot += w;
|
|
@@ -71,7 +71,7 @@ export function install(ctx: Ctx): void {
|
|
|
71
71
|
const cols = els.cols;
|
|
72
72
|
cols.style.display = model.columns.length ? "" : "none";
|
|
73
73
|
if (!cols.firstChild) {
|
|
74
|
-
model.columns.forEach((c, i) => { const h = el("div", "hd"); h.textContent = (c.short ?? c.title).toUpperCase(); h.title = c.title; h.dataset.i = String(i); cols.appendChild(h); });
|
|
74
|
+
model.columns.forEach((c, i) => { const h = el("div", "hd"); h.textContent = c.continues ? "" : (c.short ?? c.title).toUpperCase(); h.title = c.title; h.dataset.i = String(i); cols.appendChild(h); });
|
|
75
75
|
let i = 0;
|
|
76
76
|
while (i < model.columns.length) {
|
|
77
77
|
const name = model.columns[i]?.phase;
|
|
@@ -165,3 +165,11 @@ test("group containment validates ids, ownership, self-reference, and skips grou
|
|
|
165
165
|
assert.ok(found.includes("contains-undefined"));
|
|
166
166
|
assert.ok(found.includes("contains-duplicate"));
|
|
167
167
|
});
|
|
168
|
+
test("a column may continue the one before it", () => {
|
|
169
|
+
const m = base(); m.columns.push({ id: "c2", title: "B", width: 150, continues: true });
|
|
170
|
+
assert.deepEqual(codes(m), []);
|
|
171
|
+
});
|
|
172
|
+
test("the first column cannot continue a column that does not exist", () => {
|
|
173
|
+
const m = base(); m.columns[0]!.continues = true;
|
|
174
|
+
assert.ok(codes(m).includes("column-continues-first"));
|
|
175
|
+
});
|
|
@@ -16,7 +16,7 @@ export interface StateDef {
|
|
|
16
16
|
motion: Motion;
|
|
17
17
|
bucket: Bucket;
|
|
18
18
|
}
|
|
19
|
-
export interface ColumnDef { id: string; title: string; short?: string; width: number; phase?: string }
|
|
19
|
+
export interface ColumnDef { id: string; title: string; short?: string; width: number; phase?: string; continues?: boolean }
|
|
20
20
|
export interface LaneDef { id: string; title: string; badge?: string; badgeTone?: "ok" | "warn" | "muted"; note?: string; gap?: boolean }
|
|
21
21
|
export interface NodeDef {
|
|
22
22
|
id: string;
|
|
@@ -111,6 +111,7 @@ export function validateModel(input: unknown): Issue[] {
|
|
|
111
111
|
if (!Array.isArray(m.lanes) || (m.layout !== "auto" && m.lanes.length === 0)) add("error", "lanes-missing", "lanes", "At least one lane is required.");
|
|
112
112
|
if (!m.states || typeof m.states !== "object" || Object.keys(m.states).length === 0) add("error", "states-missing", "states", "At least one state is required.");
|
|
113
113
|
const columns = Array.isArray(m.columns) ? m.columns : [];
|
|
114
|
+
if (columns[0]?.continues) add("error", "column-continues-first", "columns[0]", "The first column cannot continue a column before it.");
|
|
114
115
|
const lanes = Array.isArray(m.lanes) ? m.lanes : [];
|
|
115
116
|
const states = m.states && typeof m.states === "object" ? m.states : {};
|
|
116
117
|
if (!m.meta || typeof m.meta !== "object" || typeof m.meta.title !== "string") add("error", "meta-missing", "meta", "meta with a title is required.");
|
|
@@ -70,7 +70,7 @@ export function install(ctx: Ctx): void {
|
|
|
70
70
|
const markdown = (): string => {
|
|
71
71
|
const out = [`# ${model.meta.title}`, "", model.meta.help, "", `As of ${model.meta.asOf}. Source: ${model.meta.source}`, "", "## States"];
|
|
72
72
|
for (const [id, s] of Object.entries(model.states)) out.push(`- **${s.word}** (${id}): tone ${s.tone}, border ${s.border}, link ${s.edge}, motion ${s.motion}, bucket ${s.bucket}`);
|
|
73
|
-
out.push("", "## Columns", ...model.columns.map((c, i) => `${i + 1}. ${c.title}${c.phase ? ` (${c.phase})` : ""}`));
|
|
73
|
+
out.push("", "## Columns", ...model.columns.filter(c => !c.continues).map((c, i) => `${i + 1}. ${c.title}${c.phase ? ` (${c.phase})` : ""}`));
|
|
74
74
|
out.push("", "## Lanes", ...model.lanes.map(l => `- ${l.title}${l.badge ? ` [${l.badge}]` : ""}${l.note ? `: ${l.note}` : ""}`));
|
|
75
75
|
out.push("", `## Size`, `${model.nodes.length} nodes, ${model.links.length} links.`);
|
|
76
76
|
if (notes.length) out.push("", "## Notes", ...notes.map(n => `${n.id}. **${n.title}**: ${n.text}`));
|
|
@@ -23,7 +23,7 @@ export function install(ctx: Ctx): PlaybackController {
|
|
|
23
23
|
// Only the elements marked in the last draw are cleared again, so a frame does not walk the whole graph.
|
|
24
24
|
let markedEdges: SVGPathElement[] = [];
|
|
25
25
|
let markedNodes: HTMLElement[] = [];
|
|
26
|
-
const token = sv("circle", { id: "token", class: "token", r: "
|
|
26
|
+
const token = sv("circle", { id: "token", class: "token", r: "4" }) as SVGCircleElement;
|
|
27
27
|
e.layer.appendChild(token);
|
|
28
28
|
const pathOf = (link: string): SVGPathElement | undefined => ctx.edgeEls[ctx.links.findIndex(l => l.id === link)];
|
|
29
29
|
const nodeOf = (id: string) => ctx.nodeEls.get(id);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { test } from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
3
|
import type { Model } from "./model.ts";
|
|
4
|
-
import { baselineView, defaultView, nodeVisible, nodeStateId, linkVisible, linkStateId, isNew, isImproved, bucketOf, canonOf, effectiveLinks, initialCollapsed, columnTitle, toggleGroupState } from "./rules.ts";
|
|
4
|
+
import { baselineView, defaultView, nodeVisible, nodeStateId, linkVisible, linkStateId, isNew, isImproved, bucketOf, canonOf, effectiveLinks, initialCollapsed, columnTitle, toggleGroupState, stripHeads } from "./rules.ts";
|
|
5
5
|
|
|
6
6
|
const m = (): Model => ({
|
|
7
7
|
version: 1,
|
|
@@ -133,3 +133,12 @@ test("toggleGroup selects a visible child on expansion and its group on collapse
|
|
|
133
133
|
assert.equal(folded.collapsed.has("group"), true);
|
|
134
134
|
assert.equal(toggleGroupState(mm, "full", starting, "missing"), null);
|
|
135
135
|
});
|
|
136
|
+
|
|
137
|
+
test("stripHeads: a continuing column joins the one before it", () => {
|
|
138
|
+
const mm = m();
|
|
139
|
+
mm.columns = [{ id: "a", title: "A", width: 100 }, { id: "p", title: "P", width: 100 }, { id: "p2", title: "P", width: 100, continues: true }, { id: "p3", title: "P", width: 100, continues: true }, { id: "z", title: "Z", width: 100 }];
|
|
140
|
+
assert.deepEqual(stripHeads(mm), [0, 1, 1, 1, 4]);
|
|
141
|
+
});
|
|
142
|
+
test("stripHeads: with no continuing column every column is its own head", () => {
|
|
143
|
+
assert.deepEqual(stripHeads(m()), [0, 1]);
|
|
144
|
+
});
|
|
@@ -10,6 +10,12 @@ export const defaultView = (m: Model): string => (m.views?.find(v => v.default)
|
|
|
10
10
|
/** A group that contains nodes starts folded unless it explicitly opts out. */
|
|
11
11
|
export const initialCollapsed = (m: Model): Set<string> => new Set(m.nodes
|
|
12
12
|
.filter(n => n.kind === "group" && n.contains && n.collapsed !== false).map(n => n.id));
|
|
13
|
+
/** For each column, the index of the column whose header and strip cell it shares: itself, or (when it
|
|
14
|
+
* `continues` another) the nearest earlier column that does not. */
|
|
15
|
+
export const stripHeads = (m: Model): number[] => {
|
|
16
|
+
let head = 0;
|
|
17
|
+
return m.columns.map((c, i) => { if (!c.continues || i === 0) head = i; return head; });
|
|
18
|
+
};
|
|
13
19
|
export const columnTitle = (m: Model, n: NodeDef): string => n.col === undefined ? "" : m.columns[n.col]?.title ?? "";
|
|
14
20
|
|
|
15
21
|
export function groupFor(m: Model, nodeId: string): NodeDef | undefined {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
:root {
|
|
2
2
|
--bg:#f6f8fa; --panel:#ffffff; --panel2:#f6f8fa; --edge:#d0d7de; --rule:#d8dee4; --fg:#1f2328; --muted:#59636e;
|
|
3
3
|
--accent:#0969da; --accent-solid:#0969da; --ok:#1a7f37; --warn:#9a6700; --crit:#cf222e; --glow:rgba(9,105,218,.22);
|
|
4
|
-
--up:#0a7ea4; --down:#8250df; --shadow:0 1px 0 rgba(31,35,40,.04), 0 6px 18px rgba(31,35,40,.08); --grid:rgba(31,35,40,.035); --dot:rgba(31,35,40,.18); --dot-size:1.2px; --dot-gap:24px; --edge-w:
|
|
4
|
+
--up:#0a7ea4; --down:#8250df; --shadow:0 1px 0 rgba(31,35,40,.04), 0 6px 18px rgba(31,35,40,.08); --grid:rgba(31,35,40,.035); --dot:rgba(31,35,40,.18); --dot-size:1.2px; --dot-gap:24px; --edge-w:0.8px; --edge-w-hot:1px; --edge-w-play:1.2px; --trail-w:1.3px; --ripple-w:1.5px;
|
|
5
5
|
--warn-bg:rgba(154,103,0,.10); --ok-bg:rgba(26,127,55,.10); --crit-bg:rgba(207,34,46,.08); --info-bg:rgba(9,105,218,.10);
|
|
6
6
|
--mono:"IBM Plex Mono", ui-monospace, "Cascadia Mono", Consolas, Menlo, monospace;
|
|
7
7
|
}
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
<style>:root {
|
|
16
16
|
--bg:#f6f8fa; --panel:#ffffff; --panel2:#f6f8fa; --edge:#d0d7de; --rule:#d8dee4; --fg:#1f2328; --muted:#59636e;
|
|
17
17
|
--accent:#0969da; --accent-solid:#0969da; --ok:#1a7f37; --warn:#9a6700; --crit:#cf222e; --glow:rgba(9,105,218,.22);
|
|
18
|
-
--up:#0a7ea4; --down:#8250df; --shadow:0 1px 0 rgba(31,35,40,.04), 0 6px 18px rgba(31,35,40,.08); --grid:rgba(31,35,40,.035); --dot:rgba(31,35,40,.18); --dot-size:1.2px; --dot-gap:24px; --edge-w:
|
|
18
|
+
--up:#0a7ea4; --down:#8250df; --shadow:0 1px 0 rgba(31,35,40,.04), 0 6px 18px rgba(31,35,40,.08); --grid:rgba(31,35,40,.035); --dot:rgba(31,35,40,.18); --dot-size:1.2px; --dot-gap:24px; --edge-w:0.8px; --edge-w-hot:1px; --edge-w-play:1.2px; --trail-w:1.3px; --ripple-w:1.5px;
|
|
19
19
|
--warn-bg:rgba(154,103,0,.10); --ok-bg:rgba(26,127,55,.10); --crit-bg:rgba(207,34,46,.08); --info-bg:rgba(9,105,218,.10);
|
|
20
20
|
--mono:"IBM Plex Mono", ui-monospace, "Cascadia Mono", Consolas, Menlo, monospace;
|
|
21
21
|
}
|
|
@@ -229,7 +229,7 @@
|
|
|
229
229
|
<div class="workspace" id="workspace">
|
|
230
230
|
<section class="canvaswrap">
|
|
231
231
|
<div id="viewport" tabindex="0" role="application" aria-label="Flow canvas. Scroll to zoom, drag to pan. Keys: V select, H hand, plus and minus zoom, 0 fit, 1 actual size, F zoom to selection, arrows pan.">
|
|
232
|
-
<div id="world"><svg id="edges" aria-hidden="true"><defs><marker id="arr" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="
|
|
232
|
+
<div id="world"><svg id="edges" aria-hidden="true"><defs><marker id="arr" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="6" markerHeight="6" markerUnits="userSpaceOnUse" orient="auto"><path d="M0 0 L8 4 L0 8 z" fill="context-stroke"/></marker></defs><g id="edgeLayer"></g></svg></div>
|
|
233
233
|
<div id="cols" aria-hidden="true"></div>
|
|
234
234
|
<div id="tools" role="toolbar" aria-label="Canvas controls" aria-orientation="vertical"></div>
|
|
235
235
|
<div id="zoomLabel" aria-hidden="true">100%</div>
|
|
@@ -286,6 +286,7 @@
|
|
|
286
286
|
if (!Array.isArray(m.lanes) || m.layout !== "auto" && m.lanes.length === 0) add("error", "lanes-missing", "lanes", "At least one lane is required.");
|
|
287
287
|
if (!m.states || typeof m.states !== "object" || Object.keys(m.states).length === 0) add("error", "states-missing", "states", "At least one state is required.");
|
|
288
288
|
const columns = Array.isArray(m.columns) ? m.columns : [];
|
|
289
|
+
if (columns[0]?.continues) add("error", "column-continues-first", "columns[0]", "The first column cannot continue a column before it.");
|
|
289
290
|
const lanes = Array.isArray(m.lanes) ? m.lanes : [];
|
|
290
291
|
const states = m.states && typeof m.states === "object" ? m.states : {};
|
|
291
292
|
if (!m.meta || typeof m.meta !== "object" || typeof m.meta.title !== "string") add("error", "meta-missing", "meta", "meta with a title is required.");
|
|
@@ -591,6 +592,13 @@
|
|
|
591
592
|
var baselineView = (m) => m.views?.[0]?.id ?? "all";
|
|
592
593
|
var defaultView = (m) => (m.views?.find((v) => v.default) ?? m.views?.[0])?.id ?? "all";
|
|
593
594
|
var initialCollapsed = (m) => new Set(m.nodes.filter((n) => n.kind === "group" && n.contains && n.collapsed !== false).map((n) => n.id));
|
|
595
|
+
var stripHeads = (m) => {
|
|
596
|
+
let head = 0;
|
|
597
|
+
return m.columns.map((c, i) => {
|
|
598
|
+
if (!c.continues || i === 0) head = i;
|
|
599
|
+
return head;
|
|
600
|
+
});
|
|
601
|
+
};
|
|
594
602
|
var columnTitle = (m, n) => n.col === void 0 ? "" : m.columns[n.col]?.title ?? "";
|
|
595
603
|
function groupFor(m, nodeId) {
|
|
596
604
|
return m.nodes.find((n) => n.kind === "group" && n.contains?.includes(nodeId));
|
|
@@ -1415,7 +1423,7 @@
|
|
|
1415
1423
|
if (!cols.firstChild) {
|
|
1416
1424
|
model.columns.forEach((c, i2) => {
|
|
1417
1425
|
const h = el("div", "hd");
|
|
1418
|
-
h.textContent = (c.short ?? c.title).toUpperCase();
|
|
1426
|
+
h.textContent = c.continues ? "" : (c.short ?? c.title).toUpperCase();
|
|
1419
1427
|
h.title = c.title;
|
|
1420
1428
|
h.dataset.i = String(i2);
|
|
1421
1429
|
cols.appendChild(h);
|
|
@@ -1801,16 +1809,18 @@
|
|
|
1801
1809
|
}
|
|
1802
1810
|
function buildStrip() {
|
|
1803
1811
|
const box = els.strip;
|
|
1812
|
+
const heads = stripHeads(model), cells = model.columns.map((c, i) => ({ c, i })).filter(({ i }) => heads[i] === i);
|
|
1804
1813
|
box.style.display = model.columns.length ? "" : "none";
|
|
1805
|
-
box.style.gridTemplateColumns = `repeat(${Math.max(1,
|
|
1814
|
+
box.style.gridTemplateColumns = `repeat(${Math.max(1, cells.length)}, minmax(0, 1fr))`;
|
|
1806
1815
|
if (!box.firstChild) {
|
|
1807
|
-
box.innerHTML =
|
|
1816
|
+
box.innerHTML = cells.map(({ c }) => `<div class="lcell"><div class="ln">${esc(c.title)}</div><div class="lb"><i class="good"></i><i class="neutral"></i><i class="gap"></i></div><div class="lv"></div></div>`).join("");
|
|
1808
1817
|
}
|
|
1809
|
-
box.querySelectorAll(".lcell").forEach((cell,
|
|
1810
|
-
|
|
1818
|
+
box.querySelectorAll(".lcell").forEach((cell, cellIndex) => {
|
|
1819
|
+
const i = cells[cellIndex]?.i ?? cellIndex;
|
|
1820
|
+
cell.style.setProperty("--i", String(cellIndex));
|
|
1811
1821
|
let tot = 0, good = 0, neutral = 0, gap = 0;
|
|
1812
1822
|
for (const n of model.nodes) {
|
|
1813
|
-
if (n.col !== i || n.kind === "chip") continue;
|
|
1823
|
+
if (heads[n.col ?? -1] !== i || n.kind === "chip") continue;
|
|
1814
1824
|
if (!visible(n.id)) continue;
|
|
1815
1825
|
const w = Math.max(1, n.weight ?? 1);
|
|
1816
1826
|
tot += w;
|
|
@@ -2029,7 +2039,7 @@
|
|
|
2029
2039
|
const markdown = () => {
|
|
2030
2040
|
const out = [`# ${model.meta.title}`, "", model.meta.help, "", `As of ${model.meta.asOf}. Source: ${model.meta.source}`, "", "## States"];
|
|
2031
2041
|
for (const [id, s] of Object.entries(model.states)) out.push(`- **${s.word}** (${id}): tone ${s.tone}, border ${s.border}, link ${s.edge}, motion ${s.motion}, bucket ${s.bucket}`);
|
|
2032
|
-
out.push("", "## Columns", ...model.columns.map((c, i) => `${i + 1}. ${c.title}${c.phase ? ` (${c.phase})` : ""}`));
|
|
2042
|
+
out.push("", "## Columns", ...model.columns.filter((c) => !c.continues).map((c, i) => `${i + 1}. ${c.title}${c.phase ? ` (${c.phase})` : ""}`));
|
|
2033
2043
|
out.push("", "## Lanes", ...model.lanes.map((l) => `- ${l.title}${l.badge ? ` [${l.badge}]` : ""}${l.note ? `: ${l.note}` : ""}`));
|
|
2034
2044
|
out.push("", `## Size`, `${model.nodes.length} nodes, ${model.links.length} links.`);
|
|
2035
2045
|
if (notes.length) out.push("", "## Notes", ...notes.map((n) => `${n.id}. **${n.title}**: ${n.text}`));
|
|
@@ -2173,7 +2183,7 @@
|
|
|
2173
2183
|
let t = 0, idx = -1, playing = false, frame = 0, timer = 0, stamp = 0, choicesKey = "";
|
|
2174
2184
|
let markedEdges = [];
|
|
2175
2185
|
let markedNodes = [];
|
|
2176
|
-
const token = sv("circle", { id: "token", class: "token", r: "
|
|
2186
|
+
const token = sv("circle", { id: "token", class: "token", r: "4" });
|
|
2177
2187
|
e.layer.appendChild(token);
|
|
2178
2188
|
const pathOf = (link) => ctx.edgeEls[ctx.links.findIndex((l) => l.id === link)];
|
|
2179
2189
|
const nodeOf = (id) => ctx.nodeEls.get(id);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
:root {
|
|
2
2
|
--bg:#f6f8fa; --panel:#ffffff; --panel2:#f6f8fa; --edge:#d0d7de; --rule:#d8dee4; --fg:#1f2328; --muted:#59636e;
|
|
3
3
|
--accent:#0969da; --accent-solid:#0969da; --ok:#1a7f37; --warn:#9a6700; --crit:#cf222e; --glow:rgba(9,105,218,.22);
|
|
4
|
-
--up:#0a7ea4; --down:#8250df; --shadow:0 1px 0 rgba(31,35,40,.04), 0 6px 18px rgba(31,35,40,.08); --grid:rgba(31,35,40,.035); --dot:rgba(31,35,40,.18); --dot-size:1.2px; --dot-gap:24px; --edge-w:
|
|
4
|
+
--up:#0a7ea4; --down:#8250df; --shadow:0 1px 0 rgba(31,35,40,.04), 0 6px 18px rgba(31,35,40,.08); --grid:rgba(31,35,40,.035); --dot:rgba(31,35,40,.18); --dot-size:1.2px; --dot-gap:24px; --edge-w:0.8px; --edge-w-hot:1px; --edge-w-play:1.2px; --trail-w:1.3px; --ripple-w:1.5px;
|
|
5
5
|
--warn-bg:rgba(154,103,0,.10); --ok-bg:rgba(26,127,55,.10); --crit-bg:rgba(207,34,46,.08); --info-bg:rgba(9,105,218,.10);
|
|
6
6
|
--mono:"IBM Plex Mono", ui-monospace, "Cascadia Mono", Consolas, Menlo, monospace;
|
|
7
7
|
}
|
|
@@ -29,6 +29,7 @@ function validateModel(input) {
|
|
|
29
29
|
if (!Array.isArray(m.lanes) || m.layout !== "auto" && m.lanes.length === 0) add("error", "lanes-missing", "lanes", "At least one lane is required.");
|
|
30
30
|
if (!m.states || typeof m.states !== "object" || Object.keys(m.states).length === 0) add("error", "states-missing", "states", "At least one state is required.");
|
|
31
31
|
const columns = Array.isArray(m.columns) ? m.columns : [];
|
|
32
|
+
if (columns[0]?.continues) add("error", "column-continues-first", "columns[0]", "The first column cannot continue a column before it.");
|
|
32
33
|
const lanes = Array.isArray(m.lanes) ? m.lanes : [];
|
|
33
34
|
const states = m.states && typeof m.states === "object" ? m.states : {};
|
|
34
35
|
if (!m.meta || typeof m.meta !== "object" || typeof m.meta.title !== "string") add("error", "meta-missing", "meta", "meta with a title is required.");
|
|
@@ -347,11 +348,19 @@ __export(rules_exports, {
|
|
|
347
348
|
linkVisible: () => linkVisible,
|
|
348
349
|
nodeStateId: () => nodeStateId,
|
|
349
350
|
nodeVisible: () => nodeVisible,
|
|
351
|
+
stripHeads: () => stripHeads,
|
|
350
352
|
toggleGroupState: () => toggleGroupState
|
|
351
353
|
});
|
|
352
354
|
var baselineView = (m) => m.views?.[0]?.id ?? "all";
|
|
353
355
|
var defaultView = (m) => (m.views?.find((v) => v.default) ?? m.views?.[0])?.id ?? "all";
|
|
354
356
|
var initialCollapsed = (m) => new Set(m.nodes.filter((n) => n.kind === "group" && n.contains && n.collapsed !== false).map((n) => n.id));
|
|
357
|
+
var stripHeads = (m) => {
|
|
358
|
+
let head = 0;
|
|
359
|
+
return m.columns.map((c, i) => {
|
|
360
|
+
if (!c.continues || i === 0) head = i;
|
|
361
|
+
return head;
|
|
362
|
+
});
|
|
363
|
+
};
|
|
355
364
|
var columnTitle = (m, n) => n.col === void 0 ? "" : m.columns[n.col]?.title ?? "";
|
|
356
365
|
function groupFor(m, nodeId) {
|
|
357
366
|
return m.nodes.find((n) => n.kind === "group" && n.contains?.includes(nodeId));
|
|
@@ -1203,7 +1212,7 @@ function install3(ctx) {
|
|
|
1203
1212
|
if (!cols.firstChild) {
|
|
1204
1213
|
model.columns.forEach((c, i2) => {
|
|
1205
1214
|
const h = el("div", "hd");
|
|
1206
|
-
h.textContent = (c.short ?? c.title).toUpperCase();
|
|
1215
|
+
h.textContent = c.continues ? "" : (c.short ?? c.title).toUpperCase();
|
|
1207
1216
|
h.title = c.title;
|
|
1208
1217
|
h.dataset.i = String(i2);
|
|
1209
1218
|
cols.appendChild(h);
|
|
@@ -1589,16 +1598,18 @@ function install5(ctx) {
|
|
|
1589
1598
|
}
|
|
1590
1599
|
function buildStrip() {
|
|
1591
1600
|
const box = els.strip;
|
|
1601
|
+
const heads = stripHeads(model), cells = model.columns.map((c, i) => ({ c, i })).filter(({ i }) => heads[i] === i);
|
|
1592
1602
|
box.style.display = model.columns.length ? "" : "none";
|
|
1593
|
-
box.style.gridTemplateColumns = `repeat(${Math.max(1,
|
|
1603
|
+
box.style.gridTemplateColumns = `repeat(${Math.max(1, cells.length)}, minmax(0, 1fr))`;
|
|
1594
1604
|
if (!box.firstChild) {
|
|
1595
|
-
box.innerHTML =
|
|
1605
|
+
box.innerHTML = cells.map(({ c }) => `<div class="lcell"><div class="ln">${esc(c.title)}</div><div class="lb"><i class="good"></i><i class="neutral"></i><i class="gap"></i></div><div class="lv"></div></div>`).join("");
|
|
1596
1606
|
}
|
|
1597
|
-
box.querySelectorAll(".lcell").forEach((cell,
|
|
1598
|
-
|
|
1607
|
+
box.querySelectorAll(".lcell").forEach((cell, cellIndex) => {
|
|
1608
|
+
const i = cells[cellIndex]?.i ?? cellIndex;
|
|
1609
|
+
cell.style.setProperty("--i", String(cellIndex));
|
|
1599
1610
|
let tot = 0, good = 0, neutral = 0, gap = 0;
|
|
1600
1611
|
for (const n of model.nodes) {
|
|
1601
|
-
if (n.col !== i || n.kind === "chip") continue;
|
|
1612
|
+
if (heads[n.col ?? -1] !== i || n.kind === "chip") continue;
|
|
1602
1613
|
if (!visible(n.id)) continue;
|
|
1603
1614
|
const w = Math.max(1, n.weight ?? 1);
|
|
1604
1615
|
tot += w;
|
|
@@ -1817,7 +1828,7 @@ function install6(ctx) {
|
|
|
1817
1828
|
const markdown = () => {
|
|
1818
1829
|
const out = [`# ${model.meta.title}`, "", model.meta.help, "", `As of ${model.meta.asOf}. Source: ${model.meta.source}`, "", "## States"];
|
|
1819
1830
|
for (const [id, s] of Object.entries(model.states)) out.push(`- **${s.word}** (${id}): tone ${s.tone}, border ${s.border}, link ${s.edge}, motion ${s.motion}, bucket ${s.bucket}`);
|
|
1820
|
-
out.push("", "## Columns", ...model.columns.map((c, i) => `${i + 1}. ${c.title}${c.phase ? ` (${c.phase})` : ""}`));
|
|
1831
|
+
out.push("", "## Columns", ...model.columns.filter((c) => !c.continues).map((c, i) => `${i + 1}. ${c.title}${c.phase ? ` (${c.phase})` : ""}`));
|
|
1821
1832
|
out.push("", "## Lanes", ...model.lanes.map((l) => `- ${l.title}${l.badge ? ` [${l.badge}]` : ""}${l.note ? `: ${l.note}` : ""}`));
|
|
1822
1833
|
out.push("", `## Size`, `${model.nodes.length} nodes, ${model.links.length} links.`);
|
|
1823
1834
|
if (notes.length) out.push("", "## Notes", ...notes.map((n) => `${n.id}. **${n.title}**: ${n.text}`));
|
|
@@ -1972,7 +1983,7 @@ function install7(ctx) {
|
|
|
1972
1983
|
let t = 0, idx = -1, playing = false, frame = 0, timer = 0, stamp = 0, choicesKey = "";
|
|
1973
1984
|
let markedEdges = [];
|
|
1974
1985
|
let markedNodes = [];
|
|
1975
|
-
const token = sv("circle", { id: "token", class: "token", r: "
|
|
1986
|
+
const token = sv("circle", { id: "token", class: "token", r: "4" });
|
|
1976
1987
|
e.layer.appendChild(token);
|
|
1977
1988
|
const pathOf = (link) => ctx.edgeEls[ctx.links.findIndex((l) => l.id === link)];
|
|
1978
1989
|
const nodeOf = (id) => ctx.nodeEls.get(id);
|
|
@@ -2437,7 +2448,7 @@ function mount(root, model, opts = {}) {
|
|
|
2437
2448
|
}
|
|
2438
2449
|
|
|
2439
2450
|
// src/lib.ts
|
|
2440
|
-
var SHELL_BODY_HTML = "<header class=\"top\">\r\n <span class=\"brand\"><i></i>flow-studio</span>\r\n <span class=\"spacer\"></span>\r\n <button id=\"notesBtn\" type=\"button\" aria-pressed=\"false\">Implementation notes (N)</button>\r\n <button id=\"theme\" type=\"button\" aria-label=\"Toggle light and dark\">☀ / ☾</button>\r\n</header>\r\n<main>\r\n <div class=\"titlerow\"><h1 id=\"title\"></h1><span class=\"readline\" id=\"readat\"></span></div>\r\n <p class=\"help\" id=\"help\"></p>\r\n <div id=\"banner\" role=\"alert\" hidden></div>\r\n\r\n <section class=\"summary\"><div class=\"layerstrip\" id=\"layerstrip\" aria-label=\"Completeness by column\"></div><div class=\"tiles\" id=\"kpis\" aria-label=\"Summary\"></div></section>\r\n\r\n <div class=\"toolbar\">\r\n <div class=\"seggrp\" role=\"tablist\" aria-label=\"View\" id=\"modeSwitch\"></div>\r\n <input id=\"q\" type=\"search\" placeholder=\"Find a group or member… ( / )\" aria-label=\"Find a group or member\">\r\n <select id=\"hops\" aria-label=\"Search radius\"><option value=\"0\">This group</option><option value=\"1\">+1 hop</option><option value=\"2\">+2 hops</option><option value=\"3\">+3 hops</option></select>\r\n <span id=\"matchinfo\" class=\"note\" style=\"margin:0\"></span>\r\n </div>\r\n\r\n <div class=\"workspace\" id=\"workspace\">\r\n <section class=\"canvaswrap\">\r\n <div id=\"viewport\" tabindex=\"0\" role=\"application\" aria-label=\"Flow canvas. Scroll to zoom, drag to pan. Keys: V select, H hand, plus and minus zoom, 0 fit, 1 actual size, F zoom to selection, arrows pan.\">\r\n <div id=\"world\"><svg id=\"edges\" aria-hidden=\"true\"><defs><marker id=\"arr\" viewBox=\"0 0 8 8\" refX=\"7\" refY=\"4\" markerWidth=\"
|
|
2451
|
+
var SHELL_BODY_HTML = "<header class=\"top\">\r\n <span class=\"brand\"><i></i>flow-studio</span>\r\n <span class=\"spacer\"></span>\r\n <button id=\"notesBtn\" type=\"button\" aria-pressed=\"false\">Implementation notes (N)</button>\r\n <button id=\"theme\" type=\"button\" aria-label=\"Toggle light and dark\">☀ / ☾</button>\r\n</header>\r\n<main>\r\n <div class=\"titlerow\"><h1 id=\"title\"></h1><span class=\"readline\" id=\"readat\"></span></div>\r\n <p class=\"help\" id=\"help\"></p>\r\n <div id=\"banner\" role=\"alert\" hidden></div>\r\n\r\n <section class=\"summary\"><div class=\"layerstrip\" id=\"layerstrip\" aria-label=\"Completeness by column\"></div><div class=\"tiles\" id=\"kpis\" aria-label=\"Summary\"></div></section>\r\n\r\n <div class=\"toolbar\">\r\n <div class=\"seggrp\" role=\"tablist\" aria-label=\"View\" id=\"modeSwitch\"></div>\r\n <input id=\"q\" type=\"search\" placeholder=\"Find a group or member… ( / )\" aria-label=\"Find a group or member\">\r\n <select id=\"hops\" aria-label=\"Search radius\"><option value=\"0\">This group</option><option value=\"1\">+1 hop</option><option value=\"2\">+2 hops</option><option value=\"3\">+3 hops</option></select>\r\n <span id=\"matchinfo\" class=\"note\" style=\"margin:0\"></span>\r\n </div>\r\n\r\n <div class=\"workspace\" id=\"workspace\">\r\n <section class=\"canvaswrap\">\r\n <div id=\"viewport\" tabindex=\"0\" role=\"application\" aria-label=\"Flow canvas. Scroll to zoom, drag to pan. Keys: V select, H hand, plus and minus zoom, 0 fit, 1 actual size, F zoom to selection, arrows pan.\">\r\n <div id=\"world\"><svg id=\"edges\" aria-hidden=\"true\"><defs><marker id=\"arr\" viewBox=\"0 0 8 8\" refX=\"7\" refY=\"4\" markerWidth=\"6\" markerHeight=\"6\" markerUnits=\"userSpaceOnUse\" orient=\"auto\"><path d=\"M0 0 L8 4 L0 8 z\" fill=\"context-stroke\"/></marker></defs><g id=\"edgeLayer\"></g></svg></div>\r\n <div id=\"cols\" aria-hidden=\"true\"></div>\r\n <div id=\"tools\" role=\"toolbar\" aria-label=\"Canvas controls\" aria-orientation=\"vertical\"></div>\r\n <div id=\"zoomLabel\" aria-hidden=\"true\">100%</div>\r\n </div>\r\n <div id=\"playbar\" hidden aria-label=\"Scenario playback\">\r\n <select id=\"pbScenario\" aria-label=\"Scenario\"></select>\r\n <button id=\"pbPlay\" type=\"button\" aria-label=\"Play\">▶</button>\r\n <button id=\"pbStep\" type=\"button\" aria-label=\"Step\">Step</button>\r\n <button id=\"pbReset\" type=\"button\" aria-label=\"Reset\">Reset</button>\r\n <select id=\"pbSpeed\" aria-label=\"Speed\"><option value=\"0.5\">0.5x</option><option value=\"1\" selected>1x</option><option value=\"2\">2x</option></select>\r\n <input id=\"pbScrub\" type=\"range\" min=\"0\" max=\"1000\" value=\"0\" aria-label=\"Playback position\" aria-valuetext=\"Step 1 of 1\">\r\n <span id=\"pbCaption\"></span><span id=\"pbCounter\"></span><span id=\"pbChoices\"></span>\r\n </div>\r\n <div class=\"keyline\" id=\"legend\" aria-label=\"Legend\"></div>\r\n <aside class=\"insp\" id=\"inspector\" aria-label=\"Inspector\" aria-hidden=\"true\"></aside>\r\n </section>\r\n </div>\r\n <div id=\"live\" class=\"sr\" role=\"status\" aria-live=\"polite\"></div>\r\n\r\n <div class=\"ledger\" id=\"ledger\"></div>\r\n <details class=\"tableview\"><summary>Table view</summary><table class=\"sources\" id=\"tbl\"></table></details>\r\n <details class=\"spec\" id=\"spec\"><summary>Model reference (for the person or agent who builds this)</summary><div id=\"specbody\"></div></details>\r\n</main>\r\n<footer id=\"footer\"></footer>\r\n<aside id=\"drawer\" aria-label=\"Implementation notes\"><h2>Implementation notes</h2><p class=\"note\">Numbered markers on the page match these cards. Hover a card to find its place; click a marker to jump to its card.</p><div id=\"notelist\"></div></aside>\r\n\r\n";
|
|
2441
2452
|
export {
|
|
2442
2453
|
K_MAX,
|
|
2443
2454
|
K_MIN,
|
|
@@ -2482,6 +2493,7 @@ export {
|
|
|
2482
2493
|
rules_exports as rules,
|
|
2483
2494
|
slaState,
|
|
2484
2495
|
stateCss,
|
|
2496
|
+
stripHeads,
|
|
2485
2497
|
suggestedExit,
|
|
2486
2498
|
toggleGroupState,
|
|
2487
2499
|
trace_exports as trace,
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
<div class="workspace" id="workspace">
|
|
22
22
|
<section class="canvaswrap">
|
|
23
23
|
<div id="viewport" tabindex="0" role="application" aria-label="Flow canvas. Scroll to zoom, drag to pan. Keys: V select, H hand, plus and minus zoom, 0 fit, 1 actual size, F zoom to selection, arrows pan.">
|
|
24
|
-
<div id="world"><svg id="edges" aria-hidden="true"><defs><marker id="arr" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="
|
|
24
|
+
<div id="world"><svg id="edges" aria-hidden="true"><defs><marker id="arr" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="6" markerHeight="6" markerUnits="userSpaceOnUse" orient="auto"><path d="M0 0 L8 4 L0 8 z" fill="context-stroke"/></marker></defs><g id="edgeLayer"></g></svg></div>
|
|
25
25
|
<div id="cols" aria-hidden="true"></div>
|
|
26
26
|
<div id="tools" role="toolbar" aria-label="Canvas controls" aria-orientation="vertical"></div>
|
|
27
27
|
<div id="zoomLabel" aria-hidden="true">100%</div>
|
|
@@ -70,13 +70,13 @@ In either route, derive node and link IDs from immutable business keys. Preserve
|
|
|
70
70
|
8. **Short titles**: titles fit one line at column width. Long descriptions belong in subtitles or member lists.
|
|
71
71
|
9. **Fixed node dimensions**: node kinds use fixed box sizes so font rendering does not shift layout.
|
|
72
72
|
10. **Reduced motion**: removes all animations while preserving static borders and status glows.
|
|
73
|
-
11. **Thin links, dotted canvas**: links are hairlines (about
|
|
73
|
+
11. **Thin links, dotted canvas**: links are hairlines (about 0.8 px, with a 6 px arrowhead) and the canvas background is faint dots, not a grid. Both come from tokens (`--edge-w`, `--dot`, ...), so an app can change them with one CSS line; see `references/design-language.md`.
|
|
74
74
|
|
|
75
75
|
## Model summary
|
|
76
76
|
|
|
77
77
|
```
|
|
78
78
|
version 1, meta {title, help, asOf, source, labels?}, layout "columns-lanes" | "free" | "auto"
|
|
79
|
-
columns [{id, title, short?, width, phase?}] stages, left to right
|
|
79
|
+
columns [{id, title, short?, width, phase?, continues?}] stages, left to right
|
|
80
80
|
lanes [{id, title, badge?, badgeTone?, note?, gap?}] rows; gap true = dashed amber lane
|
|
81
81
|
states {id: {word, tone, border, edge, motion, bucket}} tone: ok|warn|crit|info|muted|accent; bucket: good|neutral|gap
|
|
82
82
|
nodes [{id, kind, lane?, col?, colSpan?, row?, x?, y?, w?, h?, state, title, subtitle?, members?, weight?,
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"type": "object", "required": ["id", "title", "width"], "additionalProperties": false,
|
|
27
27
|
"properties": {
|
|
28
28
|
"id": { "type": "string" }, "title": { "type": "string" }, "short": { "type": "string" },
|
|
29
|
-
"width": { "type": "number", "minimum": 60 }, "phase": { "type": "string" }
|
|
29
|
+
"width": { "type": "number", "minimum": 60 }, "phase": { "type": "string" }, "continues": { "type": "boolean" }
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
},
|
|
@@ -28,11 +28,11 @@ Tokens are defined on `:root` and adapted for dark mode via `@media (prefers-col
|
|
|
28
28
|
| `--dot` | `rgba(31,35,40,.18)` | `rgba(255,255,255,.16)` | Colour of the canvas background dots |
|
|
29
29
|
| `--dot-size` | `1.2px` | Same | Radius of a background dot |
|
|
30
30
|
| `--dot-gap` | `24px` | Same | Spacing of the background dots |
|
|
31
|
-
| `--edge-w` | `
|
|
32
|
-
| `--edge-w-hot` | `
|
|
33
|
-
| `--edge-w-play` | `1.
|
|
34
|
-
| `--trail-w` | `1.
|
|
35
|
-
| `--ripple-w` | `
|
|
31
|
+
| `--edge-w` | `0.8px` | Same | Width of a normal link (thin, like a hairline; motion carries the state) |
|
|
32
|
+
| `--edge-w-hot` | `1px` | Same | Width of a traced or selected link |
|
|
33
|
+
| `--edge-w-play` | `1.2px` | Same | Width of a link while playback runs along it |
|
|
34
|
+
| `--trail-w` | `1.3px` | Same | Width of the draw-in trail on a selected path (divided by the zoom) |
|
|
35
|
+
| `--ripple-w` | `1.5px` | Same | Width of the what-if ripple (divided by the zoom) |
|
|
36
36
|
| `--ok-bg` | `rgba(26,127,55,.10)` | `rgba(63,185,80,.12)` | Subtle tinted background for healthy states |
|
|
37
37
|
| `--warn-bg` | `rgba(154,103,0,.10)` | `rgba(210,153,34,.14)` | Subtle tinted background for warning states |
|
|
38
38
|
| `--crit-bg` | `rgba(207,34,46,.08)` | `rgba(248,81,73,.12)` | Subtle tinted background for critical states |
|
|
@@ -48,6 +48,7 @@ Columns provide vertical partitioning for time, stages, or architecture tiers.
|
|
|
48
48
|
| `short` | `string` | Optional | - | Compact abbreviated title for tight viewports. |
|
|
49
49
|
| `width` | `number` | Required | - | Column width in pixels (minimum 60). |
|
|
50
50
|
| `phase` | `string` | Optional | - | Super-category phase grouping shown in top banner. |
|
|
51
|
+
| `continues` | `boolean` | Optional | `false` | This column is a wrapped continuation of the column before it (for example one long list laid out in three side-by-side columns). It shows no header label and no strip cell of its own: its nodes count into the cell of the column it continues. Not allowed on the first column. |
|
|
51
52
|
|
|
52
53
|
---
|
|
53
54
|
|
|
@@ -22,14 +22,23 @@ test("dots are set by tokens: size, spacing and a colour for light and dark", ()
|
|
|
22
22
|
});
|
|
23
23
|
|
|
24
24
|
test("links are thin by default and every width comes from a token", () => {
|
|
25
|
-
assert.match(css, /--edge-w:\s*
|
|
25
|
+
assert.match(css, /--edge-w:\s*0\.8px/);
|
|
26
26
|
assert.match(css, /path\.edge \{ fill:none; stroke-width:var\(--edge-w\)/);
|
|
27
27
|
assert.match(css, /path\.edge\.hot \{[^}]*stroke-width:var\(--edge-w-hot\)/);
|
|
28
28
|
assert.match(css, /path\.trail \{[^}]*stroke-width:calc\(var\(--trail-w\)/);
|
|
29
29
|
assert.match(css, /path\.ripple \{[^}]*stroke-width:calc\(var\(--ripple-w\)/);
|
|
30
|
-
// Thin means at most 1
|
|
31
|
-
const w = (name) => Number(css.match(new RegExp(`${name}
|
|
32
|
-
assert.ok(w("--edge-w") <= 1
|
|
30
|
+
// Thin means a hairline: at most 1 px for a plain link, 1.2 px for a highlighted or playing one.
|
|
31
|
+
const w = (name) => Number(css.match(new RegExp(`${name}: *([0-9.]+)px`))[1]);
|
|
32
|
+
assert.ok(w("--edge-w") <= 1 && w("--edge-w-hot") <= 1.2 && w("--edge-w-play") <= 1.2 && w("--ripple-w") <= 1.5 && w("--trail-w") <= 1.5);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("the arrowhead is small: at most 7 px, and the playback dot at most 5 px in radius", () => {
|
|
36
|
+
const shell = readFileSync(join(root, "templates/shell-body.html"), "utf8");
|
|
37
|
+
const marker = shell.match(/<marker id="arr"[^>]*markerWidth="(\d+)" markerHeight="(\d+)"/);
|
|
38
|
+
assert.ok(marker, "the arrow marker exists");
|
|
39
|
+
assert.ok(Number(marker[1]) <= 7 && Number(marker[2]) <= 7, `arrowhead is ${marker[1]} x ${marker[2]}`);
|
|
40
|
+
const playback = readFileSync(join(root, "src/playbackui.ts"), "utf8");
|
|
41
|
+
assert.ok(Number(playback.match(/class: "token", r: "(\d+(?:\.\d+)?)"/)[1]) <= 5);
|
|
33
42
|
});
|
|
34
43
|
|
|
35
44
|
test("forced-colours mode drops the dot pattern", () => {
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { el, on, type Ctx } from "./ctx.ts";
|
|
5
5
|
import { esc } from "./model.ts";
|
|
6
6
|
import { DASH, toneVar, cssId } from "./stateCss.ts";
|
|
7
|
-
import { bucketOf, columnTitle, effectiveLinks, nodeStateId, nodeVisible } from "./rules.ts";
|
|
7
|
+
import { bucketOf, columnTitle, effectiveLinks, nodeStateId, nodeVisible, stripHeads } from "./rules.ts";
|
|
8
8
|
import { matches } from "./interact.ts";
|
|
9
9
|
|
|
10
10
|
const DEFAULT_LABELS: Record<string, string> = {
|
|
@@ -34,16 +34,19 @@ export function install(ctx: Ctx): void {
|
|
|
34
34
|
|
|
35
35
|
function buildStrip(): void {
|
|
36
36
|
const box = els.strip;
|
|
37
|
+
// One cell per head column: a column that `continues` another counts into the cell before it.
|
|
38
|
+
const heads = stripHeads(model), cells = model.columns.map((c, i) => ({ c, i })).filter(({ i }) => heads[i] === i);
|
|
37
39
|
box.style.display = model.columns.length ? "" : "none";
|
|
38
|
-
box.style.gridTemplateColumns = `repeat(${Math.max(1,
|
|
40
|
+
box.style.gridTemplateColumns = `repeat(${Math.max(1, cells.length)}, minmax(0, 1fr))`;
|
|
39
41
|
if (!box.firstChild) {
|
|
40
|
-
box.innerHTML =
|
|
42
|
+
box.innerHTML = cells.map(({ c }) => `<div class="lcell"><div class="ln">${esc(c.title)}</div><div class="lb"><i class="good"></i><i class="neutral"></i><i class="gap"></i></div><div class="lv"></div></div>`).join("");
|
|
41
43
|
}
|
|
42
|
-
box.querySelectorAll<HTMLElement>(".lcell").forEach((cell,
|
|
43
|
-
|
|
44
|
+
box.querySelectorAll<HTMLElement>(".lcell").forEach((cell, cellIndex) => {
|
|
45
|
+
const i = cells[cellIndex]?.i ?? cellIndex;
|
|
46
|
+
cell.style.setProperty("--i", String(cellIndex));
|
|
44
47
|
let tot = 0, good = 0, neutral = 0, gap = 0;
|
|
45
48
|
for (const n of model.nodes) {
|
|
46
|
-
if (n.col !== i || n.kind === "chip") continue;
|
|
49
|
+
if (heads[n.col ?? -1] !== i || n.kind === "chip") continue;
|
|
47
50
|
if (!visible(n.id)) continue;
|
|
48
51
|
const w = Math.max(1, n.weight ?? 1);
|
|
49
52
|
tot += w;
|
|
@@ -71,7 +71,7 @@ export function install(ctx: Ctx): void {
|
|
|
71
71
|
const cols = els.cols;
|
|
72
72
|
cols.style.display = model.columns.length ? "" : "none";
|
|
73
73
|
if (!cols.firstChild) {
|
|
74
|
-
model.columns.forEach((c, i) => { const h = el("div", "hd"); h.textContent = (c.short ?? c.title).toUpperCase(); h.title = c.title; h.dataset.i = String(i); cols.appendChild(h); });
|
|
74
|
+
model.columns.forEach((c, i) => { const h = el("div", "hd"); h.textContent = c.continues ? "" : (c.short ?? c.title).toUpperCase(); h.title = c.title; h.dataset.i = String(i); cols.appendChild(h); });
|
|
75
75
|
let i = 0;
|
|
76
76
|
while (i < model.columns.length) {
|
|
77
77
|
const name = model.columns[i]?.phase;
|
|
@@ -165,3 +165,11 @@ test("group containment validates ids, ownership, self-reference, and skips grou
|
|
|
165
165
|
assert.ok(found.includes("contains-undefined"));
|
|
166
166
|
assert.ok(found.includes("contains-duplicate"));
|
|
167
167
|
});
|
|
168
|
+
test("a column may continue the one before it", () => {
|
|
169
|
+
const m = base(); m.columns.push({ id: "c2", title: "B", width: 150, continues: true });
|
|
170
|
+
assert.deepEqual(codes(m), []);
|
|
171
|
+
});
|
|
172
|
+
test("the first column cannot continue a column that does not exist", () => {
|
|
173
|
+
const m = base(); m.columns[0]!.continues = true;
|
|
174
|
+
assert.ok(codes(m).includes("column-continues-first"));
|
|
175
|
+
});
|
|
@@ -16,7 +16,7 @@ export interface StateDef {
|
|
|
16
16
|
motion: Motion;
|
|
17
17
|
bucket: Bucket;
|
|
18
18
|
}
|
|
19
|
-
export interface ColumnDef { id: string; title: string; short?: string; width: number; phase?: string }
|
|
19
|
+
export interface ColumnDef { id: string; title: string; short?: string; width: number; phase?: string; continues?: boolean }
|
|
20
20
|
export interface LaneDef { id: string; title: string; badge?: string; badgeTone?: "ok" | "warn" | "muted"; note?: string; gap?: boolean }
|
|
21
21
|
export interface NodeDef {
|
|
22
22
|
id: string;
|
|
@@ -111,6 +111,7 @@ export function validateModel(input: unknown): Issue[] {
|
|
|
111
111
|
if (!Array.isArray(m.lanes) || (m.layout !== "auto" && m.lanes.length === 0)) add("error", "lanes-missing", "lanes", "At least one lane is required.");
|
|
112
112
|
if (!m.states || typeof m.states !== "object" || Object.keys(m.states).length === 0) add("error", "states-missing", "states", "At least one state is required.");
|
|
113
113
|
const columns = Array.isArray(m.columns) ? m.columns : [];
|
|
114
|
+
if (columns[0]?.continues) add("error", "column-continues-first", "columns[0]", "The first column cannot continue a column before it.");
|
|
114
115
|
const lanes = Array.isArray(m.lanes) ? m.lanes : [];
|
|
115
116
|
const states = m.states && typeof m.states === "object" ? m.states : {};
|
|
116
117
|
if (!m.meta || typeof m.meta !== "object" || typeof m.meta.title !== "string") add("error", "meta-missing", "meta", "meta with a title is required.");
|
|
@@ -70,7 +70,7 @@ export function install(ctx: Ctx): void {
|
|
|
70
70
|
const markdown = (): string => {
|
|
71
71
|
const out = [`# ${model.meta.title}`, "", model.meta.help, "", `As of ${model.meta.asOf}. Source: ${model.meta.source}`, "", "## States"];
|
|
72
72
|
for (const [id, s] of Object.entries(model.states)) out.push(`- **${s.word}** (${id}): tone ${s.tone}, border ${s.border}, link ${s.edge}, motion ${s.motion}, bucket ${s.bucket}`);
|
|
73
|
-
out.push("", "## Columns", ...model.columns.map((c, i) => `${i + 1}. ${c.title}${c.phase ? ` (${c.phase})` : ""}`));
|
|
73
|
+
out.push("", "## Columns", ...model.columns.filter(c => !c.continues).map((c, i) => `${i + 1}. ${c.title}${c.phase ? ` (${c.phase})` : ""}`));
|
|
74
74
|
out.push("", "## Lanes", ...model.lanes.map(l => `- ${l.title}${l.badge ? ` [${l.badge}]` : ""}${l.note ? `: ${l.note}` : ""}`));
|
|
75
75
|
out.push("", `## Size`, `${model.nodes.length} nodes, ${model.links.length} links.`);
|
|
76
76
|
if (notes.length) out.push("", "## Notes", ...notes.map(n => `${n.id}. **${n.title}**: ${n.text}`));
|
|
@@ -23,7 +23,7 @@ export function install(ctx: Ctx): PlaybackController {
|
|
|
23
23
|
// Only the elements marked in the last draw are cleared again, so a frame does not walk the whole graph.
|
|
24
24
|
let markedEdges: SVGPathElement[] = [];
|
|
25
25
|
let markedNodes: HTMLElement[] = [];
|
|
26
|
-
const token = sv("circle", { id: "token", class: "token", r: "
|
|
26
|
+
const token = sv("circle", { id: "token", class: "token", r: "4" }) as SVGCircleElement;
|
|
27
27
|
e.layer.appendChild(token);
|
|
28
28
|
const pathOf = (link: string): SVGPathElement | undefined => ctx.edgeEls[ctx.links.findIndex(l => l.id === link)];
|
|
29
29
|
const nodeOf = (id: string) => ctx.nodeEls.get(id);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { test } from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
3
|
import type { Model } from "./model.ts";
|
|
4
|
-
import { baselineView, defaultView, nodeVisible, nodeStateId, linkVisible, linkStateId, isNew, isImproved, bucketOf, canonOf, effectiveLinks, initialCollapsed, columnTitle, toggleGroupState } from "./rules.ts";
|
|
4
|
+
import { baselineView, defaultView, nodeVisible, nodeStateId, linkVisible, linkStateId, isNew, isImproved, bucketOf, canonOf, effectiveLinks, initialCollapsed, columnTitle, toggleGroupState, stripHeads } from "./rules.ts";
|
|
5
5
|
|
|
6
6
|
const m = (): Model => ({
|
|
7
7
|
version: 1,
|
|
@@ -133,3 +133,12 @@ test("toggleGroup selects a visible child on expansion and its group on collapse
|
|
|
133
133
|
assert.equal(folded.collapsed.has("group"), true);
|
|
134
134
|
assert.equal(toggleGroupState(mm, "full", starting, "missing"), null);
|
|
135
135
|
});
|
|
136
|
+
|
|
137
|
+
test("stripHeads: a continuing column joins the one before it", () => {
|
|
138
|
+
const mm = m();
|
|
139
|
+
mm.columns = [{ id: "a", title: "A", width: 100 }, { id: "p", title: "P", width: 100 }, { id: "p2", title: "P", width: 100, continues: true }, { id: "p3", title: "P", width: 100, continues: true }, { id: "z", title: "Z", width: 100 }];
|
|
140
|
+
assert.deepEqual(stripHeads(mm), [0, 1, 1, 1, 4]);
|
|
141
|
+
});
|
|
142
|
+
test("stripHeads: with no continuing column every column is its own head", () => {
|
|
143
|
+
assert.deepEqual(stripHeads(m()), [0, 1]);
|
|
144
|
+
});
|
|
@@ -10,6 +10,12 @@ export const defaultView = (m: Model): string => (m.views?.find(v => v.default)
|
|
|
10
10
|
/** A group that contains nodes starts folded unless it explicitly opts out. */
|
|
11
11
|
export const initialCollapsed = (m: Model): Set<string> => new Set(m.nodes
|
|
12
12
|
.filter(n => n.kind === "group" && n.contains && n.collapsed !== false).map(n => n.id));
|
|
13
|
+
/** For each column, the index of the column whose header and strip cell it shares: itself, or (when it
|
|
14
|
+
* `continues` another) the nearest earlier column that does not. */
|
|
15
|
+
export const stripHeads = (m: Model): number[] => {
|
|
16
|
+
let head = 0;
|
|
17
|
+
return m.columns.map((c, i) => { if (!c.continues || i === 0) head = i; return head; });
|
|
18
|
+
};
|
|
13
19
|
export const columnTitle = (m: Model, n: NodeDef): string => n.col === undefined ? "" : m.columns[n.col]?.title ?? "";
|
|
14
20
|
|
|
15
21
|
export function groupFor(m: Model, nodeId: string): NodeDef | undefined {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
:root {
|
|
2
2
|
--bg:#f6f8fa; --panel:#ffffff; --panel2:#f6f8fa; --edge:#d0d7de; --rule:#d8dee4; --fg:#1f2328; --muted:#59636e;
|
|
3
3
|
--accent:#0969da; --accent-solid:#0969da; --ok:#1a7f37; --warn:#9a6700; --crit:#cf222e; --glow:rgba(9,105,218,.22);
|
|
4
|
-
--up:#0a7ea4; --down:#8250df; --shadow:0 1px 0 rgba(31,35,40,.04), 0 6px 18px rgba(31,35,40,.08); --grid:rgba(31,35,40,.035); --dot:rgba(31,35,40,.18); --dot-size:1.2px; --dot-gap:24px; --edge-w:
|
|
4
|
+
--up:#0a7ea4; --down:#8250df; --shadow:0 1px 0 rgba(31,35,40,.04), 0 6px 18px rgba(31,35,40,.08); --grid:rgba(31,35,40,.035); --dot:rgba(31,35,40,.18); --dot-size:1.2px; --dot-gap:24px; --edge-w:0.8px; --edge-w-hot:1px; --edge-w-play:1.2px; --trail-w:1.3px; --ripple-w:1.5px;
|
|
5
5
|
--warn-bg:rgba(154,103,0,.10); --ok-bg:rgba(26,127,55,.10); --crit-bg:rgba(207,34,46,.08); --info-bg:rgba(9,105,218,.10);
|
|
6
6
|
--mono:"IBM Plex Mono", ui-monospace, "Cascadia Mono", Consolas, Menlo, monospace;
|
|
7
7
|
}
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
<style>:root {
|
|
16
16
|
--bg:#f6f8fa; --panel:#ffffff; --panel2:#f6f8fa; --edge:#d0d7de; --rule:#d8dee4; --fg:#1f2328; --muted:#59636e;
|
|
17
17
|
--accent:#0969da; --accent-solid:#0969da; --ok:#1a7f37; --warn:#9a6700; --crit:#cf222e; --glow:rgba(9,105,218,.22);
|
|
18
|
-
--up:#0a7ea4; --down:#8250df; --shadow:0 1px 0 rgba(31,35,40,.04), 0 6px 18px rgba(31,35,40,.08); --grid:rgba(31,35,40,.035); --dot:rgba(31,35,40,.18); --dot-size:1.2px; --dot-gap:24px; --edge-w:
|
|
18
|
+
--up:#0a7ea4; --down:#8250df; --shadow:0 1px 0 rgba(31,35,40,.04), 0 6px 18px rgba(31,35,40,.08); --grid:rgba(31,35,40,.035); --dot:rgba(31,35,40,.18); --dot-size:1.2px; --dot-gap:24px; --edge-w:0.8px; --edge-w-hot:1px; --edge-w-play:1.2px; --trail-w:1.3px; --ripple-w:1.5px;
|
|
19
19
|
--warn-bg:rgba(154,103,0,.10); --ok-bg:rgba(26,127,55,.10); --crit-bg:rgba(207,34,46,.08); --info-bg:rgba(9,105,218,.10);
|
|
20
20
|
--mono:"IBM Plex Mono", ui-monospace, "Cascadia Mono", Consolas, Menlo, monospace;
|
|
21
21
|
}
|
|
@@ -229,7 +229,7 @@
|
|
|
229
229
|
<div class="workspace" id="workspace">
|
|
230
230
|
<section class="canvaswrap">
|
|
231
231
|
<div id="viewport" tabindex="0" role="application" aria-label="Flow canvas. Scroll to zoom, drag to pan. Keys: V select, H hand, plus and minus zoom, 0 fit, 1 actual size, F zoom to selection, arrows pan.">
|
|
232
|
-
<div id="world"><svg id="edges" aria-hidden="true"><defs><marker id="arr" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="
|
|
232
|
+
<div id="world"><svg id="edges" aria-hidden="true"><defs><marker id="arr" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="6" markerHeight="6" markerUnits="userSpaceOnUse" orient="auto"><path d="M0 0 L8 4 L0 8 z" fill="context-stroke"/></marker></defs><g id="edgeLayer"></g></svg></div>
|
|
233
233
|
<div id="cols" aria-hidden="true"></div>
|
|
234
234
|
<div id="tools" role="toolbar" aria-label="Canvas controls" aria-orientation="vertical"></div>
|
|
235
235
|
<div id="zoomLabel" aria-hidden="true">100%</div>
|
|
@@ -286,6 +286,7 @@
|
|
|
286
286
|
if (!Array.isArray(m.lanes) || m.layout !== "auto" && m.lanes.length === 0) add("error", "lanes-missing", "lanes", "At least one lane is required.");
|
|
287
287
|
if (!m.states || typeof m.states !== "object" || Object.keys(m.states).length === 0) add("error", "states-missing", "states", "At least one state is required.");
|
|
288
288
|
const columns = Array.isArray(m.columns) ? m.columns : [];
|
|
289
|
+
if (columns[0]?.continues) add("error", "column-continues-first", "columns[0]", "The first column cannot continue a column before it.");
|
|
289
290
|
const lanes = Array.isArray(m.lanes) ? m.lanes : [];
|
|
290
291
|
const states = m.states && typeof m.states === "object" ? m.states : {};
|
|
291
292
|
if (!m.meta || typeof m.meta !== "object" || typeof m.meta.title !== "string") add("error", "meta-missing", "meta", "meta with a title is required.");
|
|
@@ -591,6 +592,13 @@
|
|
|
591
592
|
var baselineView = (m) => m.views?.[0]?.id ?? "all";
|
|
592
593
|
var defaultView = (m) => (m.views?.find((v) => v.default) ?? m.views?.[0])?.id ?? "all";
|
|
593
594
|
var initialCollapsed = (m) => new Set(m.nodes.filter((n) => n.kind === "group" && n.contains && n.collapsed !== false).map((n) => n.id));
|
|
595
|
+
var stripHeads = (m) => {
|
|
596
|
+
let head = 0;
|
|
597
|
+
return m.columns.map((c, i) => {
|
|
598
|
+
if (!c.continues || i === 0) head = i;
|
|
599
|
+
return head;
|
|
600
|
+
});
|
|
601
|
+
};
|
|
594
602
|
var columnTitle = (m, n) => n.col === void 0 ? "" : m.columns[n.col]?.title ?? "";
|
|
595
603
|
function groupFor(m, nodeId) {
|
|
596
604
|
return m.nodes.find((n) => n.kind === "group" && n.contains?.includes(nodeId));
|
|
@@ -1415,7 +1423,7 @@
|
|
|
1415
1423
|
if (!cols.firstChild) {
|
|
1416
1424
|
model.columns.forEach((c, i2) => {
|
|
1417
1425
|
const h = el("div", "hd");
|
|
1418
|
-
h.textContent = (c.short ?? c.title).toUpperCase();
|
|
1426
|
+
h.textContent = c.continues ? "" : (c.short ?? c.title).toUpperCase();
|
|
1419
1427
|
h.title = c.title;
|
|
1420
1428
|
h.dataset.i = String(i2);
|
|
1421
1429
|
cols.appendChild(h);
|
|
@@ -1801,16 +1809,18 @@
|
|
|
1801
1809
|
}
|
|
1802
1810
|
function buildStrip() {
|
|
1803
1811
|
const box = els.strip;
|
|
1812
|
+
const heads = stripHeads(model), cells = model.columns.map((c, i) => ({ c, i })).filter(({ i }) => heads[i] === i);
|
|
1804
1813
|
box.style.display = model.columns.length ? "" : "none";
|
|
1805
|
-
box.style.gridTemplateColumns = `repeat(${Math.max(1,
|
|
1814
|
+
box.style.gridTemplateColumns = `repeat(${Math.max(1, cells.length)}, minmax(0, 1fr))`;
|
|
1806
1815
|
if (!box.firstChild) {
|
|
1807
|
-
box.innerHTML =
|
|
1816
|
+
box.innerHTML = cells.map(({ c }) => `<div class="lcell"><div class="ln">${esc(c.title)}</div><div class="lb"><i class="good"></i><i class="neutral"></i><i class="gap"></i></div><div class="lv"></div></div>`).join("");
|
|
1808
1817
|
}
|
|
1809
|
-
box.querySelectorAll(".lcell").forEach((cell,
|
|
1810
|
-
|
|
1818
|
+
box.querySelectorAll(".lcell").forEach((cell, cellIndex) => {
|
|
1819
|
+
const i = cells[cellIndex]?.i ?? cellIndex;
|
|
1820
|
+
cell.style.setProperty("--i", String(cellIndex));
|
|
1811
1821
|
let tot = 0, good = 0, neutral = 0, gap = 0;
|
|
1812
1822
|
for (const n of model.nodes) {
|
|
1813
|
-
if (n.col !== i || n.kind === "chip") continue;
|
|
1823
|
+
if (heads[n.col ?? -1] !== i || n.kind === "chip") continue;
|
|
1814
1824
|
if (!visible(n.id)) continue;
|
|
1815
1825
|
const w = Math.max(1, n.weight ?? 1);
|
|
1816
1826
|
tot += w;
|
|
@@ -2029,7 +2039,7 @@
|
|
|
2029
2039
|
const markdown = () => {
|
|
2030
2040
|
const out = [`# ${model.meta.title}`, "", model.meta.help, "", `As of ${model.meta.asOf}. Source: ${model.meta.source}`, "", "## States"];
|
|
2031
2041
|
for (const [id, s] of Object.entries(model.states)) out.push(`- **${s.word}** (${id}): tone ${s.tone}, border ${s.border}, link ${s.edge}, motion ${s.motion}, bucket ${s.bucket}`);
|
|
2032
|
-
out.push("", "## Columns", ...model.columns.map((c, i) => `${i + 1}. ${c.title}${c.phase ? ` (${c.phase})` : ""}`));
|
|
2042
|
+
out.push("", "## Columns", ...model.columns.filter((c) => !c.continues).map((c, i) => `${i + 1}. ${c.title}${c.phase ? ` (${c.phase})` : ""}`));
|
|
2033
2043
|
out.push("", "## Lanes", ...model.lanes.map((l) => `- ${l.title}${l.badge ? ` [${l.badge}]` : ""}${l.note ? `: ${l.note}` : ""}`));
|
|
2034
2044
|
out.push("", `## Size`, `${model.nodes.length} nodes, ${model.links.length} links.`);
|
|
2035
2045
|
if (notes.length) out.push("", "## Notes", ...notes.map((n) => `${n.id}. **${n.title}**: ${n.text}`));
|
|
@@ -2173,7 +2183,7 @@
|
|
|
2173
2183
|
let t = 0, idx = -1, playing = false, frame = 0, timer = 0, stamp = 0, choicesKey = "";
|
|
2174
2184
|
let markedEdges = [];
|
|
2175
2185
|
let markedNodes = [];
|
|
2176
|
-
const token = sv("circle", { id: "token", class: "token", r: "
|
|
2186
|
+
const token = sv("circle", { id: "token", class: "token", r: "4" });
|
|
2177
2187
|
e.layer.appendChild(token);
|
|
2178
2188
|
const pathOf = (link) => ctx.edgeEls[ctx.links.findIndex((l) => l.id === link)];
|
|
2179
2189
|
const nodeOf = (id) => ctx.nodeEls.get(id);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
:root {
|
|
2
2
|
--bg:#f6f8fa; --panel:#ffffff; --panel2:#f6f8fa; --edge:#d0d7de; --rule:#d8dee4; --fg:#1f2328; --muted:#59636e;
|
|
3
3
|
--accent:#0969da; --accent-solid:#0969da; --ok:#1a7f37; --warn:#9a6700; --crit:#cf222e; --glow:rgba(9,105,218,.22);
|
|
4
|
-
--up:#0a7ea4; --down:#8250df; --shadow:0 1px 0 rgba(31,35,40,.04), 0 6px 18px rgba(31,35,40,.08); --grid:rgba(31,35,40,.035); --dot:rgba(31,35,40,.18); --dot-size:1.2px; --dot-gap:24px; --edge-w:
|
|
4
|
+
--up:#0a7ea4; --down:#8250df; --shadow:0 1px 0 rgba(31,35,40,.04), 0 6px 18px rgba(31,35,40,.08); --grid:rgba(31,35,40,.035); --dot:rgba(31,35,40,.18); --dot-size:1.2px; --dot-gap:24px; --edge-w:0.8px; --edge-w-hot:1px; --edge-w-play:1.2px; --trail-w:1.3px; --ripple-w:1.5px;
|
|
5
5
|
--warn-bg:rgba(154,103,0,.10); --ok-bg:rgba(26,127,55,.10); --crit-bg:rgba(207,34,46,.08); --info-bg:rgba(9,105,218,.10);
|
|
6
6
|
--mono:"IBM Plex Mono", ui-monospace, "Cascadia Mono", Consolas, Menlo, monospace;
|
|
7
7
|
}
|
|
@@ -29,6 +29,7 @@ function validateModel(input) {
|
|
|
29
29
|
if (!Array.isArray(m.lanes) || m.layout !== "auto" && m.lanes.length === 0) add("error", "lanes-missing", "lanes", "At least one lane is required.");
|
|
30
30
|
if (!m.states || typeof m.states !== "object" || Object.keys(m.states).length === 0) add("error", "states-missing", "states", "At least one state is required.");
|
|
31
31
|
const columns = Array.isArray(m.columns) ? m.columns : [];
|
|
32
|
+
if (columns[0]?.continues) add("error", "column-continues-first", "columns[0]", "The first column cannot continue a column before it.");
|
|
32
33
|
const lanes = Array.isArray(m.lanes) ? m.lanes : [];
|
|
33
34
|
const states = m.states && typeof m.states === "object" ? m.states : {};
|
|
34
35
|
if (!m.meta || typeof m.meta !== "object" || typeof m.meta.title !== "string") add("error", "meta-missing", "meta", "meta with a title is required.");
|
|
@@ -347,11 +348,19 @@ __export(rules_exports, {
|
|
|
347
348
|
linkVisible: () => linkVisible,
|
|
348
349
|
nodeStateId: () => nodeStateId,
|
|
349
350
|
nodeVisible: () => nodeVisible,
|
|
351
|
+
stripHeads: () => stripHeads,
|
|
350
352
|
toggleGroupState: () => toggleGroupState
|
|
351
353
|
});
|
|
352
354
|
var baselineView = (m) => m.views?.[0]?.id ?? "all";
|
|
353
355
|
var defaultView = (m) => (m.views?.find((v) => v.default) ?? m.views?.[0])?.id ?? "all";
|
|
354
356
|
var initialCollapsed = (m) => new Set(m.nodes.filter((n) => n.kind === "group" && n.contains && n.collapsed !== false).map((n) => n.id));
|
|
357
|
+
var stripHeads = (m) => {
|
|
358
|
+
let head = 0;
|
|
359
|
+
return m.columns.map((c, i) => {
|
|
360
|
+
if (!c.continues || i === 0) head = i;
|
|
361
|
+
return head;
|
|
362
|
+
});
|
|
363
|
+
};
|
|
355
364
|
var columnTitle = (m, n) => n.col === void 0 ? "" : m.columns[n.col]?.title ?? "";
|
|
356
365
|
function groupFor(m, nodeId) {
|
|
357
366
|
return m.nodes.find((n) => n.kind === "group" && n.contains?.includes(nodeId));
|
|
@@ -1203,7 +1212,7 @@ function install3(ctx) {
|
|
|
1203
1212
|
if (!cols.firstChild) {
|
|
1204
1213
|
model.columns.forEach((c, i2) => {
|
|
1205
1214
|
const h = el("div", "hd");
|
|
1206
|
-
h.textContent = (c.short ?? c.title).toUpperCase();
|
|
1215
|
+
h.textContent = c.continues ? "" : (c.short ?? c.title).toUpperCase();
|
|
1207
1216
|
h.title = c.title;
|
|
1208
1217
|
h.dataset.i = String(i2);
|
|
1209
1218
|
cols.appendChild(h);
|
|
@@ -1589,16 +1598,18 @@ function install5(ctx) {
|
|
|
1589
1598
|
}
|
|
1590
1599
|
function buildStrip() {
|
|
1591
1600
|
const box = els.strip;
|
|
1601
|
+
const heads = stripHeads(model), cells = model.columns.map((c, i) => ({ c, i })).filter(({ i }) => heads[i] === i);
|
|
1592
1602
|
box.style.display = model.columns.length ? "" : "none";
|
|
1593
|
-
box.style.gridTemplateColumns = `repeat(${Math.max(1,
|
|
1603
|
+
box.style.gridTemplateColumns = `repeat(${Math.max(1, cells.length)}, minmax(0, 1fr))`;
|
|
1594
1604
|
if (!box.firstChild) {
|
|
1595
|
-
box.innerHTML =
|
|
1605
|
+
box.innerHTML = cells.map(({ c }) => `<div class="lcell"><div class="ln">${esc(c.title)}</div><div class="lb"><i class="good"></i><i class="neutral"></i><i class="gap"></i></div><div class="lv"></div></div>`).join("");
|
|
1596
1606
|
}
|
|
1597
|
-
box.querySelectorAll(".lcell").forEach((cell,
|
|
1598
|
-
|
|
1607
|
+
box.querySelectorAll(".lcell").forEach((cell, cellIndex) => {
|
|
1608
|
+
const i = cells[cellIndex]?.i ?? cellIndex;
|
|
1609
|
+
cell.style.setProperty("--i", String(cellIndex));
|
|
1599
1610
|
let tot = 0, good = 0, neutral = 0, gap = 0;
|
|
1600
1611
|
for (const n of model.nodes) {
|
|
1601
|
-
if (n.col !== i || n.kind === "chip") continue;
|
|
1612
|
+
if (heads[n.col ?? -1] !== i || n.kind === "chip") continue;
|
|
1602
1613
|
if (!visible(n.id)) continue;
|
|
1603
1614
|
const w = Math.max(1, n.weight ?? 1);
|
|
1604
1615
|
tot += w;
|
|
@@ -1817,7 +1828,7 @@ function install6(ctx) {
|
|
|
1817
1828
|
const markdown = () => {
|
|
1818
1829
|
const out = [`# ${model.meta.title}`, "", model.meta.help, "", `As of ${model.meta.asOf}. Source: ${model.meta.source}`, "", "## States"];
|
|
1819
1830
|
for (const [id, s] of Object.entries(model.states)) out.push(`- **${s.word}** (${id}): tone ${s.tone}, border ${s.border}, link ${s.edge}, motion ${s.motion}, bucket ${s.bucket}`);
|
|
1820
|
-
out.push("", "## Columns", ...model.columns.map((c, i) => `${i + 1}. ${c.title}${c.phase ? ` (${c.phase})` : ""}`));
|
|
1831
|
+
out.push("", "## Columns", ...model.columns.filter((c) => !c.continues).map((c, i) => `${i + 1}. ${c.title}${c.phase ? ` (${c.phase})` : ""}`));
|
|
1821
1832
|
out.push("", "## Lanes", ...model.lanes.map((l) => `- ${l.title}${l.badge ? ` [${l.badge}]` : ""}${l.note ? `: ${l.note}` : ""}`));
|
|
1822
1833
|
out.push("", `## Size`, `${model.nodes.length} nodes, ${model.links.length} links.`);
|
|
1823
1834
|
if (notes.length) out.push("", "## Notes", ...notes.map((n) => `${n.id}. **${n.title}**: ${n.text}`));
|
|
@@ -1972,7 +1983,7 @@ function install7(ctx) {
|
|
|
1972
1983
|
let t = 0, idx = -1, playing = false, frame = 0, timer = 0, stamp = 0, choicesKey = "";
|
|
1973
1984
|
let markedEdges = [];
|
|
1974
1985
|
let markedNodes = [];
|
|
1975
|
-
const token = sv("circle", { id: "token", class: "token", r: "
|
|
1986
|
+
const token = sv("circle", { id: "token", class: "token", r: "4" });
|
|
1976
1987
|
e.layer.appendChild(token);
|
|
1977
1988
|
const pathOf = (link) => ctx.edgeEls[ctx.links.findIndex((l) => l.id === link)];
|
|
1978
1989
|
const nodeOf = (id) => ctx.nodeEls.get(id);
|
|
@@ -2437,7 +2448,7 @@ function mount(root, model, opts = {}) {
|
|
|
2437
2448
|
}
|
|
2438
2449
|
|
|
2439
2450
|
// src/lib.ts
|
|
2440
|
-
var SHELL_BODY_HTML = "<header class=\"top\">\r\n <span class=\"brand\"><i></i>flow-studio</span>\r\n <span class=\"spacer\"></span>\r\n <button id=\"notesBtn\" type=\"button\" aria-pressed=\"false\">Implementation notes (N)</button>\r\n <button id=\"theme\" type=\"button\" aria-label=\"Toggle light and dark\">☀ / ☾</button>\r\n</header>\r\n<main>\r\n <div class=\"titlerow\"><h1 id=\"title\"></h1><span class=\"readline\" id=\"readat\"></span></div>\r\n <p class=\"help\" id=\"help\"></p>\r\n <div id=\"banner\" role=\"alert\" hidden></div>\r\n\r\n <section class=\"summary\"><div class=\"layerstrip\" id=\"layerstrip\" aria-label=\"Completeness by column\"></div><div class=\"tiles\" id=\"kpis\" aria-label=\"Summary\"></div></section>\r\n\r\n <div class=\"toolbar\">\r\n <div class=\"seggrp\" role=\"tablist\" aria-label=\"View\" id=\"modeSwitch\"></div>\r\n <input id=\"q\" type=\"search\" placeholder=\"Find a group or member… ( / )\" aria-label=\"Find a group or member\">\r\n <select id=\"hops\" aria-label=\"Search radius\"><option value=\"0\">This group</option><option value=\"1\">+1 hop</option><option value=\"2\">+2 hops</option><option value=\"3\">+3 hops</option></select>\r\n <span id=\"matchinfo\" class=\"note\" style=\"margin:0\"></span>\r\n </div>\r\n\r\n <div class=\"workspace\" id=\"workspace\">\r\n <section class=\"canvaswrap\">\r\n <div id=\"viewport\" tabindex=\"0\" role=\"application\" aria-label=\"Flow canvas. Scroll to zoom, drag to pan. Keys: V select, H hand, plus and minus zoom, 0 fit, 1 actual size, F zoom to selection, arrows pan.\">\r\n <div id=\"world\"><svg id=\"edges\" aria-hidden=\"true\"><defs><marker id=\"arr\" viewBox=\"0 0 8 8\" refX=\"7\" refY=\"4\" markerWidth=\"
|
|
2451
|
+
var SHELL_BODY_HTML = "<header class=\"top\">\r\n <span class=\"brand\"><i></i>flow-studio</span>\r\n <span class=\"spacer\"></span>\r\n <button id=\"notesBtn\" type=\"button\" aria-pressed=\"false\">Implementation notes (N)</button>\r\n <button id=\"theme\" type=\"button\" aria-label=\"Toggle light and dark\">☀ / ☾</button>\r\n</header>\r\n<main>\r\n <div class=\"titlerow\"><h1 id=\"title\"></h1><span class=\"readline\" id=\"readat\"></span></div>\r\n <p class=\"help\" id=\"help\"></p>\r\n <div id=\"banner\" role=\"alert\" hidden></div>\r\n\r\n <section class=\"summary\"><div class=\"layerstrip\" id=\"layerstrip\" aria-label=\"Completeness by column\"></div><div class=\"tiles\" id=\"kpis\" aria-label=\"Summary\"></div></section>\r\n\r\n <div class=\"toolbar\">\r\n <div class=\"seggrp\" role=\"tablist\" aria-label=\"View\" id=\"modeSwitch\"></div>\r\n <input id=\"q\" type=\"search\" placeholder=\"Find a group or member… ( / )\" aria-label=\"Find a group or member\">\r\n <select id=\"hops\" aria-label=\"Search radius\"><option value=\"0\">This group</option><option value=\"1\">+1 hop</option><option value=\"2\">+2 hops</option><option value=\"3\">+3 hops</option></select>\r\n <span id=\"matchinfo\" class=\"note\" style=\"margin:0\"></span>\r\n </div>\r\n\r\n <div class=\"workspace\" id=\"workspace\">\r\n <section class=\"canvaswrap\">\r\n <div id=\"viewport\" tabindex=\"0\" role=\"application\" aria-label=\"Flow canvas. Scroll to zoom, drag to pan. Keys: V select, H hand, plus and minus zoom, 0 fit, 1 actual size, F zoom to selection, arrows pan.\">\r\n <div id=\"world\"><svg id=\"edges\" aria-hidden=\"true\"><defs><marker id=\"arr\" viewBox=\"0 0 8 8\" refX=\"7\" refY=\"4\" markerWidth=\"6\" markerHeight=\"6\" markerUnits=\"userSpaceOnUse\" orient=\"auto\"><path d=\"M0 0 L8 4 L0 8 z\" fill=\"context-stroke\"/></marker></defs><g id=\"edgeLayer\"></g></svg></div>\r\n <div id=\"cols\" aria-hidden=\"true\"></div>\r\n <div id=\"tools\" role=\"toolbar\" aria-label=\"Canvas controls\" aria-orientation=\"vertical\"></div>\r\n <div id=\"zoomLabel\" aria-hidden=\"true\">100%</div>\r\n </div>\r\n <div id=\"playbar\" hidden aria-label=\"Scenario playback\">\r\n <select id=\"pbScenario\" aria-label=\"Scenario\"></select>\r\n <button id=\"pbPlay\" type=\"button\" aria-label=\"Play\">▶</button>\r\n <button id=\"pbStep\" type=\"button\" aria-label=\"Step\">Step</button>\r\n <button id=\"pbReset\" type=\"button\" aria-label=\"Reset\">Reset</button>\r\n <select id=\"pbSpeed\" aria-label=\"Speed\"><option value=\"0.5\">0.5x</option><option value=\"1\" selected>1x</option><option value=\"2\">2x</option></select>\r\n <input id=\"pbScrub\" type=\"range\" min=\"0\" max=\"1000\" value=\"0\" aria-label=\"Playback position\" aria-valuetext=\"Step 1 of 1\">\r\n <span id=\"pbCaption\"></span><span id=\"pbCounter\"></span><span id=\"pbChoices\"></span>\r\n </div>\r\n <div class=\"keyline\" id=\"legend\" aria-label=\"Legend\"></div>\r\n <aside class=\"insp\" id=\"inspector\" aria-label=\"Inspector\" aria-hidden=\"true\"></aside>\r\n </section>\r\n </div>\r\n <div id=\"live\" class=\"sr\" role=\"status\" aria-live=\"polite\"></div>\r\n\r\n <div class=\"ledger\" id=\"ledger\"></div>\r\n <details class=\"tableview\"><summary>Table view</summary><table class=\"sources\" id=\"tbl\"></table></details>\r\n <details class=\"spec\" id=\"spec\"><summary>Model reference (for the person or agent who builds this)</summary><div id=\"specbody\"></div></details>\r\n</main>\r\n<footer id=\"footer\"></footer>\r\n<aside id=\"drawer\" aria-label=\"Implementation notes\"><h2>Implementation notes</h2><p class=\"note\">Numbered markers on the page match these cards. Hover a card to find its place; click a marker to jump to its card.</p><div id=\"notelist\"></div></aside>\r\n\r\n";
|
|
2441
2452
|
export {
|
|
2442
2453
|
K_MAX,
|
|
2443
2454
|
K_MIN,
|
|
@@ -2482,6 +2493,7 @@ export {
|
|
|
2482
2493
|
rules_exports as rules,
|
|
2483
2494
|
slaState,
|
|
2484
2495
|
stateCss,
|
|
2496
|
+
stripHeads,
|
|
2485
2497
|
suggestedExit,
|
|
2486
2498
|
toggleGroupState,
|
|
2487
2499
|
trace_exports as trace,
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
<div class="workspace" id="workspace">
|
|
22
22
|
<section class="canvaswrap">
|
|
23
23
|
<div id="viewport" tabindex="0" role="application" aria-label="Flow canvas. Scroll to zoom, drag to pan. Keys: V select, H hand, plus and minus zoom, 0 fit, 1 actual size, F zoom to selection, arrows pan.">
|
|
24
|
-
<div id="world"><svg id="edges" aria-hidden="true"><defs><marker id="arr" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="
|
|
24
|
+
<div id="world"><svg id="edges" aria-hidden="true"><defs><marker id="arr" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="6" markerHeight="6" markerUnits="userSpaceOnUse" orient="auto"><path d="M0 0 L8 4 L0 8 z" fill="context-stroke"/></marker></defs><g id="edgeLayer"></g></svg></div>
|
|
25
25
|
<div id="cols" aria-hidden="true"></div>
|
|
26
26
|
<div id="tools" role="toolbar" aria-label="Canvas controls" aria-orientation="vertical"></div>
|
|
27
27
|
<div id="zoomLabel" aria-hidden="true">100%</div>
|
package/bundles/manifest.json
CHANGED
package/package.json
CHANGED