@jmbarzee/force-graph 0.0.1

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,213 @@
1
+ import { default as React } from 'react';
2
+ import { Graph, GraphNode } from '../graph/model';
3
+ import { FilterState, AxisLocks } from '../filter/types';
4
+ import { AxisId } from '../graph/axis';
5
+ import { Theme } from '../graph/scheme';
6
+ import { ThemeChoice } from './graph-view/useTheme';
7
+ import { LayoutState } from '../graph/layout';
8
+ import { Taxonomy } from '../graph/taxonomy';
9
+ import { ActiveGroup, NodeGroup } from '../graph/groups';
10
+ import { SearchState } from './FilterBar';
11
+ import { FocusRequest } from './graph-view/focus';
12
+ import { GraphDiagnostic } from '../graph/validate';
13
+ export type { SearchState } from './FilterBar';
14
+ export type { FocusRequest } from './graph-view/focus';
15
+ /** A node's position in the layout, in world units. */
16
+ export interface Position {
17
+ x: number;
18
+ y: number;
19
+ }
20
+ /** The host's action for a node, offered in the hover box: its label, and what it does with the host's own node. */
21
+ export interface NodeAction {
22
+ label: string;
23
+ onSelect: (node: GraphNode) => void;
24
+ }
25
+ /** What `groupsControl` may ask of the view. */
26
+ export interface GroupsControl {
27
+ /** Emphasise one group (a legend row under the pointer), or none; the view glows it stronger. */
28
+ emphasize(id: string | null): void;
29
+ }
30
+ /** Which parts of the pane's own chrome are offered; every part is on by default. */
31
+ export interface GraphViewChrome {
32
+ /** The search control in the bar. */
33
+ search?: boolean;
34
+ /** The lock toggle on each axis. Default: only when the host controls `locks` with `onLocksChange` — a one-view host has nothing to lock against, and a value without its callback is read-only. */
35
+ locks?: boolean;
36
+ /** The Fit / theme / Play cluster and the node count. */
37
+ transport?: boolean;
38
+ /** The pane's own list of the graph's defects under the bar. */
39
+ diagnostics?: boolean;
40
+ /** The shortcuts panel and the `?` key that opens it. */
41
+ shortcuts?: boolean;
42
+ /** The control panel (the forces, the node scale). */
43
+ controls?: boolean;
44
+ /** The Auto / Light / Dark button. It sits in the transport cluster, so `transport: false` hides it too. */
45
+ themeToggle?: boolean;
46
+ /** The "dependencies / dependents" line in the hover box. */
47
+ directionLine?: boolean;
48
+ /** Centre and zoom to a node's neighbours on double-click. */
49
+ activateZoom?: boolean;
50
+ }
51
+ /**
52
+ * What a host may ask of a mounted view. `focusNode` is the whole focus
53
+ * recipe in one call: widen the filter just enough on the axes shown
54
+ * (overriding a lock, which flashes), centre on the node once the layout has
55
+ * settled, and select it.
56
+ */
57
+ export interface GraphViewHandle {
58
+ /** Reveal, centre and select a node by id. False when the id is not in the graph or the filter cannot be widened to show it. */
59
+ focusNode(id: string): boolean;
60
+ /** Select a node, or clear the selection with null. An id the graph does not hold is ignored. */
61
+ select(id: string | null): void;
62
+ /** Frame the visible nodes. */
63
+ fit(): void;
64
+ /** Every node's position, by id — a layout the host may keep and hand back as `defaultPositions`. */
65
+ getPositions(): Map<string, Position>;
66
+ /** Move nodes to the given positions (pinned ones stay) and let the layout settle around them. */
67
+ setPositions(positions: ReadonlyMap<string, Position>): void;
68
+ }
69
+ /**
70
+ * The view's inputs. Only `graph` is required, with a taxonomy from either the
71
+ * `taxonomy` prop or an `TaxonomyContext.Provider` above. Every piece of state
72
+ * follows the React convention: pass the value and its callback to control it,
73
+ * or neither and the view keeps it; a callback alone still reports changes.
74
+ */
75
+ export interface GraphViewProps {
76
+ /** The graph, built by the host from whatever it reads. Keep one object until its nodes or edges change. */
77
+ graph: Graph;
78
+ /** The taxonomy, when not supplied through `TaxonomyContext`. */
79
+ taxonomy?: Taxonomy;
80
+ /**
81
+ * Where the pane's shortcuts listen. `pane` (the default): the root is
82
+ * focusable and handles the keys that reach it, so a host page keeps Tab
83
+ * and Space for its own controls. `global`: the window — for a host whose
84
+ * whole page is this pane, or a tab shell's active tab. `none`: no
85
+ * shortcuts — a tab shell's hidden tab.
86
+ */
87
+ keyboard?: 'pane' | 'global' | 'none';
88
+ /** Where nodes start, by id — a layout the host kept (`getPositions`). Read whenever the simulation is built; a node the live layout already holds keeps its place. */
89
+ defaultPositions?: ReadonlyMap<string, Position>;
90
+ /** Chip badge counts per axis and value — the host's reading of its own data. Absent, the graph's entity counts (the copies of one `copyKey` once). */
91
+ counts?: ReadonlyMap<string, ReadonlyMap<string, number>>;
92
+ /** The host's findings bars, rendered under the filter chips. */
93
+ findings?: React.ReactNode;
94
+ /** Groups to glow, over the whole graph; the view restricts them to the visible set and colours them. */
95
+ groups?: readonly NodeGroup[];
96
+ /** The host's control for its groups (`GroupLegend`, or its own), rendered inside the pane — which sets `--gv-toolbar-height` on its root so a control can anchor under the bar — and handed the groups as drawn, colours included, and the view's `emphasize`. */
97
+ groupsControl?: (active: readonly ActiveGroup[], control: GroupsControl) => React.ReactNode;
98
+ /** Edges to mark, by id, with a dashed band behind their stroke; a graduated edge is marked when the edge it stands for is. */
99
+ markedEdges?: ReadonlySet<string>;
100
+ /** The host's action for a node, offered in the hover box: one for every node, or the host's answer per node (null for none). */
101
+ nodeAction?: NodeAction | ((node: GraphNode) => NodeAction | null);
102
+ /** Which parts of the pane's chrome are offered. Default: everything, the lock toggles only when `locks` is controlled with its callback. */
103
+ chrome?: GraphViewChrome;
104
+ /** The search control's title and placeholder, for a host with its own words. */
105
+ searchTitle?: string;
106
+ searchPlaceholder?: string;
107
+ /**
108
+ * The selected node, when the host controls the selection: the view shows
109
+ * this and nothing else while it is defined. A selection the user makes
110
+ * arrives through `onSelectionChange`; the host answers by setting this, or
111
+ * does not. Absent, the view keeps the selection itself.
112
+ */
113
+ selectedId?: string | null;
114
+ /** Where the view's own selection starts when the host does not control it. Read once. */
115
+ defaultSelectedId?: string | null;
116
+ /**
117
+ * A request to reveal, centre and select a node — the declarative form of
118
+ * the handle's `focusNode`. Compared by value: a new `id` or `key` is a new
119
+ * request. It waits until its node is in the graph, then `onFocusResult`
120
+ * reports whether the filter could show it. Null, or a newer request,
121
+ * withdraws a waiting one, unreported.
122
+ */
123
+ focus?: FocusRequest | null;
124
+ /** A focus request's outcome, once its node is in the graph: whether the node ended visible. A withdrawn request is not reported. */
125
+ onFocusResult?: (request: FocusRequest, shown: boolean) => void;
126
+ /** Added to the root's class list; the root keeps its own. */
127
+ className?: string;
128
+ /** Merged onto the root's style; the scheme's variables stay. */
129
+ style?: React.CSSProperties;
130
+ /**
131
+ * The selection per axis. Controlled while defined: the view shows it and
132
+ * never rewrites it — a host that controls the filter re-seeds it for a
133
+ * new graph itself (`reseedFilter`). Absent, the view keeps the filter:
134
+ * `defaultFilter`, or `initialFilter(taxonomy, graph)` re-seeded from each
135
+ * new graph on every axis still at the selection it opened with.
136
+ */
137
+ filter?: FilterState;
138
+ /**
139
+ * Every change to the filter — the user's and, when the view keeps the
140
+ * filter itself, its own re-seed on a graph change. `touched` names the
141
+ * axes the user changed in the bar; a write of the view's own (its re-seed,
142
+ * a focus's widening) names none — so a host that controls the filter can
143
+ * keep the set `reseedFilter` takes.
144
+ */
145
+ onFilterChange?: (next: FilterState, touched: readonly AxisId[]) => void;
146
+ /**
147
+ * Where the view's own filter starts when the host does not control it —
148
+ * a start that depends on the host's state, which a descriptor's
149
+ * `initialSelection` cannot see. Read once; a host that gives it has
150
+ * chosen its start, so a later graph re-seeds nothing.
151
+ */
152
+ defaultFilter?: FilterState;
153
+ /** Which axes are locked against a cross-view adoption. Default: `defaultLocks`, else none. Controlling it with `onLocksChange` shows the lock toggles. */
154
+ locks?: AxisLocks;
155
+ onLocksChange?: (next: AxisLocks) => void;
156
+ /** Where the view's own locks start when the host does not control them. Read once. */
157
+ defaultLocks?: AxisLocks;
158
+ /** The locks a host-side focus into this view overrode (`useLinkedViews`'s `switchTo`); flashed once, beside the view's own. */
159
+ overriddenLocks?: ReadonlySet<string>;
160
+ /** The axes shown, in order. Default: `defaultAxes`, else every declared filter axis. Editable in the bar unless controlled without a callback. */
161
+ axes?: readonly string[];
162
+ onAxesChange?: (next: readonly string[]) => void;
163
+ /** Where the view's own axes start when the host does not control them. Read once. */
164
+ defaultAxes?: readonly string[];
165
+ /** The search control. Default: `defaultSearch`, else closed and empty. */
166
+ search?: SearchState;
167
+ onSearchChange?: (next: SearchState) => void;
168
+ /** Where the view's own search starts when the host does not control it. Read once. */
169
+ defaultSearch?: SearchState;
170
+ /** The pane's theme choice. Default: `defaultTheme`, else `system`. */
171
+ theme?: ThemeChoice;
172
+ onThemeChange?: (next: ThemeChoice) => void;
173
+ /** Where the view's own theme choice starts when the host does not control it. Read once. */
174
+ defaultTheme?: ThemeChoice;
175
+ /** What `system` resolves to, for a host that knows its own theme. Given, the pane reads no ancestor's `data-theme` and no OS preference for it. */
176
+ systemTheme?: Theme;
177
+ /**
178
+ * The layout — the force tuning and the node scale, as the user set them.
179
+ * Controlled while defined: the view reads it, retunes it for the graph and
180
+ * taxonomy it faces (a tuning on an axis the graph no longer carries
181
+ * returns to the style axis, reported through `onLayoutChange`), and
182
+ * otherwise never writes it. Absent, the view keeps the layout:
183
+ * `defaultLayout`, else the taxonomy's defaults.
184
+ */
185
+ layout?: LayoutState;
186
+ /** Every change to the layout — the user's tuning in the control panel and the view's retune for a new graph or taxonomy. */
187
+ onLayoutChange?: (next: LayoutState) => void;
188
+ /** Where the view's own layout starts when the host does not control it — `layoutFromJSON`'s answer, say. Read once. */
189
+ defaultLayout?: LayoutState;
190
+ /**
191
+ * The selection the user made — a click, Enter, Escape, a search pick, a
192
+ * focus landing, the handle — as the host's own node, or null when they
193
+ * cleared it. Not called for a change the host made through `selectedId`.
194
+ * When the view keeps the selection itself, also null when the selected
195
+ * node leaves the graph.
196
+ */
197
+ onSelectionChange?: (node: GraphNode | null) => void;
198
+ /** The graph's diagnostics, whenever they are recomputed — the first graph's included, and an empty list is a call. */
199
+ onDiagnostics?: (diagnostics: readonly GraphDiagnostic[]) => void;
200
+ /** A node double-clicked. */
201
+ onNodeActivate?: (node: GraphNode) => void;
202
+ /** The node under the pointer, or null. */
203
+ onHover?: (node: GraphNode | null) => void;
204
+ /** The theme the pane resolved to draw in — the choice, or the host's for `system`. */
205
+ onThemeResolved?: (theme: Theme) => void;
206
+ }
207
+ /**
208
+ * The graph view: the host's `Graph`, laid out by forces, filtered by the
209
+ * bar, highlighted on hover and selection, tuned by the control panel and
210
+ * drawn on a canvas. Only `graph` and a taxonomy are required; every piece
211
+ * of state is controlled by the host or kept by the view.
212
+ */
213
+ export declare const GraphView: React.ForwardRefExoticComponent<GraphViewProps & React.RefAttributes<GraphViewHandle>>;
@@ -0,0 +1,25 @@
1
+ import { ActiveGroup } from '../graph/groups';
2
+ /** The legend's inputs: the items, the groups as drawn, and the host's toggle and emphasis. */
3
+ export interface GroupLegendProps {
4
+ /** Every group the host offers, in order, with its label. */
5
+ items: readonly {
6
+ id: string;
7
+ label: string;
8
+ }[];
9
+ /** The groups as drawn, from `groupsControl` — their colours. An item on but not among them has no node the view draws. */
10
+ active: readonly ActiveGroup[];
11
+ /** Which items are on. Absent: every item is on; the toggles follow `onToggle`. */
12
+ enabled?: ReadonlySet<string>;
13
+ /** Turn an item on or off; shown as a checkbox when given. */
14
+ onToggle?: (id: string) => void;
15
+ /** Hover emphasis — hand it `groupsControl`'s `emphasize`. */
16
+ onEmphasize?: (id: string | null) => void;
17
+ title?: string;
18
+ className?: string;
19
+ }
20
+ /**
21
+ * A legend for the group overlay: every group the host offers with the colour
22
+ * the view drew it in, a checkbox when the host lets the user turn groups on
23
+ * and off, and hover emphasis through the view's own `emphasize`.
24
+ */
25
+ export declare function GroupLegend({ items, active, enabled, onToggle, onEmphasize, title, className }: GroupLegendProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * A request to reveal, centre and select a node — the declarative form of the
3
+ * handle's `focusNode`, for a host whose pane mounts lazily or whose data
4
+ * arrives after the request. Compared by value: a new `id` or `key` is a new
5
+ * request, the same pair on a re-render is not, so a host repeats a request
6
+ * for one node by changing `key`. A request waits until its node is in the
7
+ * graph; then the view runs the focus recipe once and `onFocusResult` says
8
+ * whether the filter could show the node. `null`, or a newer request,
9
+ * withdraws a waiting one; a withdrawn request is not reported.
10
+ */
11
+ export interface FocusRequest {
12
+ id: string;
13
+ key?: number | string;
14
+ }
15
+ /** Whether two requests are the same ask: both absent, or the same id and key. */
16
+ export declare function sameFocusRequest(a: FocusRequest | null, b: FocusRequest | null): boolean;
@@ -0,0 +1,9 @@
1
+ import { FocusRequest } from './focus';
2
+ /**
3
+ * A focus request and the function that raises one: `requestFocus(id)` is a
4
+ * new request every time, for the same node too, so the view acts on each
5
+ * ask; `requestFocus(null)` withdraws a waiting one. Pass the request as the
6
+ * view's `focus`. A host with two views through `useLinkedViews` has this in
7
+ * `linked.focus(view, node)` and needs none of its own.
8
+ */
9
+ export declare function useFocusRequest(): [FocusRequest | null, (id: string | null) => void];
@@ -0,0 +1,85 @@
1
+ import { AxisId } from '../../graph/axis';
2
+ import { Graph, GraphNode } from '../../graph/model';
3
+ import { Taxonomy } from '../../graph/taxonomy';
4
+ import { LinkedViewState } from '../../filter/linked';
5
+ import { AxisLocks, FilterState, ReconcileResult, ViewTransition } from '../../filter/types';
6
+ import { SearchState } from '../FilterBar';
7
+ import { FocusRequest } from './focus';
8
+ export type { LinkedViewState } from '../../filter/linked';
9
+ /** The state props of one linked view — spread onto its `GraphView` or `FilterBar`. */
10
+ export interface LinkedViewProps {
11
+ filter: FilterState;
12
+ /** A change to the filter; `touched` names the axes the user changed (the bar's and the pane's writes name them), which a later graph then leaves alone. */
13
+ onFilterChange: (next: FilterState, touched?: readonly AxisId[]) => void;
14
+ locks: AxisLocks;
15
+ onLocksChange: (next: AxisLocks) => void;
16
+ axes: readonly AxisId[];
17
+ onAxesChange: (next: readonly AxisId[]) => void;
18
+ /** The locks the last switch into this view overrode — a focus's, which the bar flashes once per set; a manual switch clears it. */
19
+ overriddenLocks: ReadonlySet<AxisId>;
20
+ /** The search the views share: the same value and callback in every view's props. */
21
+ search: SearchState;
22
+ onSearchChange: (next: SearchState) => void;
23
+ /** The focus `linked.focus` asked of this view, until the pane reports it; a `FilterBar` ignores it. */
24
+ focus: FocusRequest | null;
25
+ onFocusResult: (request: FocusRequest, shown: boolean) => void;
26
+ }
27
+ /** What `useLinkedViews` is given: the taxonomy, the views by name, where each starts, the graph the default filter is seeded from, and the view shown first. */
28
+ export interface LinkedViewsOptions<V extends string> {
29
+ taxonomy: Taxonomy;
30
+ /** The views, by the names the host switches between. Read once. */
31
+ views: readonly V[];
32
+ /** Where each view starts; a part left out defaults — the taxonomy's default filter over `graph`, no locks, every declared axis. Read once. A filter given here counts as the user's on every axis where it differs from that default. */
33
+ initial?: (view: V) => {
34
+ filter?: FilterState;
35
+ locks?: AxisLocks;
36
+ axes?: readonly AxisId[];
37
+ } | undefined;
38
+ /** The graph the views show: the default filter is seeded from it, a later one re-seeds every view on the axes the user has not touched, and a focus widens by its whole chips. */
39
+ graph?: Pick<Graph, 'nodes'>;
40
+ /** The view shown first. Default: the first of `views`. Read once. */
41
+ start?: V;
42
+ /** Where the shared search starts — a persisted query, say. Default: closed and empty. Read once. */
43
+ initialSearch?: SearchState;
44
+ }
45
+ /** The state two or more views share through one bar: each view's props, the view shown, the switch and the focus between views, and the whole for persistence. */
46
+ export interface LinkedViews<V extends string> {
47
+ /** One view's state props. Throws for a view not in `views`. */
48
+ of(view: V): LinkedViewProps;
49
+ /** The view shown: `start`, then the last `switchTo` or `focus`. */
50
+ readonly active: V;
51
+ /**
52
+ * Switch to `to` from the active view and make it active: a manual switch
53
+ * (the default) adopts the active view's selections on the destination's
54
+ * axes except where locked; a focus widens the destination to show the
55
+ * target and flashes the locks it overrode. Returns the destination's new
56
+ * filter and the overridden locks.
57
+ */
58
+ switchTo(to: V, transition?: ViewTransition): ReconcileResult;
59
+ /**
60
+ * Show `node` in `view`: a focus switch to it, and — for a `GraphView` —
61
+ * a request in `of(view).focus` that centres on and selects the node once
62
+ * it is in the graph, cleared when the pane reports it.
63
+ */
64
+ focus(view: V, node: GraphNode): ReconcileResult;
65
+ /** Rewrite one view's filter from its current value — a host rule that follows one view's data. */
66
+ updateFilter(view: V, fn: (filter: FilterState) => FilterState): void;
67
+ /** Rewrite every view's filter at once — a stale value pruned when the data changes. */
68
+ updateFilters(fn: (filter: FilterState, view: V) => FilterState): void;
69
+ /** The search the views share. */
70
+ readonly search: SearchState;
71
+ /** Every view's filter, locks and axes, for a host that persists them (with `active` and `search`). */
72
+ readonly state: Readonly<Record<V, LinkedViewState>>;
73
+ }
74
+ /**
75
+ * One record per view — filter, locks, axes, the locks the last focus into it
76
+ * overrode, the focus asked of it — with each view's props (`of`), the view
77
+ * shown (`active`), `switchTo` and `focus`, a per-view or all-views filter
78
+ * rewrite, the shared `search`, and the whole as `state`. `views`, `initial`,
79
+ * `start` and `initialSearch` are read once. A later `graph` re-seeds every
80
+ * view on the axes the user has not touched — the ones no write through
81
+ * `onFilterChange` named, and on which a filter `initial` gave did not differ
82
+ * from the default — so a host that mounts empty is not left blank and the
83
+ * user's own selections stand.
84
+ */
85
+ export declare function useLinkedViews<V extends string>({ taxonomy, views, initial, graph, start, initialSearch }: LinkedViewsOptions<V>): LinkedViews<V>;
@@ -0,0 +1,5 @@
1
+ import { FilterState } from '../../filter/types';
2
+ /** The values that just turned on, per axis. */
3
+ export type RecentlyChanged = ReadonlyMap<string, ReadonlySet<string>>;
4
+ /** The values that just turned on in a filter, per axis, for about 450 ms — the bar's chip flash — derived from the filter alone. */
5
+ export declare function useRecentlyChanged(rawFilter: FilterState): RecentlyChanged;
@@ -0,0 +1,11 @@
1
+ import { default as React } from 'react';
2
+ import { Taxonomy } from '../../graph/taxonomy';
3
+ /** The taxonomy for every view and bar below the provider, so a host passes it once. */
4
+ export declare const TaxonomyContext: React.Context<Taxonomy | null>;
5
+ /**
6
+ * The taxonomy in scope.
7
+ *
8
+ * Throws when no provider is above the caller. That is the point: a wrong
9
+ * taxonomy is invisible, a missing one should not be.
10
+ */
11
+ export declare function useTaxonomy(): Taxonomy;
@@ -0,0 +1,26 @@
1
+ import { default as React } from 'react';
2
+ import { Theme } from '../../graph/scheme';
3
+ /** A theme choice: `system` follows the host, `light` and `dark` are fixed. */
4
+ export type ThemeChoice = 'system' | Theme;
5
+ /** The three choices, in the order the pane's button cycles them. */
6
+ export declare const THEME_CHOICES: readonly ThemeChoice[];
7
+ /** A persisted or host-supplied choice, or system for anything outside the three: discard the field, not the state. */
8
+ export declare function themeChoiceOf(value: unknown): ThemeChoice;
9
+ /** What an element that marks a theme says: its `data-theme`. */
10
+ export interface ThemeMarker {
11
+ getAttribute(name: string): string | null;
12
+ }
13
+ /** The one attribute the library reads a host's theme from. */
14
+ export declare const THEME_MARKER_SELECTOR = "[data-theme]";
15
+ /** The theme a marker declares; a `data-theme` outside light and dark declares nothing. */
16
+ export declare function themeOfMarker(marker: ThemeMarker | null): Theme | undefined;
17
+ /** What the host has decided ABOVE `el`: the nearest marking ancestor, else the OS. */
18
+ export declare function hostTheme(el: Element | null): Theme;
19
+ /**
20
+ * The host's theme for the element at `ref`, re-read when any ancestor's
21
+ * `data-theme` changes and when the OS preference changes. With `enabled`
22
+ * false nothing is read or observed, and the answer is light.
23
+ */
24
+ export declare function useHostTheme(ref: React.RefObject<HTMLElement | null>, enabled?: boolean): Theme;
25
+ /** The resolved theme for the pane rooted at `ref`, given the user's choice and, for `system`, what the host says it means — else what its ancestors and the OS say. */
26
+ export declare function useTheme(ref: React.RefObject<HTMLElement | null>, choice: ThemeChoice, systemTheme?: Theme): Theme;
@@ -0,0 +1,26 @@
1
+ import { AxisId } from '../graph/axis';
2
+ import { Graph } from '../graph/model';
3
+ import { Taxonomy } from '../graph/taxonomy';
4
+ import { AxisLocks, FilterState, ReconcileResult, ViewTransition } from './types';
5
+ /** One view's share of the linked state: what it filters on, what it holds against adoption, and what the last focus into it overrode. */
6
+ export interface LinkedViewState {
7
+ filter: FilterState;
8
+ locks: AxisLocks;
9
+ /** The axes this view shows, in order — the ones a switch reconciles. */
10
+ axes: readonly AxisId[];
11
+ /** The locks the last switch into this view overrode: a focus's, a new set per focus so a bar flashes them once; a manual switch overrides none and clears it. */
12
+ overriddenLocks: ReadonlySet<AxisId>;
13
+ }
14
+ /** The one empty set every view starts from, so an unchanged state keeps its identity. */
15
+ export declare const NO_LOCKS: ReadonlySet<AxisId>;
16
+ /**
17
+ * Switch to `to` from `from`: on a manual switch `to` adopts `from`'s
18
+ * selections on the axes it shows, except where locked; on a focus `to`
19
+ * widens just enough to show the target — to the whole chip holding its
20
+ * value when `graph` is given — overriding locks (recorded). The same state
21
+ * object comes back when nothing changed.
22
+ */
23
+ export declare function linkedSwitch<V extends string>(state: Readonly<Record<V, LinkedViewState>>, taxonomy: Pick<Taxonomy, 'axisFor'>, to: V, from: V, transition: ViewTransition, graph?: Pick<Graph, 'nodes'>): {
24
+ state: Readonly<Record<V, LinkedViewState>>;
25
+ result: ReconcileResult;
26
+ };
@@ -0,0 +1,38 @@
1
+ import { FilterState, AxisLocks, ViewTransition, ReconcileResult } from './types';
2
+ import { AxisId } from '../graph/axis';
3
+ import { Graph } from '../graph/model';
4
+ import { Taxonomy } from '../graph/taxonomy';
5
+ /** What a reconciliation reads: the destination view's filter, locks and the axes it shows, the source view's filter, the transition, and the graph when a focus should widen by whole chips. */
6
+ export interface ReconcileInput {
7
+ /** The view being switched to: its filter as it stands. */
8
+ destination: FilterState;
9
+ /** The view being switched from. Read on a manual switch; a focus reads only the destination. */
10
+ source: FilterState;
11
+ /** The destination's locks. */
12
+ locks: AxisLocks;
13
+ transition: ViewTransition;
14
+ /** Resolves the axes' policies. */
15
+ taxonomy: Pick<Taxonomy, 'axisFor'>;
16
+ /**
17
+ * The axes the destination view SHOWS, by id — not every declared axis:
18
+ * an axis the user removed from the bar is not filtering there, and
19
+ * adopting a selection on it would revive what they reset. One the
20
+ * taxonomy does not declare is skipped.
21
+ */
22
+ axes: readonly AxisId[];
23
+ /**
24
+ * The graph the destination shows. Given, a focus widens to the WHOLE
25
+ * chip holding the target's value — the bar toggles a chip as a unit, so
26
+ * a selection that split one would read as on with half its nodes hidden,
27
+ * and the first click would turn the chip off. Absent, the value alone.
28
+ */
29
+ graph?: Pick<Graph, 'nodes'>;
30
+ }
31
+ /**
32
+ * The destination view's filter after a switch: on a manual switch it adopts
33
+ * the source's selections on the axes it shows, except where locked; on a
34
+ * focus it widens just enough to show the target, overriding locks, which
35
+ * come back in `overriddenLocks` for the bar to flash. The same filter object
36
+ * comes back when nothing changed.
37
+ */
38
+ export declare function reconcileFilter({ destination, source, locks, transition, taxonomy, axes, graph }: ReconcileInput): ReconcileResult;
@@ -0,0 +1,12 @@
1
+ import { AxisId } from '../graph/axis';
2
+ import { FilterState } from './types';
3
+ /** Toggle one value on one axis. */
4
+ export declare function toggleValue(filter: FilterState, axis: AxisId, value: string): FilterState;
5
+ /**
6
+ * Toggle a group of values on one axis as a unit.
7
+ *
8
+ * A chip may cover several values — one covers three. "Any on" turns the whole
9
+ * group off; otherwise the whole group goes on, so a partially-on group has a
10
+ * single unambiguous next state.
11
+ */
12
+ export declare function toggleGroup(filter: FilterState, axis: AxisId, values: readonly string[]): FilterState;
@@ -0,0 +1,162 @@
1
+ import { AxisId, AxisValues, ResolvedAxisDescriptor } from '../graph/axis';
2
+ import { Graph } from '../graph/model';
3
+ import { Taxonomy } from '../graph/taxonomy';
4
+ /** The chosen values on one axis. Empty means what the axis's descriptor says. */
5
+ export type AxisSelection = ReadonlySet<string>;
6
+ /**
7
+ * A selection per axis. A `Map`, not a `Record`: an axis the host did not
8
+ * declare has no key, so a misspelt axis is a miss the type sees rather than
9
+ * an `undefined` every downstream `.has` reads as "nothing selected".
10
+ */
11
+ export type FilterState = ReadonlyMap<AxisId, AxisSelection>;
12
+ /**
13
+ * Which axes are locked. A locked axis keeps its selection when a `manual`
14
+ * switch adopts the source view's; a focus widens it anyway and reports it.
15
+ */
16
+ export type AxisLocks = ReadonlyMap<AxisId, boolean>;
17
+ /**
18
+ * The selection on one axis, or an empty one.
19
+ *
20
+ * Always returns the *same* empty Set, so an absent axis does not change
21
+ * identity between reads — a fresh `new Set()` per call would defeat exactly the
22
+ * memoisation this module exists to protect.
23
+ */
24
+ export declare function selectionFor(state: FilterState, axis: AxisId): AxisSelection;
25
+ /** Every axis the state carries a selection for. */
26
+ export declare function axesOf(state: FilterState): AxisId[];
27
+ /** Whether two selections hold the same values. */
28
+ export declare function selectionsEqual(a: AxisSelection, b: AxisSelection): boolean;
29
+ /**
30
+ * Replace one axis's selection, copy-on-write.
31
+ *
32
+ * Returns `state` **itself** when the new selection is content-equal to the old
33
+ * one, so a no-op edit changes no identity anywhere. Otherwise only the edited
34
+ * axis gets a new Set; every other axis is carried over by reference.
35
+ */
36
+ export declare function withSelection(state: FilterState, axis: AxisId, next: AxisSelection): FilterState;
37
+ /** Structural equality across every axis either state mentions. */
38
+ export declare function filterStatesEqual(a: FilterState, b: FilterState): boolean;
39
+ /** Structural equality of two lock maps. */
40
+ export declare function locksEqual(a: AxisLocks, b: AxisLocks): boolean;
41
+ /** Element-wise equality of two lists of ids. */
42
+ export declare function listEqual(a: readonly string[], b: readonly string[]): boolean;
43
+ /** Set one axis's lock, copy-on-write. */
44
+ export declare function withLock(locks: AxisLocks, axis: AxisId, locked: boolean): AxisLocks;
45
+ /** Whether an axis is locked against adoption. */
46
+ export declare function lockedFor(locks: AxisLocks, axis: AxisId): boolean;
47
+ /**
48
+ * Where a focus transition wants to land: the target's value per axis. An
49
+ * axis the target says nothing about is absent, which the reconciler reads
50
+ * as "no expansion needed". A host that also needs the target's name for a
51
+ * view of its own carries it in a type of its own.
52
+ */
53
+ export type FocusTarget = {
54
+ values: Readonly<Record<AxisId, string | undefined>>;
55
+ };
56
+ /** How a view is being switched to: by the user (`manual`: adopt the source's selections) or to show a node (`focus`: widen for its values). */
57
+ export type ViewTransition = {
58
+ kind: 'manual';
59
+ } | {
60
+ kind: 'focus';
61
+ target: FocusTarget;
62
+ };
63
+ /**
64
+ * A reconciled filter, and which locked axes a focus had to widen to show its
65
+ * target — the ones the destination's bar flashes.
66
+ */
67
+ export type ReconcileResult = {
68
+ filter: FilterState;
69
+ overriddenLocks: ReadonlySet<AxisId>;
70
+ };
71
+ /**
72
+ * The filter a view starts from: on every filter axis, what its descriptor's
73
+ * `initialSelection` chooses from the values the graph presents there —
74
+ * every value on an allow-list, none on a narrowing axis, or the host's own
75
+ * choice.
76
+ */
77
+ export declare function initialFilter(taxonomy: Pick<Taxonomy, 'filterAxes'>, graph: Pick<Graph, 'nodes'>): FilterState;
78
+ /**
79
+ * A filter re-seeded for a graph that changed: every filter axis the user has
80
+ * not touched takes its opening selection on `graph` (`initialFilter`'s), so
81
+ * a host that mounts empty while its data loads is not left blank and a value
82
+ * first seen later is not born hidden; an axis in `touched` keeps its
83
+ * selection. Which axes the user touched is state, not an inference — the
84
+ * bar names the axis of every change it makes (`onFilterChange`'s second
85
+ * argument), the view and `useLinkedViews` keep the set, and a host that
86
+ * controls its filter keeps its own and passes it here from its data effect.
87
+ * Absent, every axis follows the graph. The same object when nothing moves.
88
+ */
89
+ export declare function reseedFilter(filter: FilterState, taxonomy: Pick<Taxonomy, 'filterAxes'>, graph: Pick<Graph, 'nodes'>, touched?: ReadonlySet<AxisId>): FilterState;
90
+ /** The axes whose selection differs between two filters — what a write touched. */
91
+ export declare function changedAxes(a: FilterState, b: FilterState): AxisId[];
92
+ /** A filter as plain data — each axis's selected values in an array — for a URL, a store or a saved layout. */
93
+ export declare function filterToJSON(filter: FilterState): Record<AxisId, string[]>;
94
+ /**
95
+ * The filter `filterToJSON` wrote, or undefined when `value` is not that
96
+ * shape — stored data is not trusted to be, and a host falls back to its
97
+ * default rather than to a filter that means something else.
98
+ */
99
+ export declare function filterFromJSON(value: unknown): FilterState | undefined;
100
+ /** Locks as plain data — a boolean per axis — beside `filterToJSON`. */
101
+ export declare function locksToJSON(locks: AxisLocks): Record<AxisId, boolean>;
102
+ /** The locks `locksToJSON` wrote, or undefined when `value` is not that shape. */
103
+ export declare function locksFromJSON(value: unknown): AxisLocks | undefined;
104
+ /** Whether a node answers to a focus target: every value the target names. */
105
+ export declare function matchesFocus(node: {
106
+ values: Readonly<Record<string, string>>;
107
+ }, target: FocusTarget): boolean;
108
+ /** What `passesSelection` reads. */
109
+ export interface PassesSelectionInput<N> {
110
+ filter: FilterState;
111
+ taxonomy: Pick<Taxonomy, 'axisFor'>;
112
+ /** The axes the view shows — the only ones that filter. One the taxonomy does not declare filters nothing. */
113
+ axes: readonly AxisId[];
114
+ /** The record judged: a graph node, or any record of the host's. */
115
+ node: N;
116
+ /** The record's value on an axis, for a record that carries its values under other names. Default: its `values`, when it has them. */
117
+ valueOn?: (node: N, axis: AxisId) => string | undefined;
118
+ }
119
+ /**
120
+ * Does a record pass the selection on the axes a view shows? The policy —
121
+ * what an empty selection means, what an absent value means — is read from
122
+ * each axis's descriptor, once, here, so a host's second view and the pane
123
+ * agree on what the filter means.
124
+ */
125
+ export declare function passesSelection<N>({ filter, taxonomy, axes, node, valueOn }: PassesSelectionInput<N>): boolean;
126
+ /** `passesSelection` over descriptors already resolved — the view's hot path, which resolves them once per render. */
127
+ export declare function passesDescriptors(filter: FilterState, axes: readonly ResolvedAxisDescriptor[], valueOn: (axis: string) => string | undefined): boolean;
128
+ /** Search matches split by the filter: the visible ones, and the hidden ones tallied on the chip hiding each. */
129
+ export interface HiddenMatches {
130
+ /** The ids of the matching nodes the filter shows. */
131
+ visible: ReadonlySet<string>;
132
+ /** How many matching nodes the filter hides. */
133
+ hiddenCount: number;
134
+ /** The hidden matches per axis and value — on the first of the axes shown that each node fails, under its value there. */
135
+ hiddenByAxis: ReadonlyMap<AxisId, ReadonlyMap<string, number>>;
136
+ }
137
+ /** What `hiddenMatchesOf` reads. */
138
+ export interface HiddenMatchesInput<N extends {
139
+ id: string;
140
+ values?: AxisValues;
141
+ }> {
142
+ /** Every node the search ran over, visible or not. */
143
+ nodes: Iterable<N>;
144
+ filter: FilterState;
145
+ taxonomy: Pick<Taxonomy, 'axisFor'>;
146
+ /** The axes the view shows, in order: the first one a hidden match fails is the chip it is counted on. */
147
+ axes: readonly AxisId[];
148
+ visibleIds: ReadonlySet<string>;
149
+ matches: (node: N) => boolean;
150
+ /** A node's value on an axis. Default: its `values`. */
151
+ valueOn?: (node: N, axis: AxisId) => string | undefined;
152
+ }
153
+ /**
154
+ * Split a search's matches by the filter, the way the pane does under its
155
+ * own bar: a match the filter shows lights up, a match it hides is counted
156
+ * on the chip hiding it — the first axis in `axes` the node fails, under the
157
+ * node's value there — so a host's second bar can wear the same badges.
158
+ */
159
+ export declare function hiddenMatchesOf<N extends {
160
+ id: string;
161
+ values?: AxisValues;
162
+ }>({ nodes, filter, taxonomy, axes, visibleIds, matches, valueOn }: HiddenMatchesInput<N>): HiddenMatches;