@squinch/core 0.1.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.
Files changed (64) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +41 -0
  3. package/dist/api.d.ts +91 -0
  4. package/dist/api.js +232 -0
  5. package/dist/browser.d.ts +3 -0
  6. package/dist/browser.js +9 -0
  7. package/dist/diff/diff.d.ts +30 -0
  8. package/dist/diff/diff.js +365 -0
  9. package/dist/fonts.generated.d.ts +1 -0
  10. package/dist/fonts.generated.js +6 -0
  11. package/dist/grammar/parser.js +22 -0
  12. package/dist/grammar/parser.terms.js +115 -0
  13. package/dist/index.d.ts +4 -0
  14. package/dist/index.js +4 -0
  15. package/dist/layout/layout.d.ts +197 -0
  16. package/dist/layout/layout.js +1721 -0
  17. package/dist/metrics.d.ts +20 -0
  18. package/dist/metrics.generated.d.ts +4 -0
  19. package/dist/metrics.generated.js +4 -0
  20. package/dist/metrics.js +57 -0
  21. package/dist/model/build.d.ts +8 -0
  22. package/dist/model/build.js +1343 -0
  23. package/dist/model/packs.d.ts +13 -0
  24. package/dist/model/packs.js +29 -0
  25. package/dist/model/source.d.ts +10 -0
  26. package/dist/model/source.js +28 -0
  27. package/dist/model/suggest.d.ts +2 -0
  28. package/dist/model/suggest.js +24 -0
  29. package/dist/model/types.d.ts +226 -0
  30. package/dist/model/types.js +24 -0
  31. package/dist/packs/node-fs.d.ts +1 -0
  32. package/dist/packs/node-fs.js +37 -0
  33. package/dist/packs/registry.d.ts +61 -0
  34. package/dist/packs/registry.js +122 -0
  35. package/dist/packs/sanitize.d.ts +12 -0
  36. package/dist/packs/sanitize.js +127 -0
  37. package/dist/packs/sysGlyphs.d.ts +2 -0
  38. package/dist/packs/sysGlyphs.js +21 -0
  39. package/dist/render/adaptive.d.ts +13 -0
  40. package/dist/render/adaptive.js +112 -0
  41. package/dist/render/html/runtime.d.ts +1 -0
  42. package/dist/render/html/runtime.generated.d.ts +1 -0
  43. package/dist/render/html/runtime.generated.js +6 -0
  44. package/dist/render/html/runtime.js +362 -0
  45. package/dist/render/html.d.ts +39 -0
  46. package/dist/render/html.js +235 -0
  47. package/dist/render/svg.d.ts +75 -0
  48. package/dist/render/svg.js +1403 -0
  49. package/dist/render/validate.d.ts +4 -0
  50. package/dist/render/validate.js +9 -0
  51. package/dist/themes/index.d.ts +84 -0
  52. package/dist/themes/index.js +90 -0
  53. package/dist/view/dive.d.ts +55 -0
  54. package/dist/view/dive.js +57 -0
  55. package/dist/view/navigate.d.ts +38 -0
  56. package/dist/view/navigate.js +81 -0
  57. package/dist/view/resolve.d.ts +92 -0
  58. package/dist/view/resolve.js +591 -0
  59. package/fonts/inter-400.ttf +0 -0
  60. package/fonts/inter-500.ttf +0 -0
  61. package/fonts/inter-600.ttf +0 -0
  62. package/fonts/mono-400.ttf +0 -0
  63. package/metrics.json +510 -0
  64. package/package.json +89 -0
