@omerdlw/base-framework 2.0.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/dist/{chunk-XASQT4CQ.js → chunk-66EYJBIP.js} +47 -27
- package/dist/chunk-66EYJBIP.js.map +1 -0
- package/dist/{chunk-65ZB4QTQ.js → chunk-CYU6SGAL.js} +3 -3
- package/dist/{chunk-65ZB4QTQ.js.map → chunk-CYU6SGAL.js.map} +1 -1
- package/dist/{chunk-6SG7V77J.js → chunk-HHWRTQKQ.js} +100 -67
- package/dist/chunk-HHWRTQKQ.js.map +1 -0
- package/dist/chunk-HLL5EHXC.js +485 -0
- package/dist/chunk-HLL5EHXC.js.map +1 -0
- package/dist/{chunk-3MSK4XGK.js → chunk-JIXBFFRD.js} +258 -405
- package/dist/chunk-JIXBFFRD.js.map +1 -0
- package/dist/{chunk-KKQT46TP.js → chunk-M57SNWXK.js} +8 -6
- package/dist/chunk-M57SNWXK.js.map +1 -0
- package/dist/chunk-RIOKLMYM.js +145 -0
- package/dist/chunk-RIOKLMYM.js.map +1 -0
- package/dist/{chunk-QXR55VVS.js → chunk-TG6NMHXI.js} +241 -162
- package/dist/chunk-TG6NMHXI.js.map +1 -0
- package/dist/index.js +7 -5
- package/dist/modules/ambient/analyze.d.ts +18 -3
- package/dist/modules/ambient/analyze.d.ts.map +1 -1
- package/dist/modules/ambient/dom.d.ts +14 -1
- package/dist/modules/ambient/dom.d.ts.map +1 -1
- package/dist/modules/ambient/hooks.d.ts.map +1 -1
- package/dist/modules/ambient/index.d.ts +2 -2
- package/dist/modules/ambient/index.d.ts.map +1 -1
- package/dist/modules/ambient/index.js +1 -1
- package/dist/modules/ambient/server/index.d.ts +1 -1
- package/dist/modules/ambient/server/index.d.ts.map +1 -1
- package/dist/modules/ambient/server/index.js +2 -2
- package/dist/modules/ambient/server/index.js.map +1 -1
- package/dist/modules/ambient/settings.d.ts +1 -0
- package/dist/modules/ambient/settings.d.ts.map +1 -1
- package/dist/modules/ambient/types.d.ts +13 -0
- package/dist/modules/ambient/types.d.ts.map +1 -1
- package/dist/modules/ambient/worker/index.js +1 -1
- package/dist/modules/context-menu/index.js +4 -3
- package/dist/modules/dock/cards.d.ts +3 -0
- package/dist/modules/dock/cards.d.ts.map +1 -1
- package/dist/modules/dock/constants.d.ts +1 -20
- package/dist/modules/dock/constants.d.ts.map +1 -1
- package/dist/modules/dock/index.js +3 -2
- package/dist/modules/dock/layout.d.ts.map +1 -1
- package/dist/modules/dock/motion.d.ts +6 -0
- package/dist/modules/dock/motion.d.ts.map +1 -1
- package/dist/modules/dock/overlay.d.ts +1 -1
- package/dist/modules/dock/overlay.d.ts.map +1 -1
- package/dist/modules/dock/routing/guards.d.ts +1 -1
- package/dist/modules/dock/routing/navigation.d.ts +7 -4
- package/dist/modules/dock/routing/navigation.d.ts.map +1 -1
- package/dist/modules/dock/runtime/display.d.ts +31 -2
- package/dist/modules/dock/runtime/display.d.ts.map +1 -1
- package/dist/modules/dock/runtime/dock.d.ts +2 -2
- package/dist/modules/dock/runtime/dock.d.ts.map +1 -1
- package/dist/modules/dock/surface/view-model.d.ts.map +1 -1
- package/dist/modules/dock/types.d.ts +9 -27
- package/dist/modules/dock/types.d.ts.map +1 -1
- package/dist/modules/index.d.ts +3 -2
- package/dist/modules/index.d.ts.map +1 -1
- package/dist/modules/index.js +7 -5
- package/dist/modules/loading/context.d.ts.map +1 -1
- package/dist/modules/loading/index.js +1 -1
- package/dist/modules/navigation/constants.d.ts +37 -0
- package/dist/modules/navigation/constants.d.ts.map +1 -0
- package/dist/modules/navigation/context.d.ts +10 -0
- package/dist/modules/navigation/context.d.ts.map +1 -0
- package/dist/modules/navigation/hooks.d.ts +29 -0
- package/dist/modules/navigation/hooks.d.ts.map +1 -0
- package/dist/modules/navigation/index.d.ts +10 -0
- package/dist/modules/navigation/index.d.ts.map +1 -0
- package/dist/modules/navigation/index.js +81 -0
- package/dist/modules/navigation/index.js.map +1 -0
- package/dist/modules/navigation/listeners.d.ts +15 -0
- package/dist/modules/navigation/listeners.d.ts.map +1 -0
- package/dist/modules/navigation/module.d.ts +25 -0
- package/dist/modules/navigation/module.d.ts.map +1 -0
- package/dist/modules/navigation/navigator.d.ts +8 -0
- package/dist/modules/navigation/navigator.d.ts.map +1 -0
- package/dist/modules/navigation/settings.d.ts +10 -0
- package/dist/modules/navigation/settings.d.ts.map +1 -0
- package/dist/modules/navigation/state.d.ts +20 -0
- package/dist/modules/navigation/state.d.ts.map +1 -0
- package/dist/modules/navigation/types.d.ts +168 -0
- package/dist/modules/navigation/types.d.ts.map +1 -0
- package/dist/modules/navigation/utils.d.ts +11 -0
- package/dist/modules/navigation/utils.d.ts.map +1 -0
- package/docs/getting-started.md +2 -0
- package/docs/modules/README.md +5 -1
- package/docs/modules/ambient.md +9 -6
- package/docs/modules/dock.md +13 -11
- package/docs/modules/loading.md +2 -2
- package/docs/modules/navigation.md +216 -0
- package/docs/proposals/navigation.md +209 -0
- package/package.json +6 -1
- package/templates/AGENTS.md +2 -0
- package/dist/chunk-3MSK4XGK.js.map +0 -1
- package/dist/chunk-6SG7V77J.js.map +0 -1
- package/dist/chunk-KKQT46TP.js.map +0 -1
- package/dist/chunk-QXR55VVS.js.map +0 -1
- package/dist/chunk-XASQT4CQ.js.map +0 -1
- package/dist/modules/dock/routing/transactions.d.ts +0 -21
- package/dist/modules/dock/routing/transactions.d.ts.map +0 -1
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import type { ExternalStore } from "../../utils";
|
|
2
|
+
/**
|
|
3
|
+
* What a departing page hands the destination. Empty by default: apps add
|
|
4
|
+
* their own keys by augmentation, so the framework depends on none of them.
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* declare module "@omerdlw/base-framework/modules/navigation" {
|
|
8
|
+
* interface NavigationPayload {
|
|
9
|
+
* palette?: AmbientPalette | null;
|
|
10
|
+
* }
|
|
11
|
+
* }
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
export interface NavigationPayload {
|
|
15
|
+
}
|
|
16
|
+
export type NavigationPayloadKey = keyof NavigationPayload & string;
|
|
17
|
+
export type NavigationStatus = "pending" | "completed" | "cancelled" | "failed" | "timed-out";
|
|
18
|
+
/** `leaving`: holds run, the route has not been asked for yet. `routing`: it has. */
|
|
19
|
+
export type NavigationPhase = "leaving" | "routing";
|
|
20
|
+
export interface NavigationTrip {
|
|
21
|
+
/** Where the trip actually landed, when it differs from `to` (a redirect). */
|
|
22
|
+
arrivedAt?: string;
|
|
23
|
+
endedAt?: number;
|
|
24
|
+
error?: unknown;
|
|
25
|
+
/** The location key the trip left from. */
|
|
26
|
+
from: string;
|
|
27
|
+
/** The href as given, passed to the router unchanged. */
|
|
28
|
+
href: string;
|
|
29
|
+
id: number;
|
|
30
|
+
payload: Readonly<NavigationPayload>;
|
|
31
|
+
phase: NavigationPhase;
|
|
32
|
+
reason?: string | null;
|
|
33
|
+
/** Aborts when the trip is cancelled, fails or times out. */
|
|
34
|
+
signal: AbortSignal;
|
|
35
|
+
/** `dock`, `history`, `navigate`, or whatever the caller passed. */
|
|
36
|
+
source: string;
|
|
37
|
+
startedAt: number;
|
|
38
|
+
status: NavigationStatus;
|
|
39
|
+
/** The destination's location key (pathname, search and hash). */
|
|
40
|
+
to: string;
|
|
41
|
+
}
|
|
42
|
+
export interface NavigationState {
|
|
43
|
+
active: NavigationTrip | null;
|
|
44
|
+
last: NavigationTrip | null;
|
|
45
|
+
/** The location key the navigator last saw arrive. */
|
|
46
|
+
location: string;
|
|
47
|
+
}
|
|
48
|
+
export type NavigationPrefetchKind = "auto" | "full" | "temporary";
|
|
49
|
+
export interface NavigationRouter {
|
|
50
|
+
back?(): void;
|
|
51
|
+
prefetch(href: string, options?: {
|
|
52
|
+
kind?: NavigationPrefetchKind;
|
|
53
|
+
onInvalidate?: () => void;
|
|
54
|
+
}): void;
|
|
55
|
+
push(href: string, options?: {
|
|
56
|
+
scroll?: boolean;
|
|
57
|
+
}): void;
|
|
58
|
+
replace(href: string, options?: {
|
|
59
|
+
scroll?: boolean;
|
|
60
|
+
}): void;
|
|
61
|
+
}
|
|
62
|
+
export interface NavigationBlock {
|
|
63
|
+
blocked: boolean;
|
|
64
|
+
message?: string;
|
|
65
|
+
[key: string]: unknown;
|
|
66
|
+
}
|
|
67
|
+
export interface NavigateOptions {
|
|
68
|
+
/** Skip `guard`. Holds still run. */
|
|
69
|
+
force?: boolean;
|
|
70
|
+
/**
|
|
71
|
+
* Runs once the trip has started and before holds. A `blocked` result
|
|
72
|
+
* cancels the trip with reason `blocked`. The dock runs its guards here.
|
|
73
|
+
*/
|
|
74
|
+
guard?: (trip: NavigationTrip) => NavigationBlock | boolean | Promise<NavigationBlock | boolean>;
|
|
75
|
+
/** Runs right before the router is called, once guard and holds passed. */
|
|
76
|
+
onCommit?: (trip: NavigationTrip) => void;
|
|
77
|
+
/** Merged over what `provide` contributes. */
|
|
78
|
+
payload?: Partial<NavigationPayload>;
|
|
79
|
+
replace?: boolean;
|
|
80
|
+
scroll?: boolean;
|
|
81
|
+
source?: string;
|
|
82
|
+
}
|
|
83
|
+
export interface NavigationResult {
|
|
84
|
+
blocked?: NavigationBlock;
|
|
85
|
+
/** `true` when the router was asked for the route. */
|
|
86
|
+
ok: boolean;
|
|
87
|
+
trip: NavigationTrip | null;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Runs when a trip leaves. The trip commits once every returned promise
|
|
91
|
+
* settles, or after `maxHoldMs`. Watch `trip.signal` to undo an exit that
|
|
92
|
+
* never lands.
|
|
93
|
+
*/
|
|
94
|
+
export type NavigationHold = (trip: NavigationTrip) => void | Promise<unknown>;
|
|
95
|
+
export interface NavigationHoldOptions {
|
|
96
|
+
/** Also hold back/forward steps. Default `true`. */
|
|
97
|
+
history?: boolean;
|
|
98
|
+
}
|
|
99
|
+
export type NavigationPayloadValue<K extends NavigationPayloadKey> = NavigationPayload[K] | (() => NavigationPayload[K]);
|
|
100
|
+
export type NavigationIntentListener = (path: string, href: string) => void;
|
|
101
|
+
export interface NavigationPrefetchOptions {
|
|
102
|
+
/** Skip the warm cache and prefetch again. */
|
|
103
|
+
force?: boolean;
|
|
104
|
+
kind?: NavigationPrefetchKind;
|
|
105
|
+
}
|
|
106
|
+
export interface NavigationIntentSettings {
|
|
107
|
+
/** Mouse dwell before a hovered link is prefetched. */
|
|
108
|
+
delayMs: number;
|
|
109
|
+
/** Which links to warm. Internal hrefs only, whatever this returns. */
|
|
110
|
+
match: (href: string) => boolean;
|
|
111
|
+
}
|
|
112
|
+
export interface NavigationSettings {
|
|
113
|
+
/** How long a settled trip stays readable by `useArrival`. */
|
|
114
|
+
arrivalTtlMs: number;
|
|
115
|
+
/** Document-level intent prefetch for `a[href]`; `false` turns it off. */
|
|
116
|
+
intent: NavigationIntentSettings | false;
|
|
117
|
+
/** Cap on holds; past it the trip commits anyway. */
|
|
118
|
+
maxHoldMs: number;
|
|
119
|
+
prefetchKind: NavigationPrefetchKind;
|
|
120
|
+
/** How long a warmed path is not prefetched again. */
|
|
121
|
+
prefetchTtlMs: number;
|
|
122
|
+
/** Patience once the route was asked for; then the trip times out. */
|
|
123
|
+
timeoutMs: number;
|
|
124
|
+
}
|
|
125
|
+
export interface NavigationSettingsInput extends Partial<Omit<NavigationSettings, "intent">> {
|
|
126
|
+
intent?: Partial<NavigationIntentSettings> | boolean;
|
|
127
|
+
}
|
|
128
|
+
export interface NavigatorOptions {
|
|
129
|
+
emit?: (event: string, payload: Record<string, unknown>) => void;
|
|
130
|
+
/** Runs `callback` after the next paint. */
|
|
131
|
+
nextFrame?: (callback: () => void) => void;
|
|
132
|
+
now?: () => number;
|
|
133
|
+
/** The browser's current location key, or `null` off the browser. */
|
|
134
|
+
readLocation?: () => string | null;
|
|
135
|
+
router?: NavigationRouter | null;
|
|
136
|
+
settings?: () => NavigationSettings;
|
|
137
|
+
}
|
|
138
|
+
export interface NavigationActions {
|
|
139
|
+
/** The fresh trip that ended at `pathname`, or `null`. */
|
|
140
|
+
arrival(pathname: string): NavigationTrip | null;
|
|
141
|
+
/** Tells the navigator the location changed. The provider calls it. */
|
|
142
|
+
arrive(location: string): void;
|
|
143
|
+
/** Cancels the active trip. */
|
|
144
|
+
cancel(reason?: string | null): boolean;
|
|
145
|
+
/** Starts a trip from the browser's own back/forward step. */
|
|
146
|
+
depart(to: string, options?: {
|
|
147
|
+
replay?: () => void;
|
|
148
|
+
source?: string;
|
|
149
|
+
}): NavigationTrip | null;
|
|
150
|
+
hold(hold: NavigationHold, options?: NavigationHoldOptions): () => void;
|
|
151
|
+
/** Whether a hold wants back/forward steps. */
|
|
152
|
+
holdsHistory(): boolean;
|
|
153
|
+
navigate(href: string, options?: NavigateOptions): Promise<NavigationResult>;
|
|
154
|
+
onIntent(listener: NavigationIntentListener): () => void;
|
|
155
|
+
prefetch(href: string, options?: NavigationPrefetchOptions): boolean;
|
|
156
|
+
provide<K extends NavigationPayloadKey>(key: K, value: NavigationPayloadValue<K>): () => void;
|
|
157
|
+
/** Points the navigator at another router (the provider's). */
|
|
158
|
+
setRouter(router: NavigationRouter | null): void;
|
|
159
|
+
}
|
|
160
|
+
export interface Navigator extends NavigationActions {
|
|
161
|
+
dispose(): void;
|
|
162
|
+
store: ExternalStore<NavigationState>;
|
|
163
|
+
}
|
|
164
|
+
export interface NavigationRuntime {
|
|
165
|
+
actions: NavigationActions;
|
|
166
|
+
store: ExternalStore<NavigationState>;
|
|
167
|
+
}
|
|
168
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/modules/navigation/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare function isSafeInternalHref(value: unknown): boolean;
|
|
2
|
+
/** The pathname of an href, without search, hash or trailing slash. */
|
|
3
|
+
export declare function pathOf(href: string): string;
|
|
4
|
+
/** An href's location key: normalized pathname plus its search and hash. */
|
|
5
|
+
export declare function locationKeyOf(href: string): string;
|
|
6
|
+
export declare function isSameLocation(left: string, right: string): boolean;
|
|
7
|
+
export declare function isSamePathname(left: string, right: string): boolean;
|
|
8
|
+
export declare function readWindowLocation(): string | null;
|
|
9
|
+
/** The link with an internal href around `target`, or `null`. */
|
|
10
|
+
export declare function internalLinkOf(target: EventTarget | null): Element | null;
|
|
11
|
+
//# sourceMappingURL=utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../src/modules/navigation/utils.ts"],"names":[],"mappings":"AAEA,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAG1D;AAED,uEAAuE;AACvE,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE3C;AAED,4EAA4E;AAC5E,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAKlD;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAEnE;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAEnE;AAED,wBAAgB,kBAAkB,IAAI,MAAM,GAAG,IAAI,CAIlD;AAED,iEAAiE;AACjE,wBAAgB,cAAc,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,GAAG,OAAO,GAAG,IAAI,CAKzE"}
|
package/docs/getting-started.md
CHANGED
|
@@ -77,6 +77,7 @@ import { ambientModule } from "@omerdlw/base-framework/modules/ambient";
|
|
|
77
77
|
import { backgroundModule } from "@omerdlw/base-framework/modules/background";
|
|
78
78
|
import { controlsModule } from "@omerdlw/base-framework/modules/controls";
|
|
79
79
|
import { loadingModule } from "@omerdlw/base-framework/modules/loading";
|
|
80
|
+
import { navigationModule } from "@omerdlw/base-framework/modules/navigation";
|
|
80
81
|
import { contextMenuModule } from "@omerdlw/base-framework/modules/context-menu";
|
|
81
82
|
|
|
82
83
|
// Define the modules your application uses
|
|
@@ -88,6 +89,7 @@ const modules = [
|
|
|
88
89
|
backgroundModule,
|
|
89
90
|
controlsModule,
|
|
90
91
|
loadingModule,
|
|
92
|
+
navigationModule,
|
|
91
93
|
contextMenuModule,
|
|
92
94
|
];
|
|
93
95
|
|
package/docs/modules/README.md
CHANGED
|
@@ -12,6 +12,7 @@ One page per module/core entry. Each page answers, from the big picture down to
|
|
|
12
12
|
| Dock | `@omerdlw/base-framework/modules/dock` | [dock.md](./dock.md) | The only chrome: card stack, surfaces/flows, HUD, operations, status overlays, guards, breadcrumbs | `dock` | `dock` |
|
|
13
13
|
| Modal | `@omerdlw/base-framework/modules/modal` | [modal.md](./modal.md) | Promise-based stacked dialogs | `modal` | `modal` |
|
|
14
14
|
| Notification | `@omerdlw/base-framework/modules/notification` | [notification.md](./notification.md) | Single-slot toasts docked to the dock; event-driven feedback | `notification` | — |
|
|
15
|
+
| Navigation | `@omerdlw/base-framework/modules/navigation` | [navigation.md](./navigation.md) | Trips: exit holds, payload to the next page, back-button hold, intent prefetch | — | — |
|
|
15
16
|
| Loading | `@omerdlw/base-framework/modules/loading` | [loading.md](./loading.md) | Loading overlay + busy flag | `loading` | `loading` |
|
|
16
17
|
| Background | `@omerdlw/base-framework/modules/background` | [background.md](./background.md) | Image / video / YouTube layer behind the page | `background` | `background` |
|
|
17
18
|
| Media | `@omerdlw/base-framework/modules/media` | [media.md](./media.md) | The active media session the dock controls operate on | `media` | `media` |
|
|
@@ -44,6 +45,7 @@ Modules never import each other. They connect through three channels only:
|
|
|
44
45
|
| dock → `loading` | `definePeer` | Page-loading flag; `stopLoading()` after navigation. |
|
|
45
46
|
| dock → `media` | `definePeer` | Play/pause icon, media controls. |
|
|
46
47
|
| dock → `notification` | `definePeer` | Is a toast visible (breadcrumbs yield). |
|
|
48
|
+
| dock → `navigation` | `useModule` | Transactions are its trips; card prefetch uses its warm cache. Without it the dock runs its own navigator. |
|
|
47
49
|
| background → `media` | `definePeer` | Registers the background video as source `background`. |
|
|
48
50
|
| ambient → `background` | `useModuleState` | Poster URL as the default tint image. |
|
|
49
51
|
| context-menu → `dock` | `useModuleState` | Active card as the default menu header. |
|
|
@@ -55,7 +57,7 @@ Every `definePeer`/`useModuleState`/`useModule` call needs the **peer id as a st
|
|
|
55
57
|
|
|
56
58
|
## 3. Install-time facts
|
|
57
59
|
|
|
58
|
-
- `src/modules/index.ts` exports every module and `defaultModules` (`background, ambient, dock, controls, contextMenu, loading, media, modal, notification`); it is the only place that lists them all. `src/app/providers.tsx` passes the list to `CoreProvider`.
|
|
60
|
+
- `src/modules/index.ts` exports every module and `defaultModules` (`background, ambient, navigation, dock, controls, contextMenu, loading, media, modal, notification`); it is the only place that lists them all. `src/app/providers.tsx` passes the list to `CoreProvider`.
|
|
59
61
|
- Install a subset to ship less: `modules={[dockModule, notificationModule, loadingModule]}`.
|
|
60
62
|
- Module ids are **camelCase** (`contextMenu`); the folder and theme id may be kebab-case (`context-menu`).
|
|
61
63
|
- A module is installable when it has a `module.tsx` (or `module.ts`) with `defineModule`; the order of the list does not matter (`uses` sorts it).
|
|
@@ -97,6 +99,7 @@ Rules of thumb:
|
|
|
97
99
|
| loading | ✅ | ✅ | ✅ | ✅ | — | ✅ | — | |
|
|
98
100
|
| media | ✅ | ✅ | ✅ | ✅ | — | ✅ | — | |
|
|
99
101
|
| modal | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | |
|
|
102
|
+
| navigation | ✅ | ✅ | ✅ | ✅ | ✅ | — | — | `navigator.ts`, `listeners.ts`, `settings.ts` |
|
|
100
103
|
| notification | ✅ | `module.ts` | ✅ | ✅ | — | ✅ | ✅ | |
|
|
101
104
|
|
|
102
105
|
### 4.2 Why some optional slots are absent
|
|
@@ -106,6 +109,7 @@ Rules of thumb:
|
|
|
106
109
|
| `ambient` | `overlay`, `motion` | Renders nothing: it writes CSS custom properties; transitions are CSS classes from the theme. |
|
|
107
110
|
| `controls` | `context`, `motion` | Stateless: entries live in the registry and layout is measured from the DOM. No animation. |
|
|
108
111
|
| `loading` | `motion`, `state` | The overlay renders `Spinner` (or a skeleton) and mounts/unmounts instantly; state is a small `useState` in the provider. |
|
|
112
|
+
| `navigation` | `overlay`, `motion` | Renders nothing: it binds the router and listens to the document; animation is the app's, in its holds. |
|
|
109
113
|
| `media` | `motion` | Renders nothing; `overlay.tsx` holds only the components that return `null` (`MediaSource`, `MediaOverlay`). |
|
|
110
114
|
| `notification` | builder | Toasts are imperative: `useToast` (in `hooks.ts`) plays the builder role. |
|
|
111
115
|
| core `error` | provider, builder, view | Boundaries are class components configured through props; `boundary.tsx` is the view, `listener.ts` and `reporter.ts` are topic files. |
|
package/docs/modules/ambient.md
CHANGED
|
@@ -119,7 +119,7 @@ Default names; see §5.2 to rename them or turn groups off. Nothing is written u
|
|
|
119
119
|
|
|
120
120
|
`colors` keys are custom property names: a key starting with `--` is used verbatim; anything else becomes `--color-<key>`. `colors` always wins over extracted values.
|
|
121
121
|
|
|
122
|
-
|
|
122
|
+
Themes on the same element stack: the latest wins each variable, and when one goes the one beneath shows again; when the last goes, the inline values (and priorities) from before are restored exactly. A page-wide theme **lingers** `linger` ms (3 s) after it goes, and the next theme to arrive takes over from it at once: so a page that leaves through a `loading.tsx` hands its colours to the next page instead of dropping to the CSS fallbacks for the length of the loading screen. With no next theme, the variables are removed when the time runs out. If `transition` is true (default) the theme's `transition` classes are added to the target while the theme is active (and removed again only for scoped targets, so the global tint keeps animating between pages).
|
|
123
123
|
|
|
124
124
|
## 5. Public API
|
|
125
125
|
|
|
@@ -135,6 +135,7 @@ On cleanup the previous inline values (and priorities) are restored exactly. If
|
|
|
135
135
|
| `extractPaletteFromImage(src, options?)` | util | Async client extraction with cache and in-flight de-duplication; resolves `null` on any failure. |
|
|
136
136
|
| `primeAmbientPalette(src, palette)`, `getCachedAmbientPalette(src)` | utils | Seed / read the client cache (e.g. with server palettes). |
|
|
137
137
|
| `analyzeAmbientPixels(pixels, options?)` | util | The pure analysis on RGBA pixels (`{ data, width, height }`). Same function on server, worker and client. |
|
|
138
|
+
| `AMBIENT_ANALYSIS_VERSION`, `skinLikeness(color)` | const / util | The analysis version (put it in long-lived palette cache keys); how skin-like an OKLCH colour is (0–1). |
|
|
138
139
|
| `findContentBounds(pixels)` | util | The letterbox / pillarbox cut used by the analysis. |
|
|
139
140
|
| `maxChroma(l, h)`, `toneOf(h, l, rel, cap?)`, `onColorFor(bg, min?)`, `contrastRatio(a, b)`, `formatOklch(lch)`, `srgbToOklab(r, g, b)` | utils | Gamut-aware OKLCH helpers. |
|
|
140
141
|
| `paletteVarMap(palette, names?)`, `resolveAmbientVarMap(…)`, `resolveCssVarNames(overrides?)` | utils | The variable maps the hooks write. |
|
|
@@ -151,6 +152,7 @@ On cleanup the previous inline values (and priorities) are restored exactly. If
|
|
|
151
152
|
| `cssVars` | `null` | Per-theme override of the variable names (same shape as `configureAmbient({ cssVars })`). |
|
|
152
153
|
| `scope` | `null` → `document.documentElement` | CSS selector string, element, or `ref`. A selector that matches nothing applies nothing. |
|
|
153
154
|
| `transition` | `true` | Add the theme's transition classes to the target. |
|
|
155
|
+
| `linger` | `configureAmbient` `linger` (3000) on the page root, `0` on a scoped target | How long (ms) the variables stay after the theme goes, unless another theme takes over first. |
|
|
154
156
|
| `initialPalette` | `null` | Palette shown before extraction finishes (and if it fails); used as is (no extraction) when `source` equals `image`. |
|
|
155
157
|
| `options` | `null` | `{ initialPalette, strategy, proxy, analysis }` for this theme. |
|
|
156
158
|
|
|
@@ -168,6 +170,7 @@ Settings live on `globalThis`, so every bundled copy of the module (one per pack
|
|
|
168
170
|
| `cssVars` | `--ambient-shadow`, `--ambient-{role}`, `--ambient-on-{role}`, `--ambient-{role}-{tone}`, `--ambient-{region}`, all 11 tones | Rename any variable; `null` turns that variable or group off; `tones: [10, 20, 90]` or `false` limits the ramp variables. |
|
|
169
171
|
| `analysis` | `DEFAULT_ANALYSIS` | `sampleSize` 128, `clusters` 7, `centerWeight` 0.5, `letterbox` true, `minHueDistance` 40, `minContrast` 4.5, `shadowLightness` 0.16. |
|
|
170
172
|
| `cacheLimit` | `100` | Client palettes kept (oldest evicted). |
|
|
173
|
+
| `linger` | `3000` (`DEFAULT_LINGER_MS`) | How long (ms) a page-wide theme's variables stay after it goes, so the next page takes over from them. `0` removes them at once. |
|
|
171
174
|
|
|
172
175
|
### 5.3 `AmbientPalette`
|
|
173
176
|
|
|
@@ -209,15 +212,15 @@ Before every request, redirects included, the host is resolved (`resolve`, defau
|
|
|
209
212
|
|
|
210
213
|
- **Loading (client).** The image URL is fetched as is with `mode: "cors"`: no `?cors=1`, so the request the page already made is answered from the HTTP cache. If that is blocked and the image is cross-origin, it is fetched once more with `cache: "reload"`: some CDNs (TMDB's among them) send CORS headers only to requests that carry an `Origin` and do not `Vary: Origin`, so the copy a plain `<img>` cached has none. Only then is the proxy tried. Give such images `crossOrigin="anonymous"` (`next/image` takes it too) and the one download serves both the `<img>` and the palette, with no blocked request in the console. Decoding uses `createImageBitmap` (asking for a 128 px wide bitmap where supported) and an `OffscreenCanvas` (or a `<canvas>`), so a full-size image is never drawn on the main thread; browsers without `createImageBitmap` fall back to an `<img>`.
|
|
211
214
|
- **Sampling.** The image is scaled to 128 px on its long side, keeping its aspect ratio. Pixels with alpha < 128 are skipped. Uniformly near-black rows/columns at the edges (letterbox / pillarbox; mean L < 0.12, max L < 0.2, at most 22% per side) are cut. Each pixel's weight falls from 1.5 at the centre to 0.5 at the corners (`centerWeight` 0.5).
|
|
212
|
-
- **Clustering.** Pixels are binned by 15-bit RGB (OKLab averaged per bin). Coloured bins (chroma ≥ 0.022) and grey bins are clustered **apart** with weighted, deterministically seeded k-means in OKLab (ΔL counts 0.45×, so hue beats lightness; vivid bins pull up to ~2.5× harder), 5 + 2 clusters by default. Splitting them keeps the greys of a dark poster from using up every cluster and swallowing its one coloured detail.
|
|
213
|
-
- **Roles.** A hue's share is that of its family (clusters within 20°). `dominant` = max share × chroma; `vibrant` = max chroma² × share^0.3; `accent` = the best hue at least `minHueDistance` (40°) from `dominant`, or `vibrant` marked `fromImage: false` when the image has one hue; `muted` = the largest low-chroma (≤ 0.09) colour, or a quiet tone of the dominant hue. Hue trust fades below L 0.28 and above L 0.9.
|
|
214
|
-
- **Tones.**
|
|
215
|
+
- **Clustering.** Pixels are binned by 15-bit RGB (OKLab averaged per bin). Coloured bins (chroma ≥ 0.022) and grey bins are clustered **apart** with weighted, deterministically seeded k-means in OKLab (ΔL counts 0.45×, so hue beats lightness; vivid bins pull up to ~2.5× harder), 5 + 2 clusters by default. Splitting them keeps the greys of a dark poster from using up every cluster and swallowing its one coloured detail. The vivid pull only shapes the clusters: a cluster's colour is the plain mean of its pixels, so a face is not tinted toward the red title it shares a cluster with.
|
|
216
|
+
- **Roles.** A hue's share is that of its family (clusters within 20°). `dominant` = max share × chroma; `vibrant` = max chroma² × share^0.3; `accent` = the best hue at least `minHueDistance` (40°) from `dominant`, or `vibrant` marked `fromImage: false` when the image has one hue; `muted` = the largest low-chroma (≤ 0.09) colour, or a quiet tone of the dominant hue. Skin, hair, wood and tungsten-lit walls (hues near 54°, chroma 0.03–0.12, see `skinLikeness`) count up to 65% less in every pick, roles and regions alike: film frames are full of them, and they are what a frame shows, rarely its mood. Hue trust fades below L 0.28 and above L 0.9.
|
|
217
|
+
- **Tones.** A role keeps about its source's **chroma**, not its share of the gamut: × 1.15 (`dominant`, regions), × 1.3 (`accent`), × 1.35 (`vibrant`), × 1 (`muted`); a faint tint is raised to at most 0.05 so it still reads as a colour; skin is held back up to 30%; and reds and oranges get a 30% lower ceiling, since they read louder than blues at the same chroma. Carrying the gamut share instead (as before `AMBIENT_ANALYSIS_VERSION` 2) turned pale faces and beige rooms into loud orange: at mid lightness orange has twice the room teal has. The ramp keeps the role's share of the gamut at each L (`toneOf`, × 0.96 of `maxChroma`), so a dark tone of blue stays blue; nothing is ever out of gamut.
|
|
215
218
|
- **`shadow`.** The hue of the shadows (clusters at L ≤ 0.5, scored by share × relative chroma) or, without coloured shadows, of `dominant`; at L `shadowLightness` ± 0.02 and up to 0.1 chroma.
|
|
216
219
|
- **`on`.** A near-white or near-black carrying a hint of the role's hue, pushed toward pure white / black until it reaches `minContrast`.
|
|
217
|
-
- **Regions.** Each region (thirds of the content box; `center` is the middle half) takes its most chromatic-and-present cluster at L 0.42–0.72; `strength` is how coloured the region is.
|
|
220
|
+
- **Regions.** Each region (thirds of the content box; `center` is the middle half) takes its most chromatic-and-present cluster, coloured by that cluster's own pixels **in the region** (not its frame-wide mean), at L 0.42–0.72 and × 1.15 its chroma; `strength` is how coloured the region is.
|
|
218
221
|
- **Neutral images.** If coloured pixels make up under 3% (weighted), the palette is grey: `neutral: true`, `dominant`, `vibrant`, `muted`, `shadow` and regions have chroma 0. A vivid detail (≥ 0.6% of the image, chroma ≥ 0.06) still becomes `accent`, e.g. the red coat in *Schindler's List*.
|
|
219
222
|
- **Cost.** About 3–8 ms per image for the analysis (Node, warm); in Chromium, ~13 ms for a cached image end to end on the main thread, ~20 ms through the worker (off the main thread). ~2.7 KB of JSON per palette.
|
|
220
|
-
- **Cache.** Client: `cacheLimit` palettes (oldest evicted), keyed by URL (+ analysis overrides); concurrent requests share one promise. Server: 500 per process.
|
|
223
|
+
- **Cache.** Client: `cacheLimit` palettes (oldest evicted), keyed by URL (+ analysis overrides); concurrent requests share one promise. Server: 500 per process. A cache that outlives a deploy (`unstable_cache`, a database) should carry `AMBIENT_ANALYSIS_VERSION` in its key, so palettes are computed again when the analysis changes.
|
|
221
224
|
- **Stability.** `colors`, `cssVars`, `options` and `initialPalette` are stabilized with `shallowEqual`, so fresh object literals each render do **not** re-apply the theme (keep `cssVars.tones` a constant array). The same pixels always give the same palette.
|
|
222
225
|
- **Failure.** Client: `extractPaletteFromImage` resolves `null` and the hooks keep `initialPalette` (or nothing); it never throws. Server: `null`, except a missing decoder.
|
|
223
226
|
|
package/docs/modules/dock.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
| **Renders** | `Dock` overlay, portalled to `document.body`, DOM id `dock-card-stack` |
|
|
12
12
|
| **Registry type** | `dock` (key policy `path`, lifecycle `route`, cleanup delay 600 ms, entries merged by priority) |
|
|
13
13
|
| **Page slice** | `usePage({ dock: {…} })` → controller `page.modules.dock` |
|
|
14
|
-
| **Peers (`uses`)** | `loading`, `media`, `notification` (optional; inert stand-ins when absent)
|
|
14
|
+
| **Peers (`uses`)** | `loading`, `media`, `navigation`, `notification` (optional; inert stand-ins when absent, a private navigator for `navigation`) |
|
|
15
15
|
| **Theme** | `dockTheme` slots in `src/config/dock.module.theme.ts` |
|
|
16
16
|
| **Events it owns** | `DOCK_STATUS_SET`, `DOCK_STATUS_CLEAR`, `DOCK_GUARD`, `DOCK_NOT_FOUND`, `DOCK_NAVIGATE_START` / `DOCK_NAVIGATE` / `DOCK_NAVIGATE_END`, `DOCK_UPDATE_BADGE` |
|
|
17
17
|
| **CSS output** | `--dock-h` on `<html>` (dock height + 16 px buffer) |
|
|
@@ -35,7 +35,7 @@ Three ideas explain everything else:
|
|
|
35
35
|
|
|
36
36
|
1. **Cards come from the registry.** A page registers a card (`usePage({ dock })`, `useDockRegistration`, or static entries in `src/app/registry.tsx`). The card whose `path` matches the URL is the active item. Expanding the stack lists all registered cards and turns it into a navigator.
|
|
37
37
|
2. **One thing owns the top card at a time ("attention").** Surface, status, HUD, operation, loading and the plain route all compete; the highest priority wins (see [§7](#7-attention-who-owns-the-top-card)).
|
|
38
|
-
3. **Navigation goes through the dock.** `navigate()` runs guards, records scroll/focus so "back" restores position, tracks a transaction (timeout, supersession), and prefetches on hover intent.
|
|
38
|
+
3. **Navigation goes through the dock.** `navigate()` runs guards, records scroll/focus so "back" restores position, tracks a transaction (timeout, supersession), and prefetches on hover intent. The transaction is a trip of the [navigation module](./navigation.md) when it is installed.
|
|
39
39
|
|
|
40
40
|
## 3. Quick start
|
|
41
41
|
|
|
@@ -270,6 +270,8 @@ How `usePage` turns the slice into registry entries (`dockModule.page`):
|
|
|
270
270
|
|
|
271
271
|
The active card is chosen from registered items in this order: a selected data-source item → exact `path` / `targetPath` match → longest path-prefix match → first item. On a 404 page only `/` and the not-found card are listed. While a navigation transaction is pending, the card for the **destination** is used so the dock changes before the route does.
|
|
272
272
|
|
|
273
|
+
When the path changes and no card claims it yet (only `/` as a prefix, or the first item), the dock **holds the previous card** on top instead of falling back to Home. This covers a `loading.tsx` boundary or a slow fetch before the new page's `usePage` registers. The held card carries `isRoutePending: true` and stays even if its own registration is gone. The hold lasts while the navigation is in flight plus `DOCK_PENDING_ROUTE_HOLD_MS` (2.5 s) after it commits. If no card arrives by then, the usual fallback applies. The first page load has nothing to hold.
|
|
274
|
+
|
|
273
275
|
## 7. Attention: who owns the top card?
|
|
274
276
|
|
|
275
277
|
`resolveDockAttention` scores each candidate and the highest wins. A new candidate with a higher score takes over immediately; when it goes away the next one returns.
|
|
@@ -339,7 +341,7 @@ The dock itself closes surfaces with these reasons: `"dock"` (route change, prog
|
|
|
339
341
|
|
|
340
342
|
Focus returns to the element that opened the surface after the last surface closes, except for `browser-back`, `dock`, `unmount`.
|
|
341
343
|
|
|
342
|
-
Lifecycle phases (`DOCK_SURFACE_PHASE`): `idle → dismissing_action → expanding_body → open → closing_anticipation → collapsing_body → restoring_header → idle`. They drive animation and
|
|
344
|
+
Lifecycle phases (`DOCK_SURFACE_PHASE`): `idle → dismissing_action → expanding_body → open → closing_anticipation → collapsing_body → restoring_header → idle`. They drive animation and are exposed as `surfacePhase` in `DockState`. In `dismissing_action`, `collapsing_body` and `restoring_header` the card faces the page: it shows the page's header and action (the surface item carries the page's action as `restingAction`) and keeps the page's measured height, so the card resizes once, straight between the page's size and the surface's. Set `timings.restoreHeader` to `0` to go from `collapsing_body` straight to `idle`.
|
|
343
345
|
|
|
344
346
|
### 8.5 Steps (wizards)
|
|
345
347
|
|
|
@@ -552,12 +554,13 @@ The starter `src/app/error.tsx` (Next.js's segment boundary, which catches page
|
|
|
552
554
|
Returns `Promise<boolean>` — `true` only when the route change was committed.
|
|
553
555
|
|
|
554
556
|
1. Rejects unsafe or non-internal hrefs and the current location (`false`).
|
|
555
|
-
2. Starts a **transaction** `{ id, from, to, source, status }` (superseding any pending one).
|
|
556
|
-
3. Unless `force`, awaits every registered guard. If one blocks → the transaction is cancelled with reason `guard`, a `GUARD` status is shown, and the promise resolves `false`. "Leave" later re-commits through the same path (source `guard-confirmation`).
|
|
557
|
-
4.
|
|
558
|
-
5.
|
|
557
|
+
2. Starts a **transaction** `{ id, from, to, source, status }` (superseding any pending one). The transaction is a navigation trip ([navigation.md](./navigation.md)): of the shared navigator when the `navigation` module is installed, otherwise of a navigator the dock creates for itself.
|
|
558
|
+
3. Unless `force`, awaits every registered guard (as the trip's `guard`). If one blocks → the transaction is cancelled with reason `guard`, a `GUARD` status is shown, and the promise resolves `false`. "Leave" later re-commits through the same path (source `guard-confirmation`, `force`).
|
|
559
|
+
4. Navigation holds run (exit animations, `useNavigationHold`).
|
|
560
|
+
5. Commit: remember scroll/focus of the old path, blur the active element, prepare the route reset policy, emit `DOCK_NAVIGATE_START`, `router.push`, emit `DOCK_NAVIGATE`.
|
|
561
|
+
6. `DOCK_NAVIGATE_END` is emitted when the location key changes (or in the next frame for same-path hash/query changes); the `loading` module is told to stop.
|
|
559
562
|
|
|
560
|
-
A transaction times out after 15 s (`stopLoading` is called). Statuses: `pending → completed | cancelled | failed | timed-out`. Expanding or navigating collapses the stack and clears hover/search.
|
|
563
|
+
A transaction times out after 15 s from its start and again from its commit (`navigation` `timeoutMs`; `stopLoading` is called). With the module installed, `dockTransaction` is also set for trips the dock did not start (back/forward, `useNavigation().navigate`), so the dock shows the destination card for them too. Statuses: `pending → completed | cancelled | failed | timed-out`. Expanding or navigating collapses the stack and clears hover/search.
|
|
561
564
|
|
|
562
565
|
### 12.2 Guards
|
|
563
566
|
|
|
@@ -577,7 +580,7 @@ const { isActive, setGuard, clearGuard } = useDockGuard({
|
|
|
577
580
|
|
|
578
581
|
### 12.3 Prefetch
|
|
579
582
|
|
|
580
|
-
Hovering a card for 90 ms prefetches its `path` with `router.prefetch` (leaving the card cancels a pending prefetch); focusing a card with the keyboard prefetches its `targetPath ?? path` **immediately**. Not prefetched: loading, overlay and surface cards, cards with `prefetchDisabled` or `dockPolicy.prefetch: false`. A route is prefetched once; if Next.js invalidates it, it can be prefetched again.
|
|
583
|
+
Hovering a card for 90 ms prefetches its `path` with `router.prefetch` (leaving the card cancels a pending prefetch); focusing a card with the keyboard prefetches its `targetPath ?? path` **immediately**. Not prefetched: loading, overlay and surface cards, cards with `prefetchDisabled` or `dockPolicy.prefetch: false`. A route is prefetched once; if Next.js invalidates it, it can be prefetched again. With the `navigation` module installed the prefetch goes through its shared warm cache (`prefetchKind`, `prefetchTtlMs`, intent listeners), the same one plain links use.
|
|
581
584
|
|
|
582
585
|
### 12.4 Route policy
|
|
583
586
|
|
|
@@ -719,8 +722,7 @@ Gotchas:
|
|
|
719
722
|
| `runtime/display.ts` | Item pipeline: registry entries → filtered items → active item (applies surface / status / media). |
|
|
720
723
|
| `runtime/pipeline.ts` | Item identity, active item detection, ancestor deduplication, loading item cleanup, and stacking reorder. |
|
|
721
724
|
| `runtime/commands.ts` | Context-action registry, `useDockContextActions`, `defineDockAction`. |
|
|
722
|
-
| `routing/navigation.ts` | `useDockNavigation
|
|
723
|
-
| `routing/transactions.ts` | Navigation transaction reducer and timeout. |
|
|
725
|
+
| `routing/navigation.ts` | `useDockNavigation` (on a navigation trip), route policy, topology, prefetch, location key, view connection. |
|
|
724
726
|
| `routing/guards.ts` | Guard registry, `useDockGuard`, guard confirmation. |
|
|
725
727
|
| `routing/continuity.ts` | Scroll/focus snapshots and return handoffs. |
|
|
726
728
|
| `routing/breadcrumbs.tsx` | Breadcrumb resolution, provider, overrides, `defineBreadcrumb`. |
|
package/docs/modules/loading.md
CHANGED
|
@@ -95,7 +95,7 @@ CheckoutLoading.use(isSubmitting); // boolean | message string | LoadingOptions
|
|
|
95
95
|
| `stopLoading()` | Turn it off. If `minDuration` has not elapsed yet, the stop is **delayed** by the remaining time. With no `minDuration` it is immediate. |
|
|
96
96
|
| `setLoading(boolean)` | Shorthand for the two above (no options). |
|
|
97
97
|
| `setSkeleton(nodeOrUpdater)` | Replace the manual skeleton; accepts `(current) => next`. No re-render when unchanged. |
|
|
98
|
-
| `withLoading(task, options \| message)` |
|
|
98
|
+
| `withLoading(task, options \| message)` | Starts its own task, awaits the promise (or calls the function and awaits its result), ends the task in `finally`. Rejections propagate. |
|
|
99
99
|
|
|
100
100
|
`page.modules.loading` = these actions plus `set(loading)` (`page.set({ loading })`).
|
|
101
101
|
|
|
@@ -115,7 +115,7 @@ The same value also feeds the dock's `isLoading` card flag (`usePage({ dock })`
|
|
|
115
115
|
- **Graceful cleanup.** The registry entry survives 600 ms after the page unmounts, so a route change does not flash the overlay off and on between pages.
|
|
116
116
|
- **`minDuration` is manual-only.** Page-registered loading ends as soon as the page stops declaring it.
|
|
117
117
|
- **Navigation.** The dock calls `stopLoading()` after a navigation completes or times out (15 s), so a `startLoading()` made before navigating does not stick.
|
|
118
|
-
- **
|
|
118
|
+
- **Overlapping tasks.** Each `withLoading` call is its own task: loading stays on until the **last** one ends, and the overlay shows the latest task's message and skeleton (the one beneath shows again when it ends). `startLoading` / `stopLoading` / `setLoading` are one more task, a single switch: calling `startLoading` twice and `stopLoading` once turns it off. `minDuration` counts from the first task to the last.
|
|
119
119
|
- **Fullscreen suppression.** While `useIsFullscreenStateActive()` is true (a full-screen view is active) the overlay is hidden.
|
|
120
120
|
- **Accessibility.** The overlay has `role="status"`, `aria-busy="true"` and `aria-label="Loading"`.
|
|
121
121
|
- **Next.js `loading.tsx`.** `src/app/loading.tsx` (a bare `Spinner`) is the route-segment fallback and is independent of this module.
|