@issuegraph/viewer 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 (67) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +156 -0
  3. package/dist/clusters.d.ts +34 -0
  4. package/dist/clusters.d.ts.map +1 -0
  5. package/dist/clusters.js +195 -0
  6. package/dist/clusters.js.map +1 -0
  7. package/dist/document.d.ts +190 -0
  8. package/dist/document.d.ts.map +1 -0
  9. package/dist/document.js +308 -0
  10. package/dist/document.js.map +1 -0
  11. package/dist/element.d.ts +95 -0
  12. package/dist/element.d.ts.map +1 -0
  13. package/dist/element.js +174 -0
  14. package/dist/element.js.map +1 -0
  15. package/dist/index.d.ts +40 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +33 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/layout.d.ts +122 -0
  20. package/dist/layout.d.ts.map +1 -0
  21. package/dist/layout.js +297 -0
  22. package/dist/layout.js.map +1 -0
  23. package/dist/mount.d.ts +64 -0
  24. package/dist/mount.d.ts.map +1 -0
  25. package/dist/mount.js +402 -0
  26. package/dist/mount.js.map +1 -0
  27. package/dist/navigation.d.ts +56 -0
  28. package/dist/navigation.d.ts.map +1 -0
  29. package/dist/navigation.js +121 -0
  30. package/dist/navigation.js.map +1 -0
  31. package/dist/parts.d.ts +89 -0
  32. package/dist/parts.d.ts.map +1 -0
  33. package/dist/parts.js +214 -0
  34. package/dist/parts.js.map +1 -0
  35. package/dist/projections/graph.d.ts +30 -0
  36. package/dist/projections/graph.d.ts.map +1 -0
  37. package/dist/projections/graph.js +644 -0
  38. package/dist/projections/graph.js.map +1 -0
  39. package/dist/projections/linear.d.ts +35 -0
  40. package/dist/projections/linear.d.ts.map +1 -0
  41. package/dist/projections/linear.js +157 -0
  42. package/dist/projections/linear.js.map +1 -0
  43. package/dist/projections/tree.d.ts +17 -0
  44. package/dist/projections/tree.d.ts.map +1 -0
  45. package/dist/projections/tree.js +209 -0
  46. package/dist/projections/tree.js.map +1 -0
  47. package/dist/render.d.ts +35 -0
  48. package/dist/render.d.ts.map +1 -0
  49. package/dist/render.js +45 -0
  50. package/dist/render.js.map +1 -0
  51. package/dist/scene.d.ts +94 -0
  52. package/dist/scene.d.ts.map +1 -0
  53. package/dist/scene.js +48 -0
  54. package/dist/scene.js.map +1 -0
  55. package/dist/styles.d.ts +17 -0
  56. package/dist/styles.d.ts.map +1 -0
  57. package/dist/styles.js +389 -0
  58. package/dist/styles.js.map +1 -0
  59. package/dist/theme.d.ts +71 -0
  60. package/dist/theme.d.ts.map +1 -0
  61. package/dist/theme.js +169 -0
  62. package/dist/theme.js.map +1 -0
  63. package/dist/vocabulary.d.ts +107 -0
  64. package/dist/vocabulary.d.ts.map +1 -0
  65. package/dist/vocabulary.js +98 -0
  66. package/dist/vocabulary.js.map +1 -0
  67. package/package.json +56 -0