@@ -0,0 +1,4 @@
1
+ export declare function validateSVG(svg: string): {
2
+ ok: boolean;
3
+ error?: string;
4
+ };
@@ -0,0 +1,9 @@
1
+ // Real XML validation for every rendered SVG — duplicate attributes, unclosed
2
+ // tags, bad entities. Cheap enough to run on every render in tests and tools.
3
+ import { XMLValidator } from "fast-xml-parser";
4
+ export function validateSVG(svg) {
5
+ const result = XMLValidator.validate(svg, { allowBooleanAttributes: false });
6
+ if (result === true)
7
+ return { ok: true };
8
+ return { ok: false, error: `${result.err.msg} (line ${result.err.line}, col ${result.err.col})` };
9
+ }
@@ -0,0 +1,84 @@
1
+ import type { Hue } from "../model/types.js";
2
+ export interface ThemeFont {
3
+ /** CSS font-family stack; first entry is the embedded face's family name. */
4
+ css: string;
5
+ /** which metrics/embedding family backs this theme */
6
+ metrics: "inter";
7
+ /** Multiplies emitted font sizes AND text measurement, so layout stays
8
+ * truthful. 1 for every shipping theme; the hook survives the sketch theme
9
+ * it was built for because a future display face may run small again. */
10
+ scale: number;
11
+ }
12
+ export interface Theme {
13
+ name: string;
14
+ canvas: string;
15
+ surface: string;
16
+ border: string;
17
+ ink: string;
18
+ muted: string;
19
+ edge: string;
20
+ asyncEdge: string;
21
+ plateText: string;
22
+ accent: string;
23
+ /** Ink inside a flow bead — dark on the light-on-dark bead, white on the
24
+ * saturated light one, so the number reads at 10px either way. */
25
+ beadText: string;
26
+ /** Warning-note plate. On its way out: docs/design retires the amber fill
27
+ * (the only third hue in a two-hue palette, and it read as a sticky note),
28
+ * and the distinction moves to a glyph. It stays until that glyph exists —
29
+ * dropping it first would silently erase an authored `style: warning`. */
30
+ warnTint: string;
31
+ surfaceAlt: string;
32
+ /** The card gradient's two stops, top to bottom: a 4% ramp that reads as a
33
+ * lit surface rather than a flat fill (docs/design). `surfaceLo` is also
34
+ * the shelf, so the strip along a card's bottom continues the same surface
35
+ * instead of reading as a separate block. */
36
+ surfaceHi: string;
37
+ surfaceLo: string;
38
+ /** Hairline between a card's body and its shelf — lighter than `border`,
39
+ * because it divides one surface rather than bounding two. */
40
+ shelfLine: string;
41
+ /** The neutral chip an icon sits on: card plates, glyph chips, shelf chips.
42
+ * Doubles as the actor tile's upper tone — an actor is a filled shape, and
43
+ * this is the quietest fill that still separates from canvas. */
44
+ plate: string;
45
+ /** The actor tile's lower gradient stop, a step under `plate`. */
46
+ actorLo: string;
47
+ /** The stacked sheets behind a container — "there is more inside". Quieter
48
+ * than the card's own border and fill: a hint, not a stack of real cards. */
49
+ sheetFill: string;
50
+ sheetBorder: string;
51
+ /** Dimmer than `muted`: a note's glyph, the title block's date — present,
52
+ * but never the thing read first. */
53
+ faint: string;
54
+ /** Dimmest tone that still passes as text: the footer wordmark. */
55
+ dim: string;
56
+ /** The 1px contact shadow under cards, leaves, actors and notes. rgba, not
57
+ * hex: the alpha is the whole effect, and it must ride `flood-color` for
58
+ * the adaptive merge to treat it as colour rather than geometry. */
59
+ shadow: string;
60
+ font: ThemeFont;
61
+ /** The dark counterpart this theme can share one adaptive file with. Only
62
+ * themes with the same font can pair: type metrics drive layout, so a
63
+ * cross-font pair would draw two different diagrams. */
64
+ pairsWith?: string;
65
+ /** The eight author hues (SPEC `color:`), one designed pair per theme —
66
+ * darker and more saturated on paper, lifted on the dark canvas. Also the
67
+ * zone kind tints (DESIGN §5: account→red, network→blue, cloud→violet,
68
+ * the rest→gray). 6-digit hex only: the zone chip concatenates an alpha
69
+ * byte onto these. */
70
+ hueRed: string;
71
+ hueAmber: string;
72
+ hueGreen: string;
73
+ hueTeal: string;
74
+ hueBlue: string;
75
+ hueViolet: string;
76
+ huePink: string;
77
+ hueGray: string;
78
+ }
79
+ /** The one place a hue name becomes a colour. `accent` is the brand token
80
+ * rather than a ninth pair, so it stays whatever the theme says it is. */
81
+ export declare const hueOf: (t: Theme, h: Hue) => string;
82
+ export declare const light: Theme;
83
+ export declare const dark: Theme;
84
+ export declare const themes: Record<string, Theme>;
@@ -0,0 +1,90 @@
1
+ /** The one place a hue name becomes a colour. `accent` is the brand token
2
+ * rather than a ninth pair, so it stays whatever the theme says it is. */
3
+ export const hueOf = (t, h) => h === "accent" ? t.accent : t[`hue${h[0].toUpperCase()}${h.slice(1)}`];
4
+ const inter = {
5
+ css: "SquinchInter, Inter, system-ui, sans-serif",
6
+ metrics: "inter",
7
+ scale: 1,
8
+ };
9
+ export const light = {
10
+ name: "light",
11
+ font: inter,
12
+ pairsWith: "dark",
13
+ // red/blue/violet/gray are the former zone tints, verbatim, so every zone
14
+ // ever rendered is byte-identical; the other four were designed beside them
15
+ hueRed: "#B5544C",
16
+ hueAmber: "#A06B12",
17
+ hueGreen: "#3F8A5C",
18
+ hueTeal: "#1F8A80",
19
+ hueBlue: "#3A6EA8",
20
+ hueViolet: "#6B5FC9",
21
+ huePink: "#B04A8A",
22
+ hueGray: "#7A776E",
23
+ canvas: "#F7F7F5",
24
+ surface: "#FFFFFF",
25
+ border: "#EAE9E5",
26
+ ink: "#1C1C1A",
27
+ muted: "#6F6E69",
28
+ edge: "#57564F",
29
+ // one purple, not two: the design exploration shipped #7C74D9 for async
30
+ // beside #5A57C9 for flow beads and flagged them as indistinguishable
31
+ asyncEdge: "#5A57C9",
32
+ plateText: "#FFFFFF",
33
+ accent: "#5A57C9",
34
+ beadText: "#FFFFFF",
35
+ warnTint: "#FBF3DC",
36
+ surfaceAlt: "#EFEFEC",
37
+ surfaceHi: "#FFFFFF",
38
+ surfaceLo: "#F2F1ED",
39
+ shelfLine: "#EFEFEC",
40
+ plate: "#EFEFEC",
41
+ actorLo: "#E4E3DE",
42
+ faint: "#8A8880",
43
+ dim: "#A5A199",
44
+ sheetFill: "#F2F1ED",
45
+ sheetBorder: "#DEDDD6",
46
+ shadow: "rgba(28,28,26,0.06)",
47
+ };
48
+ export const dark = {
49
+ name: "dark",
50
+ font: inter,
51
+ hueRed: "#D08078",
52
+ hueAmber: "#D9B168",
53
+ hueGreen: "#6FC29A",
54
+ hueTeal: "#4AC4B8",
55
+ hueBlue: "#6E9CD0",
56
+ hueViolet: "#9C93E8",
57
+ huePink: "#D685BB",
58
+ hueGray: "#8F8C82",
59
+ canvas: "#161618",
60
+ surface: "#212124",
61
+ border: "#3B3B40",
62
+ ink: "#EDEDEA",
63
+ muted: "#9C9B94",
64
+ edge: "#8D8C84",
65
+ asyncEdge: "#8B88E8",
66
+ plateText: "#FFFFFF",
67
+ accent: "#8B88E8",
68
+ // the dark bead is a light lavender disc, so its number is ink, not white
69
+ beadText: "#1B1B2E",
70
+ warnTint: "#3A3423",
71
+ surfaceAlt: "#1D1D20",
72
+ surfaceHi: "#26262A",
73
+ surfaceLo: "#1D1D20",
74
+ shelfLine: "#2E2E33",
75
+ plate: "#2F2F34",
76
+ actorLo: "#26262A",
77
+ faint: "#7A796F",
78
+ dim: "#6E6D67",
79
+ sheetFill: "#1D1D20",
80
+ sheetBorder: "#33333A",
81
+ shadow: "rgba(0,0,0,0.5)",
82
+ };
83
+ // Light and dark are the shipping pair. The sketch, sketch-dark and contrast
84
+ // themes were retired in the docs/design restyle (2026-08): the restyle's card
85
+ // anatomy — gradient ramps, contact shadows, the stacked-sheet affordance, the
86
+ // segmented chip grammar — is a designed surface with no hand-drawn or
87
+ // pure-black translation, and three unreviewed palettes silently riding every
88
+ // geometry change is a cost with no reader. Adding a theme back means designing
89
+ // it against docs/design, not swapping a palette.
90
+ export const themes = { light, dark };
@@ -0,0 +1,55 @@
1
+ export interface Box {
2
+ x: number;
3
+ y: number;
4
+ w: number;
5
+ h: number;
6
+ }
7
+ /** How far the eye travels. Anything past ~3× reads as a jump cut, not a move. */
8
+ export declare const CAP = 3.2;
9
+ /** The incoming layer travels a fraction of the outgoing one — a full mirror
10
+ * overshoots and feels like two separate animations played back to back. */
11
+ export declare const TRAVEL = 0.62;
12
+ /** The dive itself, and the anchorless fallback for a hop between two views at
13
+ * the same altitude, where there is no shared card to travel through. */
14
+ export declare const DIVE: {
15
+ ms: number;
16
+ ease: string;
17
+ };
18
+ export declare const CUT: {
19
+ ms: number;
20
+ ease: string;
21
+ };
22
+ export declare const centre: (b: Box) => {
23
+ x: number;
24
+ y: number;
25
+ };
26
+ export declare const clamp: (v: number, lo: number, hi: number) => number;
27
+ /** Scale factor for a dive through `anchor` within `view`, clamped both ways. */
28
+ export declare const scaleFor: (view: Box, anchor: Box) => number;
29
+ export interface DiveInput {
30
+ /** the visible canvas, in canvas space */
31
+ view: Box;
32
+ /** the outgoing layer's box */
33
+ ghostBox: Box;
34
+ /** the incoming layer's box */
35
+ liveBox: Box;
36
+ /** the shared card. Absent = a lateral hop, which gets the cut instead. */
37
+ anchor?: Box;
38
+ dir: "in" | "out";
39
+ }
40
+ export interface DiveOutput {
41
+ ms: number;
42
+ ease: string;
43
+ gOrigin: string;
44
+ lOrigin: string;
45
+ gEnd: string;
46
+ lStart: string;
47
+ }
48
+ /**
49
+ * Zooming in, the old picture flies past — the anchor grows to fill the screen —
50
+ * while the new one emerges from where that card sat. Zooming out is the exact
51
+ * inverse, which is why one pair of expressions covers both directions. That
52
+ * symmetry is the thing worth testing: it is easy to break one direction while
53
+ * the other still looks right.
54
+ */
55
+ export declare function diveTransforms(input: DiveInput): DiveOutput;
@@ -0,0 +1,57 @@
1
+ // The anchored dive, as arithmetic.
2
+ //
3
+ // Changing altitude animates about the one card the two views share, so the
4
+ // reader never has to re-find their place (DESIGN §11, docs/notes/zoom-
5
+ // transitions.md). All of the geometry lives here, taking four measured
6
+ // rectangles and returning the transforms — no DOM, so it can be tested, and so
7
+ // `scripts/hero-gif.mts` can re-derive the README animation from the same
8
+ // constants instead of its own copy of them. Two copies in two languages with a
9
+ // comment asserting they match is not a thing that stays true.
10
+ //
11
+ // It sat in `apps/spa/src/lib/` while the playground was the only thing that
12
+ // zoomed. The interactive HTML export zooms too and is built by core, so this
13
+ // moved here rather than becoming the second copy its own header warns about.
14
+ /** How far the eye travels. Anything past ~3× reads as a jump cut, not a move. */
15
+ export const CAP = 3.2;
16
+ /** The incoming layer travels a fraction of the outgoing one — a full mirror
17
+ * overshoots and feels like two separate animations played back to back. */
18
+ export const TRAVEL = 0.62;
19
+ /** The dive itself, and the anchorless fallback for a hop between two views at
20
+ * the same altitude, where there is no shared card to travel through. */
21
+ export const DIVE = { ms: 460, ease: "cubic-bezier(.32,.72,0,1)" };
22
+ export const CUT = { ms: 240, ease: "cubic-bezier(.4,0,.2,1)" };
23
+ export const centre = (b) => ({ x: b.x + b.w / 2, y: b.y + b.h / 2 });
24
+ export const clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
25
+ /** Scale factor for a dive through `anchor` within `view`, clamped both ways. */
26
+ export const scaleFor = (view, anchor) => clamp(Math.min(view.w / anchor.w, view.h / anchor.h), 1.15, CAP);
27
+ /**
28
+ * Zooming in, the old picture flies past — the anchor grows to fill the screen —
29
+ * while the new one emerges from where that card sat. Zooming out is the exact
30
+ * inverse, which is why one pair of expressions covers both directions. That
31
+ * symmetry is the thing worth testing: it is easy to break one direction while
32
+ * the other still looks right.
33
+ */
34
+ export function diveTransforms(input) {
35
+ const { view, ghostBox, liveBox, anchor, dir } = input;
36
+ const { ms, ease } = anchor ? DIVE : CUT;
37
+ if (!anchor)
38
+ return { ms, ease, gOrigin: "50% 50%", lOrigin: "50% 50%", gEnd: "scale(.97)", lStart: "scale(1.03)" };
39
+ const k = scaleFor(view, anchor);
40
+ const kIn = 1 + (k - 1) * TRAVEL;
41
+ const A = centre(anchor), V = centre(view);
42
+ const dx = V.x - A.x, dy = V.y - A.y; // anchor centre → screen centre
43
+ const into = dir === "in";
44
+ const gPoint = into ? A : V;
45
+ const lPoint = into ? V : A;
46
+ return {
47
+ ms, ease,
48
+ gOrigin: `${gPoint.x - ghostBox.x}px ${gPoint.y - ghostBox.y}px`,
49
+ lOrigin: `${lPoint.x - liveBox.x}px ${lPoint.y - liveBox.y}px`,
50
+ gEnd: into
51
+ ? `translate(${dx}px, ${dy}px) scale(${k})`
52
+ : `translate(${-dx}px, ${-dy}px) scale(${1 / k})`,
53
+ lStart: into
54
+ ? `translate(${-dx}px, ${-dy}px) scale(${1 / kIn})`
55
+ : `translate(${dx}px, ${dy}px) scale(${kIn})`,
56
+ };
57
+ }
@@ -0,0 +1,38 @@
1
+ /** A view as a zoom target: what it is called, and the container it looks at. */
2
+ export interface NavView {
3
+ name: string;
4
+ scope?: string;
5
+ title?: string;
6
+ auto?: boolean;
7
+ }
8
+ /**
9
+ * The one card that stands for `inner` inside a view scoped to `outer` — the
10
+ * element both altitudes have in common, and therefore the thing to anchor a
11
+ * zoom on. Undefined when the two scopes are not nested (a lateral hop), which
12
+ * is what makes the caller fall back to a cut rather than a dive.
13
+ */
14
+ export declare function stepToward(outer: string | undefined, inner: string | undefined): string | undefined;
15
+ /** The ancestor trail of a scope, outermost first — `a.b.c` → a, a.b, a.b.c. */
16
+ export declare function ancestors(scope: string | undefined): string[];
17
+ /** The scope one level out, or undefined at the top. */
18
+ export declare function parentScope(scope: string | undefined): string | undefined;
19
+ /** Zoom target for a clicked element: the view scoped to that container. */
20
+ export declare function viewForPath(views: NavView[], activeView: string | undefined, path: string): NavView | undefined;
21
+ /**
22
+ * How a move to `target` should be animated, derived from how the two scopes
23
+ * relate — never from which control was clicked, which is what makes the
24
+ * breadcrumb, the view tabs and a click on a card all behave the same. A
25
+ * lateral hop (same altitude, different lens) shares no card, so it comes back
26
+ * with no anchor and the caller cuts instead of diving.
27
+ */
28
+ export declare function hop(views: NavView[], activeView: string | undefined, target: string): {
29
+ dir: "in" | "out";
30
+ anchor?: string;
31
+ };
32
+ /** Ancestor trail of the current scope, each hop a view we can jump to. */
33
+ export declare function crumbs(views: NavView[], activeScope: string | undefined): {
34
+ label: string;
35
+ view?: string;
36
+ }[];
37
+ /** One altitude back up: the nearest ancestor that has a view of its own. */
38
+ export declare function upView(views: NavView[], activeView: string | undefined, activeScope: string | undefined): string | undefined;
@@ -0,0 +1,81 @@
1
+ // Navigation arithmetic over dotted scope paths. Pure, and the thing that
2
+ // decides zoom direction for every move between altitudes.
3
+ //
4
+ // Half of this was `apps/spa/src/lib/path.ts` and half was trapped inside
5
+ // `App.tsx` as useMemos — fine while the playground was the only surface that
6
+ // navigated. The interactive HTML export navigates too, and is built by core,
7
+ // so it lives beside `resolve.ts` where the other pure view logic is: same
8
+ // category (functions over the view set), same testability.
9
+ //
10
+ // One semantic to preserve exactly: `viewForPath` and `crumbs` both take the
11
+ // FIRST view matching a scope. `examples/microservices` has three views scoped
12
+ // to `orders` (`orders`, `orders-pci`, `checkout`), so clicking that card lands
13
+ // on `orders` and the breadcrumb labels the hop `orders`. That is
14
+ // declaration-order dependent and deliberately not "improved" here.
15
+ /**
16
+ * The one card that stands for `inner` inside a view scoped to `outer` — the
17
+ * element both altitudes have in common, and therefore the thing to anchor a
18
+ * zoom on. Undefined when the two scopes are not nested (a lateral hop), which
19
+ * is what makes the caller fall back to a cut rather than a dive.
20
+ */
21
+ export function stepToward(outer, inner) {
22
+ if (!inner)
23
+ return undefined;
24
+ if (!outer)
25
+ return inner.split(".")[0];
26
+ if (inner === outer || !inner.startsWith(`${outer}.`))
27
+ return undefined;
28
+ return `${outer}.${inner.slice(outer.length + 1).split(".")[0]}`;
29
+ }
30
+ /** The ancestor trail of a scope, outermost first — `a.b.c` → a, a.b, a.b.c. */
31
+ export function ancestors(scope) {
32
+ if (!scope)
33
+ return [];
34
+ const parts = scope.split(".");
35
+ return parts.map((_, i) => parts.slice(0, i + 1).join("."));
36
+ }
37
+ /** The scope one level out, or undefined at the top. */
38
+ export function parentScope(scope) {
39
+ if (!scope || !scope.includes("."))
40
+ return undefined;
41
+ return scope.slice(0, scope.lastIndexOf("."));
42
+ }
43
+ /** Zoom target for a clicked element: the view scoped to that container. */
44
+ export function viewForPath(views, activeView, path) {
45
+ return views.find((v) => v.scope === path && v.name !== activeView);
46
+ }
47
+ /**
48
+ * How a move to `target` should be animated, derived from how the two scopes
49
+ * relate — never from which control was clicked, which is what makes the
50
+ * breadcrumb, the view tabs and a click on a card all behave the same. A
51
+ * lateral hop (same altitude, different lens) shares no card, so it comes back
52
+ * with no anchor and the caller cuts instead of diving.
53
+ */
54
+ export function hop(views, activeView, target) {
55
+ const from = views.find((v) => v.name === activeView)?.scope;
56
+ const to = views.find((v) => v.name === target)?.scope;
57
+ const down = stepToward(from, to);
58
+ if (down)
59
+ return { dir: "in", anchor: down };
60
+ const up = stepToward(to, from);
61
+ if (up)
62
+ return { dir: "out", anchor: up };
63
+ return { dir: "in" };
64
+ }
65
+ /** Ancestor trail of the current scope, each hop a view we can jump to. */
66
+ export function crumbs(views, activeScope) {
67
+ const trail = [];
68
+ const root = views.find((v) => !v.scope);
69
+ if (root)
70
+ trail.push({ label: "landscape", view: root.name });
71
+ if (!activeScope)
72
+ return trail;
73
+ const parts = activeScope.split(".");
74
+ for (const [i, path] of ancestors(activeScope).entries())
75
+ trail.push({ label: parts[i], view: views.find((v) => v.scope === path)?.name });
76
+ return trail;
77
+ }
78
+ /** One altitude back up: the nearest ancestor that has a view of its own. */
79
+ export function upView(views, activeView, activeScope) {
80
+ return [...crumbs(views, activeScope)].reverse().find((c) => c.view && c.view !== activeView)?.view;
81
+ }
@@ -0,0 +1,92 @@
1
+ import type { Diagnostic, EdgeAnimate, Hue, SModel, SView } from "../model/types.js";
2
+ export interface VNode {
3
+ path: string;
4
+ /** `person` is a leaf the restyle draws differently (a solid actor tile, no
5
+ * border): the initiating human is not a service, and the shape says so
6
+ * before the icon does. Context keeps the muted leaf treatment either way —
7
+ * a periphery card is scenery, and scenery does not need a second shape. */
8
+ kind: "leaf" | "person" | "card" | "context-card" | "context-leaf";
9
+ label: string;
10
+ icon?: {
11
+ pack: string;
12
+ id: string;
13
+ };
14
+ glyph?: {
15
+ pack: string;
16
+ id: string;
17
+ };
18
+ /** Vendor mark composited onto the icon plate's corner (leaf nodes) — the
19
+ * licence-clean vocabulary for platforms with no icon pack (SPEC §nodes). */
20
+ badge?: {
21
+ pack: string;
22
+ id: string;
23
+ };
24
+ tagline?: string;
25
+ preview: {
26
+ pack: string;
27
+ id: string;
28
+ }[];
29
+ /** Leaves beyond the ones `preview` shows — the shelf's `+N`. Counted from
30
+ * the icons that would have been drawn, so it never claims more than the
31
+ * strip actually left out. */
32
+ more?: number;
33
+ /** A short ownership label for the card's shelf: the team, domain or
34
+ * namespace a system belongs to (`domain: "payments"`). Free text. */
35
+ domain?: string;
36
+ tags: string[];
37
+ /** someone else's system — DESIGN §3's hatched surface. A property of the
38
+ * thing itself, so a container carries it for its whole card. */
39
+ external?: boolean;
40
+ description?: string;
41
+ frame?: string;
42
+ /** Effective hue: the view's `color #tag` if one matches, else the element's
43
+ * own `color:`. Context cards ignore it — scenery stays muted. */
44
+ color?: Hue;
45
+ }
46
+ export interface VFrame {
47
+ path: string;
48
+ label: string;
49
+ /** Immediate enclosing frame — same field, same meaning as `VNode.frame`.
50
+ * Only `expand *` produces it: explicit expands never nest (SPEC §5). */
51
+ frame?: string;
52
+ /** Same resolution as `VNode.color`; drawn as the frame's stroke. */
53
+ color?: Hue;
54
+ }
55
+ export interface VEdge {
56
+ id: string;
57
+ from: string;
58
+ to: string;
59
+ label?: string;
60
+ async: boolean;
61
+ /** Which animation, if any (SPEC §edges). Async edges default to `flow`
62
+ * unless `animate: false`; sync edges are still unless they opt in. */
63
+ animate?: EdgeAnimate;
64
+ /** Dash pattern beyond each arrow's default: async is dashed already, so
65
+ * only `dotted` changes it; sync edges can declare either. */
66
+ style?: "dashed" | "dotted";
67
+ count: number;
68
+ /** Effective tags. An aggregate carries the union of what it merged, so a
69
+ * lens over a tag still finds the trunk that hides a tagged edge inside it. */
70
+ tags: string[];
71
+ /** Effective hue (view `color #tag` over the edge's own `color:`). A trunk
72
+ * keeps one only when every member agrees, like `animate`/`style`. */
73
+ color?: Hue;
74
+ /** Which ends get an arrowhead: `->`/`~>` one, `<->` both, `--` none. The
75
+ * view graph used to reduce every arrow to `async: boolean`, so two of the
76
+ * four kinds the grammar accepts drew as a plain one-way arrow. */
77
+ heads: "one" | "both" | "none";
78
+ }
79
+ export interface ViewGraph {
80
+ nodes: VNode[];
81
+ edges: VEdge[];
82
+ frames: VFrame[];
83
+ /** `show flow` badges: edge id → step numbers (a lifted edge can carry
84
+ * several); numbering is the flow's own — steps hidden at this altitude
85
+ * keep their numbers out of the sequence, truthfully. */
86
+ flow?: {
87
+ label: string;
88
+ byEdge: Record<string, number[]>;
89
+ };
90
+ diagnostics: Diagnostic[];
91
+ }
92
+ export declare function resolveView(model: SModel, view: SView): ViewGraph;