growing-panes 0.2.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 +17 -0
- package/LICENSE +21 -0
- package/README.md +164 -0
- package/dist/index.cjs +404 -0
- package/dist/index.d.cts +11 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +373 -0
- package/dist/react.cjs +42 -0
- package/dist/react.d.cts +11 -0
- package/dist/react.d.ts +11 -0
- package/dist/react.js +16 -0
- package/dist/types-DW8hgJuJ.d.cts +131 -0
- package/dist/types-DW8hgJuJ.d.ts +131 -0
- package/package.json +97 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# growing-panes
|
|
2
|
+
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 275cd21: Add the initial headless pane stack, state transition API, typed configuration,
|
|
8
|
+
visibility policies, subscriptions, and navigation manager.
|
|
9
|
+
|
|
10
|
+
### Patch Changes
|
|
11
|
+
|
|
12
|
+
- 275cd21: Add manager and pure state-machine examples for the headless core API.
|
|
13
|
+
- 1cfbc05: Add an interactive project site, package homepage metadata, and GitHub Pages
|
|
14
|
+
deployment workflow.
|
|
15
|
+
- 275cd21: Add a `growing-panes/react` entry point with `usePaneSnapshot` and
|
|
16
|
+
`useVisiblePanes` hooks.
|
|
17
|
+
- e3b1f35: Remove the legacy AngularJS directive, bundled artifact, and demo application.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2014 Joseph Hsu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# growing-panes
|
|
2
|
+
|
|
3
|
+
Headless stack-based pane navigation for JavaScript and TypeScript.
|
|
4
|
+
|
|
5
|
+
Growing Panes manages pane definitions, navigation state, and the set of panes
|
|
6
|
+
that should be displayed. It does not require a DOM, router, or UI framework.
|
|
7
|
+
|
|
8
|
+
[View the interactive demo](https://jhsu.github.io/growing-panes/).
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
pnpm add growing-panes
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
Define the pane types and their navigation relationships:
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import { createPaneManager, definePaneConfig } from "growing-panes";
|
|
22
|
+
|
|
23
|
+
type AppPanes = {
|
|
24
|
+
projects: {
|
|
25
|
+
params: undefined;
|
|
26
|
+
meta: { title: string };
|
|
27
|
+
};
|
|
28
|
+
project: {
|
|
29
|
+
params: { projectId: string };
|
|
30
|
+
meta: { title: string };
|
|
31
|
+
};
|
|
32
|
+
task: {
|
|
33
|
+
params: { taskId: string };
|
|
34
|
+
meta: { title: string };
|
|
35
|
+
};
|
|
36
|
+
preview: {
|
|
37
|
+
params: { taskId: string };
|
|
38
|
+
meta: { title: string };
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const config = definePaneConfig<AppPanes>({
|
|
43
|
+
display: { maxVisible: 2 },
|
|
44
|
+
panes: {
|
|
45
|
+
projects: {
|
|
46
|
+
meta: { title: "Projects" },
|
|
47
|
+
},
|
|
48
|
+
project: {
|
|
49
|
+
canOpenFrom: ["projects"],
|
|
50
|
+
meta: { title: "Project" },
|
|
51
|
+
},
|
|
52
|
+
task: {
|
|
53
|
+
canOpenFrom: ["project"],
|
|
54
|
+
meta: { title: "Task" },
|
|
55
|
+
},
|
|
56
|
+
preview: {
|
|
57
|
+
canOpenFrom: ["task"],
|
|
58
|
+
display: { maxVisible: 3 },
|
|
59
|
+
meta: { title: "Preview" },
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
initialStack: [{ pane: "projects" }],
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const panes = createPaneManager(config);
|
|
66
|
+
|
|
67
|
+
panes.navigate({
|
|
68
|
+
pane: "project",
|
|
69
|
+
params: { projectId: "p-123" },
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
panes.navigate({
|
|
73
|
+
pane: "task",
|
|
74
|
+
params: { taskId: "t-456" },
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const snapshot = panes.getSnapshot();
|
|
78
|
+
|
|
79
|
+
snapshot.stack; // projects, project, task
|
|
80
|
+
snapshot.visiblePanes; // project, task
|
|
81
|
+
snapshot.activePane; // task
|
|
82
|
+
snapshot.canGoBack; // true
|
|
83
|
+
|
|
84
|
+
panes.back();
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Manager API
|
|
88
|
+
|
|
89
|
+
The manager provides these navigation operations:
|
|
90
|
+
|
|
91
|
+
- `navigate(target)` appends a pane and makes it active.
|
|
92
|
+
- `back({ steps })` removes panes from the end of the stack.
|
|
93
|
+
- `backTo({ key })` or `backTo({ pane })` selects an existing ancestor.
|
|
94
|
+
- `replace(target)` replaces the active pane.
|
|
95
|
+
- `reset(stack)` replaces the complete stack.
|
|
96
|
+
- `canNavigate(target)` checks a transition without changing state.
|
|
97
|
+
- `subscribe(listener)` observes successful state changes.
|
|
98
|
+
- `dispatch(event)` sends a state-machine event directly.
|
|
99
|
+
|
|
100
|
+
Snapshots are immutable. `visiblePanes`, `activePane`, and `canGoBack` are
|
|
101
|
+
derived from the stack rather than stored independently.
|
|
102
|
+
|
|
103
|
+
## Pure state transitions
|
|
104
|
+
|
|
105
|
+
The core can also be used without the manager wrapper:
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
import { createPaneState, transitionPaneState } from "growing-panes";
|
|
109
|
+
|
|
110
|
+
const state = createPaneState(config);
|
|
111
|
+
const transition = transitionPaneState(config, state, {
|
|
112
|
+
type: "navigate",
|
|
113
|
+
target: {
|
|
114
|
+
pane: "project",
|
|
115
|
+
params: { projectId: "p-123" },
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
transition.state;
|
|
120
|
+
transition.change;
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
This reducer-style API is synchronous and deterministic. Data loading, URL
|
|
124
|
+
synchronization, rendering, animations, and focus management belong in
|
|
125
|
+
application code or framework adapters.
|
|
126
|
+
|
|
127
|
+
## React
|
|
128
|
+
|
|
129
|
+
React applications can subscribe through the optional `growing-panes/react`
|
|
130
|
+
entry point. Importing the core package does not load React.
|
|
131
|
+
|
|
132
|
+
```tsx
|
|
133
|
+
import { usePaneSnapshot } from "growing-panes/react";
|
|
134
|
+
|
|
135
|
+
function PaneContainer({ manager }) {
|
|
136
|
+
const snapshot = usePaneSnapshot(manager);
|
|
137
|
+
|
|
138
|
+
return snapshot.visiblePanes.map((entry) => (
|
|
139
|
+
<PaneRenderer key={entry.key} entry={entry} />
|
|
140
|
+
));
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Use `useVisiblePanes(manager)` when only the visible entry list is needed.
|
|
145
|
+
React 18 or newer is required for these hooks.
|
|
146
|
+
|
|
147
|
+
## Development
|
|
148
|
+
|
|
149
|
+
```sh
|
|
150
|
+
pnpm install
|
|
151
|
+
pnpm check
|
|
152
|
+
pnpm changeset
|
|
153
|
+
pnpm site:dev
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
The fuller design discussion is in
|
|
157
|
+
[`docs/core-api-draft.md`](docs/core-api-draft.md).
|
|
158
|
+
|
|
159
|
+
## Examples
|
|
160
|
+
|
|
161
|
+
- [`examples/manager.ts`](examples/manager.ts) uses the subscription-based
|
|
162
|
+
manager API for a projects and tasks flow.
|
|
163
|
+
- [`examples/state-machine.ts`](examples/state-machine.ts) uses the pure
|
|
164
|
+
transition API for a recursive file browser.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
createPaneManager: () => createPaneManager,
|
|
24
|
+
createPaneSnapshot: () => createPaneSnapshot,
|
|
25
|
+
createPaneState: () => createPaneState,
|
|
26
|
+
definePaneConfig: () => definePaneConfig,
|
|
27
|
+
transitionPaneState: () => transitionPaneState
|
|
28
|
+
});
|
|
29
|
+
module.exports = __toCommonJS(index_exports);
|
|
30
|
+
|
|
31
|
+
// src/core.ts
|
|
32
|
+
var DEFAULT_MAX_VISIBLE = 2;
|
|
33
|
+
function assertPositiveInteger(value, label) {
|
|
34
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
35
|
+
throw new Error(`${label} must be a positive integer.`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function assertNonNegativeInteger(value, label) {
|
|
39
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
40
|
+
throw new Error(`${label} must be a non-negative integer.`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function hasOwn(object, key) {
|
|
44
|
+
return Object.prototype.hasOwnProperty.call(object, key);
|
|
45
|
+
}
|
|
46
|
+
function resolveConfig(config) {
|
|
47
|
+
const paneNames = Object.keys(config.panes);
|
|
48
|
+
if (paneNames.length === 0) {
|
|
49
|
+
throw new Error("At least one pane definition is required.");
|
|
50
|
+
}
|
|
51
|
+
const maxVisible = config.display?.maxVisible ?? DEFAULT_MAX_VISIBLE;
|
|
52
|
+
assertPositiveInteger(maxVisible, "display.maxVisible");
|
|
53
|
+
for (const pane of paneNames) {
|
|
54
|
+
const definition = config.panes[pane];
|
|
55
|
+
const paneMaxVisible = definition.display?.maxVisible;
|
|
56
|
+
if (paneMaxVisible !== void 0) {
|
|
57
|
+
assertPositiveInteger(
|
|
58
|
+
paneMaxVisible,
|
|
59
|
+
`panes.${pane}.display.maxVisible`
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
for (const parent of definition.canOpenFrom ?? []) {
|
|
63
|
+
if (!hasOwn(config.panes, parent)) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
`Pane "${pane}" references unknown parent pane "${parent}".`
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const initialStack = config.initialStack ?? [];
|
|
71
|
+
const minimumDepth = config.minimumDepth ?? (initialStack.length === 0 ? 0 : 1);
|
|
72
|
+
assertNonNegativeInteger(minimumDepth, "minimumDepth");
|
|
73
|
+
if (minimumDepth > initialStack.length) {
|
|
74
|
+
throw new Error(
|
|
75
|
+
"minimumDepth cannot be greater than the initial stack length."
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
panes: config.panes,
|
|
80
|
+
initialStack,
|
|
81
|
+
maxVisible,
|
|
82
|
+
minimumDepth,
|
|
83
|
+
selectVisiblePanes: config.selectVisiblePanes
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function definitionFor(config, pane) {
|
|
87
|
+
if (!hasOwn(config.panes, pane)) {
|
|
88
|
+
throw new Error(`Unknown pane "${pane}".`);
|
|
89
|
+
}
|
|
90
|
+
return config.panes[pane];
|
|
91
|
+
}
|
|
92
|
+
function generatedKey(pane, seed, stack) {
|
|
93
|
+
const base = `${pane}:${seed}`;
|
|
94
|
+
let key = base;
|
|
95
|
+
let suffix = 2;
|
|
96
|
+
while (stack.some((entry) => entry.key === key)) {
|
|
97
|
+
key = `${base}:${suffix}`;
|
|
98
|
+
suffix += 1;
|
|
99
|
+
}
|
|
100
|
+
return key;
|
|
101
|
+
}
|
|
102
|
+
function entryFromTarget(target, depth, seed, stack) {
|
|
103
|
+
const key = target.key ?? generatedKey(target.pane, seed, stack);
|
|
104
|
+
if (key.length === 0) {
|
|
105
|
+
throw new Error("Pane keys cannot be empty.");
|
|
106
|
+
}
|
|
107
|
+
if (stack.some((entry) => entry.key === key)) {
|
|
108
|
+
throw new Error(`Duplicate pane key "${key}".`);
|
|
109
|
+
}
|
|
110
|
+
return Object.freeze({
|
|
111
|
+
key,
|
|
112
|
+
pane: target.pane,
|
|
113
|
+
params: "params" in target ? target.params : void 0,
|
|
114
|
+
data: "data" in target ? target.data : void 0,
|
|
115
|
+
depth
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
function assertCanOpen(config, pane, parent) {
|
|
119
|
+
const definition = definitionFor(config, pane);
|
|
120
|
+
const allowedParents = definition.canOpenFrom;
|
|
121
|
+
if (allowedParents === void 0) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (parent === void 0 || !allowedParents.includes(parent.pane)) {
|
|
125
|
+
const from = parent === void 0 ? "an empty stack" : `"${parent.pane}"`;
|
|
126
|
+
throw new Error(`Pane "${pane}" cannot open from ${from}.`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function buildStack(config, targets, seedOffset) {
|
|
130
|
+
const stack = [];
|
|
131
|
+
targets.forEach((target, index) => {
|
|
132
|
+
definitionFor(config, target.pane);
|
|
133
|
+
if (index > 0) {
|
|
134
|
+
assertCanOpen(config, target.pane, stack[index - 1]);
|
|
135
|
+
}
|
|
136
|
+
stack.push(
|
|
137
|
+
entryFromTarget(target, index, seedOffset + index + 1, stack)
|
|
138
|
+
);
|
|
139
|
+
});
|
|
140
|
+
return Object.freeze(stack);
|
|
141
|
+
}
|
|
142
|
+
function freezeState(stack, revision) {
|
|
143
|
+
return Object.freeze({
|
|
144
|
+
stack: Object.freeze([...stack]),
|
|
145
|
+
revision
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
function validateVisiblePanes(stack, selected) {
|
|
149
|
+
const indexes = selected.map(
|
|
150
|
+
(selectedEntry) => stack.findIndex((entry) => entry.key === selectedEntry.key)
|
|
151
|
+
);
|
|
152
|
+
if (indexes.some((index) => index === -1)) {
|
|
153
|
+
throw new Error("selectVisiblePanes returned an entry outside the stack.");
|
|
154
|
+
}
|
|
155
|
+
if (new Set(indexes).size !== indexes.length) {
|
|
156
|
+
throw new Error("selectVisiblePanes returned duplicate entries.");
|
|
157
|
+
}
|
|
158
|
+
if (indexes.some(
|
|
159
|
+
(index, position) => position > 0 && index <= indexes[position - 1]
|
|
160
|
+
)) {
|
|
161
|
+
throw new Error("selectVisiblePanes must preserve stack order.");
|
|
162
|
+
}
|
|
163
|
+
if (stack.length > 0 && !indexes.includes(stack.length - 1)) {
|
|
164
|
+
throw new Error("selectVisiblePanes must include the active pane.");
|
|
165
|
+
}
|
|
166
|
+
return Object.freeze(indexes.map((index) => stack[index]));
|
|
167
|
+
}
|
|
168
|
+
function snapshotFromResolvedConfig(config, state) {
|
|
169
|
+
const activePane = state.stack.at(-1);
|
|
170
|
+
const activeDefinition = activePane ? definitionFor(config, activePane.pane) : void 0;
|
|
171
|
+
const selected = config.selectVisiblePanes ? config.selectVisiblePanes(state.stack, {
|
|
172
|
+
activeDefinition,
|
|
173
|
+
definitions: config.panes
|
|
174
|
+
}) : state.stack.slice(
|
|
175
|
+
-(activeDefinition?.display?.maxVisible ?? config.maxVisible)
|
|
176
|
+
);
|
|
177
|
+
const visiblePanes = validateVisiblePanes(state.stack, selected);
|
|
178
|
+
return Object.freeze({
|
|
179
|
+
stack: state.stack,
|
|
180
|
+
revision: state.revision,
|
|
181
|
+
visiblePanes,
|
|
182
|
+
activePane,
|
|
183
|
+
canGoBack: state.stack.length > config.minimumDepth
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
function transitionWithResolvedConfig(config, state, event) {
|
|
187
|
+
const nextRevision = state.revision + 1;
|
|
188
|
+
switch (event.type) {
|
|
189
|
+
case "navigate": {
|
|
190
|
+
definitionFor(config, event.target.pane);
|
|
191
|
+
assertCanOpen(config, event.target.pane, state.stack.at(-1));
|
|
192
|
+
const added = entryFromTarget(
|
|
193
|
+
event.target,
|
|
194
|
+
state.stack.length,
|
|
195
|
+
nextRevision,
|
|
196
|
+
state.stack
|
|
197
|
+
);
|
|
198
|
+
const nextState = freezeState([...state.stack, added], nextRevision);
|
|
199
|
+
return {
|
|
200
|
+
state: nextState,
|
|
201
|
+
change: Object.freeze({ type: "navigate", added })
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
case "back": {
|
|
205
|
+
const steps = event.steps ?? 1;
|
|
206
|
+
assertPositiveInteger(steps, "back steps");
|
|
207
|
+
const removeCount = Math.min(
|
|
208
|
+
steps,
|
|
209
|
+
Math.max(0, state.stack.length - config.minimumDepth)
|
|
210
|
+
);
|
|
211
|
+
if (removeCount === 0) {
|
|
212
|
+
return { state, change: void 0 };
|
|
213
|
+
}
|
|
214
|
+
const removed = Object.freeze(state.stack.slice(-removeCount));
|
|
215
|
+
const nextState = freezeState(
|
|
216
|
+
state.stack.slice(0, -removeCount),
|
|
217
|
+
nextRevision
|
|
218
|
+
);
|
|
219
|
+
return {
|
|
220
|
+
state: nextState,
|
|
221
|
+
change: Object.freeze({ type: "back", removed })
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
case "back-to": {
|
|
225
|
+
let index = -1;
|
|
226
|
+
if (event.target.key !== void 0) {
|
|
227
|
+
index = state.stack.findIndex(
|
|
228
|
+
(entry) => entry.key === event.target.key
|
|
229
|
+
);
|
|
230
|
+
} else {
|
|
231
|
+
for (let cursor = state.stack.length - 1; cursor >= 0; cursor -= 1) {
|
|
232
|
+
if (state.stack[cursor]?.pane === event.target.pane) {
|
|
233
|
+
index = cursor;
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if (index === -1) {
|
|
239
|
+
const description = event.target.key !== void 0 ? `key "${event.target.key}"` : `pane "${event.target.pane}"`;
|
|
240
|
+
throw new Error(
|
|
241
|
+
`Cannot go back to ${description}; it is not in the stack.`
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
if (index + 1 < config.minimumDepth) {
|
|
245
|
+
throw new Error("Cannot go back below minimumDepth.");
|
|
246
|
+
}
|
|
247
|
+
if (index === state.stack.length - 1) {
|
|
248
|
+
return { state, change: void 0 };
|
|
249
|
+
}
|
|
250
|
+
const removed = Object.freeze(state.stack.slice(index + 1));
|
|
251
|
+
const nextState = freezeState(
|
|
252
|
+
state.stack.slice(0, index + 1),
|
|
253
|
+
nextRevision
|
|
254
|
+
);
|
|
255
|
+
return {
|
|
256
|
+
state: nextState,
|
|
257
|
+
change: Object.freeze({ type: "back-to", removed })
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
case "replace": {
|
|
261
|
+
if (state.stack.length === 0) {
|
|
262
|
+
throw new Error("Cannot replace a pane in an empty stack.");
|
|
263
|
+
}
|
|
264
|
+
definitionFor(config, event.target.pane);
|
|
265
|
+
const parent = state.stack.at(-2);
|
|
266
|
+
assertCanOpen(config, event.target.pane, parent);
|
|
267
|
+
const removed = state.stack.at(-1);
|
|
268
|
+
const parentStack = state.stack.slice(0, -1);
|
|
269
|
+
const added = entryFromTarget(
|
|
270
|
+
event.target,
|
|
271
|
+
state.stack.length - 1,
|
|
272
|
+
nextRevision,
|
|
273
|
+
parentStack
|
|
274
|
+
);
|
|
275
|
+
const nextState = freezeState([...parentStack, added], nextRevision);
|
|
276
|
+
return {
|
|
277
|
+
state: nextState,
|
|
278
|
+
change: Object.freeze({ type: "replace", added, removed })
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
case "reset": {
|
|
282
|
+
const targets = event.stack ?? config.initialStack;
|
|
283
|
+
if (targets.length < config.minimumDepth) {
|
|
284
|
+
throw new Error("Reset stack cannot be smaller than minimumDepth.");
|
|
285
|
+
}
|
|
286
|
+
const added = buildStack(config, targets, nextRevision);
|
|
287
|
+
const removed = Object.freeze([...state.stack]);
|
|
288
|
+
if (added.length === state.stack.length && added.every((entry, index) => {
|
|
289
|
+
const current = state.stack[index];
|
|
290
|
+
return current?.key === entry.key && current.pane === entry.pane && current.params === entry.params && current.data === entry.data;
|
|
291
|
+
})) {
|
|
292
|
+
return { state, change: void 0 };
|
|
293
|
+
}
|
|
294
|
+
const nextState = freezeState(added, nextRevision);
|
|
295
|
+
return {
|
|
296
|
+
state: nextState,
|
|
297
|
+
change: Object.freeze({ type: "reset", added, removed })
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
function definePaneConfig(config) {
|
|
303
|
+
return config;
|
|
304
|
+
}
|
|
305
|
+
function createPaneState(config) {
|
|
306
|
+
const resolved = resolveConfig(config);
|
|
307
|
+
const stack = buildStack(resolved, resolved.initialStack, 0);
|
|
308
|
+
return freezeState(stack, 0);
|
|
309
|
+
}
|
|
310
|
+
function transitionPaneState(config, state, event) {
|
|
311
|
+
return transitionWithResolvedConfig(resolveConfig(config), state, event);
|
|
312
|
+
}
|
|
313
|
+
function createPaneSnapshot(config, state) {
|
|
314
|
+
return snapshotFromResolvedConfig(resolveConfig(config), state);
|
|
315
|
+
}
|
|
316
|
+
var internal = {
|
|
317
|
+
resolveConfig,
|
|
318
|
+
snapshotFromResolvedConfig,
|
|
319
|
+
transitionWithResolvedConfig
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
// src/manager.ts
|
|
323
|
+
function createPaneManager(config) {
|
|
324
|
+
const resolved = internal.resolveConfig(config);
|
|
325
|
+
let state = createPaneState(config);
|
|
326
|
+
let snapshot = internal.snapshotFromResolvedConfig(resolved, state);
|
|
327
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
328
|
+
const dispatch = (event) => {
|
|
329
|
+
const transition = internal.transitionWithResolvedConfig(
|
|
330
|
+
resolved,
|
|
331
|
+
state,
|
|
332
|
+
event
|
|
333
|
+
);
|
|
334
|
+
if (transition.change === void 0) {
|
|
335
|
+
return snapshot;
|
|
336
|
+
}
|
|
337
|
+
const nextSnapshot = internal.snapshotFromResolvedConfig(
|
|
338
|
+
resolved,
|
|
339
|
+
transition.state
|
|
340
|
+
);
|
|
341
|
+
state = transition.state;
|
|
342
|
+
snapshot = nextSnapshot;
|
|
343
|
+
for (const listener of [...listeners]) {
|
|
344
|
+
listener(snapshot, transition.change);
|
|
345
|
+
}
|
|
346
|
+
return snapshot;
|
|
347
|
+
};
|
|
348
|
+
return {
|
|
349
|
+
getSnapshot() {
|
|
350
|
+
return snapshot;
|
|
351
|
+
},
|
|
352
|
+
getDefinition(pane) {
|
|
353
|
+
if (!Object.prototype.hasOwnProperty.call(resolved.panes, pane)) {
|
|
354
|
+
throw new Error(`Unknown pane "${pane}".`);
|
|
355
|
+
}
|
|
356
|
+
return resolved.panes[pane];
|
|
357
|
+
},
|
|
358
|
+
navigate(target) {
|
|
359
|
+
return dispatch({ type: "navigate", target });
|
|
360
|
+
},
|
|
361
|
+
back(options = {}) {
|
|
362
|
+
return dispatch(
|
|
363
|
+
options.steps === void 0 ? { type: "back" } : { type: "back", steps: options.steps }
|
|
364
|
+
);
|
|
365
|
+
},
|
|
366
|
+
backTo(target) {
|
|
367
|
+
return dispatch({ type: "back-to", target });
|
|
368
|
+
},
|
|
369
|
+
replace(target) {
|
|
370
|
+
return dispatch({ type: "replace", target });
|
|
371
|
+
},
|
|
372
|
+
reset(stack) {
|
|
373
|
+
return dispatch(
|
|
374
|
+
stack === void 0 ? { type: "reset" } : { type: "reset", stack }
|
|
375
|
+
);
|
|
376
|
+
},
|
|
377
|
+
canNavigate(target) {
|
|
378
|
+
try {
|
|
379
|
+
internal.transitionWithResolvedConfig(resolved, state, {
|
|
380
|
+
type: "navigate",
|
|
381
|
+
target
|
|
382
|
+
});
|
|
383
|
+
return true;
|
|
384
|
+
} catch {
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
387
|
+
},
|
|
388
|
+
dispatch,
|
|
389
|
+
subscribe(listener) {
|
|
390
|
+
listeners.add(listener);
|
|
391
|
+
return () => {
|
|
392
|
+
listeners.delete(listener);
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
398
|
+
0 && (module.exports = {
|
|
399
|
+
createPaneManager,
|
|
400
|
+
createPaneSnapshot,
|
|
401
|
+
createPaneState,
|
|
402
|
+
definePaneConfig,
|
|
403
|
+
transitionPaneState
|
|
404
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { P as PaneSchema, a as PaneManagerConfig, b as PaneState, c as PaneSnapshot, D as DefinedPaneConfig, d as PaneEvent, e as PaneTransition, f as PaneManager } from './types-DW8hgJuJ.cjs';
|
|
2
|
+
export { g as PaneBackToTarget, h as PaneChange, i as PaneData, j as PaneDefinition, k as PaneDefinitions, l as PaneDisplayContext, m as PaneEntry, n as PaneEntryFor, o as PaneListener, p as PaneMeta, q as PaneName, r as PaneParams, s as PaneTarget, t as PaneTargetFor } from './types-DW8hgJuJ.cjs';
|
|
3
|
+
|
|
4
|
+
declare function definePaneConfig<Schema extends PaneSchema>(config: PaneManagerConfig<Schema>): DefinedPaneConfig<Schema>;
|
|
5
|
+
declare function createPaneState<Schema extends PaneSchema>(config: PaneManagerConfig<Schema>): PaneState<Schema>;
|
|
6
|
+
declare function transitionPaneState<Schema extends PaneSchema>(config: PaneManagerConfig<Schema>, state: PaneState<Schema>, event: PaneEvent<Schema>): PaneTransition<Schema>;
|
|
7
|
+
declare function createPaneSnapshot<Schema extends PaneSchema>(config: PaneManagerConfig<Schema>, state: PaneState<Schema>): PaneSnapshot<Schema>;
|
|
8
|
+
|
|
9
|
+
declare function createPaneManager<Schema extends PaneSchema>(config: PaneManagerConfig<Schema>): PaneManager<Schema>;
|
|
10
|
+
|
|
11
|
+
export { DefinedPaneConfig, PaneEvent, PaneManager, PaneManagerConfig, PaneSchema, PaneSnapshot, PaneState, PaneTransition, createPaneManager, createPaneSnapshot, createPaneState, definePaneConfig, transitionPaneState };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { P as PaneSchema, a as PaneManagerConfig, b as PaneState, c as PaneSnapshot, D as DefinedPaneConfig, d as PaneEvent, e as PaneTransition, f as PaneManager } from './types-DW8hgJuJ.js';
|
|
2
|
+
export { g as PaneBackToTarget, h as PaneChange, i as PaneData, j as PaneDefinition, k as PaneDefinitions, l as PaneDisplayContext, m as PaneEntry, n as PaneEntryFor, o as PaneListener, p as PaneMeta, q as PaneName, r as PaneParams, s as PaneTarget, t as PaneTargetFor } from './types-DW8hgJuJ.js';
|
|
3
|
+
|
|
4
|
+
declare function definePaneConfig<Schema extends PaneSchema>(config: PaneManagerConfig<Schema>): DefinedPaneConfig<Schema>;
|
|
5
|
+
declare function createPaneState<Schema extends PaneSchema>(config: PaneManagerConfig<Schema>): PaneState<Schema>;
|
|
6
|
+
declare function transitionPaneState<Schema extends PaneSchema>(config: PaneManagerConfig<Schema>, state: PaneState<Schema>, event: PaneEvent<Schema>): PaneTransition<Schema>;
|
|
7
|
+
declare function createPaneSnapshot<Schema extends PaneSchema>(config: PaneManagerConfig<Schema>, state: PaneState<Schema>): PaneSnapshot<Schema>;
|
|
8
|
+
|
|
9
|
+
declare function createPaneManager<Schema extends PaneSchema>(config: PaneManagerConfig<Schema>): PaneManager<Schema>;
|
|
10
|
+
|
|
11
|
+
export { DefinedPaneConfig, PaneEvent, PaneManager, PaneManagerConfig, PaneSchema, PaneSnapshot, PaneState, PaneTransition, createPaneManager, createPaneSnapshot, createPaneState, definePaneConfig, transitionPaneState };
|