@@ -0,0 +1,94 @@
1
+ /**
2
+ * What a projection returns.
3
+ *
4
+ * A scene is the drawn tree PLUS the traversal it implies. Keeping the two
5
+ * together is what lets keyboard navigation walk the order rather than the
6
+ * picture: the graph projection places stations by geometry but publishes its
7
+ * `focusOrder` in rank order, and `navigate` reads only the published order. A
8
+ * projection that returned markup alone would leave every consumer to recover
9
+ * an order from coordinates, which is exactly the rule the design forbids.
10
+ */
11
+ import type { ElementSpec } from './element.ts';
12
+ /** Which projection produced a scene. */
13
+ export type Projection = 'linear' | 'graph' | 'tree';
14
+ /** The neighbours a lateral key press moves to, when there are any. */
15
+ export interface LateralNeighbours {
16
+ readonly left?: string | undefined;
17
+ readonly right?: string | undefined;
18
+ }
19
+ export interface Scene {
20
+ readonly projection: Projection;
21
+ readonly root: ElementSpec;
22
+ /**
23
+ * Every key the vertical keys walk, in the order they walk them. For the
24
+ * spine that is RANK ORDER — never the order boxes happen to sit in.
25
+ */
26
+ readonly focusOrder: readonly string[];
27
+ /**
28
+ * Every key that can hold focus at all — `focusOrder` FIRST, then any key
29
+ * reachable only sideways.
30
+ *
31
+ * The two differ because a lateral move is not a position in the order: a
32
+ * gutter node is somewhere focus can GO without being somewhere `ArrowDown`
33
+ * ever stops. Resolving focus against `focusOrder` alone bounced focus off
34
+ * every such node on the next redraw, and rendered no tab stop for it — so
35
+ * membership questions ask this, and `focusOrder` answers ordering questions.
36
+ * `focusOrder` leads, so the first entry is the same under either.
37
+ */
38
+ readonly navigable: readonly string[];
39
+ /** Key -> its lateral neighbours. Absent for keys with none. */
40
+ readonly lateral: ReadonlyMap<string, LateralNeighbours>;
41
+ /**
42
+ * Key -> the key that REPRESENTS it in this projection. Absent for a key
43
+ * that represents itself, which is almost all of them.
44
+ *
45
+ * A `together-with` unit is one station in the linear and graph projections
46
+ * and its partners get no row of their own, so `navigable` deliberately does
47
+ * not hold them. That made a selection of a partner unstateable: the key is a
48
+ * real issue, so every entry point accepted it, and then focus could not
49
+ * follow it anywhere — `resolveFocusKey` found it in no order and fell to the
50
+ * FIRST entry, an unrelated issue. The tree publishes an EMPTY map, because
51
+ * it draws every key its own row and a partner there is its own subject.
52
+ *
53
+ * THIS IS NOT A DROP. `reconcile` carries a selection across a projection
54
+ * switch whole, deliberately — a switch changes representation, never
55
+ * subject. Mapping a partner to its lead keeps that promise rather than
56
+ * breaking it: the unit IS the subject, and this is the name this projection
57
+ * has for it.
58
+ */
59
+ readonly stationOf: ReadonlyMap<string, string>;
60
+ /** Anything the projection had to drop or refuse, for the host to surface. */
61
+ readonly diagnostics: readonly string[];
62
+ }
63
+ /** The attribute every focusable element carries, and the key it announces. */
64
+ export declare const KEY_ATTRIBUTE = "data-ig-key";
65
+ /**
66
+ * The attribute DECORATION carries — a `together-with` enclosure, its connector.
67
+ *
68
+ * Two different questions were being answered by one attribute. The FOCUS INDEX
69
+ * needs exactly one element per key, or `focus()` lands on whichever the
70
+ * renderer happened to emit first — which for the enclosure is a non-tabbable
71
+ * `<rect>` painted deliberately behind the node. POINTER identity is the other
72
+ * question, and the design fixes its answer: the connector IS a click target.
73
+ *
74
+ * Separating the attributes answers both. `keyAt` reads this one as a fallback
75
+ * so a click or hover on the enclosure still names its slot, while the index
76
+ * that drives focus sees only {@link KEY_ATTRIBUTE}.
77
+ */
78
+ export declare const GROUP_ATTRIBUTE = "data-ig-group";
79
+ /**
80
+ * Which key holds the roving tab stop.
81
+ *
82
+ * ONE RULE, USED BY THE PROJECTIONS AND BY `reconcile`. They have to agree: the
83
+ * projections decide which element renders `tabindex="0"`, `reconcile` decides
84
+ * what `handle.state.focused` reports, and a viewer whose markup and whose
85
+ * reported state disagree is one a keyboard cannot enter. Two spellings of
86
+ * "which key is focused" is exactly how they came to disagree — a requested key
87
+ * the projection no longer draws rendered NO tab stop at all.
88
+ *
89
+ * Selection is the fallback before the first item because a switch changes
90
+ * representation, never subject: landing focus on what the reader was looking
91
+ * at beats landing it at the top.
92
+ */
93
+ export declare function resolveFocusKey(navigable: readonly string[], focused: string | null | undefined, selected: string | null | undefined): string | null;
94
+ //# sourceMappingURL=scene.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scene.d.ts","sourceRoot":"","sources":["../src/scene.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAEhD,yCAAyC;AACzC,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,OAAO,GAAG,MAAM,CAAC;AAErD,uEAAuE;AACvE,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACnC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACrC;AAED,MAAM,WAAW,KAAK;IACpB,QAAQ,CAAC,UAAU,EAAE,UAAU,CAAC;IAChC,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC;IAC3B;;;OAGG;IACH,QAAQ,CAAC,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;IACvC;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,CAAC;IACtC,gEAAgE;IAChE,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;IACzD;;;;;;;;;;;;;;;;;OAiBG;IACH,QAAQ,CAAC,SAAS,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChD,8EAA8E;IAC9E,QAAQ,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;CACzC;AAED,+EAA+E;AAC/E,eAAO,MAAM,aAAa,gBAAgB,CAAC;AAE3C;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,eAAe,kBAAkB,CAAC;AAE/C;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAC7B,SAAS,EAAE,SAAS,MAAM,EAAE,EAC5B,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAClC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAClC,MAAM,GAAG,IAAI,CAIf"}
package/dist/scene.js ADDED
@@ -0,0 +1,48 @@
1
+ /**
2
+ * What a projection returns.
3
+ *
4
+ * A scene is the drawn tree PLUS the traversal it implies. Keeping the two
5
+ * together is what lets keyboard navigation walk the order rather than the
6
+ * picture: the graph projection places stations by geometry but publishes its
7
+ * `focusOrder` in rank order, and `navigate` reads only the published order. A
8
+ * projection that returned markup alone would leave every consumer to recover
9
+ * an order from coordinates, which is exactly the rule the design forbids.
10
+ */
11
+ /** The attribute every focusable element carries, and the key it announces. */
12
+ export const KEY_ATTRIBUTE = 'data-ig-key';
13
+ /**
14
+ * The attribute DECORATION carries — a `together-with` enclosure, its connector.
15
+ *
16
+ * Two different questions were being answered by one attribute. The FOCUS INDEX
17
+ * needs exactly one element per key, or `focus()` lands on whichever the
18
+ * renderer happened to emit first — which for the enclosure is a non-tabbable
19
+ * `<rect>` painted deliberately behind the node. POINTER identity is the other
20
+ * question, and the design fixes its answer: the connector IS a click target.
21
+ *
22
+ * Separating the attributes answers both. `keyAt` reads this one as a fallback
23
+ * so a click or hover on the enclosure still names its slot, while the index
24
+ * that drives focus sees only {@link KEY_ATTRIBUTE}.
25
+ */
26
+ export const GROUP_ATTRIBUTE = 'data-ig-group';
27
+ /**
28
+ * Which key holds the roving tab stop.
29
+ *
30
+ * ONE RULE, USED BY THE PROJECTIONS AND BY `reconcile`. They have to agree: the
31
+ * projections decide which element renders `tabindex="0"`, `reconcile` decides
32
+ * what `handle.state.focused` reports, and a viewer whose markup and whose
33
+ * reported state disagree is one a keyboard cannot enter. Two spellings of
34
+ * "which key is focused" is exactly how they came to disagree — a requested key
35
+ * the projection no longer draws rendered NO tab stop at all.
36
+ *
37
+ * Selection is the fallback before the first item because a switch changes
38
+ * representation, never subject: landing focus on what the reader was looking
39
+ * at beats landing it at the top.
40
+ */
41
+ export function resolveFocusKey(navigable, focused, selected) {
42
+ if (focused !== null && focused !== undefined && navigable.includes(focused))
43
+ return focused;
44
+ if (selected !== null && selected !== undefined && navigable.includes(selected))
45
+ return selected;
46
+ return navigable[0] ?? null;
47
+ }
48
+ //# sourceMappingURL=scene.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scene.js","sourceRoot":"","sources":["../src/scene.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AA0DH,+EAA+E;AAC/E,MAAM,CAAC,MAAM,aAAa,GAAG,aAAa,CAAC;AAE3C;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,eAAe,CAAC;AAE/C;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,eAAe,CAC7B,SAA4B,EAC5B,OAAkC,EAClC,QAAmC;IAEnC,IAAI,OAAO,KAAK,IAAI,IAAI,OAAO,KAAK,SAAS,IAAI,SAAS,CAAC,QAAQ,CAAC,OAAO,CAAC;QAAE,OAAO,OAAO,CAAC;IAC7F,IAAI,QAAQ,KAAK,IAAI,IAAI,QAAQ,KAAK,SAAS,IAAI,SAAS,CAAC,QAAQ,CAAC,QAAQ,CAAC;QAAE,OAAO,QAAQ,CAAC;IACjG,OAAO,SAAS,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC;AAC9B,CAAC"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The structural stylesheet.
3
+ *
4
+ * It carries layout, weight and state — never a value. Every colour, size and
5
+ * spacing here is a `var(--ig-…)` reference resolved by whatever theme the host
6
+ * installs, which is what makes "supply a second theme through custom
7
+ * properties" a real capability rather than a claim. `styles.test.ts` scans
8
+ * this string for a literal colour or a fixed pixel length and fails on either,
9
+ * so the rule is enforced against the bytes rather than remembered.
10
+ *
11
+ * Shipped as a string, not a `.css` file: an entry that imports CSS cannot be
12
+ * loaded by a bare Node runtime (`ERR_UNKNOWN_FILE_EXTENSION`), and this
13
+ * package's floor is checked by a smoke test that imports the built entry. A
14
+ * string also means no consumer needs a bundler to use it.
15
+ */
16
+ export declare const viewerStylesheet = "\n.ig-viewer {\n background: var(--ig-bg);\n color: var(--ig-text-body);\n font-family: var(--ig-font-ui);\n font-size: var(--ig-font-size);\n line-height: var(--ig-line-height);\n position: relative;\n}\n\n.ig-viewer *,\n.ig-viewer *::before,\n.ig-viewer *::after {\n box-sizing: border-box;\n}\n\n.ig-viewer :focus-visible {\n outline: var(--ig-focus-ring) solid var(--ig-focus);\n outline-offset: var(--ig-space-tight);\n}\n\n.ig-list {\n list-style: none;\n margin: 0;\n padding: 0;\n}\n\n/* \u2500\u2500 the spine \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.ig-slot {\n align-items: center;\n background: var(--ig-surface);\n border: var(--ig-stroke) solid var(--ig-line);\n border-radius: var(--ig-radius);\n column-gap: var(--ig-space);\n display: grid;\n grid-template-columns: auto auto 1fr auto;\n margin-bottom: var(--ig-space-tight);\n min-height: var(--ig-row-height);\n padding: var(--ig-space-tight) var(--ig-space);\n}\n\n.ig-slot[data-held='true'] {\n background: var(--ig-surface-2);\n border-style: dashed;\n}\n\n.ig-slot[aria-current='true'] {\n border-color: var(--ig-accent);\n}\n\n/* A CANVAS-OWNED NODE IS SELECTABLE TOO, and only the rail rows had a selected\n look \u2014 so clicking a gutter, excluded or tracker-held node set aria-current on\n the group and changed nothing a reader could see. A pointer does not normally\n raise :focus-visible either, so those selections had no visible state at all\n on the channel most likely to make them. Same accent the rail uses, so one\n selection reads the same whichever surface drew it. */\n.ig-node-group[aria-current='true'] .ig-node {\n stroke: var(--ig-accent);\n stroke-width: calc(var(--ig-stroke) * 2);\n}\n\n.ig-rank {\n color: var(--ig-text);\n font-family: var(--ig-font-mono);\n font-variant-numeric: tabular-nums;\n min-width: calc(var(--ig-char-width) * 4);\n text-align: right;\n}\n\n.ig-rank[data-held='true'] {\n color: var(--ig-text-muted);\n}\n\n/* \u2500\u2500 readiness stations \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.ig-station {\n block-size: var(--ig-station-size);\n border: var(--ig-stroke) solid var(--ig-station-held);\n border-radius: 50%;\n box-shadow: 0 0 0 var(--ig-station-halo) var(--ig-bg);\n display: inline-block;\n inline-size: var(--ig-station-size);\n}\n\n.ig-station[data-fill='filled'] {\n background: var(--ig-station-ready);\n border-color: var(--ig-station-ready);\n}\n\n.ig-station[data-fill='hollow'] {\n background: transparent;\n border-color: var(--ig-station-pending);\n}\n\n.ig-station[data-fill='dashed'] {\n background: transparent;\n border-style: dashed;\n border-color: var(--ig-station-held);\n}\n\n/* \u2500\u2500 titles, identity, provenance \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.ig-title {\n color: var(--ig-text);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.ig-id,\n.ig-count {\n color: var(--ig-text-muted);\n font-family: var(--ig-font-mono);\n font-variant-numeric: tabular-nums;\n}\n\n.ig-link {\n color: var(--ig-accent);\n text-decoration: none;\n}\n\n.ig-link:hover {\n text-decoration: underline;\n}\n\n.ig-provenance,\n.ig-hold {\n color: var(--ig-text-muted);\n font-size: var(--ig-font-size-small);\n grid-column: 3 / -1;\n}\n\n.ig-strike {\n text-decoration: line-through;\n}\n\n/* \u2500\u2500 edge badges \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n/* SPANNED FROM THE TITLE COLUMN, LIKE EVERY OTHER FULL-WIDTH ROW MEMBER. Left\n unplaced this was the fifth child of a four-column grid, so it auto-placed\n into column ONE of a second row \u2014 the rank track, an auto-sized column that\n takes the width of its widest item. One relationship badge then set the width\n of the rank column for the whole row, pushing the station and title across\n and squeezing the title to pay for it. Badges belong with the row's other\n metadata, which is what 3 / -1 already means here for provenance and holds.\n (No backticks in this file: the stylesheet is a template literal.) */\n.ig-badges {\n display: flex;\n flex-wrap: wrap;\n gap: var(--ig-space-tight);\n grid-column: 3 / -1;\n}\n\n.ig-badge {\n align-items: center;\n /* Not currentColor any more: the label and the border now carry DIFFERENT\n colours deliberately \u2014 see below. Left as currentColor the border would\n follow the text to --ig-text and the hue channel would vanish from the\n badge entirely, which is the opposite of the fix. */\n border: var(--ig-stroke) solid var(--ig-line);\n border-radius: var(--ig-radius);\n color: var(--ig-text);\n display: inline-flex;\n font-size: var(--ig-font-size-small);\n gap: var(--ig-space-tight);\n padding: 0 var(--ig-space-tight);\n}\n\n/* THE HUE IS A BORDER COLOUR HERE, NOT A TEXT COLOUR \u2014 the theme holds the\n edge hues to the 3:1 NON-TEXT bar, and says so where it defines them, so\n painting badge LABELS with them contradicted the palette's own claim. It was\n not merely theoretical: at this size duplicate-of measured 3.98:1 on\n --ig-surface and decomposed-from 4.37:1, both under the 4.5:1 the text test\n asserts for every text colour. The label now takes --ig-text, which that\n test already proves on all three surfaces, and the hue keeps the non-text\n use it was measured for.\n NO CHANNEL IS LOST. Hue is one of four redundant channels and it is still\n carried by the border; the dash pattern and the glyph are untouched; and the\n vocabulary test independently proves all five stay distinguishable with hue\n removed ENTIRELY, which is the stronger claim. */\n.ig-badge[data-edge='blocked-by'] { border-color: var(--ig-edge-blocked-by); border-style: solid; }\n.ig-badge[data-edge='serialize-with'] { border-color: var(--ig-edge-serialize-with); border-style: double; }\n.ig-badge[data-edge='together-with'] { border-color: var(--ig-edge-together-with); border-style: solid; }\n.ig-badge[data-edge='duplicate-of'] { border-color: var(--ig-edge-duplicate-of); border-style: dotted; }\n.ig-badge[data-edge='decomposed-from'] { border-color: var(--ig-edge-decomposed-from); border-style: dashed; }\n\n.ig-glyph {\n font-family: var(--ig-font-mono);\n}\n\n/* \u2500\u2500 the footer group: holds that earn no rank slot \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.ig-footer {\n border-top: var(--ig-stroke) solid var(--ig-line);\n margin-top: var(--ig-space);\n padding-top: var(--ig-space);\n}\n\n.ig-footer-title {\n color: var(--ig-text-muted);\n font-size: var(--ig-font-size-small);\n margin: 0 0 var(--ig-space-tight);\n}\n\n/* \u2500\u2500 the graph canvas \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n/* The stage carries the LAYOUT's own size, so one SVG unit is one CSS pixel\n and an absolutely-positioned rail row lands on the node it names. A\n percentage-width canvas would rescale under the rail and drift. It scrolls\n rather than shrinking, because shrinking would silently break that. */\n.ig-stage {\n block-size: var(--ig-stage-h);\n inline-size: var(--ig-stage-w);\n max-inline-size: 100%;\n overflow-x: auto;\n overflow-y: hidden;\n position: relative;\n}\n\n.ig-canvas {\n block-size: var(--ig-stage-h);\n display: block;\n inline-size: var(--ig-stage-w);\n}\n\n/* The ranks and stations FOR the spine nodes, sitting on them. */\n.ig-rail {\n inset: 0;\n pointer-events: none;\n position: absolute;\n}\n\n.ig-rail-row {\n block-size: var(--ig-row-h);\n inline-size: var(--ig-row-w);\n inset-block-start: var(--ig-row-y);\n inset-inline-start: var(--ig-row-x);\n margin-bottom: 0;\n min-block-size: 0;\n pointer-events: auto;\n position: absolute;\n}\n\n.ig-node {\n fill: var(--ig-surface);\n stroke: var(--ig-line);\n stroke-width: var(--ig-stroke);\n}\n\n.ig-node[data-held='true'] {\n fill: var(--ig-surface-2);\n stroke-dasharray: 4 3;\n}\n\n.ig-node-label {\n fill: var(--ig-text);\n font-family: var(--ig-font-ui);\n font-size: var(--ig-font-size-small);\n}\n\n.ig-edge {\n fill: none;\n stroke-width: var(--ig-stroke);\n}\n\n.ig-edge[data-edge='blocked-by'] { stroke: var(--ig-edge-blocked-by); }\n.ig-edge[data-edge='serialize-with'] { stroke: var(--ig-edge-serialize-with); }\n.ig-edge[data-edge='together-with'] { stroke: var(--ig-edge-together-with); }\n.ig-edge[data-edge='duplicate-of'] { stroke: var(--ig-edge-duplicate-of); }\n.ig-edge[data-edge='decomposed-from'] { stroke: var(--ig-edge-decomposed-from); }\n\n/* The dash pattern is set per element from the edge vocabulary, never here \u2014\n one source for the channel the colour-blind-safety claim rests on. */\n.ig-enclosure {\n fill: none;\n stroke: var(--ig-edge-together-with);\n stroke-width: var(--ig-stroke);\n}\n\n.ig-connector {\n stroke: var(--ig-edge-together-with);\n stroke-width: var(--ig-stroke-connector);\n}\n\n/* currentColor on the marker resolves to the inherited text colour, not to the\n edge's stroke \u2014 so a terminal has to be given the hue explicitly or it renders\n in body text and the fourth channel silently collapses. */\n.ig-terminal {\n color: var(--ig-text-body);\n stroke-width: var(--ig-stroke);\n}\n\n.ig-terminal[data-edge='blocked-by'] { color: var(--ig-edge-blocked-by); }\n.ig-terminal[data-edge='serialize-with'] { color: var(--ig-edge-serialize-with); }\n.ig-terminal[data-edge='together-with'] { color: var(--ig-edge-together-with); }\n.ig-terminal[data-edge='duplicate-of'] { color: var(--ig-edge-duplicate-of); }\n.ig-terminal[data-edge='decomposed-from'] { color: var(--ig-edge-decomposed-from); }\n\n/* \u2500\u2500 refusals and empty states \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.ig-refusal,\n.ig-empty {\n background: var(--ig-surface-2);\n border: var(--ig-stroke) solid var(--ig-line);\n border-radius: var(--ig-radius);\n color: var(--ig-text-body);\n padding: var(--ig-space);\n}\n\n.ig-refusal-next {\n color: var(--ig-text);\n margin: var(--ig-space-tight) 0 0;\n}\n\n/* The capsule is INFORMATIONAL \u2014 a plain list item again. It was briefly a\n button, to make the refusal's advertised action keyboard-reachable; the\n action itself has since gone, because this package cannot narrow a document\n and so could never complete it. With the control removed the UA button reset\n goes too: there is no button look left to undo. */\n.ig-capsule {\n align-items: baseline;\n background: var(--ig-surface);\n border: var(--ig-stroke) solid var(--ig-line);\n border-radius: var(--ig-radius);\n display: flex;\n gap: var(--ig-space);\n margin-top: var(--ig-space-tight);\n padding: var(--ig-space-tight) var(--ig-space);\n}\n\n.ig-refusal-omitted {\n color: var(--ig-text-muted);\n font-size: var(--ig-font-size-small);\n margin: var(--ig-space-tight) 0 0;\n}\n\n/* \u2500\u2500 the decomposition tree \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.ig-tree,\n.ig-tree .ig-list {\n list-style: none;\n margin: 0;\n padding: 0;\n}\n\n.ig-tree .ig-list {\n border-left: var(--ig-stroke) dashed var(--ig-edge-decomposed-from);\n margin-left: var(--ig-space);\n padding-left: var(--ig-space);\n}\n\n.ig-tree-item {\n padding: var(--ig-space-tight) 0;\n}\n\n/* \u2500\u2500 legend \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.ig-legend {\n border: 0;\n display: flex;\n flex-wrap: wrap;\n gap: var(--ig-space);\n margin: 0 0 var(--ig-space);\n padding: 0;\n}\n\n.ig-legend-caption {\n color: var(--ig-text-muted);\n font-size: var(--ig-font-size-small);\n padding: 0;\n}\n";
17
+ //# sourceMappingURL=styles.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"styles.d.ts","sourceRoot":"","sources":["../src/styles.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,eAAO,MAAM,gBAAgB,i6aAoX5B,CAAC"}
package/dist/styles.js ADDED
@@ -0,0 +1,389 @@
1
+ /**
2
+ * The structural stylesheet.
3
+ *
4
+ * It carries layout, weight and state — never a value. Every colour, size and
5
+ * spacing here is a `var(--ig-…)` reference resolved by whatever theme the host
6
+ * installs, which is what makes "supply a second theme through custom
7
+ * properties" a real capability rather than a claim. `styles.test.ts` scans
8
+ * this string for a literal colour or a fixed pixel length and fails on either,
9
+ * so the rule is enforced against the bytes rather than remembered.
10
+ *
11
+ * Shipped as a string, not a `.css` file: an entry that imports CSS cannot be
12
+ * loaded by a bare Node runtime (`ERR_UNKNOWN_FILE_EXTENSION`), and this
13
+ * package's floor is checked by a smoke test that imports the built entry. A
14
+ * string also means no consumer needs a bundler to use it.
15
+ */
16
+ export const viewerStylesheet = `
17
+ .ig-viewer {
18
+ background: var(--ig-bg);
19
+ color: var(--ig-text-body);
20
+ font-family: var(--ig-font-ui);
21
+ font-size: var(--ig-font-size);
22
+ line-height: var(--ig-line-height);
23
+ position: relative;
24
+ }
25
+
26
+ .ig-viewer *,
27
+ .ig-viewer *::before,
28
+ .ig-viewer *::after {
29
+ box-sizing: border-box;
30
+ }
31
+
32
+ .ig-viewer :focus-visible {
33
+ outline: var(--ig-focus-ring) solid var(--ig-focus);
34
+ outline-offset: var(--ig-space-tight);
35
+ }
36
+
37
+ .ig-list {
38
+ list-style: none;
39
+ margin: 0;
40
+ padding: 0;
41
+ }
42
+
43
+ /* ── the spine ─────────────────────────────────────────────────────────── */
44
+
45
+ .ig-slot {
46
+ align-items: center;
47
+ background: var(--ig-surface);
48
+ border: var(--ig-stroke) solid var(--ig-line);
49
+ border-radius: var(--ig-radius);
50
+ column-gap: var(--ig-space);
51
+ display: grid;
52
+ grid-template-columns: auto auto 1fr auto;
53
+ margin-bottom: var(--ig-space-tight);
54
+ min-height: var(--ig-row-height);
55
+ padding: var(--ig-space-tight) var(--ig-space);
56
+ }
57
+
58
+ .ig-slot[data-held='true'] {
59
+ background: var(--ig-surface-2);
60
+ border-style: dashed;
61
+ }
62
+
63
+ .ig-slot[aria-current='true'] {
64
+ border-color: var(--ig-accent);
65
+ }
66
+
67
+ /* A CANVAS-OWNED NODE IS SELECTABLE TOO, and only the rail rows had a selected
68
+ look — so clicking a gutter, excluded or tracker-held node set aria-current on
69
+ the group and changed nothing a reader could see. A pointer does not normally
70
+ raise :focus-visible either, so those selections had no visible state at all
71
+ on the channel most likely to make them. Same accent the rail uses, so one
72
+ selection reads the same whichever surface drew it. */
73
+ .ig-node-group[aria-current='true'] .ig-node {
74
+ stroke: var(--ig-accent);
75
+ stroke-width: calc(var(--ig-stroke) * 2);
76
+ }
77
+
78
+ .ig-rank {
79
+ color: var(--ig-text);
80
+ font-family: var(--ig-font-mono);
81
+ font-variant-numeric: tabular-nums;
82
+ min-width: calc(var(--ig-char-width) * 4);
83
+ text-align: right;
84
+ }
85
+
86
+ .ig-rank[data-held='true'] {
87
+ color: var(--ig-text-muted);
88
+ }
89
+
90
+ /* ── readiness stations ────────────────────────────────────────────────── */
91
+
92
+ .ig-station {
93
+ block-size: var(--ig-station-size);
94
+ border: var(--ig-stroke) solid var(--ig-station-held);
95
+ border-radius: 50%;
96
+ box-shadow: 0 0 0 var(--ig-station-halo) var(--ig-bg);
97
+ display: inline-block;
98
+ inline-size: var(--ig-station-size);
99
+ }
100
+
101
+ .ig-station[data-fill='filled'] {
102
+ background: var(--ig-station-ready);
103
+ border-color: var(--ig-station-ready);
104
+ }
105
+
106
+ .ig-station[data-fill='hollow'] {
107
+ background: transparent;
108
+ border-color: var(--ig-station-pending);
109
+ }
110
+
111
+ .ig-station[data-fill='dashed'] {
112
+ background: transparent;
113
+ border-style: dashed;
114
+ border-color: var(--ig-station-held);
115
+ }
116
+
117
+ /* ── titles, identity, provenance ──────────────────────────────────────── */
118
+
119
+ .ig-title {
120
+ color: var(--ig-text);
121
+ overflow: hidden;
122
+ text-overflow: ellipsis;
123
+ white-space: nowrap;
124
+ }
125
+
126
+ .ig-id,
127
+ .ig-count {
128
+ color: var(--ig-text-muted);
129
+ font-family: var(--ig-font-mono);
130
+ font-variant-numeric: tabular-nums;
131
+ }
132
+
133
+ .ig-link {
134
+ color: var(--ig-accent);
135
+ text-decoration: none;
136
+ }
137
+
138
+ .ig-link:hover {
139
+ text-decoration: underline;
140
+ }
141
+
142
+ .ig-provenance,
143
+ .ig-hold {
144
+ color: var(--ig-text-muted);
145
+ font-size: var(--ig-font-size-small);
146
+ grid-column: 3 / -1;
147
+ }
148
+
149
+ .ig-strike {
150
+ text-decoration: line-through;
151
+ }
152
+
153
+ /* ── edge badges ───────────────────────────────────────────────────────── */
154
+
155
+ /* SPANNED FROM THE TITLE COLUMN, LIKE EVERY OTHER FULL-WIDTH ROW MEMBER. Left
156
+ unplaced this was the fifth child of a four-column grid, so it auto-placed
157
+ into column ONE of a second row — the rank track, an auto-sized column that
158
+ takes the width of its widest item. One relationship badge then set the width
159
+ of the rank column for the whole row, pushing the station and title across
160
+ and squeezing the title to pay for it. Badges belong with the row's other
161
+ metadata, which is what 3 / -1 already means here for provenance and holds.
162
+ (No backticks in this file: the stylesheet is a template literal.) */
163
+ .ig-badges {
164
+ display: flex;
165
+ flex-wrap: wrap;
166
+ gap: var(--ig-space-tight);
167
+ grid-column: 3 / -1;
168
+ }
169
+
170
+ .ig-badge {
171
+ align-items: center;
172
+ /* Not currentColor any more: the label and the border now carry DIFFERENT
173
+ colours deliberately — see below. Left as currentColor the border would
174
+ follow the text to --ig-text and the hue channel would vanish from the
175
+ badge entirely, which is the opposite of the fix. */
176
+ border: var(--ig-stroke) solid var(--ig-line);
177
+ border-radius: var(--ig-radius);
178
+ color: var(--ig-text);
179
+ display: inline-flex;
180
+ font-size: var(--ig-font-size-small);
181
+ gap: var(--ig-space-tight);
182
+ padding: 0 var(--ig-space-tight);
183
+ }
184
+
185
+ /* THE HUE IS A BORDER COLOUR HERE, NOT A TEXT COLOUR — the theme holds the
186
+ edge hues to the 3:1 NON-TEXT bar, and says so where it defines them, so
187
+ painting badge LABELS with them contradicted the palette's own claim. It was
188
+ not merely theoretical: at this size duplicate-of measured 3.98:1 on
189
+ --ig-surface and decomposed-from 4.37:1, both under the 4.5:1 the text test
190
+ asserts for every text colour. The label now takes --ig-text, which that
191
+ test already proves on all three surfaces, and the hue keeps the non-text
192
+ use it was measured for.
193
+ NO CHANNEL IS LOST. Hue is one of four redundant channels and it is still
194
+ carried by the border; the dash pattern and the glyph are untouched; and the
195
+ vocabulary test independently proves all five stay distinguishable with hue
196
+ removed ENTIRELY, which is the stronger claim. */
197
+ .ig-badge[data-edge='blocked-by'] { border-color: var(--ig-edge-blocked-by); border-style: solid; }
198
+ .ig-badge[data-edge='serialize-with'] { border-color: var(--ig-edge-serialize-with); border-style: double; }
199
+ .ig-badge[data-edge='together-with'] { border-color: var(--ig-edge-together-with); border-style: solid; }
200
+ .ig-badge[data-edge='duplicate-of'] { border-color: var(--ig-edge-duplicate-of); border-style: dotted; }
201
+ .ig-badge[data-edge='decomposed-from'] { border-color: var(--ig-edge-decomposed-from); border-style: dashed; }
202
+
203
+ .ig-glyph {
204
+ font-family: var(--ig-font-mono);
205
+ }
206
+
207
+ /* ── the footer group: holds that earn no rank slot ────────────────────── */
208
+
209
+ .ig-footer {
210
+ border-top: var(--ig-stroke) solid var(--ig-line);
211
+ margin-top: var(--ig-space);
212
+ padding-top: var(--ig-space);
213
+ }
214
+
215
+ .ig-footer-title {
216
+ color: var(--ig-text-muted);
217
+ font-size: var(--ig-font-size-small);
218
+ margin: 0 0 var(--ig-space-tight);
219
+ }
220
+
221
+ /* ── the graph canvas ──────────────────────────────────────────────────── */
222
+
223
+ /* The stage carries the LAYOUT's own size, so one SVG unit is one CSS pixel
224
+ and an absolutely-positioned rail row lands on the node it names. A
225
+ percentage-width canvas would rescale under the rail and drift. It scrolls
226
+ rather than shrinking, because shrinking would silently break that. */
227
+ .ig-stage {
228
+ block-size: var(--ig-stage-h);
229
+ inline-size: var(--ig-stage-w);
230
+ max-inline-size: 100%;
231
+ overflow-x: auto;
232
+ overflow-y: hidden;
233
+ position: relative;
234
+ }
235
+
236
+ .ig-canvas {
237
+ block-size: var(--ig-stage-h);
238
+ display: block;
239
+ inline-size: var(--ig-stage-w);
240
+ }
241
+
242
+ /* The ranks and stations FOR the spine nodes, sitting on them. */
243
+ .ig-rail {
244
+ inset: 0;
245
+ pointer-events: none;
246
+ position: absolute;
247
+ }
248
+
249
+ .ig-rail-row {
250
+ block-size: var(--ig-row-h);
251
+ inline-size: var(--ig-row-w);
252
+ inset-block-start: var(--ig-row-y);
253
+ inset-inline-start: var(--ig-row-x);
254
+ margin-bottom: 0;
255
+ min-block-size: 0;
256
+ pointer-events: auto;
257
+ position: absolute;
258
+ }
259
+
260
+ .ig-node {
261
+ fill: var(--ig-surface);
262
+ stroke: var(--ig-line);
263
+ stroke-width: var(--ig-stroke);
264
+ }
265
+
266
+ .ig-node[data-held='true'] {
267
+ fill: var(--ig-surface-2);
268
+ stroke-dasharray: 4 3;
269
+ }
270
+
271
+ .ig-node-label {
272
+ fill: var(--ig-text);
273
+ font-family: var(--ig-font-ui);
274
+ font-size: var(--ig-font-size-small);
275
+ }
276
+
277
+ .ig-edge {
278
+ fill: none;
279
+ stroke-width: var(--ig-stroke);
280
+ }
281
+
282
+ .ig-edge[data-edge='blocked-by'] { stroke: var(--ig-edge-blocked-by); }
283
+ .ig-edge[data-edge='serialize-with'] { stroke: var(--ig-edge-serialize-with); }
284
+ .ig-edge[data-edge='together-with'] { stroke: var(--ig-edge-together-with); }
285
+ .ig-edge[data-edge='duplicate-of'] { stroke: var(--ig-edge-duplicate-of); }
286
+ .ig-edge[data-edge='decomposed-from'] { stroke: var(--ig-edge-decomposed-from); }
287
+
288
+ /* The dash pattern is set per element from the edge vocabulary, never here —
289
+ one source for the channel the colour-blind-safety claim rests on. */
290
+ .ig-enclosure {
291
+ fill: none;
292
+ stroke: var(--ig-edge-together-with);
293
+ stroke-width: var(--ig-stroke);
294
+ }
295
+
296
+ .ig-connector {
297
+ stroke: var(--ig-edge-together-with);
298
+ stroke-width: var(--ig-stroke-connector);
299
+ }
300
+
301
+ /* currentColor on the marker resolves to the inherited text colour, not to the
302
+ edge's stroke — so a terminal has to be given the hue explicitly or it renders
303
+ in body text and the fourth channel silently collapses. */
304
+ .ig-terminal {
305
+ color: var(--ig-text-body);
306
+ stroke-width: var(--ig-stroke);
307
+ }
308
+
309
+ .ig-terminal[data-edge='blocked-by'] { color: var(--ig-edge-blocked-by); }
310
+ .ig-terminal[data-edge='serialize-with'] { color: var(--ig-edge-serialize-with); }
311
+ .ig-terminal[data-edge='together-with'] { color: var(--ig-edge-together-with); }
312
+ .ig-terminal[data-edge='duplicate-of'] { color: var(--ig-edge-duplicate-of); }
313
+ .ig-terminal[data-edge='decomposed-from'] { color: var(--ig-edge-decomposed-from); }
314
+
315
+ /* ── refusals and empty states ─────────────────────────────────────────── */
316
+
317
+ .ig-refusal,
318
+ .ig-empty {
319
+ background: var(--ig-surface-2);
320
+ border: var(--ig-stroke) solid var(--ig-line);
321
+ border-radius: var(--ig-radius);
322
+ color: var(--ig-text-body);
323
+ padding: var(--ig-space);
324
+ }
325
+
326
+ .ig-refusal-next {
327
+ color: var(--ig-text);
328
+ margin: var(--ig-space-tight) 0 0;
329
+ }
330
+
331
+ /* The capsule is INFORMATIONAL — a plain list item again. It was briefly a
332
+ button, to make the refusal's advertised action keyboard-reachable; the
333
+ action itself has since gone, because this package cannot narrow a document
334
+ and so could never complete it. With the control removed the UA button reset
335
+ goes too: there is no button look left to undo. */
336
+ .ig-capsule {
337
+ align-items: baseline;
338
+ background: var(--ig-surface);
339
+ border: var(--ig-stroke) solid var(--ig-line);
340
+ border-radius: var(--ig-radius);
341
+ display: flex;
342
+ gap: var(--ig-space);
343
+ margin-top: var(--ig-space-tight);
344
+ padding: var(--ig-space-tight) var(--ig-space);
345
+ }
346
+
347
+ .ig-refusal-omitted {
348
+ color: var(--ig-text-muted);
349
+ font-size: var(--ig-font-size-small);
350
+ margin: var(--ig-space-tight) 0 0;
351
+ }
352
+
353
+ /* ── the decomposition tree ────────────────────────────────────────────── */
354
+
355
+ .ig-tree,
356
+ .ig-tree .ig-list {
357
+ list-style: none;
358
+ margin: 0;
359
+ padding: 0;
360
+ }
361
+
362
+ .ig-tree .ig-list {
363
+ border-left: var(--ig-stroke) dashed var(--ig-edge-decomposed-from);
364
+ margin-left: var(--ig-space);
365
+ padding-left: var(--ig-space);
366
+ }
367
+
368
+ .ig-tree-item {
369
+ padding: var(--ig-space-tight) 0;
370
+ }
371
+
372
+ /* ── legend ────────────────────────────────────────────────────────────── */
373
+
374
+ .ig-legend {
375
+ border: 0;
376
+ display: flex;
377
+ flex-wrap: wrap;
378
+ gap: var(--ig-space);
379
+ margin: 0 0 var(--ig-space);
380
+ padding: 0;
381
+ }
382
+
383
+ .ig-legend-caption {
384
+ color: var(--ig-text-muted);
385
+ font-size: var(--ig-font-size-small);
386
+ padding: 0;
387
+ }
388
+ `;
389
+ //# sourceMappingURL=styles.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"styles.js","sourceRoot":"","sources":["../src/styles.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,MAAM,CAAC,MAAM,gBAAgB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoX/B,CAAC"}
@@ -0,0 +1,71 @@
1
+ /**
2
+ * The theme: every value the viewer draws with, in one place, emitted as CSS
3
+ * custom properties.
4
+ *
5
+ * THE SHIPPED PALETTE IS THE DEFAULT THEME, NOT THE STYLING. It is dark, and
6
+ * light mode is deliberately absent rather than forgotten — a host that wants
7
+ * one supplies it through these same properties, which is exactly what makes
8
+ * this a theme rather than a look. The forcing function is that the package has
9
+ * to render two of them from one set of components; if a value cannot be
10
+ * overridden here, it is a bug in this file and not a styling choice elsewhere.
11
+ *
12
+ * GEOMETRY IS SINGLE-SOURCED AS NUMBERS. Layout maths needs real numbers (an
13
+ * SVG endpoint is a coordinate, not a `var()`), and CSS needs custom
14
+ * properties. Declaring both by hand would be two sources that drift, so the
15
+ * numbers are canonical and {@link themeCss} renders the properties FROM them.
16
+ * Retheming geometry therefore moves the drawing and the stylesheet together.
17
+ */
18
+ /**
19
+ * Every colour the viewer uses, by token name. Declared as a tuple so the
20
+ * token list and the theme type cannot disagree: a colour added here is a
21
+ * compile error in every theme that does not supply it.
22
+ */
23
+ export declare const COLOR_TOKENS: readonly ["--ig-bg", "--ig-surface", "--ig-surface-2", "--ig-line", "--ig-text", "--ig-text-body", "--ig-text-muted", "--ig-accent", "--ig-focus", "--ig-station-ready", "--ig-station-pending", "--ig-station-held", "--ig-edge-blocked-by", "--ig-edge-serialize-with", "--ig-edge-together-with", "--ig-edge-duplicate-of", "--ig-edge-decomposed-from"];
24
+ export type ColorToken = (typeof COLOR_TOKENS)[number];
25
+ /** Type tokens. `lineHeight` is unitless; the rest carry their own units. */
26
+ export declare const TYPE_TOKENS: readonly ["--ig-font-ui", "--ig-font-mono", "--ig-font-size", "--ig-font-size-small", "--ig-line-height"];
27
+ export type TypeToken = (typeof TYPE_TOKENS)[number];
28
+ /** Geometry tokens. Every value is a number of CSS pixels. */
29
+ export declare const METRIC_TOKENS: readonly ["--ig-space", "--ig-space-tight", "--ig-radius", "--ig-row-height", "--ig-station-size", "--ig-station-halo", "--ig-stroke", "--ig-stroke-connector", "--ig-terminal-length", "--ig-terminal-width", "--ig-gutter-width", "--ig-spine-width", "--ig-char-width", "--ig-focus-ring"];
30
+ export type MetricToken = (typeof METRIC_TOKENS)[number];
31
+ /** Every custom property the stylesheet may reference. */
32
+ export declare const THEME_TOKENS: readonly string[];
33
+ export interface Theme {
34
+ readonly colors: Readonly<Record<ColorToken, string>>;
35
+ readonly type: Readonly<Record<TypeToken, string>>;
36
+ /** Numbers, in CSS pixels. Layout reads these; `themeCss` renders them. */
37
+ readonly metrics: Readonly<Record<MetricToken, number>>;
38
+ }
39
+ /**
40
+ * The default theme.
41
+ *
42
+ * Contrast is a claim this palette makes and `theme.test.ts` measures: every
43
+ * text colour clears WCAG AA's 4.5:1 against all three surfaces, and every edge
44
+ * hue clears the 3:1 non-text bar that applies to a line or a badge outline.
45
+ * `--ig-text-muted` is the tight one — it carries sentence-length copy at small
46
+ * sizes, which is why it is the value the tests pin most precisely.
47
+ */
48
+ export declare const defaultTheme: Theme;
49
+ /** Everything a caller may override, with every field optional. */
50
+ export interface ThemeOverride {
51
+ readonly colors?: Partial<Record<ColorToken, string>> | undefined;
52
+ readonly type?: Partial<Record<TypeToken, string>> | undefined;
53
+ readonly metrics?: Partial<Record<MetricToken, number>> | undefined;
54
+ }
55
+ /**
56
+ * Merge an override onto a base theme, per token.
57
+ *
58
+ * A partial override is the shape a second theme actually takes — changing a
59
+ * palette rarely means restating the geometry — and requiring a whole `Theme`
60
+ * would make the cheap case impossible to express.
61
+ */
62
+ export declare function extendTheme(base: Theme, override: ThemeOverride): Theme;
63
+ /**
64
+ * Render a theme as one CSS rule of custom properties.
65
+ *
66
+ * Values are emitted verbatim for colours and type — a theme's author owns
67
+ * their spelling — and metrics gain a `px` unit here, which is the single place
68
+ * the numbers become CSS.
69
+ */
70
+ export declare function themeCss(theme: Theme, selector?: string): string;
71
+ //# sourceMappingURL=theme.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"theme.d.ts","sourceRoot":"","sources":["../src/theme.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH;;;;GAIG;AACH,eAAO,MAAM,YAAY,6VAkBd,CAAC;AAEZ,MAAM,MAAM,UAAU,GAAG,CAAC,OAAO,YAAY,CAAC,CAAC,MAAM,CAAC,CAAC;AAEvD,6EAA6E;AAC7E,eAAO,MAAM,WAAW,2GAMb,CAAC;AAEZ,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,CAAC,CAAC;AAErD,8DAA8D;AAC9D,eAAO,MAAM,aAAa,+RAef,CAAC;AAEZ,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,aAAa,CAAC,CAAC,MAAM,CAAC,CAAC;AAEzD,0DAA0D;AAC1D,eAAO,MAAM,YAAY,EAAE,SAAS,MAAM,EAIxC,CAAC;AAEH,MAAM,WAAW,KAAK;IACpB,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC,CAAC;IACtD,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC,CAAC;IACnD,2EAA2E;IAC3E,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC,CAAC;CACzD;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,YAAY,EAAE,KAuDzB,CAAC;AAEH,mEAAmE;AACnE,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC,GAAG,SAAS,CAAC;IAClE,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC,GAAG,SAAS,CAAC;IAC/D,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC,GAAG,SAAS,CAAC;CACrE;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,aAAa,GAAG,KAAK,CAMvE;AAED;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,KAAK,EAAE,QAAQ,SAAU,GAAG,MAAM,CAMjE"}