unfold-nav 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/CHANGELOG.md +15 -0
- package/LICENSE +21 -0
- package/README.md +421 -0
- package/dist/element.d.cts +37 -0
- package/dist/element.d.ts +37 -0
- package/dist/icons.d.cts +12 -0
- package/dist/icons.d.ts +12 -0
- package/dist/index.d.cts +32 -0
- package/dist/index.d.ts +32 -0
- package/dist/insets.d.cts +4 -0
- package/dist/insets.d.ts +4 -0
- package/dist/layout.d.cts +197 -0
- package/dist/layout.d.ts +197 -0
- package/dist/navigation.d.cts +2 -0
- package/dist/navigation.d.ts +2 -0
- package/dist/options.d.cts +6 -0
- package/dist/options.d.ts +6 -0
- package/dist/styles.d.cts +1 -0
- package/dist/styles.d.ts +1 -0
- package/dist/tree.d.cts +22 -0
- package/dist/tree.d.ts +22 -0
- package/dist/types.d.cts +108 -0
- package/dist/types.d.ts +108 -0
- package/dist/unfold-nav.js +2211 -0
- package/dist/unfold-nav.js.map +1 -0
- package/dist/unfold-nav.umd.cjs +446 -0
- package/dist/unfold-nav.umd.cjs.map +1 -0
- package/package.json +71 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure geometry: where nodes go, where their labels go, and how edges are drawn.
|
|
3
|
+
* Coordinates are viewport pixels (y grows downwards, angles are clockwise from +x).
|
|
4
|
+
*
|
|
5
|
+
* The guarantee this file is built around: nothing overlaps. Every node reserves room for its label at
|
|
6
|
+
* the moment its fan is placed, a fan is only accepted when all of its nodes *and* labels fit clear of
|
|
7
|
+
* everything already on screen (nodes, labels, edges, the viewport edge), and later fans have to keep
|
|
8
|
+
* clear of what's reserved. When that's impossible (a tiny screen, a huge menu) labels that don't fit
|
|
9
|
+
* are left out rather than drawn on top of something.
|
|
10
|
+
*/
|
|
11
|
+
export interface Vec {
|
|
12
|
+
x: number;
|
|
13
|
+
y: number;
|
|
14
|
+
}
|
|
15
|
+
export interface Circle extends Vec {
|
|
16
|
+
r: number;
|
|
17
|
+
}
|
|
18
|
+
export interface Rect {
|
|
19
|
+
x: number;
|
|
20
|
+
y: number;
|
|
21
|
+
w: number;
|
|
22
|
+
h: number;
|
|
23
|
+
}
|
|
24
|
+
export interface Size {
|
|
25
|
+
w: number;
|
|
26
|
+
h: number;
|
|
27
|
+
}
|
|
28
|
+
/** A label's possible sizes, preferred first (e.g. one line, then wrapped onto two). */
|
|
29
|
+
export type Sizes = Size | Size[];
|
|
30
|
+
export type EdgeKind = 'curved' | 'straight' | 'step' | 'none';
|
|
31
|
+
export declare function distToRect(p: Vec, r: Rect): number;
|
|
32
|
+
export declare function rectsTouch(a: Rect, b: Rect, pad?: number): boolean;
|
|
33
|
+
export declare const rectContains: (r: Rect, p: Vec) => boolean;
|
|
34
|
+
/** Distance from a point to a polyline. */
|
|
35
|
+
export declare function polyDist(p: Vec, poly: Vec[]): number;
|
|
36
|
+
export declare function polyHitsRect(poly: Vec[], r: Rect): boolean;
|
|
37
|
+
export interface EdgeShape {
|
|
38
|
+
/** SVG path data. */
|
|
39
|
+
d: string;
|
|
40
|
+
/** The same edge as a polyline, for overlap checks. */
|
|
41
|
+
points: Vec[];
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The edge from `a` to `b`, trimmed to both circles.
|
|
45
|
+
* - `curved` edges leave the parent along the direction it was itself reached from, so deeper branches
|
|
46
|
+
* read like a growing tree.
|
|
47
|
+
* - `step` edges use separate radial ports and two rounded turns, like traces on a circuit board.
|
|
48
|
+
* Siblings never share the long axis-aligned stem that a single elbow would create.
|
|
49
|
+
*/
|
|
50
|
+
export declare function edgeShape(a: Circle, b: Circle, style: EdgeKind, parentAngle: number | null): EdgeShape;
|
|
51
|
+
/** SVG path data for an edge (see `edgeShape`). */
|
|
52
|
+
export declare const edgePath: (a: Circle, b: Circle, style: EdgeKind, parentAngle: number | null) => string;
|
|
53
|
+
export interface LabelContext {
|
|
54
|
+
bounds: Rect;
|
|
55
|
+
/** Every node and the hub. */
|
|
56
|
+
circles: Circle[];
|
|
57
|
+
/** Labels already on screen or reserved. */
|
|
58
|
+
taken: Rect[];
|
|
59
|
+
/** Edge polylines a label must not cross (empty to allow crossing). */
|
|
60
|
+
edges: Vec[][];
|
|
61
|
+
/** Clearance between any node's rim and any label: room for hover growth, rings and halos. */
|
|
62
|
+
pad: number;
|
|
63
|
+
/**
|
|
64
|
+
* Clearance from the label's own node, if different. Lets the highlighted label sit closer to the
|
|
65
|
+
* other nodes: only the highlighted node grows, so the others need less room.
|
|
66
|
+
*/
|
|
67
|
+
ownPad?: number;
|
|
68
|
+
/** Extra distances from the node to try, nearest first (default `LABEL_RINGS`). */
|
|
69
|
+
rings?: number[];
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Places several labels so that none of them overlap. Greedy in the given order first; if that leaves
|
|
73
|
+
* one out, the most constrained label goes first, then the reverse order. Returns rects by index.
|
|
74
|
+
*/
|
|
75
|
+
export declare function placeLabels(items: {
|
|
76
|
+
node: Circle;
|
|
77
|
+
size: Sizes;
|
|
78
|
+
angle: number;
|
|
79
|
+
}[], ctx: LabelContext, place?: (item: {
|
|
80
|
+
node: Circle;
|
|
81
|
+
size: Sizes;
|
|
82
|
+
angle: number;
|
|
83
|
+
}, ctx: LabelContext) => Rect | null): (Rect | null)[];
|
|
84
|
+
/**
|
|
85
|
+
* Finds a spot for a label next to `node` that touches nothing in `ctx`, trying the preferred direction
|
|
86
|
+
* first, and each size variant in turn (the full-width label before a wrapped one). `node` must be the
|
|
87
|
+
* same object as its entry in `ctx.circles`. Null when nothing fits.
|
|
88
|
+
*/
|
|
89
|
+
export declare function placeLabel(node: Circle, sizes: Sizes, angle: number, ctx: LabelContext): Rect | null;
|
|
90
|
+
export interface FanInput {
|
|
91
|
+
/** The parent node, or the hub for the top level. */
|
|
92
|
+
origin: Circle;
|
|
93
|
+
/** Direction the origin itself was reached from (null for the hub). Shapes curved edges. */
|
|
94
|
+
parentAngle: number | null;
|
|
95
|
+
count: number;
|
|
96
|
+
/** Preferred direction the fan opens towards (radians). */
|
|
97
|
+
direction: number;
|
|
98
|
+
/** Node radius. */
|
|
99
|
+
radius: number;
|
|
100
|
+
/** Preferred distance from origin to each node centre. */
|
|
101
|
+
distance: number;
|
|
102
|
+
/** Minimum free space between neighbouring nodes. */
|
|
103
|
+
gap: number;
|
|
104
|
+
/** Nodes and labels must stay inside. */
|
|
105
|
+
bounds: Rect;
|
|
106
|
+
/** Everything already placed, except the origin. */
|
|
107
|
+
obstacles: Circle[];
|
|
108
|
+
/** Label rects already reserved. */
|
|
109
|
+
blocked?: Rect[];
|
|
110
|
+
/** Edges already drawn. */
|
|
111
|
+
edges?: Vec[][];
|
|
112
|
+
edgeStyle?: EdgeKind;
|
|
113
|
+
/** Label size(s) to reserve for each new node (null: no label). */
|
|
114
|
+
labels?: (Sizes | null)[];
|
|
115
|
+
/** Label size(s) to reserve for the origin itself (an unfolded branch keeps its name on screen). */
|
|
116
|
+
originLabel?: Sizes | null;
|
|
117
|
+
/** The edge leading into the origin (its label must not cross it). */
|
|
118
|
+
parentEdge?: Vec[] | null;
|
|
119
|
+
/** See `LabelContext.pad`. */
|
|
120
|
+
labelPad?: number;
|
|
121
|
+
/** Allow a full ring around the origin (for a free-floating button). */
|
|
122
|
+
ring?: boolean;
|
|
123
|
+
/** A comfortable angle between neighbours, used when there is room. */
|
|
124
|
+
preferredStep?: number;
|
|
125
|
+
/** Upper bound for the angle the whole fan covers. */
|
|
126
|
+
maxSpread?: number;
|
|
127
|
+
/** How far the fan may rotate away from `direction` before other directions are considered. */
|
|
128
|
+
maxDeviation?: number;
|
|
129
|
+
}
|
|
130
|
+
export interface Fan {
|
|
131
|
+
points: Vec[];
|
|
132
|
+
/** Reserved label rect per node; null where no label was asked for or none fits. */
|
|
133
|
+
labels: (Rect | null)[];
|
|
134
|
+
/** Reserved label rect for the origin (see `FanInput.originLabel`). */
|
|
135
|
+
originLabel?: Rect | null;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Places `count` nodes around `origin`, each with room for its label.
|
|
139
|
+
*
|
|
140
|
+
* Search order: the preferred distance first, growing until something fits; within a distance, the
|
|
141
|
+
* candidate closest to the preferred direction. Passes, each more lenient: (1) strict, near the preferred
|
|
142
|
+
* direction; (2) strict, any direction, wider fans; (3) as (2) but edges may cross labels and nodes;
|
|
143
|
+
* (4) nodes still never overlap, but labels that don't fit are left out. Only if there's no room for the
|
|
144
|
+
* nodes themselves is the least-bad placement used.
|
|
145
|
+
*/
|
|
146
|
+
export declare function placeFan(f: FanInput): Fan;
|
|
147
|
+
export interface Level {
|
|
148
|
+
/** Unique key for the level (the ids of the expanded path joined), used for caching. */
|
|
149
|
+
key: string;
|
|
150
|
+
/** `null` for the top level (children of the button). */
|
|
151
|
+
parentId: string | null;
|
|
152
|
+
childIds: string[];
|
|
153
|
+
}
|
|
154
|
+
export interface PlacedNode extends Vec {
|
|
155
|
+
/** Centre of the parent (the button for top-level pages). */
|
|
156
|
+
from: Vec;
|
|
157
|
+
/** Direction from the parent to this node. */
|
|
158
|
+
angle: number;
|
|
159
|
+
/** Direction the parent itself was reached from (null for top-level pages). */
|
|
160
|
+
parentAngle: number | null;
|
|
161
|
+
/** Room reserved for this node's label; null when labels are off or none fits. */
|
|
162
|
+
label: Rect | null;
|
|
163
|
+
/**
|
|
164
|
+
* The label uses room reserved for a dimmed page's label (only for the level you're looking at, whose
|
|
165
|
+
* labels are always shown). That dimmed label must then stay hidden while this one is visible.
|
|
166
|
+
*/
|
|
167
|
+
borrowed?: boolean;
|
|
168
|
+
/** The edge from the parent as a polyline. */
|
|
169
|
+
edge: Vec[];
|
|
170
|
+
/** Room reserved for this node's label while it is unfolded (see `GraphInput.branchLabelSize`). */
|
|
171
|
+
pathLabel?: Rect | null;
|
|
172
|
+
}
|
|
173
|
+
export interface GraphInput {
|
|
174
|
+
hub: Circle;
|
|
175
|
+
bounds: Rect;
|
|
176
|
+
levels: Level[];
|
|
177
|
+
nodeRadius: number;
|
|
178
|
+
distance: number;
|
|
179
|
+
gap: number;
|
|
180
|
+
edgeStyle?: EdgeKind;
|
|
181
|
+
/** Size(s) of each node's label, to reserve room for it. Omit to lay out without labels. */
|
|
182
|
+
labelSize?: (id: string) => Sizes | null;
|
|
183
|
+
/** Size(s) of an unfolded branch's label (it may carry an extra hint); defaults to `labelSize`. */
|
|
184
|
+
branchLabelSize?: (id: string) => Sizes | null;
|
|
185
|
+
/** See `LabelContext.pad`. */
|
|
186
|
+
labelPad?: number;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Lays out the visible part of the tree level by level. Each level only depends on the levels before it,
|
|
190
|
+
* so a fan never moves when something deeper unfolds; fans are cached per level key.
|
|
191
|
+
*
|
|
192
|
+
* When a node unfolds, its own reserved label space is released (its children usually want that
|
|
193
|
+
* direction); every other reserved label stays out of bounds for deeper levels.
|
|
194
|
+
*/
|
|
195
|
+
export declare function layoutGraph(g: GraphInput, cache?: Map<string, Fan>): Map<string, PlacedNode>;
|
|
196
|
+
/** Index of the node under the pointer, or -1. Picks the nearest node within its radius + slop. */
|
|
197
|
+
export declare function hitTest(p: Vec, nodes: Circle[], slop: number): number;
|
package/dist/layout.d.ts
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure geometry: where nodes go, where their labels go, and how edges are drawn.
|
|
3
|
+
* Coordinates are viewport pixels (y grows downwards, angles are clockwise from +x).
|
|
4
|
+
*
|
|
5
|
+
* The guarantee this file is built around: nothing overlaps. Every node reserves room for its label at
|
|
6
|
+
* the moment its fan is placed, a fan is only accepted when all of its nodes *and* labels fit clear of
|
|
7
|
+
* everything already on screen (nodes, labels, edges, the viewport edge), and later fans have to keep
|
|
8
|
+
* clear of what's reserved. When that's impossible (a tiny screen, a huge menu) labels that don't fit
|
|
9
|
+
* are left out rather than drawn on top of something.
|
|
10
|
+
*/
|
|
11
|
+
export interface Vec {
|
|
12
|
+
x: number;
|
|
13
|
+
y: number;
|
|
14
|
+
}
|
|
15
|
+
export interface Circle extends Vec {
|
|
16
|
+
r: number;
|
|
17
|
+
}
|
|
18
|
+
export interface Rect {
|
|
19
|
+
x: number;
|
|
20
|
+
y: number;
|
|
21
|
+
w: number;
|
|
22
|
+
h: number;
|
|
23
|
+
}
|
|
24
|
+
export interface Size {
|
|
25
|
+
w: number;
|
|
26
|
+
h: number;
|
|
27
|
+
}
|
|
28
|
+
/** A label's possible sizes, preferred first (e.g. one line, then wrapped onto two). */
|
|
29
|
+
export type Sizes = Size | Size[];
|
|
30
|
+
export type EdgeKind = 'curved' | 'straight' | 'step' | 'none';
|
|
31
|
+
export declare function distToRect(p: Vec, r: Rect): number;
|
|
32
|
+
export declare function rectsTouch(a: Rect, b: Rect, pad?: number): boolean;
|
|
33
|
+
export declare const rectContains: (r: Rect, p: Vec) => boolean;
|
|
34
|
+
/** Distance from a point to a polyline. */
|
|
35
|
+
export declare function polyDist(p: Vec, poly: Vec[]): number;
|
|
36
|
+
export declare function polyHitsRect(poly: Vec[], r: Rect): boolean;
|
|
37
|
+
export interface EdgeShape {
|
|
38
|
+
/** SVG path data. */
|
|
39
|
+
d: string;
|
|
40
|
+
/** The same edge as a polyline, for overlap checks. */
|
|
41
|
+
points: Vec[];
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The edge from `a` to `b`, trimmed to both circles.
|
|
45
|
+
* - `curved` edges leave the parent along the direction it was itself reached from, so deeper branches
|
|
46
|
+
* read like a growing tree.
|
|
47
|
+
* - `step` edges use separate radial ports and two rounded turns, like traces on a circuit board.
|
|
48
|
+
* Siblings never share the long axis-aligned stem that a single elbow would create.
|
|
49
|
+
*/
|
|
50
|
+
export declare function edgeShape(a: Circle, b: Circle, style: EdgeKind, parentAngle: number | null): EdgeShape;
|
|
51
|
+
/** SVG path data for an edge (see `edgeShape`). */
|
|
52
|
+
export declare const edgePath: (a: Circle, b: Circle, style: EdgeKind, parentAngle: number | null) => string;
|
|
53
|
+
export interface LabelContext {
|
|
54
|
+
bounds: Rect;
|
|
55
|
+
/** Every node and the hub. */
|
|
56
|
+
circles: Circle[];
|
|
57
|
+
/** Labels already on screen or reserved. */
|
|
58
|
+
taken: Rect[];
|
|
59
|
+
/** Edge polylines a label must not cross (empty to allow crossing). */
|
|
60
|
+
edges: Vec[][];
|
|
61
|
+
/** Clearance between any node's rim and any label: room for hover growth, rings and halos. */
|
|
62
|
+
pad: number;
|
|
63
|
+
/**
|
|
64
|
+
* Clearance from the label's own node, if different. Lets the highlighted label sit closer to the
|
|
65
|
+
* other nodes: only the highlighted node grows, so the others need less room.
|
|
66
|
+
*/
|
|
67
|
+
ownPad?: number;
|
|
68
|
+
/** Extra distances from the node to try, nearest first (default `LABEL_RINGS`). */
|
|
69
|
+
rings?: number[];
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Places several labels so that none of them overlap. Greedy in the given order first; if that leaves
|
|
73
|
+
* one out, the most constrained label goes first, then the reverse order. Returns rects by index.
|
|
74
|
+
*/
|
|
75
|
+
export declare function placeLabels(items: {
|
|
76
|
+
node: Circle;
|
|
77
|
+
size: Sizes;
|
|
78
|
+
angle: number;
|
|
79
|
+
}[], ctx: LabelContext, place?: (item: {
|
|
80
|
+
node: Circle;
|
|
81
|
+
size: Sizes;
|
|
82
|
+
angle: number;
|
|
83
|
+
}, ctx: LabelContext) => Rect | null): (Rect | null)[];
|
|
84
|
+
/**
|
|
85
|
+
* Finds a spot for a label next to `node` that touches nothing in `ctx`, trying the preferred direction
|
|
86
|
+
* first, and each size variant in turn (the full-width label before a wrapped one). `node` must be the
|
|
87
|
+
* same object as its entry in `ctx.circles`. Null when nothing fits.
|
|
88
|
+
*/
|
|
89
|
+
export declare function placeLabel(node: Circle, sizes: Sizes, angle: number, ctx: LabelContext): Rect | null;
|
|
90
|
+
export interface FanInput {
|
|
91
|
+
/** The parent node, or the hub for the top level. */
|
|
92
|
+
origin: Circle;
|
|
93
|
+
/** Direction the origin itself was reached from (null for the hub). Shapes curved edges. */
|
|
94
|
+
parentAngle: number | null;
|
|
95
|
+
count: number;
|
|
96
|
+
/** Preferred direction the fan opens towards (radians). */
|
|
97
|
+
direction: number;
|
|
98
|
+
/** Node radius. */
|
|
99
|
+
radius: number;
|
|
100
|
+
/** Preferred distance from origin to each node centre. */
|
|
101
|
+
distance: number;
|
|
102
|
+
/** Minimum free space between neighbouring nodes. */
|
|
103
|
+
gap: number;
|
|
104
|
+
/** Nodes and labels must stay inside. */
|
|
105
|
+
bounds: Rect;
|
|
106
|
+
/** Everything already placed, except the origin. */
|
|
107
|
+
obstacles: Circle[];
|
|
108
|
+
/** Label rects already reserved. */
|
|
109
|
+
blocked?: Rect[];
|
|
110
|
+
/** Edges already drawn. */
|
|
111
|
+
edges?: Vec[][];
|
|
112
|
+
edgeStyle?: EdgeKind;
|
|
113
|
+
/** Label size(s) to reserve for each new node (null: no label). */
|
|
114
|
+
labels?: (Sizes | null)[];
|
|
115
|
+
/** Label size(s) to reserve for the origin itself (an unfolded branch keeps its name on screen). */
|
|
116
|
+
originLabel?: Sizes | null;
|
|
117
|
+
/** The edge leading into the origin (its label must not cross it). */
|
|
118
|
+
parentEdge?: Vec[] | null;
|
|
119
|
+
/** See `LabelContext.pad`. */
|
|
120
|
+
labelPad?: number;
|
|
121
|
+
/** Allow a full ring around the origin (for a free-floating button). */
|
|
122
|
+
ring?: boolean;
|
|
123
|
+
/** A comfortable angle between neighbours, used when there is room. */
|
|
124
|
+
preferredStep?: number;
|
|
125
|
+
/** Upper bound for the angle the whole fan covers. */
|
|
126
|
+
maxSpread?: number;
|
|
127
|
+
/** How far the fan may rotate away from `direction` before other directions are considered. */
|
|
128
|
+
maxDeviation?: number;
|
|
129
|
+
}
|
|
130
|
+
export interface Fan {
|
|
131
|
+
points: Vec[];
|
|
132
|
+
/** Reserved label rect per node; null where no label was asked for or none fits. */
|
|
133
|
+
labels: (Rect | null)[];
|
|
134
|
+
/** Reserved label rect for the origin (see `FanInput.originLabel`). */
|
|
135
|
+
originLabel?: Rect | null;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Places `count` nodes around `origin`, each with room for its label.
|
|
139
|
+
*
|
|
140
|
+
* Search order: the preferred distance first, growing until something fits; within a distance, the
|
|
141
|
+
* candidate closest to the preferred direction. Passes, each more lenient: (1) strict, near the preferred
|
|
142
|
+
* direction; (2) strict, any direction, wider fans; (3) as (2) but edges may cross labels and nodes;
|
|
143
|
+
* (4) nodes still never overlap, but labels that don't fit are left out. Only if there's no room for the
|
|
144
|
+
* nodes themselves is the least-bad placement used.
|
|
145
|
+
*/
|
|
146
|
+
export declare function placeFan(f: FanInput): Fan;
|
|
147
|
+
export interface Level {
|
|
148
|
+
/** Unique key for the level (the ids of the expanded path joined), used for caching. */
|
|
149
|
+
key: string;
|
|
150
|
+
/** `null` for the top level (children of the button). */
|
|
151
|
+
parentId: string | null;
|
|
152
|
+
childIds: string[];
|
|
153
|
+
}
|
|
154
|
+
export interface PlacedNode extends Vec {
|
|
155
|
+
/** Centre of the parent (the button for top-level pages). */
|
|
156
|
+
from: Vec;
|
|
157
|
+
/** Direction from the parent to this node. */
|
|
158
|
+
angle: number;
|
|
159
|
+
/** Direction the parent itself was reached from (null for top-level pages). */
|
|
160
|
+
parentAngle: number | null;
|
|
161
|
+
/** Room reserved for this node's label; null when labels are off or none fits. */
|
|
162
|
+
label: Rect | null;
|
|
163
|
+
/**
|
|
164
|
+
* The label uses room reserved for a dimmed page's label (only for the level you're looking at, whose
|
|
165
|
+
* labels are always shown). That dimmed label must then stay hidden while this one is visible.
|
|
166
|
+
*/
|
|
167
|
+
borrowed?: boolean;
|
|
168
|
+
/** The edge from the parent as a polyline. */
|
|
169
|
+
edge: Vec[];
|
|
170
|
+
/** Room reserved for this node's label while it is unfolded (see `GraphInput.branchLabelSize`). */
|
|
171
|
+
pathLabel?: Rect | null;
|
|
172
|
+
}
|
|
173
|
+
export interface GraphInput {
|
|
174
|
+
hub: Circle;
|
|
175
|
+
bounds: Rect;
|
|
176
|
+
levels: Level[];
|
|
177
|
+
nodeRadius: number;
|
|
178
|
+
distance: number;
|
|
179
|
+
gap: number;
|
|
180
|
+
edgeStyle?: EdgeKind;
|
|
181
|
+
/** Size(s) of each node's label, to reserve room for it. Omit to lay out without labels. */
|
|
182
|
+
labelSize?: (id: string) => Sizes | null;
|
|
183
|
+
/** Size(s) of an unfolded branch's label (it may carry an extra hint); defaults to `labelSize`. */
|
|
184
|
+
branchLabelSize?: (id: string) => Sizes | null;
|
|
185
|
+
/** See `LabelContext.pad`. */
|
|
186
|
+
labelPad?: number;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Lays out the visible part of the tree level by level. Each level only depends on the levels before it,
|
|
190
|
+
* so a fan never moves when something deeper unfolds; fans are cached per level key.
|
|
191
|
+
*
|
|
192
|
+
* When a node unfolds, its own reserved label space is released (its children usually want that
|
|
193
|
+
* direction); every other reserved label stays out of bounds for deeper levels.
|
|
194
|
+
*/
|
|
195
|
+
export declare function layoutGraph(g: GraphInput, cache?: Map<string, Fan>): Map<string, PlacedNode>;
|
|
196
|
+
/** Index of the node under the pointer, or -1. Picks the nearest node within its radius + slop. */
|
|
197
|
+
export declare function hitTest(p: Vec, nodes: Circle[], slop: number): number;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { IconSource, UnfoldNavOptions, UnfoldNavStrings } from './types.cjs';
|
|
2
|
+
export declare const DEFAULT_STRINGS: UnfoldNavStrings;
|
|
3
|
+
export declare const DEFAULT_OPTIONS: UnfoldNavOptions;
|
|
4
|
+
export declare function isIconSource(value: unknown): value is IconSource;
|
|
5
|
+
/** Validate before applying changes, so an invalid update leaves a working navigation intact. */
|
|
6
|
+
export declare function normalizeOptions(current: UnfoldNavOptions, patch: Partial<UnfoldNavOptions>): UnfoldNavOptions;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { IconSource, UnfoldNavOptions, UnfoldNavStrings } from './types.js';
|
|
2
|
+
export declare const DEFAULT_STRINGS: UnfoldNavStrings;
|
|
3
|
+
export declare const DEFAULT_OPTIONS: UnfoldNavOptions;
|
|
4
|
+
export declare function isIconSource(value: unknown): value is IconSource;
|
|
5
|
+
/** Validate before applying changes, so an invalid update leaves a working navigation intact. */
|
|
6
|
+
export declare function normalizeOptions(current: UnfoldNavOptions, patch: Partial<UnfoldNavOptions>): UnfoldNavOptions;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const STYLES: string;
|
package/dist/styles.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const STYLES: string;
|
package/dist/tree.d.cts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { NavPage } from './types.cjs';
|
|
2
|
+
export interface TreeNode {
|
|
3
|
+
id: string;
|
|
4
|
+
page: NavPage;
|
|
5
|
+
parent: TreeNode | null;
|
|
6
|
+
children: TreeNode[];
|
|
7
|
+
/** 0 for the (possibly virtual) root, 1 for top-level pages. */
|
|
8
|
+
depth: number;
|
|
9
|
+
index: number;
|
|
10
|
+
}
|
|
11
|
+
export interface Tree {
|
|
12
|
+
root: TreeNode;
|
|
13
|
+
byId: Map<string, TreeNode>;
|
|
14
|
+
}
|
|
15
|
+
export declare function buildTree(pages: NavPage[] | NavPage | null | undefined): Tree;
|
|
16
|
+
/** Ids from the top level down to `node` (inclusive). Empty for the root. */
|
|
17
|
+
export declare function pathTo(node: TreeNode | null | undefined): string[];
|
|
18
|
+
/**
|
|
19
|
+
* The page matching `currentUrl`: an exact path (+ optional query/hash) match wins, otherwise the deepest page whose
|
|
20
|
+
* path is a prefix of the current one (`/blog` for `/blog/some-post`). The site root `/` only matches exactly.
|
|
21
|
+
*/
|
|
22
|
+
export declare function findCurrent(tree: Tree, currentUrl: string, base: string): TreeNode | null;
|
package/dist/tree.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { NavPage } from './types.js';
|
|
2
|
+
export interface TreeNode {
|
|
3
|
+
id: string;
|
|
4
|
+
page: NavPage;
|
|
5
|
+
parent: TreeNode | null;
|
|
6
|
+
children: TreeNode[];
|
|
7
|
+
/** 0 for the (possibly virtual) root, 1 for top-level pages. */
|
|
8
|
+
depth: number;
|
|
9
|
+
index: number;
|
|
10
|
+
}
|
|
11
|
+
export interface Tree {
|
|
12
|
+
root: TreeNode;
|
|
13
|
+
byId: Map<string, TreeNode>;
|
|
14
|
+
}
|
|
15
|
+
export declare function buildTree(pages: NavPage[] | NavPage | null | undefined): Tree;
|
|
16
|
+
/** Ids from the top level down to `node` (inclusive). Empty for the root. */
|
|
17
|
+
export declare function pathTo(node: TreeNode | null | undefined): string[];
|
|
18
|
+
/**
|
|
19
|
+
* The page matching `currentUrl`: an exact path (+ optional query/hash) match wins, otherwise the deepest page whose
|
|
20
|
+
* path is a prefix of the current one (`/blog` for `/blog/some-post`). The site root `/` only matches exactly.
|
|
21
|
+
*/
|
|
22
|
+
export declare function findCurrent(tree: Tree, currentUrl: string, base: string): TreeNode | null;
|
package/dist/types.d.cts
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anything that can be rendered as an icon:
|
|
3
|
+
* - inline SVG / HTML markup: `'<svg …>…</svg>'` (treated as trusted markup)
|
|
4
|
+
* - an image URL: `'/icons/home.svg'`, `'https://…/a.png'`, `'data:image/…'`
|
|
5
|
+
* - an emoji or short text: `'🏠'`, `'A'`
|
|
6
|
+
* - a name your `iconResolver` understands: `'home'`
|
|
7
|
+
* - a DOM node (cloned per use) or a function returning one of the above
|
|
8
|
+
*/
|
|
9
|
+
export type IconSource = string | Node | (() => Node | string);
|
|
10
|
+
export interface NavPage {
|
|
11
|
+
/** Stable id. Generated from the tree position when omitted. */
|
|
12
|
+
id?: string;
|
|
13
|
+
label: string;
|
|
14
|
+
/** Where selecting the page goes. Pages without `href` act as groups (or as actions, see `unfold-select`). */
|
|
15
|
+
href?: string;
|
|
16
|
+
/** `_blank` opens in a new tab. */
|
|
17
|
+
target?: string;
|
|
18
|
+
icon?: IconSource;
|
|
19
|
+
/** Shown under the label while the page is highlighted, and exposed to screen readers. */
|
|
20
|
+
description?: string;
|
|
21
|
+
/** Per-node accent colour (any CSS colour). */
|
|
22
|
+
color?: string;
|
|
23
|
+
disabled?: boolean;
|
|
24
|
+
children?: NavPage[];
|
|
25
|
+
/** Free-form payload, handed back in events. */
|
|
26
|
+
data?: unknown;
|
|
27
|
+
}
|
|
28
|
+
export type Position = 'top-left' | 'top' | 'top-right' | 'left' | 'center' | 'right' | 'bottom-left' | 'bottom' | 'bottom-right'
|
|
29
|
+
/** Render the button in document flow (e.g. inside a header); the graph still unfolds in a top-layer overlay. */
|
|
30
|
+
| 'inline';
|
|
31
|
+
export type LabelMode = 'auto' | 'always' | 'hover' | 'none';
|
|
32
|
+
export type DockStyle = 'float' | 'bar';
|
|
33
|
+
export type ArrowKeys = 'tree' | 'spatial';
|
|
34
|
+
/** Text the component speaks or shows. Override for other languages. `{label}` is replaced. */
|
|
35
|
+
export interface UnfoldNavStrings {
|
|
36
|
+
/** Accessible name of the list of pages. */
|
|
37
|
+
pages: string;
|
|
38
|
+
/** The centre button at the top level. */
|
|
39
|
+
close: string;
|
|
40
|
+
/** The centre button one level down. */
|
|
41
|
+
backToTop: string;
|
|
42
|
+
/** The centre button deeper down; `{label}` is the page it goes back to. */
|
|
43
|
+
back: string;
|
|
44
|
+
}
|
|
45
|
+
export type EdgeStyle = 'curved' | 'straight' | 'step' | 'none';
|
|
46
|
+
export type Theme = 'auto' | 'light' | 'dark';
|
|
47
|
+
export interface SelectDetail {
|
|
48
|
+
page: NavPage;
|
|
49
|
+
/** Pages from the top level down to (and including) the selected page. */
|
|
50
|
+
trail: NavPage[];
|
|
51
|
+
/** How the selection happened. */
|
|
52
|
+
via: 'release' | 'click' | 'keyboard';
|
|
53
|
+
}
|
|
54
|
+
export interface UnfoldNavOptions {
|
|
55
|
+
/** The site map: an array of top-level pages, or a single root page whose children are the top level. */
|
|
56
|
+
pages: NavPage[] | NavPage;
|
|
57
|
+
/** Where the button sits. Default `bottom-right`. */
|
|
58
|
+
position: Position;
|
|
59
|
+
/**
|
|
60
|
+
* `float`: the button floats over the page. `bar`: it sits in a strip along its edge.
|
|
61
|
+
* Use the published `--unfold-inset-*` values for page padding to keep content clear.
|
|
62
|
+
*/
|
|
63
|
+
dock: DockStyle;
|
|
64
|
+
/** Distance from the viewport edge in px (`number` or `{ x, y }`). Default 24. */
|
|
65
|
+
offset: number | {
|
|
66
|
+
x: number;
|
|
67
|
+
y: number;
|
|
68
|
+
};
|
|
69
|
+
/** Press duration (ms) before the graph opens in drag mode. Default 220. */
|
|
70
|
+
holdDelay: number;
|
|
71
|
+
/** Dwell time (ms) over a branch before it unfolds while dragging. Default 140. */
|
|
72
|
+
expandDelay: number;
|
|
73
|
+
/** A short tap opens the graph in tap-through mode. Default true. */
|
|
74
|
+
openOnTap: boolean;
|
|
75
|
+
/** Diameter of page nodes in px. Default 52. */
|
|
76
|
+
nodeSize: number;
|
|
77
|
+
/** Diameter of the button in px. Default 60. */
|
|
78
|
+
triggerSize: number;
|
|
79
|
+
/** Preferred distance between a node and its children in px. Default 96. */
|
|
80
|
+
spacing: number;
|
|
81
|
+
/** Minimum free space between neighbouring nodes in px. Default 16. */
|
|
82
|
+
gap: number;
|
|
83
|
+
/** `auto`: labels for the level you're on + highlighted node. `always`, `hover` or `none`. Default `auto`. */
|
|
84
|
+
labels: LabelMode;
|
|
85
|
+
edges: EdgeStyle;
|
|
86
|
+
/** Dim and blur the page behind the graph. Default true. */
|
|
87
|
+
backdrop: boolean;
|
|
88
|
+
/** Vibrate on open / highlight where supported. Default true. */
|
|
89
|
+
haptics: boolean;
|
|
90
|
+
theme: Theme;
|
|
91
|
+
/** URL of the current page. `undefined` = use `location`, `null` = none. */
|
|
92
|
+
current: string | null | undefined;
|
|
93
|
+
/** Accessible name of the button and the navigation dialog. Default `Navigation`. */
|
|
94
|
+
label: string;
|
|
95
|
+
/**
|
|
96
|
+
* `tree` (default): the standard tree keys: Up/Down previous/next, Right opens a branch or enters it,
|
|
97
|
+
* Left closes it or goes to the parent. `spatial`: arrows move to the nearest page in that direction.
|
|
98
|
+
*/
|
|
99
|
+
arrowKeys: ArrowKeys;
|
|
100
|
+
/** Override the built-in text (for other languages). */
|
|
101
|
+
strings: Partial<UnfoldNavStrings>;
|
|
102
|
+
/** Icon for the button. Defaults to a small graph glyph. */
|
|
103
|
+
triggerIcon: IconSource | undefined;
|
|
104
|
+
/** Turn icon names (`icon: 'home'`) into markup or nodes, e.g. from an icon library. */
|
|
105
|
+
iconResolver: ((name: string, page?: NavPage) => Node | string | null | undefined) | undefined;
|
|
106
|
+
/** Custom navigation for client-side routers. Called instead of `location.assign`. */
|
|
107
|
+
navigate: ((page: NavPage, detail: SelectDetail) => void) | undefined;
|
|
108
|
+
}
|