@graview/primitives 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.
- package/LICENSE +96 -0
- package/README.md +25 -0
- package/dist/arrange-bar.d.ts +49 -0
- package/dist/arrange-bar.d.ts.map +1 -0
- package/dist/arrange-bar.js +264 -0
- package/dist/arrange-bar.js.map +1 -0
- package/dist/chat.d.ts +46 -0
- package/dist/chat.d.ts.map +1 -0
- package/dist/chat.js +343 -0
- package/dist/chat.js.map +1 -0
- package/dist/companion.d.ts +58 -0
- package/dist/companion.d.ts.map +1 -0
- package/dist/companion.js +433 -0
- package/dist/companion.js.map +1 -0
- package/dist/connections.d.ts +23 -0
- package/dist/connections.d.ts.map +1 -0
- package/dist/connections.js +100 -0
- package/dist/connections.js.map +1 -0
- package/dist/default-views.d.ts +35 -0
- package/dist/default-views.d.ts.map +1 -0
- package/dist/default-views.js +605 -0
- package/dist/default-views.js.map +1 -0
- package/dist/editable.d.ts +71 -0
- package/dist/editable.d.ts.map +1 -0
- package/dist/editable.js +225 -0
- package/dist/editable.js.map +1 -0
- package/dist/figure.d.ts +30 -0
- package/dist/figure.d.ts.map +1 -0
- package/dist/figure.js +66 -0
- package/dist/figure.js.map +1 -0
- package/dist/find.d.ts +5 -0
- package/dist/find.d.ts.map +1 -0
- package/dist/find.js +252 -0
- package/dist/find.js.map +1 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +44 -0
- package/dist/index.js.map +1 -0
- package/dist/installation.d.ts +17 -0
- package/dist/installation.d.ts.map +1 -0
- package/dist/installation.js +37 -0
- package/dist/installation.js.map +1 -0
- package/dist/ladder.d.ts +2 -0
- package/dist/ladder.d.ts.map +1 -0
- package/dist/ladder.js +53 -0
- package/dist/ladder.js.map +1 -0
- package/dist/lens/arranging.d.ts +41 -0
- package/dist/lens/arranging.d.ts.map +1 -0
- package/dist/lens/arranging.js +38 -0
- package/dist/lens/arranging.js.map +1 -0
- package/dist/lens/board.d.ts +146 -0
- package/dist/lens/board.d.ts.map +1 -0
- package/dist/lens/board.js +658 -0
- package/dist/lens/board.js.map +1 -0
- package/dist/lens/calendar-dates.d.ts +19 -0
- package/dist/lens/calendar-dates.d.ts.map +1 -0
- package/dist/lens/calendar-dates.js +55 -0
- package/dist/lens/calendar-dates.js.map +1 -0
- package/dist/lens/calendar-drawing.d.ts +59 -0
- package/dist/lens/calendar-drawing.d.ts.map +1 -0
- package/dist/lens/calendar-drawing.js +229 -0
- package/dist/lens/calendar-drawing.js.map +1 -0
- package/dist/lens/calendar-options.d.ts +107 -0
- package/dist/lens/calendar-options.d.ts.map +1 -0
- package/dist/lens/calendar-options.js +19 -0
- package/dist/lens/calendar-options.js.map +1 -0
- package/dist/lens/calendar-placing.d.ts +35 -0
- package/dist/lens/calendar-placing.d.ts.map +1 -0
- package/dist/lens/calendar-placing.js +113 -0
- package/dist/lens/calendar-placing.js.map +1 -0
- package/dist/lens/calendar-spans.d.ts +48 -0
- package/dist/lens/calendar-spans.d.ts.map +1 -0
- package/dist/lens/calendar-spans.js +146 -0
- package/dist/lens/calendar-spans.js.map +1 -0
- package/dist/lens/calendar-view.d.ts +34 -0
- package/dist/lens/calendar-view.d.ts.map +1 -0
- package/dist/lens/calendar-view.js +228 -0
- package/dist/lens/calendar-view.js.map +1 -0
- package/dist/lens/calendar.d.ts +43 -0
- package/dist/lens/calendar.d.ts.map +1 -0
- package/dist/lens/calendar.js +43 -0
- package/dist/lens/calendar.js.map +1 -0
- package/dist/lens/coverage.d.ts +183 -0
- package/dist/lens/coverage.d.ts.map +1 -0
- package/dist/lens/coverage.js +732 -0
- package/dist/lens/coverage.js.map +1 -0
- package/dist/lens/horizon.d.ts +14 -0
- package/dist/lens/horizon.d.ts.map +1 -0
- package/dist/lens/horizon.js +20 -0
- package/dist/lens/horizon.js.map +1 -0
- package/dist/lens/more.d.ts +16 -0
- package/dist/lens/more.d.ts.map +1 -0
- package/dist/lens/more.js +17 -0
- package/dist/lens/more.js.map +1 -0
- package/dist/lens/plan-lens.d.ts +17 -0
- package/dist/lens/plan-lens.d.ts.map +1 -0
- package/dist/lens/plan-lens.js +24 -0
- package/dist/lens/plan-lens.js.map +1 -0
- package/dist/lens/plan-state.d.ts +122 -0
- package/dist/lens/plan-state.d.ts.map +1 -0
- package/dist/lens/plan-state.js +152 -0
- package/dist/lens/plan-state.js.map +1 -0
- package/dist/lens/plan-view.d.ts +10 -0
- package/dist/lens/plan-view.d.ts.map +1 -0
- package/dist/lens/plan-view.js +576 -0
- package/dist/lens/plan-view.js.map +1 -0
- package/dist/lens/plan.d.ts +25 -0
- package/dist/lens/plan.d.ts.map +1 -0
- package/dist/lens/plan.js +25 -0
- package/dist/lens/plan.js.map +1 -0
- package/dist/lens/reach.d.ts +47 -0
- package/dist/lens/reach.d.ts.map +1 -0
- package/dist/lens/reach.js +67 -0
- package/dist/lens/reach.js.map +1 -0
- package/dist/lens/timeline.d.ts +137 -0
- package/dist/lens/timeline.d.ts.map +1 -0
- package/dist/lens/timeline.js +426 -0
- package/dist/lens/timeline.js.map +1 -0
- package/dist/markup.d.ts +23 -0
- package/dist/markup.d.ts.map +1 -0
- package/dist/markup.js +24 -0
- package/dist/markup.js.map +1 -0
- package/dist/places.d.ts +24 -0
- package/dist/places.d.ts.map +1 -0
- package/dist/places.js +227 -0
- package/dist/places.js.map +1 -0
- package/dist/popover.d.ts +28 -0
- package/dist/popover.d.ts.map +1 -0
- package/dist/popover.js +59 -0
- package/dist/popover.js.map +1 -0
- package/dist/primitives/index.d.ts +214 -0
- package/dist/primitives/index.d.ts.map +1 -0
- package/dist/primitives/index.js +381 -0
- package/dist/primitives/index.js.map +1 -0
- package/dist/profile.d.ts +65 -0
- package/dist/profile.d.ts.map +1 -0
- package/dist/profile.js +233 -0
- package/dist/profile.js.map +1 -0
- package/dist/quick-relations.d.ts +16 -0
- package/dist/quick-relations.d.ts.map +1 -0
- package/dist/quick-relations.js +183 -0
- package/dist/quick-relations.js.map +1 -0
- package/dist/relation-key.d.ts +44 -0
- package/dist/relation-key.d.ts.map +1 -0
- package/dist/relation-key.js +196 -0
- package/dist/relation-key.js.map +1 -0
- package/dist/seat.d.ts +137 -0
- package/dist/seat.d.ts.map +1 -0
- package/dist/seat.js +162 -0
- package/dist/seat.js.map +1 -0
- package/dist/seats.d.ts +21 -0
- package/dist/seats.d.ts.map +1 -0
- package/dist/seats.js +62 -0
- package/dist/seats.js.map +1 -0
- package/dist/seeding.d.ts +150 -0
- package/dist/seeding.d.ts.map +1 -0
- package/dist/seeding.js +538 -0
- package/dist/seeding.js.map +1 -0
- package/dist/shell.d.ts +74 -0
- package/dist/shell.d.ts.map +1 -0
- package/dist/shell.js +86 -0
- package/dist/shell.js.map +1 -0
- package/dist/theme.d.ts +50 -0
- package/dist/theme.d.ts.map +1 -0
- package/dist/theme.js +1692 -0
- package/dist/theme.js.map +1 -0
- package/dist/wordmark.d.ts +15 -0
- package/dist/wordmark.d.ts.map +1 -0
- package/dist/wordmark.js +55 -0
- package/dist/wordmark.js.map +1 -0
- package/dist/workbench/activity.d.ts +75 -0
- package/dist/workbench/activity.d.ts.map +1 -0
- package/dist/workbench/activity.js +259 -0
- package/dist/workbench/activity.js.map +1 -0
- package/dist/workbench/agent-seat.d.ts +51 -0
- package/dist/workbench/agent-seat.d.ts.map +1 -0
- package/dist/workbench/agent-seat.js +112 -0
- package/dist/workbench/agent-seat.js.map +1 -0
- package/dist/workbench/answer-args.d.ts +31 -0
- package/dist/workbench/answer-args.d.ts.map +1 -0
- package/dist/workbench/answer-args.js +257 -0
- package/dist/workbench/answer-args.js.map +1 -0
- package/dist/workbench/back-out.d.ts +71 -0
- package/dist/workbench/back-out.d.ts.map +1 -0
- package/dist/workbench/back-out.js +238 -0
- package/dist/workbench/back-out.js.map +1 -0
- package/dist/workbench/following.d.ts +7 -0
- package/dist/workbench/following.d.ts.map +1 -0
- package/dist/workbench/following.js +26 -0
- package/dist/workbench/following.js.map +1 -0
- package/dist/workbench/index.d.ts +23 -0
- package/dist/workbench/index.d.ts.map +1 -0
- package/dist/workbench/index.js +23 -0
- package/dist/workbench/index.js.map +1 -0
- package/dist/workbench/inspector.d.ts +27 -0
- package/dist/workbench/inspector.d.ts.map +1 -0
- package/dist/workbench/inspector.js +745 -0
- package/dist/workbench/inspector.js.map +1 -0
- package/dist/workbench/standing.d.ts +16 -0
- package/dist/workbench/standing.d.ts.map +1 -0
- package/dist/workbench/standing.js +102 -0
- package/dist/workbench/standing.js.map +1 -0
- package/dist/workbench/trail.d.ts +35 -0
- package/dist/workbench/trail.d.ts.map +1 -0
- package/dist/workbench/trail.js +150 -0
- package/dist/workbench/trail.js.map +1 -0
- package/package.json +62 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type AnySchema } from "@graview/core";
|
|
2
|
+
import { type ReactViewRegistry } from "@graview/react";
|
|
3
|
+
/**
|
|
4
|
+
* A generic view for every cell of the matrix, derived from the declaration.
|
|
5
|
+
*
|
|
6
|
+
* This is what makes "a new node kind renders acceptably at all three
|
|
7
|
+
* fidelities with zero custom view code" true rather than aspirational: the
|
|
8
|
+
* declaration already carries the label, the plural and the fields, so a
|
|
9
|
+
* sensible rendering exists the moment the kind does. An author overrides
|
|
10
|
+
* only the cells they actually care about.
|
|
11
|
+
*/
|
|
12
|
+
import { hueFor } from "@graview/render";
|
|
13
|
+
export { hueFor };
|
|
14
|
+
/** The declared accent when the installation named one, else the hash. */
|
|
15
|
+
export declare function useHue(kind: string): number;
|
|
16
|
+
/**
|
|
17
|
+
* WHAT AN OPENED DISTRICT LISTS: the rows the layout kept room for
|
|
18
|
+
* (`openedRows`), in as many columns as its names can be READ in. Two
|
|
19
|
+
* columns of ninety-five pixels turned every vehicle into "2026 Ma…"; a
|
|
20
|
+
* name needs about seven and a half pixels a character in a 212-pixel
|
|
21
|
+
* roster, and a district of long names keeps one column.
|
|
22
|
+
*/
|
|
23
|
+
export declare function rosterOf(names: readonly string[], rows: number): {
|
|
24
|
+
readonly columns: number;
|
|
25
|
+
readonly shown: number;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* How many members a drive-in's thumbnail hands its lens: enough for its
|
|
29
|
+
* shape, never the population. A thumbnail is 58 pixels wide: twelve rows
|
|
30
|
+
* is already more than it can show, and a matrix of 12 × 12 is 144 cells
|
|
31
|
+
* where 24 × 24 was 576.
|
|
32
|
+
*/
|
|
33
|
+
export declare const THUMBNAIL_BUDGET = 12;
|
|
34
|
+
export declare function registerDefaultViews<S extends AnySchema>(schema: S, registry?: ReactViewRegistry<S>): ReactViewRegistry<S>;
|
|
35
|
+
//# sourceMappingURL=default-views.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"default-views.d.ts","sourceRoot":"","sources":["../src/default-views.tsx"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,SAAS,EAIf,MAAM,eAAe,CAAC;AAEvB,OAAO,EAUL,KAAK,iBAAiB,EAEN,MAAM,gBAAgB,CAAC;AAOzC;;;;;;;;GAQG;AAIH,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAEzC,OAAO,EAAE,MAAM,EAAE,CAAC;AAElB,0EAA0E;AAC1E,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAG3C;AAyBD;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG;IAAE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAMrH;AAED;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,KAAK,CAAC;AAuJnC,wBAAgB,oBAAoB,CAAC,CAAC,SAAS,SAAS,EACtD,MAAM,EAAE,CAAC,EACT,QAAQ,GAAE,iBAAiB,CAAC,CAAC,CAAuB,GACnD,iBAAiB,CAAC,CAAC,CAAC,CA+zBtB"}
|
|
@@ -0,0 +1,605 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { arrange, describeNode, labelOf, readableFields, violationsTouching, } from "@graview/core";
|
|
3
|
+
import { aggregateId, kindCardId, marqueeHeightFor, rosterRows, withFocus, withJackIn, withOverview, withPast, withWithin } from "@graview/layout";
|
|
4
|
+
import { createViews, useFound, useGraview, useKit, useNavigation, useReached, useSceneStill, useSelection, useViolations, markDefaultView } from "@graview/react";
|
|
5
|
+
import { memo, useEffect, useMemo, useRef, useState } from "react";
|
|
6
|
+
import { ArrangeBar, arrangementOf, withArrangement } from "./arrange-bar.js";
|
|
7
|
+
import { Connections } from "./connections.js";
|
|
8
|
+
import { EditableTitle, Fields } from "./editable.js";
|
|
9
|
+
import { Aggregate, Chip, Panel, Roster } from "./primitives/index.js";
|
|
10
|
+
/**
|
|
11
|
+
* A generic view for every cell of the matrix, derived from the declaration.
|
|
12
|
+
*
|
|
13
|
+
* This is what makes "a new node kind renders acceptably at all three
|
|
14
|
+
* fidelities with zero custom view code" true rather than aspirational: the
|
|
15
|
+
* declaration already carries the label, the plural and the fields, so a
|
|
16
|
+
* sensible rendering exists the moment the kind does. An author overrides
|
|
17
|
+
* only the cells they actually care about.
|
|
18
|
+
*/
|
|
19
|
+
// The canonical hue lives beside the other colour logic in @graview/render;
|
|
20
|
+
// re-exported here because every view author already imports it from views.
|
|
21
|
+
import { hueFor } from "@graview/render";
|
|
22
|
+
import { hasFigure, KindFigure } from "./figure.js";
|
|
23
|
+
export { hueFor };
|
|
24
|
+
/** The declared accent when the installation named one, else the hash. */
|
|
25
|
+
export function useHue(kind) {
|
|
26
|
+
const { brand } = useGraview();
|
|
27
|
+
return hueFor(kind, brand?.accents);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The field a shortened label was shortened FROM, when there is one.
|
|
31
|
+
*
|
|
32
|
+
* Matched by stem rather than by name, because which field a label came from
|
|
33
|
+
* is the app's business — `label: (node) => summarise(node.text)` names no
|
|
34
|
+
* field the framework can see, and asking every app to declare it would be
|
|
35
|
+
* asking them to repeat themselves.
|
|
36
|
+
*/
|
|
37
|
+
function longFormOf(node, short) {
|
|
38
|
+
if (!short.endsWith("…") && !short.endsWith("..."))
|
|
39
|
+
return undefined;
|
|
40
|
+
const stem = short.replace(/[…]|\.\.\.$/g, "").trim();
|
|
41
|
+
if (stem.length < 12)
|
|
42
|
+
return undefined;
|
|
43
|
+
for (const value of Object.values(node)) {
|
|
44
|
+
if (typeof value !== "string")
|
|
45
|
+
continue;
|
|
46
|
+
if (value.length > stem.length && value.startsWith(stem))
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* WHAT AN OPENED DISTRICT LISTS: the rows the layout kept room for
|
|
53
|
+
* (`openedRows`), in as many columns as its names can be READ in. Two
|
|
54
|
+
* columns of ninety-five pixels turned every vehicle into "2026 Ma…"; a
|
|
55
|
+
* name needs about seven and a half pixels a character in a 212-pixel
|
|
56
|
+
* roster, and a district of long names keeps one column.
|
|
57
|
+
*/
|
|
58
|
+
export function rosterOf(names, rows) {
|
|
59
|
+
const widest = Math.max(0, ...names.slice(0, rows * 2).map((name) => name.length));
|
|
60
|
+
const columns = widest * 7.5 + 28 <= 212 / 2 ? 2 : 1;
|
|
61
|
+
const room = rows * columns;
|
|
62
|
+
// The count line needs no row of its own: it is part of the room reserved.
|
|
63
|
+
return { columns, shown: Math.min(names.length, room) };
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* How many members a drive-in's thumbnail hands its lens: enough for its
|
|
67
|
+
* shape, never the population. A thumbnail is 58 pixels wide: twelve rows
|
|
68
|
+
* is already more than it can show, and a matrix of 12 × 12 is 144 cells
|
|
69
|
+
* where 24 × 24 was 576.
|
|
70
|
+
*/
|
|
71
|
+
export const THUMBNAIL_BUDGET = 12;
|
|
72
|
+
/*
|
|
73
|
+
* ONE THUMBNAIL A FRAME. A city of districts with named pictures would build
|
|
74
|
+
* them all in the frame the scene came to rest in — four lenses in one long
|
|
75
|
+
* task. Each waits its turn, one per animation frame.
|
|
76
|
+
*/
|
|
77
|
+
const turns = [];
|
|
78
|
+
let turning = false;
|
|
79
|
+
function takeTurn(mount) {
|
|
80
|
+
turns.push(mount);
|
|
81
|
+
const next = () => {
|
|
82
|
+
const run = turns.shift();
|
|
83
|
+
if (!run) {
|
|
84
|
+
turning = false;
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
run();
|
|
88
|
+
requestAnimationFrame(next);
|
|
89
|
+
};
|
|
90
|
+
if (!turning && typeof requestAnimationFrame !== "undefined") {
|
|
91
|
+
turning = true;
|
|
92
|
+
requestAnimationFrame(next);
|
|
93
|
+
}
|
|
94
|
+
else if (typeof requestAnimationFrame === "undefined")
|
|
95
|
+
next();
|
|
96
|
+
return () => {
|
|
97
|
+
const at = turns.indexOf(mount);
|
|
98
|
+
if (at >= 0)
|
|
99
|
+
turns.splice(at, 1);
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* The members worth drawing when not all can be: the flagged first, then
|
|
104
|
+
* the most connected, in their own order otherwise — the same reading the
|
|
105
|
+
* relation band's relevance gives (docs/scale.md).
|
|
106
|
+
*/
|
|
107
|
+
function mostRelevant(members, budget, flagged, graph) {
|
|
108
|
+
if (members.length <= budget)
|
|
109
|
+
return members;
|
|
110
|
+
const trouble = new Set(flagged ?? []);
|
|
111
|
+
return members
|
|
112
|
+
.map((node, index) => ({ node, index, trouble: trouble.has(node.id) ? 0 : 1, ties: -graph.neighbors(node.id).length }))
|
|
113
|
+
.sort((a, b) => a.trouble - b.trouble || a.ties - b.ties || a.index - b.index)
|
|
114
|
+
.slice(0, budget)
|
|
115
|
+
.sort((a, b) => a.index - b.index)
|
|
116
|
+
.map((entry) => entry.node);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* DRAWN WHEN SEEN, AND WHEN STILL. A thumbnail off the screen, or built
|
|
120
|
+
* while the camera flies, spends a frame on a picture nobody can read. It
|
|
121
|
+
* mounts the first time it is on screen with the scene at rest, and then
|
|
122
|
+
* stays — unmounting on every flight would rebuild it on every landing.
|
|
123
|
+
*/
|
|
124
|
+
function WhenSeen({ children }) {
|
|
125
|
+
const still = useSceneStill();
|
|
126
|
+
const box = useRef(null);
|
|
127
|
+
const [seen, setSeen] = useState(false);
|
|
128
|
+
const [shown, setShown] = useState(false);
|
|
129
|
+
useEffect(() => {
|
|
130
|
+
const element = box.current;
|
|
131
|
+
if (!element || seen)
|
|
132
|
+
return;
|
|
133
|
+
if (typeof IntersectionObserver === "undefined") {
|
|
134
|
+
setSeen(true);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const watch = new IntersectionObserver((entries) => {
|
|
138
|
+
if (entries.some((entry) => entry.isIntersecting))
|
|
139
|
+
setSeen(true);
|
|
140
|
+
}, { rootMargin: "120px" });
|
|
141
|
+
watch.observe(element);
|
|
142
|
+
return () => watch.disconnect();
|
|
143
|
+
}, [seen]);
|
|
144
|
+
useEffect(() => {
|
|
145
|
+
if (!seen || !still || shown)
|
|
146
|
+
return;
|
|
147
|
+
return takeTurn(() => setShown(true));
|
|
148
|
+
}, [seen, still, shown]);
|
|
149
|
+
return (
|
|
150
|
+
/*
|
|
151
|
+
* A real box, not `display: contents`: an element with no box never
|
|
152
|
+
* intersects anything. And a box as tall as a picture, not one pixel:
|
|
153
|
+
* drawn at a twentieth of its size a pixel is a twentieth of one, which
|
|
154
|
+
* the browser counts as no area at all — the Albums district's two
|
|
155
|
+
* pictures waited forever while the Artists one, drawn twice as large,
|
|
156
|
+
* was seen.
|
|
157
|
+
*/
|
|
158
|
+
_jsx("span", { ref: box, style: { display: "block", minHeight: shown ? undefined : 360 }, "data-graview-thumbnail": shown ? "drawn" : "waiting", children: shown ? children : null }));
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* A LENS DRAWN SMALL, AND ONLY WHEN WHAT IT DRAWS CHANGES.
|
|
162
|
+
*
|
|
163
|
+
* The board renders the real component for every showing a kind has, and
|
|
164
|
+
* the card around it re-renders on every frame of a flight — so choosing
|
|
165
|
+
* a picture re-drew three matrices and a calendar sixty times a second
|
|
166
|
+
* while the camera moved, which is where "slow and glitchy" came from.
|
|
167
|
+
* Nothing about a thumbnail depends on the size of the card it sits in:
|
|
168
|
+
* memoised on what it actually draws, the flight costs one render each.
|
|
169
|
+
*/
|
|
170
|
+
const Picture = memo(function Picture({ lens: Lens, nodes, label, flagged, budget, total, }) {
|
|
171
|
+
return (_jsx(Lens, { nodes: nodes, label: label, ...(budget !== undefined ? { budget } : {}), ...(total !== undefined ? { total } : {}), fidelity: "full", cardinality: "many", mode: "scene", selected: false, ...(flagged ? { flagged } : {}) }));
|
|
172
|
+
}, (was, now) => was.lens === now.lens &&
|
|
173
|
+
was.label === now.label &&
|
|
174
|
+
was.total === now.total &&
|
|
175
|
+
was.nodes.length === now.nodes.length &&
|
|
176
|
+
was.nodes.every((node, at) => node === now.nodes[at]) &&
|
|
177
|
+
(was.flagged ?? []).length === (now.flagged ?? []).length &&
|
|
178
|
+
(was.flagged ?? []).every((id, at) => id === (now.flagged ?? [])[at]));
|
|
179
|
+
export function registerDefaultViews(schema, registry = createViews(schema)) {
|
|
180
|
+
for (const kind of schema.kinds) {
|
|
181
|
+
const definition = schema.tryDefinition(kind);
|
|
182
|
+
const plural = definition?.plural ?? `${String(kind)}s`;
|
|
183
|
+
const Full = (props) => {
|
|
184
|
+
const node = props.node;
|
|
185
|
+
/*
|
|
186
|
+
* WHAT IS WRONG WITH IT, ON IT.
|
|
187
|
+
*
|
|
188
|
+
* A flagged record wore the warning TONE and said nothing: a
|
|
189
|
+
* background tint, no words, no mark, nothing in the accessibility
|
|
190
|
+
* tree. The chips in a district get a "⚠" in their own label and the
|
|
191
|
+
* routed record page carries the rule's sentence — the biggest drawing
|
|
192
|
+
* of the same record, the one you travelled to, was the only place the
|
|
193
|
+
* problem existed purely as a shade. That is W-014's shape, one view
|
|
194
|
+
* along: emphasis painted and not said.
|
|
195
|
+
*/
|
|
196
|
+
const violations = useViolations();
|
|
197
|
+
if (!node)
|
|
198
|
+
return null;
|
|
199
|
+
/*
|
|
200
|
+
* On a page, the heading is the WHOLE thing.
|
|
201
|
+
*
|
|
202
|
+
* A kind whose label is its own prose shortened — a rationale, a note, a
|
|
203
|
+
* reason — gets a heading cut at sixty characters, which is right on a
|
|
204
|
+
* card in the scene and wrong on a page that has room for all of it.
|
|
205
|
+
* Shortening then ALSO listing the full text below is worse again: the
|
|
206
|
+
* same sentence twice, one of them mutilated.
|
|
207
|
+
*
|
|
208
|
+
* So the page looks for the long form of its own heading and uses that,
|
|
209
|
+
* and `Fields` then drops it as already said. The abbreviation was for
|
|
210
|
+
* the card.
|
|
211
|
+
*/
|
|
212
|
+
const short = labelOf(definition, node);
|
|
213
|
+
const heading = props.mode === "fullscreen" ? (longFormOf(node, short) ?? short) : short;
|
|
214
|
+
const touching = violationsTouching(violations, [node.id]);
|
|
215
|
+
return (_jsxs(Panel
|
|
216
|
+
// The name is the rename control. Nothing else on the page repeats
|
|
217
|
+
// it, so the thing you click to change it is the thing itself.
|
|
218
|
+
, {
|
|
219
|
+
// The name is the rename control. Nothing else on the page repeats
|
|
220
|
+
// it, so the thing you click to change it is the thing itself.
|
|
221
|
+
title: _jsx(EditableTitle, { nodeId: node.id, children: heading }),
|
|
222
|
+
/*
|
|
223
|
+
* What THIS node is, when the declaration can say — and only the
|
|
224
|
+
* kind's own description as a fallback.
|
|
225
|
+
*
|
|
226
|
+
* A subtitle that is the kind's description is identical on every
|
|
227
|
+
* node of that kind, which makes it furniture rather than
|
|
228
|
+
* information: it tells you what a rationale is, on a page you
|
|
229
|
+
* reached by choosing one particular rationale.
|
|
230
|
+
*/
|
|
231
|
+
subtitle:
|
|
232
|
+
// `describeNode` falls back to "<kind> <label>", which would put
|
|
233
|
+
// the heading back under the heading — so only take it when the
|
|
234
|
+
// declaration actually supplied one.
|
|
235
|
+
definition?.describe
|
|
236
|
+
? describeNode(definition, node)
|
|
237
|
+
: (definition?.description ?? String(kind)), selected: props.selected, ...(props.flagged?.includes(node.id) ? { tone: "warning" } : {}),
|
|
238
|
+
// A card in the scene; a document on a page. Same component, and the
|
|
239
|
+
// only difference is which of those two things it is sitting on.
|
|
240
|
+
fit: true, children: [_jsxs("div", { style: {
|
|
241
|
+
display: "grid",
|
|
242
|
+
gridTemplateColumns: "minmax(200px, 1fr) minmax(220px, 1.4fr)",
|
|
243
|
+
gap: "22px 34px",
|
|
244
|
+
alignItems: "start",
|
|
245
|
+
}, children: [_jsx(Fields, { id: node.id,
|
|
246
|
+
// What the panel already said: its title and its subtitle. A
|
|
247
|
+
// record repeating its own heading is what made this read as a
|
|
248
|
+
// debug dump rather than as a page about something.
|
|
249
|
+
shown: [heading, short, definition?.description] }), _jsx(Connections, { id: node.id, empty: `Nothing is connected to this ${String(kind)} yet.` })] }), touching.length > 0 ? (_jsx("p", { "data-graview-broken": "", style: {
|
|
250
|
+
margin: "14px 0 0",
|
|
251
|
+
paddingTop: 12,
|
|
252
|
+
borderTop: "1px solid var(--graview-edge)",
|
|
253
|
+
fontSize: "0.875rem",
|
|
254
|
+
color: "var(--graview-warn)",
|
|
255
|
+
}, children: touching.map((violation) => violation.message).join(" · ") })) : null] }));
|
|
256
|
+
};
|
|
257
|
+
/**
|
|
258
|
+
* Summary is not "full, but smaller". Receded text must stay LEGIBLE, so
|
|
259
|
+
* this drops to a denser arrangement rather than scaling the same content
|
|
260
|
+
* down into mush — that is the whole reason fidelity is an axis.
|
|
261
|
+
*/
|
|
262
|
+
const Summary = (props) => {
|
|
263
|
+
const node = props.node;
|
|
264
|
+
if (!node)
|
|
265
|
+
return null;
|
|
266
|
+
// The same question the record on a page asks, answered by the same
|
|
267
|
+
// function — they used to answer it separately and diverge.
|
|
268
|
+
const fields = readableFields(node, definition, {
|
|
269
|
+
limit: 3,
|
|
270
|
+
said: [labelOf(definition, node)],
|
|
271
|
+
glance: true,
|
|
272
|
+
});
|
|
273
|
+
const hue = useHue(kind);
|
|
274
|
+
return (_jsx(Panel, { title: labelOf(definition, node), selected: props.selected, tone: props.flagged?.includes(node.id) ? "warning" : "muted", fit: true, children: _jsx(Roster, { items: fields.map((field) => ({
|
|
275
|
+
id: field.key,
|
|
276
|
+
// Alone on a chip a number or a yes/no is no fact at all.
|
|
277
|
+
label: field.alone,
|
|
278
|
+
hue,
|
|
279
|
+
})), max: 3 }) }));
|
|
280
|
+
};
|
|
281
|
+
const Glyph = (props) => {
|
|
282
|
+
const node = props.node;
|
|
283
|
+
const hue = useHue(kind);
|
|
284
|
+
// The mark a broken rule leaves is the kit's, not a literal here.
|
|
285
|
+
const flag = useKit().marks.flag;
|
|
286
|
+
if (!node)
|
|
287
|
+
return null;
|
|
288
|
+
const broken = props.flagged?.includes(node.id) ?? false;
|
|
289
|
+
return (_jsx(Chip, { label: broken ? `${labelOf(definition, node)} ${flag}` : labelOf(definition, node), hue: hue, selected: props.selected, title: broken ? `${String(kind)} — implicated in a problem` : String(kind) }));
|
|
290
|
+
};
|
|
291
|
+
const Group = (props) => {
|
|
292
|
+
const { brand, store } = useGraview();
|
|
293
|
+
const { view, go } = useNavigation();
|
|
294
|
+
const members = props.nodes ?? [];
|
|
295
|
+
/*
|
|
296
|
+
* THE DEFAULT PICTURE ARRANGES TOO. What a kind can be sorted, filtered
|
|
297
|
+
* and grouped by is in its declaration, and the choice travels in the
|
|
298
|
+
* stop (`in.sort`, `in.filter`, `in.group`, `in.q`) like the calendar's
|
|
299
|
+
* month does — so an arranged district is a link, and Back restores it.
|
|
300
|
+
* The row is drawn at full fidelity, where there is room to read it;
|
|
301
|
+
* the arrangement holds at every fidelity, because the stop does.
|
|
302
|
+
*/
|
|
303
|
+
const arrangement = arrangementOf(view);
|
|
304
|
+
const arranged = arrange(members, arrangement, { schema, graph: store.graph, flagged: new Set(props.flagged ?? []) });
|
|
305
|
+
const item = (member) => ({
|
|
306
|
+
id: member.id,
|
|
307
|
+
label: labelOf(schema.tryDefinition(member.kind), member),
|
|
308
|
+
hue: hueFor(member.kind, brand?.accents),
|
|
309
|
+
});
|
|
310
|
+
const bar = props.fidelity === "full" && members.length > 1 ? (_jsx(ArrangeBar, { schema: schema, graph: store.graph, kind: String(kind), arrangement: arrangement, onChange: (next) => go(withArrangement(view, next)), kept: { shown: arranged.nodes.length, of: members.length }, style: { marginBottom: 8 } })) : null;
|
|
311
|
+
// A receded group still has to report trouble inside it, or the only
|
|
312
|
+
// way to find a problem is to open every group in turn.
|
|
313
|
+
const flagged = (nodes) => nodes.some((member) => props.flagged?.includes(member.id));
|
|
314
|
+
if (!arranged.grouped) {
|
|
315
|
+
return (_jsxs("div", { style: { display: "grid", gap: 4 }, children: [bar, _jsx(Aggregate, { label: props.label ?? plural, count: arranged.nodes.length, flagged: flagged(arranged.nodes), items: arranged.nodes.map(item) })] }));
|
|
316
|
+
}
|
|
317
|
+
return (_jsxs("div", { style: { display: "grid", gap: 8 }, "data-graview-grouped": arrangement.group?.by, children: [bar, arranged.groups.map((group) => (_jsx(Aggregate, { label: `${props.label ?? plural} · ${group.label}`, count: group.nodes.length, flagged: flagged(group.nodes), items: group.nodes.map(item) }, group.key || "-")))] }));
|
|
318
|
+
};
|
|
319
|
+
/**
|
|
320
|
+
* A group at glyph fidelity.
|
|
321
|
+
*
|
|
322
|
+
* It names its MEMBERS, not just how many there are. "4 AGREEMENTS" tells
|
|
323
|
+
* you a number and nothing you wanted to know; the whole point of a
|
|
324
|
+
* context plane is to be glanceable, and a count is not a glance at
|
|
325
|
+
* anything. What fits, fits; the rest is "+n".
|
|
326
|
+
*/
|
|
327
|
+
/**
|
|
328
|
+
* A group at glyph fidelity — the card the kinds plane is made of.
|
|
329
|
+
*
|
|
330
|
+
* Its name, how many there are, and what it is FOR on hover. The members
|
|
331
|
+
* used to be listed and it was the wrong answer: ten cards each showing
|
|
332
|
+
* three truncated names is ten unreadable things, and "which kinds exist
|
|
333
|
+
* and how big are they" is the question a map of kinds is asked. The
|
|
334
|
+
* description has been on every declaration since the first commit and
|
|
335
|
+
* nothing has ever shown it.
|
|
336
|
+
*/
|
|
337
|
+
const GroupGlyph = (props) => {
|
|
338
|
+
const members = props.nodes ?? [];
|
|
339
|
+
const roster = rosterOf(members.map((member) => labelOf(definition, member)), props.openedRows ?? rosterRows(members.length));
|
|
340
|
+
const hue = useHue(kind);
|
|
341
|
+
/*
|
|
342
|
+
* THE KIND'S OWN DRAWING, where it has one.
|
|
343
|
+
*
|
|
344
|
+
* The isometric city gives every kind the same block; a figure is
|
|
345
|
+
* what makes one of them a person and another a plot of ground. The
|
|
346
|
+
* brand's say comes first, so an installation with its own drawing
|
|
347
|
+
* keeps the domain's declaration as the domain's.
|
|
348
|
+
*/
|
|
349
|
+
const { brand } = useGraview();
|
|
350
|
+
const figure = hasFigure(String(kind), schema, brand);
|
|
351
|
+
const flag = useKit().marks.flag;
|
|
352
|
+
const { selection } = useSelection();
|
|
353
|
+
const { toggle, view, go } = useNavigation();
|
|
354
|
+
const { views, hiddenKinds, store } = useGraview();
|
|
355
|
+
/*
|
|
356
|
+
* THE DRIVE-IN'S MARQUEE. A kind with a named picture has a drive-in
|
|
357
|
+
* from altitude: a dark screen on its plot and, under it, the showings
|
|
358
|
+
* as real buttons. Pressing one focuses the kind with that showing and
|
|
359
|
+
* descends in one gesture — the lens is already drawn at the plot, so
|
|
360
|
+
* the descent reads as walking up to the screen. On the drive-in that
|
|
361
|
+
* is focused, pressing another showing switches the picture without
|
|
362
|
+
* descending; pressing the one already showing walks up to it.
|
|
363
|
+
*/
|
|
364
|
+
const showings = view.overview && !hiddenKinds.has(String(kind)) ? views.places().filter((place) => place.kind === String(kind)) : [];
|
|
365
|
+
const showingNow = props.focused ? (view.within?.["view"] ?? showings[0]?.as) : undefined;
|
|
366
|
+
// A thumbnail is a picture of the lens, not the lens: its most relevant members, never the population.
|
|
367
|
+
const thumbnailMembers = useMemo(() => (showings.length > 0 ? mostRelevant(members, THUMBNAIL_BUDGET, props.flagged, store.graph) : []),
|
|
368
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
369
|
+
[showings.length, members, props.flagged, store]);
|
|
370
|
+
const broken = members.filter((member) => props.flagged?.includes(member.id)).length;
|
|
371
|
+
const trouble = broken > 0;
|
|
372
|
+
const accent = props.focused || props.raised;
|
|
373
|
+
/*
|
|
374
|
+
* A kind the focus does not touch is QUIETER, not hidden.
|
|
375
|
+
*
|
|
376
|
+
* The layout already draws it smaller and further back; this is the
|
|
377
|
+
* other half of the same reading, so the card does not look like a
|
|
378
|
+
* full-size one that merely shrank. It stays a real card with a real
|
|
379
|
+
* name — a secondary relation is still somewhere you can go.
|
|
380
|
+
*/
|
|
381
|
+
const secondary = props.rank === "secondary" && !accent;
|
|
382
|
+
const nested = props.nestedUnder !== undefined;
|
|
383
|
+
/*
|
|
384
|
+
* How many of this kind's members the SELECTION reaches, one edge away.
|
|
385
|
+
*
|
|
386
|
+
* "What is this thing actually tied to" was unanswerable from the
|
|
387
|
+
* shelf: you selected a span in the calendar and every kind card sat
|
|
388
|
+
* unchanged. The implicated set has always known; the card just never
|
|
389
|
+
* read it. Selected members themselves do not count — a card does not
|
|
390
|
+
* announce a tie to the thing that IS the selection.
|
|
391
|
+
*/
|
|
392
|
+
const chosen = new Set(selection);
|
|
393
|
+
// What the selection reaches — never what a search lit, which is counted apart below.
|
|
394
|
+
const reached = useReached();
|
|
395
|
+
const tied = accent
|
|
396
|
+
? 0
|
|
397
|
+
: members.filter((member) => reached.includes(member.id) && !chosen.has(member.id)).length;
|
|
398
|
+
/*
|
|
399
|
+
* WHAT THE WORDS FOUND HERE. With a search open, a district with hits
|
|
400
|
+
* is lit and says how many; one with none recedes. Pressing the count
|
|
401
|
+
* descends into the district already narrowed by the same words — the
|
|
402
|
+
* arrangement's `in.q` — so search hands off to the arrangement rather
|
|
403
|
+
* than competing with it.
|
|
404
|
+
*/
|
|
405
|
+
const found = useFound();
|
|
406
|
+
const hits = found && !nested ? (found.byKind[String(kind)] ?? 0) : 0;
|
|
407
|
+
/*
|
|
408
|
+
* The block's height from altitude, in viewBox units: population under
|
|
409
|
+
* a square root, so one giant kind is a tall building rather than a
|
|
410
|
+
* tower the scene scrolls for.
|
|
411
|
+
*/
|
|
412
|
+
const rise = nested ? 8 : Math.min(46, 10 + Math.round(Math.sqrt(members.length) * 8));
|
|
413
|
+
/*
|
|
414
|
+
* The district's NAME, lifted out of the markup because a figure stands
|
|
415
|
+
* beside it and a name written twice is a name that drifts.
|
|
416
|
+
*/
|
|
417
|
+
const name = (_jsx("span", { style: {
|
|
418
|
+
/*
|
|
419
|
+
* A DISTRICT'S NAME IS READ, not glanced at. Thirteen pixels
|
|
420
|
+
* before the kinds plane's own recession put it on the screen at
|
|
421
|
+
* ten, and the kind above it at under eight — small enough that
|
|
422
|
+
* the bottom of the picture was a row of grey marks rather than
|
|
423
|
+
* a map of the domain.
|
|
424
|
+
*/
|
|
425
|
+
fontSize: nested ? "0.75rem" : "0.9375rem",
|
|
426
|
+
lineHeight: 1.25,
|
|
427
|
+
letterSpacing: "0.05em",
|
|
428
|
+
textTransform: "uppercase",
|
|
429
|
+
color: accent ? "var(--graview-accent)" : "var(--graview-ink-muted)",
|
|
430
|
+
/*
|
|
431
|
+
* A tucked card is a corner peeking out from behind its parent,
|
|
432
|
+
* so its name gets one line. "Unavailability" wrapped to two and
|
|
433
|
+
* pushed the count out of a card that has no spare rows — and
|
|
434
|
+
* the alternative, growing the tuck until it fits, makes it the
|
|
435
|
+
* same size as the thing it is meant to be behind.
|
|
436
|
+
*/
|
|
437
|
+
...(nested
|
|
438
|
+
? { whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }
|
|
439
|
+
: { overflowWrap: "anywhere" }),
|
|
440
|
+
}, children: props.label ?? plural }));
|
|
441
|
+
return (_jsxs("div", { className: "graview-kind-card", "data-graview-rank": props.rank, "data-graview-landmark": figure || undefined, "data-graview-nested": nested || undefined, "data-graview-tied": tied || undefined, "data-graview-hits": found && !nested ? hits : undefined, "data-graview-emphasis": found && !nested ? (hits > 0 ? "lit" : "dimmed") : undefined, ...(definition?.description || nested
|
|
442
|
+
? {
|
|
443
|
+
title: nested
|
|
444
|
+
? [props.label ?? plural, definition?.description].filter(Boolean).join(" — ")
|
|
445
|
+
: definition.description,
|
|
446
|
+
}
|
|
447
|
+
: {}), style: {
|
|
448
|
+
position: "relative",
|
|
449
|
+
height: "100%",
|
|
450
|
+
["--graview-hue"]: Math.round(hue),
|
|
451
|
+
/*
|
|
452
|
+
* THE ROOM THE MARQUEE TAKES, told to the drawing under it. The
|
|
453
|
+
* building is sized from the card's height, and the layout grew
|
|
454
|
+
* the card by the marquee's band — so the building grew into the
|
|
455
|
+
* band and the showings stood on its roof. The band's height is
|
|
456
|
+
* the layout's own estimate, and the building sizes from what is
|
|
457
|
+
* left.
|
|
458
|
+
*/
|
|
459
|
+
["--graview-marquee-room"]: showings.length > 0 ? `${marqueeHeightFor(showings.map((place) => place.title), 132) + (figure ? 4 : 42)}px` : "0px",
|
|
460
|
+
}, children: [figure ? (_jsx("span", { className: "graview-kind-block graview-kind-landmark", "data-graview-opened": props.opened || undefined, "aria-hidden": "true", style: { ["--graview-rise"]: rise }, children: _jsx(KindFigure, { kind: String(kind), schema: schema, ...(brand ? { brand } : {}), size: 96 }) })) : /*
|
|
461
|
+
* A KIND WITH NO DRAWING STANDS AS ITS VILLAGE. The anonymous iso
|
|
462
|
+
* block — one box for every kind, its population a height nobody
|
|
463
|
+
* read — is retired at altitude: the members stand as buildings
|
|
464
|
+
* on the plot the scene draws under this card, and the population
|
|
465
|
+
* is the size of the cluster.
|
|
466
|
+
*/
|
|
467
|
+
null, _jsxs("div", { className: "graview-kind-face", "data-graview-opened": props.opened || undefined, style: {
|
|
468
|
+
display: "flex",
|
|
469
|
+
flexDirection: "column",
|
|
470
|
+
gap: nested ? 1 : 3,
|
|
471
|
+
padding: nested ? "5px 7px" : "7px 10px",
|
|
472
|
+
borderRadius: "var(--graview-radius-sm, 9px)",
|
|
473
|
+
/*
|
|
474
|
+
* PLANE 2 IS A GLYPH. Its own fidelity says so.
|
|
475
|
+
*
|
|
476
|
+
* These were 190 by 107 holding a name, a number and a mark, and
|
|
477
|
+
* the band they sit in took a fifth of the window. A card sized
|
|
478
|
+
* for content it does not have is not a map, it is a margin.
|
|
479
|
+
*/
|
|
480
|
+
justifyContent: "center",
|
|
481
|
+
position: "relative",
|
|
482
|
+
height: "100%",
|
|
483
|
+
overflow: "hidden",
|
|
484
|
+
boxSizing: "border-box",
|
|
485
|
+
border: accent
|
|
486
|
+
? "1px solid var(--graview-accent)"
|
|
487
|
+
: tied > 0
|
|
488
|
+
? "1px solid var(--graview-accent-dim)"
|
|
489
|
+
: `1px solid hsl(${Math.round(hue)} 55% var(--graview-tint-lightness) / 0.34)`,
|
|
490
|
+
// The kind you are looking at is brighter and lit, not labelled:
|
|
491
|
+
// a ninety-pixel card has no room for a word that says so.
|
|
492
|
+
/*
|
|
493
|
+
* An OPAQUE face under the tint. A card that was only a wash of
|
|
494
|
+
* colour let every connector show through it — a road running
|
|
495
|
+
* through a building rather than behind it — and in daylight the
|
|
496
|
+
* wash alone read as a pastel sticky note.
|
|
497
|
+
*/
|
|
498
|
+
backgroundColor: "var(--graview-panel)",
|
|
499
|
+
backgroundImage: `linear-gradient(hsl(${Math.round(hue)} 55% var(--graview-tint-lightness) / calc(var(--graview-tint-alpha) * ${accent ? 1.1 : secondary ? 0.28 : 0.5})), hsl(${Math.round(hue)} 55% var(--graview-tint-lightness) / calc(var(--graview-tint-alpha) * ${accent ? 1.1 : secondary ? 0.28 : 0.5})))`,
|
|
500
|
+
boxShadow: accent ? "0 0 0 1px var(--graview-accent-dim)" : undefined,
|
|
501
|
+
}, children: [!nested && members.length > 0 ? (_jsx("div", { "data-graview-tally": members.length, "data-graview-broken": broken || undefined, "aria-hidden": "true", style: {
|
|
502
|
+
// On the card's own edge, out of the flow: a glyph this size
|
|
503
|
+
// has no spare rows, and taking one made the content overflow.
|
|
504
|
+
position: "absolute",
|
|
505
|
+
left: 0,
|
|
506
|
+
right: 0,
|
|
507
|
+
top: 0,
|
|
508
|
+
display: "flex",
|
|
509
|
+
height: 3,
|
|
510
|
+
background: `hsl(${Math.round(hue)} 55% var(--graview-tint-lightness) / 0.4)`,
|
|
511
|
+
}, children: broken > 0 ? (_jsx("span", { style: {
|
|
512
|
+
width: `${Math.max(6, Math.round((broken / members.length) * 100))}%`,
|
|
513
|
+
background: "var(--graview-warn)",
|
|
514
|
+
} })) : null })) : null, figure ? (_jsxs("div", { style: { display: "flex", alignItems: "flex-start", gap: nested ? 4 : 6 }, children: [_jsx(KindFigure, { kind: String(kind), schema: schema, ...(brand ? { brand } : {}),
|
|
515
|
+
/*
|
|
516
|
+
* The size the LINE can carry, not the size the card looks
|
|
517
|
+
* like it could: the name beside it sets the row's height, and
|
|
518
|
+
* a drawing taller than that grows the row and puts the card's
|
|
519
|
+
* content over its own edge again — which is what twenty-six
|
|
520
|
+
* did on the shortest card in the strip.
|
|
521
|
+
*/
|
|
522
|
+
size: nested ? 14 : 18 }), name] })) : (name), _jsxs("div", { style: { display: "flex", alignItems: "center", gap: 5, fontSize: "0.875rem" }, children: [_jsx("span", { style: {
|
|
523
|
+
fontVariantNumeric: "tabular-nums",
|
|
524
|
+
color: trouble ? "var(--graview-warn)" : "var(--graview-ink-faint)",
|
|
525
|
+
}, children: members.length === 0 ? "none yet" : `${trouble ? `${flag} ` : ""}${members.length}` }), (props.retired ?? 0) > 0 ? (_jsxs("button", { type: "button", className: "graview-kind-past", "data-graview-retired": props.retired, title: `${props.retired} retired ${plural.toLowerCase()} — press to widen the view to the past`, onClick: (event) => {
|
|
526
|
+
event.stopPropagation();
|
|
527
|
+
go(withPast(view, true));
|
|
528
|
+
}, onDoubleClick: (event) => event.stopPropagation(), onPointerDown: (event) => event.stopPropagation(), children: ["+", props.retired, " past"] })) : null, hits > 0 ? (_jsxs("button", { type: "button", className: "graview-kind-hits", "data-testid": `hits-${String(kind)}`, title: `${hits} ${hits === 1 ? "match" : "matches"} for “${view.q ?? ""}” — press to go in, narrowed`, onClick: (event) => {
|
|
529
|
+
event.stopPropagation();
|
|
530
|
+
// The same descent a double-click on the lit district makes: one transition, not three.
|
|
531
|
+
go(withJackIn(view, kindCardId(String(kind)), { ownPicture: true, ...(view.q ? { carry: view.q } : {}) }));
|
|
532
|
+
}, onDoubleClick: (event) => event.stopPropagation(), onPointerDown: (event) => event.stopPropagation(), children: [hits, " match"] })) : null, tied > 0 ? (_jsxs("span", { style: { fontSize: "0.8125rem", color: "var(--graview-accent)" }, children: [tied, " tied"] })) : null, props.hasOwnView ? (_jsx("span", { title: `${plural} has a view of its own`, "aria-label": "has its own view", style: { fontSize: "0.6875rem", color: "var(--graview-accent)" }, children: "\u25C6" })) : null, !nested && members.length > 0 && !props.focused ? (_jsxs("button", { type: "button", className: "graview-kind-open", "data-testid": `open-${String(kind)}`, "aria-expanded": props.opened ?? false, title: props.opened ? "Close it up" : `See the ${plural} in here`, onClick: (event) => {
|
|
533
|
+
event.stopPropagation();
|
|
534
|
+
toggle(kindCardId(String(kind)));
|
|
535
|
+
}, onDoubleClick: (event) => event.stopPropagation(), onPointerDown: (event) => event.stopPropagation(), children: [_jsx("span", { className: "graview-kind-open-word", children: props.opened ? "close" : "open" }), " ", props.opened ? "▴" : "▾"] })) : null] }), props.opened ? (
|
|
536
|
+
/*
|
|
537
|
+
* BUILDINGS. An opened neighbourhood lays its members out inside
|
|
538
|
+
* its plot as a small grid on the lattice — `side` to a row, the
|
|
539
|
+
* plot's own width in cells — rather than a column of chips. Each
|
|
540
|
+
* is still a Chip with a pick id, so measuring, ties and the
|
|
541
|
+
* keyboard work for free; past `side × side` the rest are counted.
|
|
542
|
+
*/
|
|
543
|
+
_jsxs("div", { className: "graview-kind-members", "data-graview-buildings": props.plot?.side ?? 1, style: {
|
|
544
|
+
display: "grid",
|
|
545
|
+
/*
|
|
546
|
+
* A NAME YOU CAN READ BEATS A GRID THAT MATCHES THE PLOT.
|
|
547
|
+
*
|
|
548
|
+
* The members were laid out `side` to a row so the chips
|
|
549
|
+
* echoed the buildings on the lattice — which at a district's
|
|
550
|
+
* own width meant sixty pixels a chip, and "Enough bodies for
|
|
551
|
+
* the drill" arrived as "Eno…". Opening a district is the
|
|
552
|
+
* gesture that asks WHICH ONE; a row of initials cannot
|
|
553
|
+
* answer it. So the columns are set by what the widest name
|
|
554
|
+
* needs, and a district only wide enough for one keeps one.
|
|
555
|
+
*/
|
|
556
|
+
gridTemplateColumns: `repeat(${roster.columns}, minmax(0, 1fr))`,
|
|
557
|
+
gap: 3,
|
|
558
|
+
}, children: [members.slice(0, roster.shown).map((member) => {
|
|
559
|
+
/*
|
|
560
|
+
* WHICH ONE. The card's own count already says "⚠ 1" —
|
|
561
|
+
* opening the district to find out which member that is was
|
|
562
|
+
* the whole point of opening it, and every chip came out
|
|
563
|
+
* unmarked. The glyph view marks a flagged node exactly this
|
|
564
|
+
* way; the members inside a district simply never asked.
|
|
565
|
+
*/
|
|
566
|
+
const broken = props.flagged?.includes(member.id) ?? false;
|
|
567
|
+
const name = labelOf(definition, member);
|
|
568
|
+
return (_jsx(Chip, { pickId: member.id, label: broken ? `${name} ${flag}` : name, title: broken ? `${name} — implicated in a problem` : undefined, hue: hue, selected: chosen.has(member.id) }, member.id));
|
|
569
|
+
}), members.length > roster.shown ? (_jsxs("span", { style: { fontSize: "0.75rem", color: "var(--graview-ink-faint)", padding: "2px 4px", gridColumn: "1 / -1" }, children: ["+", members.length - roster.shown, " more \u2014 double-click to go in"] })) : null] })) : null] }), !nested && showings.length > 0 ? (_jsx("div", { className: "graview-drive-in", "data-testid": `drive-in-${String(kind)}`, "data-graview-showing": showingNow, onPointerDown: (event) => event.stopPropagation(), onDoubleClick: (event) => event.stopPropagation(), children: _jsx("div", { className: "graview-drive-in-marquee", role: "group", "aria-label": `${plural}: pictures`, "data-graview-thumbs": showings.length === 1 ? "one" : "two", children: showings.map((place) => {
|
|
570
|
+
const showing = showingNow === place.as;
|
|
571
|
+
const registration = views.resolve(String(kind), { cardinality: "many", fidelity: "full" }, place.as);
|
|
572
|
+
const Lens = registration?.view;
|
|
573
|
+
return (
|
|
574
|
+
/*
|
|
575
|
+
* A FRAME HOLDING A PICTURE AND A BUTTON, not a button
|
|
576
|
+
* holding a picture. The lens drawn small keeps its own
|
|
577
|
+
* controls — a calendar's Previous and Next — and a
|
|
578
|
+
* button inside a button is invalid HTML however inert
|
|
579
|
+
* the inner one is: React said so on every chapter
|
|
580
|
+
* with a calendar. The press lies over the picture, so
|
|
581
|
+
* the whole thumbnail is still one target.
|
|
582
|
+
*/
|
|
583
|
+
_jsxs("div", { className: "graview-drive-in-thumb", "data-graview-pressed": showing || undefined, children: [_jsx("span", { className: "graview-drive-in-thumb-picture", "aria-hidden": "true", inert: true, children: Lens ? (_jsx("span", { className: "graview-drive-in-thumb-natural", children: _jsx(WhenSeen, { children: _jsx(Picture, { lens: Lens, nodes: thumbnailMembers, label: place.title, budget: THUMBNAIL_BUDGET, total: members.length, ...(props.flagged ? { flagged: props.flagged } : {}) }) }) })) : null }), _jsx("span", { className: "graview-drive-in-thumb-title", "aria-hidden": "true", children: place.title }), _jsx("button", { type: "button", className: "graview-drive-in-thumb-press", "data-testid": `showing-${place.as}`, "aria-label": `${plural}: ${place.title}`, "aria-pressed": showing, title: showing ? `${place.title} is showing` : `Show ${place.title} on the billboard`, onClick: (event) => {
|
|
584
|
+
event.stopPropagation();
|
|
585
|
+
if (showing && props.focused)
|
|
586
|
+
return;
|
|
587
|
+
go(withWithin(withFocus(withOverview(view, true), aggregateId(String(kind))), "view", place.as));
|
|
588
|
+
} })] }, place.as));
|
|
589
|
+
}) }) })) : null] }));
|
|
590
|
+
};
|
|
591
|
+
// Marked so the interface can tell a view the app chose from one the
|
|
592
|
+
// framework fell back to.
|
|
593
|
+
Group.generic = true;
|
|
594
|
+
GroupGlyph.generic = true;
|
|
595
|
+
registry
|
|
596
|
+
.register(kind, { cardinality: "one", fidelity: "full" }, Full)
|
|
597
|
+
.register(kind, { cardinality: "one", fidelity: "summary" }, Summary)
|
|
598
|
+
.register(kind, { cardinality: "one", fidelity: "glyph" }, Glyph)
|
|
599
|
+
.register(kind, { cardinality: "many", fidelity: "full" }, markDefaultView(Group))
|
|
600
|
+
.register(kind, { cardinality: "many", fidelity: "summary" }, markDefaultView(Group))
|
|
601
|
+
.register(kind, { cardinality: "many", fidelity: "glyph" }, markDefaultView(GroupGlyph));
|
|
602
|
+
}
|
|
603
|
+
return registry;
|
|
604
|
+
}
|
|
605
|
+
//# sourceMappingURL=default-views.js.map
|