@smartmemory/wiki-components 0.4.0 → 0.6.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/README.md +23 -3
- package/package.json +26 -4
- package/src/Ledger.jsx +47 -0
- package/src/data-client.d.ts +40 -1
- package/src/data-client.jsx +5 -0
- package/src/graph/components/wiki/DocCard.jsx +46 -0
- package/src/graph/components/wiki/GraphBreadcrumb.jsx +23 -0
- package/src/graph/components/wiki/GraphCanvas.jsx +663 -0
- package/src/graph/components/wiki/GraphCanvas3D.jsx +1366 -0
- package/src/graph/components/wiki/GraphDocPane.jsx +56 -0
- package/src/graph/components/wiki/GraphLegend.jsx +119 -0
- package/src/graph/components/wiki/GraphLevelPicker.jsx +35 -0
- package/src/graph/components/wiki/graph/FilterChips.jsx +45 -0
- package/src/graph/components/wiki/graph/Omnibox.jsx +74 -0
- package/src/graph/components/wiki/graph/badges.js +46 -0
- package/src/graph/components/wiki/graph/bubbles3d.js +95 -0
- package/src/graph/components/wiki/graph/canvasBase.js +138 -0
- package/src/graph/components/wiki/graph/containment.js +49 -0
- package/src/graph/components/wiki/graph/overlay.js +134 -0
- package/src/graph/components/wiki/graph/pixiBase.js +210 -0
- package/src/graph/components/wiki/graph/threeBase.js +231 -0
- package/src/graph/hooks/useDebounced.js +11 -0
- package/src/graph/host.jsx +56 -0
- package/src/graph/index.d.ts +26 -0
- package/src/graph/index.js +4 -0
- package/src/graph/lib/areaFamilies.js +32 -0
- package/src/graph/lib/camFollow.js +30 -0
- package/src/graph/lib/canvasPort.js +93 -0
- package/src/graph/lib/cardVisibility.js +58 -0
- package/src/graph/lib/docCard.js +28 -0
- package/src/graph/lib/forceWorker.js +123 -0
- package/src/graph/lib/forceWorker3d.js +136 -0
- package/src/graph/lib/graph3d.js +330 -0
- package/src/graph/lib/graphArrows.js +108 -0
- package/src/graph/lib/graphExplore.js +546 -0
- package/src/graph/lib/graphFind.js +46 -0
- package/src/graph/lib/graphLasso.js +44 -0
- package/src/graph/lib/graphLevels.js +367 -0
- package/src/graph/lib/graphOrient.js +145 -0
- package/src/graph/lib/graphPath.js +86 -0
- package/src/graph/lib/graphPin.js +48 -0
- package/src/graph/lib/graphPress.js +27 -0
- package/src/graph/lib/graphShell.js +296 -0
- package/src/graph/lib/graphTheme.js +50 -0
- package/src/graph/lib/graphView.js +343 -0
- package/src/graph/lib/graphViews.js +103 -0
- package/src/graph/lib/groupBubbles.js +128 -0
- package/src/graph/lib/hubLines.js +51 -0
- package/src/graph/lib/idleTurn.js +31 -0
- package/src/graph/lib/labelDepth.js +59 -0
- package/src/graph/lib/layoutClient.js +69 -0
- package/src/graph/lib/layoutEngine.js +184 -0
- package/src/graph/lib/linkWeight.js +67 -0
- package/src/graph/lib/orphanPull.js +10 -0
- package/src/graph/lib/perf.js +14 -0
- package/src/graph/lib/reducedMotion.js +36 -0
- package/src/graph/lib/robustFit.js +62 -0
- package/src/graph/lib/wikiGraph.js +298 -0
- package/src/graph/pages/GraphPage.jsx +703 -0
- package/src/index.d.ts +11 -2
- package/src/index.js +4 -1
- package/src/lib.js +3 -0
- package/src/parts/LedgerTimeline.jsx +20 -0
- package/src/parts/Markdown.jsx +22 -0
package/README.md
CHANGED
|
@@ -26,7 +26,7 @@ npm install @smartmemory/wiki-components react react-dom date-fns lucide-react
|
|
|
26
26
|
|
|
27
27
|
## Contract
|
|
28
28
|
|
|
29
|
-
`src/data-client.d.ts` is the contract:
|
|
29
|
+
`src/data-client.d.ts` is the contract: twelve calls (`identity`, `featureHub`, `decisions`, `decisionsPage`, `sourcesPage`, `docsByArea`, `ledger`, `upcomingCalls`, `search`, `graphAll`, `graphPage`, `note`), the endpoint and
|
|
30
30
|
request body each maps to on the wiki API, and the result types (from `wiki/contracts/query.schema.json`). The same list is
|
|
31
31
|
runtime data in `DATA_CLIENT_CALLS`; `ui/test/wiki-components.test.jsx` pins the typed file, the table and the app's client
|
|
32
32
|
to each other. `<WikiDataProvider>` refuses a client missing a method.
|
|
@@ -34,7 +34,8 @@ to each other. `<WikiDataProvider>` refuses a client missing a method.
|
|
|
34
34
|
## Use
|
|
35
35
|
|
|
36
36
|
```jsx
|
|
37
|
-
import { WikiDataProvider, FeatureHub, Search, Decisions, Sources, AreaDocs, wikiPath } from "@smartmemory/wiki-components";
|
|
37
|
+
import { WikiDataProvider, FeatureHub, Search, Decisions, Sources, AreaDocs, Ledger, wikiPath } from "@smartmemory/wiki-components";
|
|
38
|
+
import { GraphHost, GraphPage } from "@smartmemory/wiki-components/graph";
|
|
38
39
|
|
|
39
40
|
<WikiDataProvider client={myClient}>
|
|
40
41
|
<FeatureHub featureId="CORE-1" workspace="wiki-eng" />
|
|
@@ -44,6 +45,8 @@ import { WikiDataProvider, FeatureHub, Search, Decisions, Sources, AreaDocs, wik
|
|
|
44
45
|
<Decisions workspace="wiki-eng" />
|
|
45
46
|
<Sources workspace="wiki-eng" />
|
|
46
47
|
<AreaDocs area="search" workspace="wiki-eng" onNavigate={go} hrefFor={href} />
|
|
48
|
+
<Ledger workspace="wiki-eng" />
|
|
49
|
+
<GraphHost value={{ workspace: "wiki-eng", useSearchParams, Link, useColorMode, aspectHint }}><GraphPage /></GraphHost>
|
|
47
50
|
</WikiDataProvider>
|
|
48
51
|
```
|
|
49
52
|
|
|
@@ -61,6 +64,23 @@ import { WikiDataProvider, FeatureHub, Search, Decisions, Sources, AreaDocs, wik
|
|
|
61
64
|
`client.docsByArea({workspace, area, cursor?, size?})` (`POST /query/docs_by_area`, `filters: {area}`, `page: {size, cursor?}`),
|
|
62
65
|
which resolves `{items, total, next_cursor, generated_at}` with `DocItem` rows. With `onNavigate` or `hrefFor`, each doc
|
|
63
66
|
lists its areas as links, the new NavTarget `{kind: "area", area}` (`wikiPath` gives `/area/<area>`; `areaUrl` is exported).
|
|
67
|
+
- `Ledger` is the last 30 days of the workspace's ledger, newest first, 50 a page, plus an "Upcoming calls" section when
|
|
68
|
+
the host serves that feed. Its data is `client.ledger({workspace, from, to, cursor?, size?})` (`POST /query/ledger`,
|
|
69
|
+
`filters: {from, to}`, ISO instants with an offset; the sort is the host's, `ts` newest first, a screen never sends it),
|
|
70
|
+
which resolves `{items, total, next_cursor, generated_at}` with `EntryItem` rows, and `client.upcomingCalls({workspace})`
|
|
71
|
+
(`GET /feeds/upcoming_calls`), which resolves `{items, errors}`, or `null` when the host does not serve the feed (the
|
|
72
|
+
section is then left out; an empty feed still shows "No upcoming calls."). The window is 30 days back to one day ahead of
|
|
73
|
+
`now`, an optional prop (ms since the epoch) for tests, else the first render's time. Also exported: `LedgerTimeline`.
|
|
74
|
+
- `Graph` (its own entry, `@smartmemory/wiki-components/graph`, so a host that never draws it does not load it) is the wiki's
|
|
75
|
+
Graph screen as it ran in the `/ui/` app, moved with its bodies unchanged (COMPOSE-WIKI-GRAPH-1a): 2D, the WebGL 2D base and the
|
|
76
|
+
lazy 3D view, filters, paths, saved views and the doc pane. It takes everything the app used to give it through
|
|
77
|
+
`<GraphHost value={{ workspace, useSearchParams, Link, useColorMode, requestTreeSync?, aspectHint }}>`: the view state lives in
|
|
78
|
+
the URL, so the host supplies `useSearchParams` (react-router's, or any hook returning `[URLSearchParams, setter]`), a `Link`
|
|
79
|
+
taking `to`, the design system's `useColorMode`, and the words for the greyed aspect options. Its data is
|
|
80
|
+
`client.graphAll({workspace})` (`POST /query/graph`, `page: {all: true}`; a host without the mode rejects with `code`
|
|
81
|
+
`"invalid_request"` and the screen pages with `client.graphPage({workspace, cursor?, size?})`) and, for the doc pane,
|
|
82
|
+
`client.note({workspace, id})` (`GET /note?workspace=&id=`). Exports: `GraphPage`, `GraphHost`, `GRAPH_ASPECTS`, `wholeGraph`,
|
|
83
|
+
`resetWebGL2Probe`. The package root also exports `MD` and `MD_PLUGINS` (note text rendering) and `INPUT`.
|
|
64
84
|
- `workspace` / `workspaces` are optional: without them a screen calls `client.identity()` and uses the caller's grants
|
|
65
85
|
(FeatureHub the first one).
|
|
66
86
|
- `onNavigate(target)` receives a plain left click on a link and the search box's submit. `hrefFor(target)` is each link's
|
|
@@ -68,7 +88,7 @@ import { WikiDataProvider, FeatureHub, Search, Decisions, Sources, AreaDocs, wik
|
|
|
68
88
|
|
|
69
89
|
## Host requirements
|
|
70
90
|
|
|
71
|
-
- **Peers:** `react` and `react-dom` `>=18 <20`, `date-fns` 3, `lucide-react
|
|
91
|
+
- **Peers:** `react` and `react-dom` `>=18 <20`, `date-fns` 3, `lucide-react`; for the Graph entry also `d3-force` 3, `d3-quadtree` 3, `react-markdown` 9 or 10 and `remark-gfm` 4 (the root entry needs `remark-gfm` for `MD_PLUGINS`), and the optional `pixi.js` 8 (WebGL 2D), `three` and `d3-force-3d` (3D). The package ships JSX source (no build step);
|
|
72
92
|
a Vite host transforms it (installed from npm or linked). Consumed as `file:` dependency, its bare imports resolve from its real path, so a host outside
|
|
73
93
|
this repo must list `react`, `react-dom` (and `lucide-react`, `date-fns`) in `resolve.dedupe` or it loads the wiki's
|
|
74
94
|
React 18 copy next to its own.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smartmemory/wiki-components",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "The wiki's feature hub
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"description": "The wiki's screens (feature hub, search, decisions, sources, docs, ledger, graph) as router-free React components fed by an injected DataClient (WIKI-UI-COMPONENTS-1). Ships JSX source for a bundler host (Vite).",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Regression Analytics LLC",
|
|
7
7
|
"repository": {
|
|
@@ -20,6 +20,10 @@
|
|
|
20
20
|
"types": "./src/index.d.ts",
|
|
21
21
|
"default": "./src/index.js"
|
|
22
22
|
},
|
|
23
|
+
"./graph": {
|
|
24
|
+
"types": "./src/graph/index.d.ts",
|
|
25
|
+
"default": "./src/graph/index.js"
|
|
26
|
+
},
|
|
23
27
|
"./data-client": {
|
|
24
28
|
"types": "./src/data-client.d.ts"
|
|
25
29
|
},
|
|
@@ -34,7 +38,25 @@
|
|
|
34
38
|
"react": ">=18 <20",
|
|
35
39
|
"react-dom": ">=18 <20",
|
|
36
40
|
"date-fns": "^3.6.0",
|
|
37
|
-
"lucide-react": ">=0.475.0"
|
|
41
|
+
"lucide-react": ">=0.475.0",
|
|
42
|
+
"d3-force": "^3.0.0",
|
|
43
|
+
"d3-quadtree": "^3.0.1",
|
|
44
|
+
"react-markdown": ">=9 <11",
|
|
45
|
+
"remark-gfm": "^4.0.1",
|
|
46
|
+
"pixi.js": "^8.22.0",
|
|
47
|
+
"three": "0.186.1",
|
|
48
|
+
"d3-force-3d": "3.0.6"
|
|
38
49
|
},
|
|
39
|
-
"sideEffects": false
|
|
50
|
+
"sideEffects": false,
|
|
51
|
+
"peerDependenciesMeta": {
|
|
52
|
+
"pixi.js": {
|
|
53
|
+
"optional": true
|
|
54
|
+
},
|
|
55
|
+
"three": {
|
|
56
|
+
"optional": true
|
|
57
|
+
},
|
|
58
|
+
"d3-force-3d": {
|
|
59
|
+
"optional": true
|
|
60
|
+
}
|
|
61
|
+
}
|
|
40
62
|
}
|
package/src/Ledger.jsx
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import React, { useMemo } from "react";
|
|
2
|
+
import { useClientCall, useDataClient, useWorkspaces } from "./data-client";
|
|
3
|
+
import { usePagedCall } from "./paged";
|
|
4
|
+
import ListPage from "./parts/ListPage";
|
|
5
|
+
import PageHeader from "./parts/PageHeader";
|
|
6
|
+
import LedgerTimeline from "./parts/LedgerTimeline";
|
|
7
|
+
import EntryRows from "./parts/EntryRows";
|
|
8
|
+
import Section from "./parts/Section";
|
|
9
|
+
import Loading from "./parts/Loading";
|
|
10
|
+
import Pager from "./parts/Pager";
|
|
11
|
+
import ErrorBox from "./parts/ErrorBox";
|
|
12
|
+
|
|
13
|
+
const DAY_MS = 86_400_000;
|
|
14
|
+
const iso = (ms) => new Date(ms).toISOString().replace(/\.\d+Z$/, "Z");
|
|
15
|
+
|
|
16
|
+
/** The ledger timeline: the last 30 days of `workspace` (default: the first of identity().workspaces), newest first, 50 a
|
|
17
|
+
* page with a pager, and an "Upcoming calls" section when the host serves that feed. The window runs from 30 days before
|
|
18
|
+
* `now` to one day after it (ms since the epoch; default: the time of the first render, kept for the life of the screen so
|
|
19
|
+
* the query key stays stable). `onNavigate` / `hrefFor` are accepted for the host contract shared by the screens; the
|
|
20
|
+
* timeline has no links yet. Data: DataClient ledger (one call per page) and upcomingCalls (one call). */
|
|
21
|
+
export default function Ledger({ workspace: given, now }) {
|
|
22
|
+
const client = useDataClient();
|
|
23
|
+
const ws = useWorkspaces(given ? [given] : undefined);
|
|
24
|
+
const workspace = given || ws.workspaces?.[0];
|
|
25
|
+
const fixedNow = useMemo(() => Date.now(), []);
|
|
26
|
+
const at = now ?? fixedNow;
|
|
27
|
+
const range = useMemo(() => ({ from: iso(at - 30 * DAY_MS), to: iso(at + DAY_MS) }), [at]);
|
|
28
|
+
const p = usePagedCall(["ledger", workspace, range], (page) => client.ledger({ workspace, ...range, ...page }), { enabled: !!workspace });
|
|
29
|
+
const calls = useClientCall(["upcoming_calls", workspace], () => client.upcomingCalls({ workspace }), { enabled: !!workspace });
|
|
30
|
+
const error = p.error || ws.error;
|
|
31
|
+
return (
|
|
32
|
+
<ListPage header={<PageHeader title="Ledger">{p.total != null && `${p.total.toLocaleString()} entries in the last 30 days, newest first`}</PageHeader>} foot={<Pager p={p} />}>
|
|
33
|
+
<ErrorBox error={error} />
|
|
34
|
+
{p.isLoading || ws.isLoading ? <Loading /> : <LedgerTimeline entries={p.items} />}
|
|
35
|
+
{/* the calendar feed is served only on a host that wires it: elsewhere (null) there is no calls section */}
|
|
36
|
+
{!calls.isLoading && calls.data !== null && (calls.data || calls.error) && (
|
|
37
|
+
<Section title="Upcoming calls" className="mt-12">
|
|
38
|
+
{(calls.data?.errors || []).map((e) => (
|
|
39
|
+
<p key={e.code} role="status" className="mb-2 rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-sm text-amber-800">Calls are incomplete: {e.message} ({e.code})</p>
|
|
40
|
+
))}
|
|
41
|
+
<ErrorBox error={calls.error} />
|
|
42
|
+
{calls.data && <EntryRows entries={calls.data.items} empty="No upcoming calls." />}
|
|
43
|
+
</Section>
|
|
44
|
+
)}
|
|
45
|
+
</ListPage>
|
|
46
|
+
);
|
|
47
|
+
}
|
package/src/data-client.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// WIKI-UI-COMPONENTS-1: the DataClient contract. Every call the FeatureHub, Search, Decisions, Sources and
|
|
1
|
+
// WIKI-UI-COMPONENTS-1: the DataClient contract. Every call the FeatureHub, Search, Decisions, Sources, AreaDocs, Ledger and Graph screens make, and nothing else.
|
|
2
2
|
// A host (the wiki's /ui/ app, Compose's wiki tab) passes one object with these async methods to <WikiDataProvider>.
|
|
3
3
|
// The components never fetch: each call below is the whole of their I/O.
|
|
4
4
|
//
|
|
@@ -12,7 +12,12 @@
|
|
|
12
12
|
// decisionsPage POST /query/decisions {query: "decisions", workspace, filters: {}, page: {size: 50, cursor?}}
|
|
13
13
|
// sourcesPage POST /query/sources {query: "sources", workspace, filters: {}, page: {size: 50, cursor?}}
|
|
14
14
|
// docsByArea POST /query/docs_by_area {query: "docs_by_area", workspace, filters: {area}, page: {size: 50, cursor?}}
|
|
15
|
+
// ledger POST /query/ledger {query: "ledger", workspace, filters: {from, to}, page: {size: 50, cursor?}} (sort is the host's: ts, newest first)
|
|
16
|
+
// upcomingCalls GET /feeds/upcoming_calls?workspace= (none) -> data_envelope; an absent feed (the wiki answers 404 not_found) resolves to null
|
|
15
17
|
// search POST /query/search {query: "search", workspace, filters: {q}, page: {}}
|
|
18
|
+
// graphAll POST /query/graph {query: "graph", workspace, filters: {}, page: {all: true}} (one-shot; a host without it rejects with code invalid_request)
|
|
19
|
+
// graphPage POST /query/graph {query: "graph", workspace, filters: {}, page: {size, cursor?}} (the paged fallback)
|
|
20
|
+
// note GET /note?workspace=&id= (none) -> {workspace, note, generated_at} (the Graph's doc pane)
|
|
16
21
|
//
|
|
17
22
|
// The same table is runtime data in data-client.jsx (DATA_CLIENT_CALLS); a test pins the two to each other.
|
|
18
23
|
// A rejected call should reject with an Error carrying `code` (the query_error code) so the screens can show it.
|
|
@@ -105,11 +110,34 @@ export interface SourceItem {
|
|
|
105
110
|
overdue: boolean | null;
|
|
106
111
|
}
|
|
107
112
|
|
|
113
|
+
/** The upcoming_calls feed (component.schema.json data_envelope): `items` are entries; `errors` say what a partial answer is missing. */
|
|
114
|
+
export interface UpcomingCalls {
|
|
115
|
+
items: EntryItem[];
|
|
116
|
+
errors: { code: string; message?: string }[];
|
|
117
|
+
}
|
|
118
|
+
|
|
108
119
|
/** x-named-queries.feature_hub.result: the feature's docs plus the feature itself (null when it is not on the roadmap). */
|
|
109
120
|
export interface FeatureHubResult extends QueryResult<DocItem> {
|
|
110
121
|
feature: FeatureItem | null;
|
|
111
122
|
}
|
|
112
123
|
|
|
124
|
+
/** The graph query result: every doc of the workspace (`items`) and the links between them (`edges`). `revision`/`layout` come on the one-shot answer, `next_cursor` on a page. */
|
|
125
|
+
export interface GraphResult {
|
|
126
|
+
items: Record<string, unknown>[];
|
|
127
|
+
edges?: Record<string, unknown>[];
|
|
128
|
+
total?: number;
|
|
129
|
+
revision?: string;
|
|
130
|
+
layout?: unknown;
|
|
131
|
+
next_cursor?: string | null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** GET /note: one note with its text. */
|
|
135
|
+
export interface NoteResult {
|
|
136
|
+
workspace: Workspace;
|
|
137
|
+
note: { title?: string; type?: string; text?: string; [field: string]: unknown };
|
|
138
|
+
generated_at: string;
|
|
139
|
+
}
|
|
140
|
+
|
|
113
141
|
/** x-named-queries.search.result (a grouped_result). */
|
|
114
142
|
export interface SearchResult {
|
|
115
143
|
groups: { label: string; items: SearchHit[]; total: number; next_cursor: string | null }[];
|
|
@@ -132,8 +160,19 @@ export interface DataClient {
|
|
|
132
160
|
/** POST /query/docs_by_area, filters {area} (required, 1 to 200 characters), page {size (default 50), cursor?}: one page of the live docs
|
|
133
161
|
* filed under `area`. `cursor` is the previous page's `next_cursor`, opaque. */
|
|
134
162
|
docsByArea(args: { workspace: Workspace; area: string; cursor?: string; size?: number }): Promise<QueryResult<DocItem>>;
|
|
163
|
+
/** POST /query/ledger, filters {from, to} (both required: ISO instants with an offset, `from` <= `to`, at most 366 days apart), page {size (default 50), cursor?}:
|
|
164
|
+
* one page of the ledger entries in the window, newest first (the sort is fixed by the host, never sent by a screen). `cursor` is the previous page's `next_cursor`, opaque. */
|
|
165
|
+
ledger(args: { workspace: Workspace; from: string; to: string; cursor?: string; size?: number }): Promise<QueryResult<EntryItem>>;
|
|
166
|
+
/** GET /feeds/upcoming_calls: the calendar feed's items, or null when this host does not serve the feed (not an error, not an empty list). */
|
|
167
|
+
upcomingCalls(args: { workspace: Workspace }): Promise<UpcomingCalls | null>;
|
|
135
168
|
/** POST /query/search, filters {q} (1 to 256 characters), page {}. One call per workspace searched. */
|
|
136
169
|
search(args: { workspace: Workspace; q: string }): Promise<SearchResult>;
|
|
170
|
+
/** POST /query/graph, filters {}, page {all: true}: the whole workspace graph in one request. A host without the mode rejects with an Error whose `code` is "invalid_request" (the screen then pages with graphPage). */
|
|
171
|
+
graphAll(args: { workspace: Workspace }): Promise<GraphResult>;
|
|
172
|
+
/** POST /query/graph, filters {}, page {size, cursor?}: one page of the graph. `cursor` is the previous page's `next_cursor`, opaque. */
|
|
173
|
+
graphPage(args: { workspace: Workspace; cursor?: string; size?: number }): Promise<GraphResult>;
|
|
174
|
+
/** GET /note?workspace=&id=: one note with its text (the Graph's doc pane). */
|
|
175
|
+
note(args: { workspace: Workspace; id: string }): Promise<NoteResult>;
|
|
137
176
|
}
|
|
138
177
|
|
|
139
178
|
/** Where a screen asks the host to go (Search only; FeatureHub has no links). The host maps it to its own route
|
package/src/data-client.jsx
CHANGED
|
@@ -9,7 +9,12 @@ export const DATA_CLIENT_CALLS = [
|
|
|
9
9
|
{ method: "decisionsPage", endpoint: "POST /query/decisions", query: "decisions", page: { size: 50 } },
|
|
10
10
|
{ method: "sourcesPage", endpoint: "POST /query/sources", query: "sources", page: { size: 50 } },
|
|
11
11
|
{ method: "docsByArea", endpoint: "POST /query/docs_by_area", query: "docs_by_area", page: { size: 50 } },
|
|
12
|
+
{ method: "ledger", endpoint: "POST /query/ledger", query: "ledger", page: { size: 50 } },
|
|
13
|
+
{ method: "upcomingCalls", endpoint: "GET /feeds/upcoming_calls" },
|
|
12
14
|
{ method: "search", endpoint: "POST /query/search", query: "search", page: {} },
|
|
15
|
+
{ method: "graphAll", endpoint: "POST /query/graph", query: "graph", page: { all: true } },
|
|
16
|
+
{ method: "graphPage", endpoint: "POST /query/graph", query: "graph", page: { size: 200 } },
|
|
17
|
+
{ method: "note", endpoint: "GET /note" },
|
|
13
18
|
];
|
|
14
19
|
|
|
15
20
|
const Ctx = createContext(null);
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import React, { useEffect, useRef } from "react";
|
|
2
|
+
import { Link } from "../../host";
|
|
3
|
+
import { docCardAt } from "../../lib/docCard";
|
|
4
|
+
|
|
5
|
+
/** The focused doc's card, drawn at its node in the 3D view (WIKI-GRAPH-EXPLORE-1b-SHELL B1). Each frame it asks the canvas for the
|
|
6
|
+
* node's `screenPositions` entry and moves itself with a style write (no React render per frame); it hides while the node is off-screen
|
|
7
|
+
* or behind the camera. Pin is left out as filed. `anchor` is the drawn id the card follows (docCard cardAnchor: the doc, or the collapsed folder drawn for it; null = hidden), `canvas` is the GraphCanvas3D ref, `to` the Open note link, `onPathTo(doc)` the Path to… action (round 3: starts the path lens from this doc).
|
|
8
|
+
* `shown` false fades it out (no fade under reduced motion) and makes it inert; `onHold` / `onRelease` fire as the pointer enters / leaves it and `onFocusIn` / `onFocusOut` as keyboard focus does (the auto-hide waits). */
|
|
9
|
+
export default function DocCard({ canvas, focus, anchor = focus?.id ?? null, to, onPathTo, shown = true, reduced = false, onHold, onRelease, onFocusIn, onFocusOut }) {
|
|
10
|
+
const box = useRef(null);
|
|
11
|
+
const id = anchor;
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
const el = box.current;
|
|
14
|
+
if (!el) return undefined;
|
|
15
|
+
// Every imperative style the RAF writes is reset here first, so a same-mount change of anchor (including to null) never leaves the
|
|
16
|
+
// previous anchor's visibility or position on the element (React sees the same declarative style and would not reset it).
|
|
17
|
+
el.style.visibility = "hidden"; el.style.left = ""; el.style.top = "";
|
|
18
|
+
if (!id) return undefined;
|
|
19
|
+
let raf = 0, last = "";
|
|
20
|
+
const tick = () => {
|
|
21
|
+
raf = requestAnimationFrame(tick);
|
|
22
|
+
const parent = el.offsetParent || el.parentElement;
|
|
23
|
+
const w = parent?.clientWidth || 0, h = parent?.clientHeight || 0;
|
|
24
|
+
const p = canvas.current?.screenPositions?.([id])?.[0];
|
|
25
|
+
const at = docCardAt(p, el.offsetWidth, el.offsetHeight, w, h);
|
|
26
|
+
const key = at ? `${Math.round(at.left)},${Math.round(at.top)}` : "";
|
|
27
|
+
if (key === last) return;
|
|
28
|
+
last = key;
|
|
29
|
+
el.style.visibility = at ? "visible" : "hidden";
|
|
30
|
+
if (at) { el.style.left = `${at.left}px`; el.style.top = `${at.top}px`; }
|
|
31
|
+
};
|
|
32
|
+
raf = requestAnimationFrame(tick);
|
|
33
|
+
return () => cancelAnimationFrame(raf);
|
|
34
|
+
}, [canvas, id]);
|
|
35
|
+
if (!focus) return null;
|
|
36
|
+
return (
|
|
37
|
+
<div ref={box} data-testid="graph-doc-card" data-shown={shown ? "true" : "false"} style={{ visibility: "hidden", opacity: shown ? 1 : 0, transition: reduced ? "none" : "opacity 300ms" }}
|
|
38
|
+
inert={shown ? undefined : ""} onPointerEnter={onHold} onPointerLeave={onRelease} onFocus={onFocusIn} onBlur={onFocusOut}
|
|
39
|
+
className={`${shown ? "pointer-events-auto" : "pointer-events-none"} absolute left-0 top-0 z-10 w-64 max-w-[70%] rounded-lg border border-neutral-200/70 bg-ground/90 p-3 shadow-sm backdrop-blur`}>
|
|
40
|
+
<h2 className="text-base font-semibold text-ink">{focus.title}</h2>
|
|
41
|
+
<p className="mt-1 break-all font-mono text-xs text-neutral-500">{focus.folder || "(root)"}</p>
|
|
42
|
+
{focus.area && <p className="mt-1 text-xs text-ink2">area {focus.area}</p>}
|
|
43
|
+
<div className="mt-2 text-[13px]"><Link to={to} className="text-blue-600 hover:underline">Open note</Link>{onPathTo && <button type="button" onClick={() => onPathTo(focus)} className="ml-3 text-blue-600 hover:underline">Path to…</button>}</div>
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
/** WIKI-GRAPH-LEVELS-1 c2: where the level view is. `crumbs` are the open folders, top-level first (`breadcrumb()` of the
|
|
4
|
+
* deepest one); "All" is always first. `onSelect(path)` collapses to that folder, `onSelect(null)` back to level 0. The
|
|
5
|
+
* current (last) entry is text, not a button. `all={false}` (the page has an auto-expanded base,
|
|
6
|
+
* WIKI-GRAPH-LEVELS-AUTOEXPAND-1) drops "All": the trail starts at the base, and `onSelect` is told the clicked path. A separate component so the later command palette can absorb it. */
|
|
7
|
+
export default function GraphBreadcrumb({ crumbs, onSelect, all = true }) {
|
|
8
|
+
const last = crumbs.length - 1;
|
|
9
|
+
const BTN = "rounded px-1 text-[13px] text-blue-600 hover:underline";
|
|
10
|
+
return (
|
|
11
|
+
<nav aria-label="Graph level" className="flex min-w-0 flex-wrap items-center gap-1 text-[13px] text-ink2">
|
|
12
|
+
{all && (last < 0 ? <span aria-current="location" className="px-1 text-ink">All</span> : <button type="button" onClick={() => onSelect(null)} className={BTN}>All</button>)}
|
|
13
|
+
{crumbs.map((c, i) => (
|
|
14
|
+
<React.Fragment key={c.path}>
|
|
15
|
+
{(all || i > 0) && <span aria-hidden="true" className="text-neutral-400">/</span>}
|
|
16
|
+
{i === last
|
|
17
|
+
? <span aria-current="location" className="px-1 text-ink">{c.label}</span>
|
|
18
|
+
: <button type="button" onClick={() => onSelect(c.path)} className={BTN}>{c.label}</button>}
|
|
19
|
+
</React.Fragment>
|
|
20
|
+
))}
|
|
21
|
+
</nav>
|
|
22
|
+
);
|
|
23
|
+
}
|