@xenosystem/blocks 0.22.0 → 0.23.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.
@@ -0,0 +1,449 @@
1
+ import { PanelModule, PanelManifest, BlockDeclaration } from '@xenosystem/block-sdk';
2
+ import { X as XenoGraphScaleState } from '../scale-XiQwxiFA.js';
3
+ import { ReactNode } from 'react';
4
+
5
+ /**
6
+ * The wire contract of `xeno.corpus.shape` — what a host hands the panel and what it gets back.
7
+ *
8
+ * The shapes are the subset of XENO Corpus's `/api/v1/model` that the canvas draws (corpus-model.mjs,
9
+ * mirrored in xeno-corpus `app/src/types.ts`). The panel computes NO verdict: every `state` arrives
10
+ * from the host, and the panel only decides where to draw it and how loudly.
11
+ *
12
+ * @module
13
+ */
14
+ /** A tenet's verdict as the host computed it. `unobserved` and `never-run` are both UNKNOWN, never held. */
15
+ type XenoCorpusState = 'held' | 'lapsed' | 'unobserved' | 'never-run' | 'attested' | (string & {});
16
+ /** What a card is. */
17
+ type XenoCorpusNodeKind = 'tenet' | 'ref' | 'rule' | 'doc' | 'proposal' | 'missing' | 'uncovered';
18
+ /** One cell of a tenet's recent-verdict strip, oldest first. */
19
+ interface XenoCorpusStripCell {
20
+ at: string;
21
+ state: string;
22
+ observed?: string | null;
23
+ }
24
+ /** A card. `key` is unique across the whole model; `lane` names the lane it sits in. */
25
+ interface XenoCorpusNode {
26
+ key: string;
27
+ lane: string;
28
+ kind: XenoCorpusNodeKind;
29
+ label: string;
30
+ state: XenoCorpusState;
31
+ sub?: string;
32
+ expect?: string;
33
+ observed?: string | null;
34
+ attested?: boolean;
35
+ binding?: boolean;
36
+ cadence?: string | null;
37
+ strip?: XenoCorpusStripCell[];
38
+ }
39
+ /** A relation between two cards. */
40
+ interface XenoCorpusEdge {
41
+ from: string;
42
+ to: string;
43
+ type: 'ref' | 'dep' | 'breach' | 'pointer' | 'super';
44
+ label: string;
45
+ }
46
+ /** A lane: one corpus in the estate (org, ancestor, product), an orphan, or the unmanaged repositories. */
47
+ interface XenoCorpusLane {
48
+ id: string;
49
+ name: string;
50
+ role: 'org' | 'ancestor' | 'product' | 'orphan' | 'uncovered';
51
+ root?: string;
52
+ flag?: string | null;
53
+ inherits?: number;
54
+ counts?: {
55
+ tenets?: number;
56
+ lapsed?: number;
57
+ unknown?: number;
58
+ attested?: number;
59
+ held?: number;
60
+ };
61
+ uncovered?: {
62
+ total?: number;
63
+ };
64
+ }
65
+ /** The whole picture. A higher `rev` is a newer picture; the panel never edits it. */
66
+ interface XenoCorpusModel {
67
+ rev?: number;
68
+ lanes: XenoCorpusLane[];
69
+ nodes: XenoCorpusNode[];
70
+ edges: XenoCorpusEdge[];
71
+ }
72
+ /** The question the canvas answers. */
73
+ type XenoCorpusLens = 'all' | 'lapsed' | 'unchecked';
74
+ /**
75
+ * Emphasis by state, from a host's filter rail. These DIM and never hide: emphasis changes how loudly a
76
+ * card speaks inside the whole picture; a lens (a question) is what hides non-answers.
77
+ */
78
+ interface XenoCorpusEmphasis {
79
+ lapsed?: boolean;
80
+ held?: boolean;
81
+ unobserved?: boolean;
82
+ rule?: boolean;
83
+ none?: boolean;
84
+ }
85
+ /** What the panel emits on `intent`. Requests only — the host decides. */
86
+ type XenoCorpusShapeIntent =
87
+ /** Open this card's detail (an inspector, a document). Double-click or Enter. */
88
+ {
89
+ kind: 'open';
90
+ key: string;
91
+ };
92
+ /** Is this value a model the panel can draw? A mis-wire is a normal event, so this checks shape, never trusts a cast. */
93
+ declare function isCorpusModel(v: unknown): v is XenoCorpusModel;
94
+
95
+ /**
96
+ * What the Shape canvas shows and where — pure, framework-free, testable in Node.
97
+ *
98
+ * Moved from xeno-corpus `app/src/views/shapeLogic.ts` and the layout in `Shape.tsx`, unchanged in
99
+ * behaviour. The design record is xeno-corpus `docs/design/SHAPE-CANVAS.md`; the three ideas, each from
100
+ * what users of graph tools ask for:
101
+ * - LENSES: open on a question ("what is false right now?"), not on the whole graph.
102
+ * - FOCUS DEPTH: a local view N links around the selection, both directions.
103
+ * - SEMANTIC ZOOM: below a zoom threshold a card shows its state and one label large enough to read.
104
+ *
105
+ * @module
106
+ */
107
+
108
+ /** Card size and lane geometry in WORLD units. The layout comes from the data: nothing is stored, nothing drifts. */
109
+ declare const NODE_W = 210;
110
+ declare const NODE_H = 74;
111
+ /** The questions. `empty` is what the canvas says when nothing answers: an empty canvas is otherwise indistinguishable from a broken one. */
112
+ declare const LENSES: readonly {
113
+ id: XenoCorpusLens;
114
+ label: string;
115
+ question: string;
116
+ empty: string;
117
+ }[];
118
+ declare const isLens: (v: unknown) => v is XenoCorpusLens;
119
+ /** Focus depths offered. Capped at 3: past three links a claim map is the whole corpus again. */
120
+ declare const DEPTHS: readonly [0, 1, 2, 3];
121
+ declare const isDepth: (v: unknown) => v is number;
122
+ /** Does a card answer the lens's question? */
123
+ declare function inLens(n: Pick<XenoCorpusNode, 'kind' | 'state'>, lens: XenoCorpusLens): boolean;
124
+ /** Keys within `depth` links of `start`, ignoring direction. depth 0 = no focus (null). */
125
+ declare function neighbourhood(edges: readonly Pick<XenoCorpusEdge, 'from' | 'to'>[], start: string | null, depth: number): Set<string> | null;
126
+ /**
127
+ * The visible set: lens AND focus. The selected card always stays visible — hiding what the person just
128
+ * clicked is the one thing a filter must never do. When focus is on it outranks the lens.
129
+ */
130
+ declare function visibleKeys(nodes: readonly Pick<XenoCorpusNode, 'key' | 'kind' | 'state'>[], edges: readonly Pick<XenoCorpusEdge, 'from' | 'to'>[], lens: XenoCorpusLens, selected: string | null, depth: number): Set<string>;
131
+ /** Below this zoom a card switches to its compact, readable form. */
132
+ declare const COMPACT_BELOW = 0.5;
133
+ /** Compact label size in WORLD units so it reads at ~12 px on screen, capped (44) so one line fits a card. */
134
+ declare const compactFont: (zoom: number) => number;
135
+ /** A placed card. */
136
+ interface Placed extends XenoCorpusNode {
137
+ x: number;
138
+ y: number;
139
+ }
140
+ /** A placed lane. */
141
+ interface LaneBox {
142
+ lane: XenoCorpusLane;
143
+ x: number;
144
+ w: number;
145
+ h: number;
146
+ }
147
+ /**
148
+ * Lanes left to right; an org lane is two columns wide (claims, then doctrine, documents and gaps).
149
+ * Pure: the same model always lays out the same way, so nothing moves between visits.
150
+ */
151
+ declare function layout(lanes: readonly XenoCorpusLane[], nodes: readonly XenoCorpusNode[]): {
152
+ boxes: LaneBox[];
153
+ placed: Placed[];
154
+ width: number;
155
+ height: number;
156
+ };
157
+ /** Which emphasis switch governs a card. */
158
+ declare function emphasisKey(n: Pick<XenoCorpusNode, 'kind' | 'state'>): keyof XenoCorpusEmphasis;
159
+ /** A state chip. D181: unknown is a hollow ring and attested the monochrome wash — never mistakable for held. */
160
+ interface Chip {
161
+ label: string;
162
+ bg: string;
163
+ ink: string;
164
+ ring: string;
165
+ }
166
+ declare function chipFor(state: string, opts?: {
167
+ attested?: boolean;
168
+ binding?: boolean;
169
+ }): Chip;
170
+ /** The chip a CARD wears: a tenet shows its verdict; an unmanaged repository has no check. */
171
+ declare const cardChip: (n: XenoCorpusNode) => Chip;
172
+ /** One verdict cell's fill. Unknown is hollow; nothing unknown is ever filled green. */
173
+ declare function cellFill(state: string): {
174
+ bg: string;
175
+ ring: string;
176
+ };
177
+ /** The small caption above a card's title. */
178
+ declare function kindLabel(n: XenoCorpusNode): string;
179
+ /** The line under a card's title. A tenet says what it expects and what it got, or that it is unknown. */
180
+ declare function subLine(n: XenoCorpusNode): string;
181
+ /** A lane's caption ("Org corpus") and its sub-line. */
182
+ declare function laneCaption(lane: XenoCorpusLane): {
183
+ role: string;
184
+ sub: string;
185
+ };
186
+ /** Edge styles: colour, dash, width, legend label. */
187
+ declare const EDGE_STYLE: Readonly<Record<XenoCorpusEdge['type'], readonly [string, string, number, string]>>;
188
+
189
+ /**
190
+ * The Shape controller — framework-free, testable in Node.
191
+ *
192
+ * Owns the VIEW state (camera, selection, question, focus depth, emphasis) and nothing else. The model
193
+ * is the host's: the panel draws it and emits two things — `selection` (what the person picked) and
194
+ * `intent` (`open` a card). It never computes a verdict and never writes the corpus.
195
+ *
196
+ * Camera math is the node graph's (`XenoGraphScale`, `zoomAboutCursor`, `fitToBounds`): one converter
197
+ * between screen and world for every canvas in the package.
198
+ *
199
+ * @module
200
+ */
201
+
202
+ /** Zoom limits. 20% is far enough to see a large estate; at 200% a card's strip is legible. */
203
+ declare const SHAPE_ZOOM: {
204
+ readonly min: 0.2;
205
+ readonly max: 2;
206
+ };
207
+ /** The host seam. */
208
+ interface ShapeHostBridge {
209
+ emit(portId: string, value: unknown): void;
210
+ }
211
+ /** Everything the view reads. */
212
+ interface ShapeState {
213
+ model: XenoCorpusModel | null;
214
+ boxes: LaneBox[];
215
+ placed: Placed[];
216
+ width: number;
217
+ height: number;
218
+ /** Cards that answer the question (and the focus). Always contains the selection. */
219
+ visible: ReadonlySet<string>;
220
+ shown: Placed[];
221
+ selected: string | null;
222
+ lens: XenoCorpusLens;
223
+ depth: number;
224
+ emphasis: Required<XenoCorpusEmphasis>;
225
+ camera: XenoGraphScaleState;
226
+ size: {
227
+ w: number;
228
+ h: number;
229
+ };
230
+ /** Below `COMPACT_BELOW` cards switch to their compact, readable form. */
231
+ compact: boolean;
232
+ /** The next camera change should animate (a fit, a focus); a wheel or a drag never does. */
233
+ animate: boolean;
234
+ edgeLabels: boolean;
235
+ /** The plain-language answer when the question has no answer; null otherwise. */
236
+ emptyAnswer: string | null;
237
+ /** Focus is on but nothing is selected: the canvas says what to do. */
238
+ focusHint: boolean;
239
+ }
240
+ /** Construction options. */
241
+ interface ShapeControllerOptions {
242
+ host: ShapeHostBridge;
243
+ lens?: XenoCorpusLens;
244
+ depth?: number;
245
+ }
246
+ declare class ShapeController {
247
+ private host;
248
+ private listeners;
249
+ private s;
250
+ private fitted;
251
+ constructor(options: ShapeControllerOptions);
252
+ /** Bound, so `useSyncExternalStore(controller.subscribe, controller.getState)` works as written. */
253
+ getState: () => Readonly<ShapeState>;
254
+ subscribe: (fn: () => void) => (() => void);
255
+ private set;
256
+ private derive;
257
+ /** A new picture. Anything that is not a model is ignored: a mis-wire must not blank the canvas. */
258
+ setModel(value: unknown): void;
259
+ /** Selection driven from outside (an inspector, a search). Does not echo back on `selection`. */
260
+ setSelection(value: unknown): void;
261
+ /** Emphasis from a host's filter rail. Unknown keys are ignored; missing keys stay on. */
262
+ setEmphasis(value: unknown): void;
263
+ select(key: string | null): void;
264
+ open(key: string): void;
265
+ setLens(lens: unknown): void;
266
+ setDepth(depth: unknown): void;
267
+ toggleEdgeLabels(): void;
268
+ /** Arrow keys move the selection spatially: nearest card in that direction, off-axis distance weighed double. */
269
+ moveSelection(dir: 'left' | 'right' | 'up' | 'down'): void;
270
+ resize(w: number, h: number): void;
271
+ private scale;
272
+ get viewport(): {
273
+ x: number;
274
+ y: number;
275
+ width: number;
276
+ height: number;
277
+ };
278
+ private camera;
279
+ /** Zoom about a screen point. Never animated: it follows the wheel. A non-positive or non-finite factor is a no-op (zoomAboutCursor). */
280
+ zoomAt(px: number, py: number, factor: number): void;
281
+ zoomBy(factor: number): void;
282
+ /** Pan by a screen-pixel delta. */
283
+ pan(dxPx: number, dyPx: number): void;
284
+ /** Centre the camera on a world point (the minimap). */
285
+ centreOn(wx: number, wy: number): void;
286
+ /** Fit everything. */
287
+ fit(animate?: boolean): void;
288
+ /** Fit what is SHOWN: a question or focus that leaves its answer off-screen is no answer. */
289
+ fitShown(animate?: boolean): void;
290
+ /** `F`: fit what answers when narrowed, everything otherwise. */
291
+ fitNow(): void;
292
+ private narrowed;
293
+ private refit;
294
+ private afterSelect;
295
+ /** The question and the depth are the viewer's: remembered so they do not reset on every open. */
296
+ serialize(): {
297
+ lens: XenoCorpusLens;
298
+ depth: number;
299
+ edgeLabels: boolean;
300
+ };
301
+ deserialize(state: unknown): void;
302
+ dispose(): void;
303
+ }
304
+
305
+ /**
306
+ * The `PanelModule` — what a host registers and mounts.
307
+ *
308
+ * @module
309
+ */
310
+
311
+ /** Everything a renderer needs: the controller plus the resolved config. */
312
+ interface CorpusShapeRenderContext {
313
+ controller: ShapeController;
314
+ config: {
315
+ minimap: boolean;
316
+ legend: boolean;
317
+ };
318
+ }
319
+ /** Options for {@link createCorpusShapePanel}. */
320
+ interface CreateCorpusShapePanelOptions {
321
+ /** Override the view. Rarely needed. */
322
+ render?: (root: HTMLElement, context: CorpusShapeRenderContext) => () => void;
323
+ }
324
+ /**
325
+ * Build the Corpus Shape panel module.
326
+ *
327
+ * @param options - An optional renderer override.
328
+ * @returns The module.
329
+ */
330
+ declare function createCorpusShapePanel(options?: CreateCorpusShapePanelOptions): PanelModule;
331
+ /** The default Corpus Shape panel module. Register THIS. */
332
+ declare const corpusShapePanel: PanelModule;
333
+
334
+ /**
335
+ * The `xeno.corpus.shape` manifest.
336
+ *
337
+ * `storage.local` and nothing else. The canvas draws a picture it is handed; it reads no file, calls no
338
+ * server and runs no check. Whatever can run a check (a pulse) belongs to the host.
339
+ *
340
+ * @module
341
+ */
342
+
343
+ /** The canonical manifest id. */
344
+ declare const CORPUS_SHAPE_PANEL_ID = "xeno.corpus.shape";
345
+ /** The `xeno.corpus.shape` manifest. */
346
+ declare const corpusShapeManifest: PanelManifest;
347
+
348
+ declare const SURFACE_TOKENS: Readonly<Record<string, string>>;
349
+
350
+ /**
351
+ * The Corpus Shape panel AS A DECLARATION — the chrome is data (the question, the focus depth, the
352
+ * zoom and fit controls, the empty state, the status line) and the SURFACE is one runtime unit
353
+ * (`corpus/ShapeSurface`) bound to the controller: lanes, cards, relations, the minimap and legend,
354
+ * the empty-answer sentence. `CorpusShapePanelView` is the code view of the same model; the
355
+ * equivalence suite holds the two byte-identical.
356
+ *
357
+ * Presentation contract: input `view` (a {@link CorpusShapeViewModel}), output `ui` (a
358
+ * {@link CorpusShapeUiIntent}) applied by the runtime to the controller's own methods.
359
+ *
360
+ * @module
361
+ */
362
+
363
+ /** The half of a renderer's config the view reads. */
364
+ interface CorpusShapeViewOptions {
365
+ minimap?: boolean;
366
+ legend?: boolean;
367
+ }
368
+ interface CorpusShapeViewModel {
369
+ /** No model has arrived. */
370
+ empty: boolean;
371
+ /** A model is here: draw the surface. */
372
+ drawn: boolean;
373
+ lens: string;
374
+ lenses: {
375
+ value: string;
376
+ label: string;
377
+ title: string;
378
+ }[];
379
+ depth: string;
380
+ depths: {
381
+ value: string;
382
+ label: string;
383
+ title: string;
384
+ }[];
385
+ zoomLabel: string;
386
+ countLabel: string;
387
+ }
388
+ type CorpusShapeUiIntent = {
389
+ kind: 'lens';
390
+ lens: string;
391
+ } | {
392
+ kind: 'depth';
393
+ depth: string;
394
+ } | {
395
+ kind: 'zoomIn';
396
+ } | {
397
+ kind: 'zoomOut';
398
+ } | {
399
+ kind: 'fit';
400
+ };
401
+ /** Derive the view model from the live controller — the runtime calls this on every draw. */
402
+ declare function corpusShapeViewState(c: ShapeController, _options?: CorpusShapeViewOptions): CorpusShapeViewModel;
403
+ /** Apply a `ui` intent to the controller — the runtime's half of the contract. */
404
+ declare function applyCorpusShapeUi(c: ShapeController, intent: CorpusShapeUiIntent): void;
405
+ /**
406
+ * The presentation manifest: the block's REAL manifest (a delivered block is mounted from THIS, so it
407
+ * must be complete) with the declaration's own two ports, and no capability claim: a declaration has no code.
408
+ */
409
+ declare const corpusShapeViewManifest: PanelManifest;
410
+ declare const BEHAVIOURS: {
411
+ 'corpus/shapePanelView': (inputs: Record<string, unknown>) => CorpusShapeViewModel;
412
+ };
413
+ /** The runtime-bound unit: the drawn canvas. */
414
+ declare const CORPUS_SHAPE_RUNTIME_UNITS: readonly ["corpus/ShapeSurface"];
415
+ declare const CORPUS_SHAPE_DECLARATION: BlockDeclaration;
416
+
417
+ /**
418
+ * The Corpus Shape view — the CODE VIEW of `CORPUS_SHAPE_DECLARATION`, drawing the same
419
+ * {@link CorpusShapeViewModel} byte-identical (the equivalence suite holds the two together). The one
420
+ * runtime-bound unit is the SURFACE (`corpus/ShapeSurface`); the chrome intents go through
421
+ * `applyCorpusShapeUi`.
422
+ *
423
+ * Composes `@xenosystem/components/chrome`. **The host must call `ensurePanelPrimitivesInjected()` from
424
+ * `@xenosystem/components/chrome`** or this renders unstyled with no error.
425
+ *
426
+ * Monochrome per `DESIGN_SYSTEM.md`; colour only for status (held, lapsed, a gap). Every status mark is a
427
+ * rounded square — no circles.
428
+ *
429
+ * @module
430
+ */
431
+
432
+ /** Props for {@link CorpusShapePanelView}. */
433
+ interface CorpusShapePanelViewProps extends CorpusShapeViewOptions {
434
+ controller: ShapeController;
435
+ }
436
+ /** The Corpus Shape panel view. */
437
+ declare function CorpusShapePanelView({ controller, ...options }: CorpusShapePanelViewProps): ReactNode;
438
+ /** The runtime units, bound to a controller — what a declaration renderer registers. */
439
+ declare function corpusShapeRuntimeComponents(controller: ShapeController, options?: CorpusShapeViewOptions): Record<(typeof CORPUS_SHAPE_RUNTIME_UNITS)[number], React.ComponentType<any>>;
440
+ /**
441
+ * The surface — `corpus/ShapeSurface`, bound to the controller: lanes, relation lines, cards (full or
442
+ * compact by zoom), the selected card's relation labels, the empty-answer sentence, the legend and the
443
+ * minimap. Drag pans, the wheel zooms about the cursor, arrows move the selection, Enter opens, F fits.
444
+ */
445
+ declare function CorpusShapeSurface({ controller, minimap, legend }: {
446
+ controller: ShapeController;
447
+ } & CorpusShapeViewOptions): ReactNode;
448
+
449
+ export { COMPACT_BELOW, BEHAVIOURS as CORPUS_SHAPE_BEHAVIOURS, CORPUS_SHAPE_DECLARATION, CORPUS_SHAPE_PANEL_ID, CORPUS_SHAPE_RUNTIME_UNITS, type Chip, CorpusShapePanelView, type CorpusShapePanelViewProps, type CorpusShapeRenderContext, CorpusShapeSurface, type CorpusShapeUiIntent, type CorpusShapeViewModel, type CorpusShapeViewOptions, type CreateCorpusShapePanelOptions, DEPTHS, EDGE_STYLE, LENSES, type LaneBox, NODE_H, NODE_W, type Placed, SHAPE_ZOOM, SURFACE_TOKENS, ShapeController, type ShapeControllerOptions, type ShapeHostBridge, type ShapeState, type XenoCorpusEdge, type XenoCorpusEmphasis, type XenoCorpusLane, type XenoCorpusLens, type XenoCorpusModel, type XenoCorpusNode, type XenoCorpusNodeKind, type XenoCorpusShapeIntent, type XenoCorpusState, type XenoCorpusStripCell, applyCorpusShapeUi, cardChip, cellFill, chipFor, compactFont, corpusShapeManifest, corpusShapePanel, corpusShapeRuntimeComponents, corpusShapeViewManifest, corpusShapeViewState, createCorpusShapePanel, emphasisKey, inLens, isCorpusModel, isDepth, isLens, kindLabel, laneCaption, layout, neighbourhood, subLine, visibleKeys };