@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,13 @@
1
+ import { iconTitle, iconColor, packMonochrome, packFullBleed } from "../packs/registry.js";
2
+ export interface IconMeta {
3
+ /** Fallback plate text when a pack has no artwork (builtin/sys glyphs). */
4
+ code: string;
5
+ color: string;
6
+ }
7
+ export declare function iconMeta(pack: string, id: string): IconMeta | undefined;
8
+ export { packMonochrome, packFullBleed, iconColor };
9
+ export declare function iconExists(pack: string, id: string): boolean;
10
+ export declare function packExists(pack: string): boolean;
11
+ export declare function iconIds(pack: string): string[];
12
+ export declare function allPackNames(): string[];
13
+ export { iconTitle };
@@ -0,0 +1,29 @@
1
+ // Model-facing pack surface. Real artwork lives in installed packs (see
2
+ // ../packs/registry.ts); builtin/sys are renderer-drawn glyphs.
3
+ import { hasIcon, hasPack, iconIds as registryIconIds, iconTitle, glyph, packNames, iconColor, packMonochrome, packFullBleed, } from "../packs/registry.js";
4
+ export function iconMeta(pack, id) {
5
+ const g = glyph(pack, id);
6
+ if (g)
7
+ return g;
8
+ if (!hasIcon(pack, id))
9
+ return undefined;
10
+ return { code: "", color: iconColor(pack, id) ?? "#6F6E69" };
11
+ }
12
+ // `iconColor` is undefined for packs that publish no colours, which is the
13
+ // honest test for "is this a brand mark?" — `iconMeta` folds a neutral grey in
14
+ // as a fallback, and a renderer that read that could not tell a trademark from
15
+ // our own generic vocabulary.
16
+ export { packMonochrome, packFullBleed, iconColor };
17
+ export function iconExists(pack, id) {
18
+ return hasIcon(pack, id);
19
+ }
20
+ export function packExists(pack) {
21
+ return hasPack(pack);
22
+ }
23
+ export function iconIds(pack) {
24
+ return registryIconIds(pack);
25
+ }
26
+ export function allPackNames() {
27
+ return packNames();
28
+ }
29
+ export { iconTitle };
@@ -0,0 +1,10 @@
1
+ /** CRLF (and lone CR) → LF. Returns the same string when there is nothing to
2
+ * do, so the common path costs one `memchr`-class scan. */
3
+ export declare const normalizeSource: (src: string) => string;
4
+ /** Positions are safe across this transform: a CR only ever sits at the end of
5
+ * a line, so every line/character pair is identical before and after. That is
6
+ * what lets a host normalize its own buffer and still map core's offsets back
7
+ * into it — see `packages/vscode/src/features.ts`. */
8
+ export declare const normalizeFiles: <T extends {
9
+ src: string;
10
+ }>(files: T[]) => T[];
@@ -0,0 +1,28 @@
1
+ // Line endings are an *input* invariant, not only an output one.
2
+ //
3
+ // The determinism contract is: same (source, packs, theme, tool version) →
4
+ // byte-identical SVG. A `.squinch` file checked out on Windows arrives CRLF.
5
+ // The grammar itself skips `\r`, so nothing *fails* — which is precisely the
6
+ // danger: anything downstream that reads the source text rather than the parse
7
+ // tree sees a different string on Windows and quietly renders a different file.
8
+ // The sketch theme was the first such reader (its jitter hashed the source, so
9
+ // a CRLF checkout wobbled differently); it is gone, and the rule outlives it —
10
+ // labels, descriptions and titleblock values are all source text that reaches
11
+ // the SVG verbatim, and a host mapping `Loc` offsets back into its own buffer
12
+ // depends on the same normalization.
13
+ //
14
+ // This is the ONE place source line endings are normalized. It is applied at
15
+ // core's two entry points (`buildProject` and `renderProject`) rather than in
16
+ // each host, because there are many hosts — CLI, VS Code, the playground, the
17
+ // lookbook builder, the gauntlet scorer, the HTML export — and the one that
18
+ // forgets produces a subtly different diagram rather than an error.
19
+ /** CRLF (and lone CR) → LF. Returns the same string when there is nothing to
20
+ * do, so the common path costs one `memchr`-class scan. */
21
+ export const normalizeSource = (src) => src.includes("\r") ? src.replace(/\r\n?/g, "\n") : src;
22
+ /** Positions are safe across this transform: a CR only ever sits at the end of
23
+ * a line, so every line/character pair is identical before and after. That is
24
+ * what lets a host normalize its own buffer and still map core's offsets back
25
+ * into it — see `packages/vscode/src/features.ts`. */
26
+ export const normalizeFiles = (files) => files.some((f) => f.src.includes("\r"))
27
+ ? files.map((f) => (f.src.includes("\r") ? { ...f, src: normalizeSource(f.src) } : f))
28
+ : files;
@@ -0,0 +1,2 @@
1
+ /** Closest candidate within a length-proportional distance budget, or undefined. */
2
+ export declare function suggest(input: string, candidates: string[]): string | undefined;
@@ -0,0 +1,24 @@
1
+ // Edit-distance suggestions for did-you-mean diagnostics.
2
+ function levenshtein(a, b) {
3
+ const m = a.length, n = b.length;
4
+ const d = Array.from({ length: m + 1 }, (_, i) => [i, ...Array(n).fill(0)]);
5
+ for (let j = 0; j <= n; j++)
6
+ d[0][j] = j;
7
+ for (let i = 1; i <= m; i++)
8
+ for (let j = 1; j <= n; j++)
9
+ d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
10
+ return d[m][n];
11
+ }
12
+ /** Closest candidate within a length-proportional distance budget, or undefined. */
13
+ export function suggest(input, candidates) {
14
+ let best;
15
+ let bestD = Math.max(2, Math.floor(input.length / 3)) + 1;
16
+ for (const c of candidates) {
17
+ const dist = levenshtein(input.toLowerCase(), c.toLowerCase());
18
+ if (dist < bestD) {
19
+ bestD = dist;
20
+ best = c;
21
+ }
22
+ }
23
+ return best;
24
+ }
@@ -0,0 +1,226 @@
1
+ export interface Loc {
2
+ from: number;
3
+ to: number;
4
+ line: number;
5
+ col: number;
6
+ }
7
+ export interface Diagnostic {
8
+ severity: "error" | "warning";
9
+ message: string;
10
+ fix?: string;
11
+ loc: Loc;
12
+ file?: string;
13
+ }
14
+ /** The one colour vocabulary (SPEC §3/§4/§Zones/§5): eight designed hues,
15
+ * each a light/dark pair in the theme, plus the brand `accent`. The same nine
16
+ * words apply to leaves, containers, edges, zones and a view's `color #tag`
17
+ * statement — never hex, because a literal cannot be right on both canvases
18
+ * and the adaptive merge swaps tokens, not values. Hue is annotation: shape
19
+ * and pattern stay the encoding (DESIGN §6), so a reader who cannot tell red
20
+ * from green loses emphasis, never meaning. */
21
+ export declare const HUES: readonly ["red", "amber", "green", "teal", "blue", "violet", "pink", "gray", "accent"];
22
+ export type Hue = (typeof HUES)[number];
23
+ export interface SNode {
24
+ path: string;
25
+ name: string;
26
+ label: string;
27
+ icon?: {
28
+ pack: string;
29
+ id: string;
30
+ };
31
+ kinds: ("external" | "datastore" | "person")[];
32
+ description?: string;
33
+ tags: string[];
34
+ attrs: Record<string, string>;
35
+ /** `color:` — the node's spine, as a card's (DESIGN §3). Validated copy of `attrs.color`. */
36
+ color?: Hue;
37
+ loc: Loc;
38
+ file?: string;
39
+ }
40
+ export interface SContainer {
41
+ path: string;
42
+ name: string;
43
+ kind: "system" | "container";
44
+ /** `external` only — someone else's system, drawn with DESIGN §3's hatched
45
+ * card surface. The other two node kinds have no card treatment and are
46
+ * refused here. */
47
+ kinds: ("external")[];
48
+ label?: string;
49
+ children: string[];
50
+ attrs: Record<string, string>;
51
+ tags: string[];
52
+ /** `color:` — the card's spine, or the frame's stroke once expanded. */
53
+ color?: Hue;
54
+ loc: Loc;
55
+ file?: string;
56
+ }
57
+ export type ArrowKind = "->" | "~>" | "<->" | "--";
58
+ /** Edge stroke styles (SPEC §edges). Sync edges default to solid, async to
59
+ * dashed; `solid` on an async edge is a check-time error — the dash IS the
60
+ * async convention. */
61
+ export declare const EDGE_STYLES: readonly ["solid", "dashed", "dotted"];
62
+ export type EdgeStyle = (typeof EDGE_STYLES)[number];
63
+ /** `animate:` vocabulary (SPEC §edges). `false` opts out; the travel values
64
+ * (flow/reverse/slow/fast) need a dash pattern to be visible, so on a sync
65
+ * edge they require `style: dashed|dotted`; `packets` draws its own pattern;
66
+ * `pulse` breathes and works on anything. One value per edge — no combos. */
67
+ export declare const EDGE_ANIMATE: readonly ["false", "flow", "reverse", "slow", "fast", "packets", "pulse", "comet"];
68
+ export type EdgeAnimate = Exclude<(typeof EDGE_ANIMATE)[number], "false">;
69
+ export interface SEdge {
70
+ id: string;
71
+ from: string;
72
+ to: string;
73
+ arrow: ArrowKind;
74
+ label?: string;
75
+ attrs: Record<string, string>;
76
+ tags: string[];
77
+ /** `color:` — stroke, head and comet. Survives lifting only when every
78
+ * aggregated member agrees, like `style`/`animate`. */
79
+ color?: Hue;
80
+ loc: Loc;
81
+ file?: string;
82
+ }
83
+ export type Side = "north" | "south" | "east" | "west";
84
+ export type RelPos = "right-of" | "left-of" | "above" | "below";
85
+ export type NoteAnchor = {
86
+ kind: "relpos";
87
+ relpos: RelPos;
88
+ target: string;
89
+ } | {
90
+ kind: "edge";
91
+ from: string;
92
+ to: string;
93
+ } | {
94
+ kind: "corner";
95
+ corner: "top-left" | "top-right" | "bottom-left" | "bottom-right";
96
+ };
97
+ export interface SNote {
98
+ anchor: NoteAnchor;
99
+ text: string;
100
+ style?: string;
101
+ loc: Loc;
102
+ }
103
+ export interface SView {
104
+ name: string;
105
+ title?: string;
106
+ theme?: string;
107
+ scope?: string;
108
+ /** Filter: keep only interior elements matching these ids/tags. Empty = no
109
+ * filter. This is the view's *which* axis; `scope` is its *where*. */
110
+ only: (string | {
111
+ tag: string;
112
+ })[];
113
+ include: (string | {
114
+ tag: string;
115
+ })[];
116
+ includeStar: boolean;
117
+ exclude: (string | {
118
+ tag: string;
119
+ })[];
120
+ expand: string[];
121
+ /** `expand *` — open every visible container to leaf depth, frames nesting
122
+ * as they go (SPEC §5). The depth counterpart of `includeStar`'s breadth. */
123
+ expandStar: boolean;
124
+ /** Outside elements to draw at their own depth instead of as their top-level
125
+ * card. Split out of `include`, which used to carry this second meaning. */
126
+ detail: string[];
127
+ context: "auto" | "off";
128
+ highlight: string[];
129
+ /** `color #tag <hue>` — colour everything visible that carries the tag.
130
+ * A lens, so it overrides an element's own `color:`; declaration order,
131
+ * last wins when two match one element (and that is a warning). */
132
+ colors: {
133
+ tag: string;
134
+ hue: Hue;
135
+ loc: Loc;
136
+ }[];
137
+ showDescriptions: boolean;
138
+ showFlow?: string;
139
+ legend: boolean;
140
+ titleblock?: Record<string, string>;
141
+ notes: SNote[];
142
+ layout: {
143
+ direction?: "down" | "right";
144
+ density?: "compact" | "comfortable" | "spacious";
145
+ lines?: "orthogonal" | "curved" | "straight";
146
+ rows?: string[][];
147
+ /** vertical bands, left to right: members share an axis exactly */
148
+ cols?: string[][];
149
+ place: {
150
+ node: string;
151
+ relpos: RelPos;
152
+ target: string;
153
+ loc: Loc;
154
+ }[];
155
+ /** `align a b c` — b and c share a's axis exactly (a is the anchor). */
156
+ align: {
157
+ nodes: string[];
158
+ loc: Loc;
159
+ }[];
160
+ routes: {
161
+ from: string;
162
+ to: string;
163
+ label?: string;
164
+ fromSide?: Side;
165
+ toSide?: Side;
166
+ loc: Loc;
167
+ }[];
168
+ /** `channel a, b, c -> db` — those edges merge into one trunk (SPEC §6 Tier 2). */
169
+ channels: {
170
+ sources: string[];
171
+ target: string;
172
+ loc: Loc;
173
+ }[];
174
+ };
175
+ loc: Loc;
176
+ file?: string;
177
+ /** Synthesized for a container that has no explicit view (SPEC §5). */
178
+ auto?: boolean;
179
+ }
180
+ export declare const ZONE_KINDS: readonly ["account", "region", "vpc", "subnet", "network", "cloud", "onprem", "custom"];
181
+ export type ZoneKind = (typeof ZONE_KINDS)[number];
182
+ export type ZoneLabelPos = "top-left" | "top-right" | "bottom-left" | "bottom-right";
183
+ export interface SZone {
184
+ id: string;
185
+ label?: string;
186
+ kind: ZoneKind;
187
+ members: string[];
188
+ icon?: {
189
+ pack: string;
190
+ id: string;
191
+ };
192
+ labelPos: ZoneLabelPos;
193
+ /** outline/chip tint override; the default derives from `kind` (DESIGN §5) */
194
+ color?: Hue;
195
+ /** A second, monospaced chip segment for the boundary's hard fact — a CIDR
196
+ * block, an account number, a region. Free text: the engine never parses
197
+ * it, it just sets it in mono so digits line up between diagrams. */
198
+ detail?: string;
199
+ loc: Loc;
200
+ file?: string;
201
+ }
202
+ export interface SFlow {
203
+ id: string;
204
+ label?: string;
205
+ steps: {
206
+ from: string;
207
+ to: string;
208
+ }[];
209
+ loc: Loc;
210
+ file?: string;
211
+ }
212
+ export interface SModel {
213
+ packs: string[];
214
+ nodes: Map<string, SNode>;
215
+ containers: Map<string, SContainer>;
216
+ edges: SEdge[];
217
+ zones: SZone[];
218
+ flows: SFlow[];
219
+ views: SView[];
220
+ fileTheme?: string;
221
+ }
222
+ export interface BuildResult {
223
+ model: SModel;
224
+ diagnostics: Diagnostic[];
225
+ ok: boolean;
226
+ }
@@ -0,0 +1,24 @@
1
+ /** The one colour vocabulary (SPEC §3/§4/§Zones/§5): eight designed hues,
2
+ * each a light/dark pair in the theme, plus the brand `accent`. The same nine
3
+ * words apply to leaves, containers, edges, zones and a view's `color #tag`
4
+ * statement — never hex, because a literal cannot be right on both canvases
5
+ * and the adaptive merge swaps tokens, not values. Hue is annotation: shape
6
+ * and pattern stay the encoding (DESIGN §6), so a reader who cannot tell red
7
+ * from green loses emphasis, never meaning. */
8
+ export const HUES = [
9
+ "red", "amber", "green", "teal", "blue", "violet", "pink", "gray", "accent",
10
+ ];
11
+ /** Edge stroke styles (SPEC §edges). Sync edges default to solid, async to
12
+ * dashed; `solid` on an async edge is a check-time error — the dash IS the
13
+ * async convention. */
14
+ export const EDGE_STYLES = ["solid", "dashed", "dotted"];
15
+ /** `animate:` vocabulary (SPEC §edges). `false` opts out; the travel values
16
+ * (flow/reverse/slow/fast) need a dash pattern to be visible, so on a sync
17
+ * edge they require `style: dashed|dotted`; `packets` draws its own pattern;
18
+ * `pulse` breathes and works on anything. One value per edge — no combos. */
19
+ export const EDGE_ANIMATE = ["false", "flow", "reverse", "slow", "fast", "packets", "pulse", "comet"];
20
+ // Deployment boundaries (SPEC §Zones): cross-cutting membership, separate
21
+ // from the ownership hierarchy. Kind drives the frame styling.
22
+ export const ZONE_KINDS = [
23
+ "account", "region", "vpc", "subnet", "network", "cloud", "onprem", "custom",
24
+ ];
@@ -0,0 +1 @@
1
+ export declare function registerPackFromDisk(packageName: string): boolean;
@@ -0,0 +1,37 @@
1
+ // Node-only pack source: finds installed packs on disk and registers them with
2
+ // synchronous loaders. Imported for side effect by the Node entry point.
3
+ import { readFileSync, existsSync } from "node:fs";
4
+ import { join, dirname } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { createRequire } from "node:module";
7
+ import { registerPack } from "./registry.js";
8
+ export function registerPackFromDisk(packageName) {
9
+ const here = dirname(fileURLToPath(import.meta.url));
10
+ const short = packageName.replace("@squinch/", "");
11
+ const candidates = [];
12
+ try {
13
+ candidates.push(dirname(createRequire(import.meta.url).resolve(`${packageName}/pack.json`)));
14
+ }
15
+ catch {
16
+ /* not resolvable via node — fall back to workspace layout */
17
+ }
18
+ candidates.push(join(here, "..", "..", "..", short));
19
+ candidates.push(join(here, "..", "..", "..", "..", short));
20
+ // bundled hosts (the VS Code extension) ship the pack beside their output
21
+ candidates.push(join(here, short));
22
+ candidates.push(join(here, "..", short));
23
+ for (const dir of candidates) {
24
+ const manifestPath = join(dir, "pack.json");
25
+ if (!existsSync(manifestPath))
26
+ continue;
27
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
28
+ registerPack(manifest, (file) => readFileSync(join(dir, "icons", file), "utf8"));
29
+ return true;
30
+ }
31
+ return false;
32
+ }
33
+ registerPackFromDisk("@squinch/pack-aws");
34
+ registerPackFromDisk("@squinch/pack-azure");
35
+ registerPackFromDisk("@squinch/pack-logos");
36
+ registerPackFromDisk("@squinch/pack-sys");
37
+ registerPackFromDisk("@squinch/pack-k8s");
@@ -0,0 +1,61 @@
1
+ import { type SanitizedIcon } from "./sanitize.js";
2
+ export interface PackManifest {
3
+ name: string;
4
+ title: string;
5
+ release?: string;
6
+ source?: string;
7
+ license?: string;
8
+ attribution?: string;
9
+ icons: Record<string, {
10
+ file: string;
11
+ title: string;
12
+ category?: string;
13
+ color?: string;
14
+ }>;
15
+ aliases?: Record<string, string>;
16
+ /** single-colour marks (e.g. Simple Icons): the renderer plates and tints
17
+ * them rather than drawing the artwork on a neutral background. */
18
+ monochrome?: boolean;
19
+ /** artwork drawn edge-to-edge in its viewBox (no built-in optical margin,
20
+ * unlike Azure's glyphs): contexts that place icons flush against a border
21
+ * — the zone-chip tab — inset it slightly instead. Nodes are unaffected. */
22
+ fullBleed?: boolean;
23
+ }
24
+ /** Returns raw SVG text for a pack-relative file. May be async (browser). */
25
+ export type AssetLoader = (file: string) => string | Promise<string>;
26
+ /** Built-in fallbacks: no assets, drawn by the renderer itself.
27
+ *
28
+ * `sys` used to live here too. It is a real disk pack now (@squinch/pack-sys,
29
+ * Lucide), and a name present in BOTH this map and the pack registry is a trap:
30
+ * `iconIds` short-circuits on this map, so every disk icon would vanish from
31
+ * search, completions and `squinch icons` while `hasIcon` still accepted it. */
32
+ export declare const BUILTIN_GLYPHS: Record<string, Record<string, {
33
+ code: string;
34
+ color: string;
35
+ }>>;
36
+ export declare function registerPack(manifest: PackManifest, load: AssetLoader): void;
37
+ export declare function hasIcon(packName: string, id: string): boolean;
38
+ export declare function hasPack(packName: string): boolean;
39
+ export declare function packNames(): string[];
40
+ export declare function iconIds(packName: string): string[];
41
+ export declare function iconTitle(packName: string, id: string): string | undefined;
42
+ /** Stable symbol id — aliases collapse onto their canonical icon. */
43
+ export declare const symbolId: (packName: string, id: string) => string;
44
+ /** Synchronous lookup used by the renderer; undefined until loaded. */
45
+ export declare function iconAsset(packName: string, id: string): SanitizedIcon | undefined;
46
+ /** Resolve assets ahead of a synchronous render (browsers). */
47
+ export declare function preloadIcons(refs: {
48
+ pack: string;
49
+ id: string;
50
+ }[]): Promise<void>;
51
+ export declare function glyph(packName: string, id: string): {
52
+ code: string;
53
+ color: string;
54
+ } | undefined;
55
+ /** True when a pack's artwork is single-colour and wants the plate treatment. */
56
+ export declare function packMonochrome(packName: string): boolean;
57
+ /** True when a pack's artwork runs edge-to-edge in its viewBox (k8s). */
58
+ export declare function packFullBleed(packName: string): boolean;
59
+ /** A pack's declared brand colour for an icon, if it has one. */
60
+ export declare function iconColor(packName: string, id: string): string | undefined;
61
+ export declare function packInfo(packName: string): PackManifest | undefined;
@@ -0,0 +1,122 @@
1
+ // Pack registry — pure and browser-safe. Hosts supply assets:
2
+ // Node : registerPackFromDisk() in ./node-fs.ts (used by the CLI + tests)
3
+ // Browser: registerPack() with a fetch-backed loader, then preloadIcons()
4
+ // Rendering itself stays synchronous, so assets must be resident before render.
5
+ import { SYS_GLYPH_ART, SYS_GLYPH_VIEWBOX } from "./sysGlyphs.js";
6
+ import { sanitizeIcon } from "./sanitize.js";
7
+ /** Built-in fallbacks: no assets, drawn by the renderer itself.
8
+ *
9
+ * `sys` used to live here too. It is a real disk pack now (@squinch/pack-sys,
10
+ * Lucide), and a name present in BOTH this map and the pack registry is a trap:
11
+ * `iconIds` short-circuits on this map, so every disk icon would vanish from
12
+ * search, completions and `squinch icons` while `hasIcon` still accepted it. */
13
+ export const BUILTIN_GLYPHS = {
14
+ builtin: {
15
+ box: { code: "▢", color: "#6F6E69" },
16
+ person: { code: "☺", color: "#6F6E69" },
17
+ },
18
+ };
19
+ const packs = new Map();
20
+ const assets = new Map();
21
+ export function registerPack(manifest, load) {
22
+ packs.set(manifest.name, { manifest, load });
23
+ }
24
+ function canonical(pack, id) {
25
+ if (pack.manifest.icons[id])
26
+ return id;
27
+ const alias = pack.manifest.aliases?.[id];
28
+ return alias && pack.manifest.icons[alias] ? alias : undefined;
29
+ }
30
+ export function hasIcon(packName, id) {
31
+ if (BUILTIN_GLYPHS[packName]?.[id])
32
+ return true;
33
+ const pack = packs.get(packName);
34
+ return !!pack && !!canonical(pack, id);
35
+ }
36
+ export function hasPack(packName) {
37
+ return !!BUILTIN_GLYPHS[packName] || packs.has(packName);
38
+ }
39
+ export function packNames() {
40
+ return [...new Set([...Object.keys(BUILTIN_GLYPHS), ...packs.keys()])].sort();
41
+ }
42
+ export function iconIds(packName) {
43
+ const builtin = BUILTIN_GLYPHS[packName];
44
+ if (builtin)
45
+ return Object.keys(builtin);
46
+ const pack = packs.get(packName);
47
+ if (!pack)
48
+ return [];
49
+ return [...Object.keys(pack.manifest.icons), ...Object.keys(pack.manifest.aliases ?? {})].sort();
50
+ }
51
+ export function iconTitle(packName, id) {
52
+ const pack = packs.get(packName);
53
+ if (!pack)
54
+ return undefined;
55
+ const key = canonical(pack, id);
56
+ return key ? pack.manifest.icons[key].title : undefined;
57
+ }
58
+ /** Stable symbol id — aliases collapse onto their canonical icon. */
59
+ export const symbolId = (packName, id) => {
60
+ const pack = packs.get(packName);
61
+ const key = pack ? canonical(pack, id) ?? id : id;
62
+ return `sq-${packName}-${key}`;
63
+ };
64
+ const cacheKey = (packName, key) => `${packName}/${key}`;
65
+ /** Synchronous lookup used by the renderer; undefined until loaded. */
66
+ export function iconAsset(packName, id) {
67
+ // first-party glyph artwork ships in-core (browser-safe, no preload needed)
68
+ const art = SYS_GLYPH_ART[packName]?.[id];
69
+ if (art)
70
+ return { viewBox: SYS_GLYPH_VIEWBOX, body: art };
71
+ const pack = packs.get(packName);
72
+ if (!pack)
73
+ return undefined;
74
+ const key = canonical(pack, id);
75
+ if (!key)
76
+ return undefined;
77
+ const hit = assets.get(cacheKey(packName, key));
78
+ if (hit)
79
+ return hit;
80
+ // Node loaders are synchronous: resolve inline so callers need no preload.
81
+ const raw = pack.load(pack.manifest.icons[key].file);
82
+ if (typeof raw !== "string")
83
+ return undefined; // async source → must preload
84
+ const asset = sanitizeIcon(raw, `${packName}-${key}`);
85
+ assets.set(cacheKey(packName, key), asset);
86
+ return asset;
87
+ }
88
+ /** Resolve assets ahead of a synchronous render (browsers). */
89
+ export async function preloadIcons(refs) {
90
+ await Promise.all(refs.map(async ({ pack: packName, id }) => {
91
+ const pack = packs.get(packName);
92
+ if (!pack)
93
+ return;
94
+ const key = canonical(pack, id);
95
+ if (!key || assets.has(cacheKey(packName, key)))
96
+ return;
97
+ const raw = await pack.load(pack.manifest.icons[key].file);
98
+ assets.set(cacheKey(packName, key), sanitizeIcon(raw, `${packName}-${key}`));
99
+ }));
100
+ }
101
+ export function glyph(packName, id) {
102
+ return BUILTIN_GLYPHS[packName]?.[id];
103
+ }
104
+ /** True when a pack's artwork is single-colour and wants the plate treatment. */
105
+ export function packMonochrome(packName) {
106
+ return packs.get(packName)?.manifest.monochrome === true;
107
+ }
108
+ /** True when a pack's artwork runs edge-to-edge in its viewBox (k8s). */
109
+ export function packFullBleed(packName) {
110
+ return packs.get(packName)?.manifest.fullBleed === true;
111
+ }
112
+ /** A pack's declared brand colour for an icon, if it has one. */
113
+ export function iconColor(packName, id) {
114
+ const pack = packs.get(packName);
115
+ if (!pack)
116
+ return undefined;
117
+ const key = canonical(pack, id);
118
+ return key ? pack.manifest.icons[key]?.color : undefined;
119
+ }
120
+ export function packInfo(packName) {
121
+ return packs.get(packName)?.manifest;
122
+ }
@@ -0,0 +1,12 @@
1
+ export interface SanitizedIcon {
2
+ /** Inner markup, ids namespaced, safe to inline. */
3
+ body: string;
4
+ /** viewBox of the original asset — placement uses it, never rewrites it. */
5
+ viewBox: string;
6
+ }
7
+ /**
8
+ * Strip everything not on the allowlist and namespace internal ids so several
9
+ * icons can coexist in one document. Does not alter geometry or colour — the
10
+ * asset itself must stay byte-faithful (CC-BY-ND).
11
+ */
12
+ export declare function sanitizeIcon(svg: string, idPrefix: string): SanitizedIcon;