@omerdlw/base-framework 2.0.1 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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-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-6ZJVLUBE.js → chunk-I5GK7VOV.js} +8 -6
- package/dist/chunk-I5GK7VOV.js.map +1 -0
- package/dist/chunk-RIOKLMYM.js +145 -0
- package/dist/chunk-RIOKLMYM.js.map +1 -0
- package/dist/{chunk-SKBITB5X.js → chunk-RRAZKP3J.js} +3 -3
- package/dist/{chunk-SKBITB5X.js.map → chunk-RRAZKP3J.js.map} +1 -1
- package/dist/{chunk-QXR55VVS.js → chunk-TG6NMHXI.js} +241 -162
- package/dist/chunk-TG6NMHXI.js.map +1 -0
- package/dist/{chunk-A5RE3F6O.js → chunk-WVRMABDN.js} +249 -427
- package/dist/chunk-WVRMABDN.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.map +1 -1
- package/dist/modules/dock/constants.d.ts +1 -35
- package/dist/modules/dock/constants.d.ts.map +1 -1
- package/dist/modules/dock/index.js +3 -2
- package/dist/modules/dock/motion.d.ts +7 -5
- package/dist/modules/dock/motion.d.ts.map +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 -3
- package/dist/modules/dock/runtime/dock.d.ts.map +1 -1
- package/dist/modules/dock/runtime/pipeline.d.ts.map +1 -1
- package/dist/modules/dock/styles.d.ts +1 -1
- package/dist/modules/dock/styles.d.ts.map +1 -1
- package/dist/modules/dock/surface/hooks.d.ts.map +1 -1
- package/dist/modules/dock/surface/stack.d.ts.map +1 -1
- package/dist/modules/dock/types.d.ts +88 -43
- 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 +31 -13
- package/docs/modules/loading.md +2 -2
- package/docs/modules/navigation.md +216 -0
- package/docs/proposals/navigation.md +209 -0
- package/docs/theming.md +1 -1
- package/package.json +6 -1
- package/templates/AGENTS.md +2 -0
- package/templates/motion/dock.ts +83 -15
- package/dist/chunk-6SG7V77J.js.map +0 -1
- package/dist/chunk-6ZJVLUBE.js.map +0 -1
- package/dist/chunk-A5RE3F6O.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
|
|
|
@@ -248,6 +248,7 @@ Everything below is exported from `@omerdlw/base-framework/modules/dock`. Anythi
|
|
|
248
248
|
| `isOverlay`, `dismissible` | `boolean` | Overlay cards dim the page and ignore outside clicks. |
|
|
249
249
|
| `dockPolicy` | `{ clearTransientState?, dismissSurfaces?, prefetch? }` | Route policy used when navigating **to** this card ([§12.4](#124-route-policy)). |
|
|
250
250
|
| `keepWhenDescendant`, `prefetchDisabled` | `boolean \| (activePath, item) => boolean`, `boolean` | By default a card whose path is a **prefix of the active path** (an ancestor, except `/`) is removed from the stack so the same section is not shown twice; `keepWhenDescendant` keeps it. `prefetchDisabled` opts out of prefetch. |
|
|
251
|
+
| `stackOrder` | `number` | Where the card sits in the stack under the active card, ascending from the card nearest to it. Cards without one keep registration order at `0`, and `/` falls back to last. Use a negative value to pull a card toward the active one. |
|
|
251
252
|
| `registry` | `RegistryMetadata` | `priority`, `source`, `lifecycle`, … for this registration. |
|
|
252
253
|
|
|
253
254
|
How `usePage` turns the slice into registry entries (`dockModule.page`):
|
|
@@ -270,6 +271,8 @@ How `usePage` turns the slice into registry entries (`dockModule.page`):
|
|
|
270
271
|
|
|
271
272
|
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
273
|
|
|
274
|
+
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.
|
|
275
|
+
|
|
273
276
|
## 7. Attention: who owns the top card?
|
|
274
277
|
|
|
275
278
|
`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.
|
|
@@ -552,12 +555,13 @@ The starter `src/app/error.tsx` (Next.js's segment boundary, which catches page
|
|
|
552
555
|
Returns `Promise<boolean>` — `true` only when the route change was committed.
|
|
553
556
|
|
|
554
557
|
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.
|
|
558
|
+
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.
|
|
559
|
+
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`).
|
|
560
|
+
4. Navigation holds run (exit animations, `useNavigationHold`).
|
|
561
|
+
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`.
|
|
562
|
+
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
563
|
|
|
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.
|
|
564
|
+
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
565
|
|
|
562
566
|
### 12.2 Guards
|
|
563
567
|
|
|
@@ -577,7 +581,7 @@ const { isActive, setGuard, clearGuard } = useDockGuard({
|
|
|
577
581
|
|
|
578
582
|
### 12.3 Prefetch
|
|
579
583
|
|
|
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.
|
|
584
|
+
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
585
|
|
|
582
586
|
### 12.4 Route policy
|
|
583
587
|
|
|
@@ -655,18 +659,33 @@ See [media.md](./media.md) for how sources are registered (`usePage({ media })`,
|
|
|
655
659
|
| :-------------------- | :-------------------------------------------------------------------------------------- |
|
|
656
660
|
| Card base height | 68 px; HUD 52 px; chrome 20 px; viewport margin 24 px |
|
|
657
661
|
| Card width | 460 px (clamped to `viewport − 16/32 px`), or the card/surface `width` |
|
|
658
|
-
| Visible stacked cards | 3 (`MAX_VISIBLE_STACKED_CARDS`); collapsed scale 0.88 per level, opacity −0.2 per level |
|
|
659
662
|
| Published height | `--dock-h` on `<html>` and `dockHeight` in state (card height + 16 px buffer) |
|
|
660
663
|
|
|
664
|
+
These are what the layout **measures**, so they stay in the module. Where the cards *rest* is yours: `motion.deck.stacking` holds the offsets, scales, depth fade and how many cards stay in view ([§17](#17-theming)). If you change a card's height, change `stacking.expanded.offsetY` with it (the starter spaces expanded cards by the card height plus a 4 px gap).
|
|
665
|
+
|
|
661
666
|
Reserve space with `useDockHeight().padding` or `padding-bottom: var(--dock-h)`. Height changes are measured with a `ResizeObserver` and ignored below 0.5 px; during surface close phases the height is locked to the base height.
|
|
662
667
|
|
|
663
668
|
## 17. Theming
|
|
664
669
|
|
|
665
670
|
The module contains **no design classes**. Every slot comes from `dockTheme` (`src/config/dock.module.theme.ts`, 101 slots, named after the component that renders them): `stack`, `backdrop`, `card*`, `banner*`, `header*`, `title*`, `description*`, `icon*` (incl. `iconBadge`, `iconOverlay*`), `command*`, `status*`, `surface*`, `control*` (incl. `controlHeaderButton`), `extension*`, `scrubber*`, `media*`, `breadcrumbs*`, `loading*`, `action*`. State variants are `data-*` modifiers on the slot itself (`data-placement`, `data-blur`, `data-shelf`, `data-anchored`, `data-ghost`, `data-fill`, `data-active`, `data-banner`, `data-multiline`, `data-interactive`, `data-text`); the header of `src/config/dock.module.theme.ts` lists them. `styles` can add inline CSS per slot (z-index, banner masks, stack size/radius).
|
|
666
671
|
|
|
667
|
-
The theme also carries the dock's **motion** (`motion: DockMotion`, [theming §5](../theming.md#5-motion-your-app-owns-every-animation))
|
|
672
|
+
The theme also carries the dock's **motion** (`motion: DockMotion`, [theming §5](../theming.md#5-motion-your-app-owns-every-animation)). Everything the dock animates, and every number that shapes it, is yours:
|
|
673
|
+
|
|
674
|
+
| Key | What you decide |
|
|
675
|
+
| :-- | :-- |
|
|
676
|
+
| `transitions`, `variants` | Every transition and variant, including `transitions.layout` for parts that slide when their neighbours change (the surface close control, the breadcrumbs). |
|
|
677
|
+
| `phases` | What the deck, header and body play in each surface phase (`idle`, `dismissAction`, `expandBody`, `open`, `anticipation`, `collapseBody`, `restoreHeader`): the deck's size/shadow transition (`null` follows its resting one, a function gets the resize distance), its transform transition and `scale` (below 1 compresses it), the body's and header's transitions, and `headerReturn`. |
|
|
678
|
+
| `timings` | How long each phase holds (`dismissAction`, `anticipation`, `expandBody`, `collapseBody`, `restoreHeader`) plus the card staggers, in seconds. The surface state machine schedules its phases from these, so they follow the app's tempo. `restoreHeader: 0` skips that phase. |
|
|
679
|
+
| `deck` | `elevation`, `peek`, `exit`, `tapScale`; `stacking` (where cards rest: collapsed/expanded offsets, scale, depth fade, the shelf, and how many cards stay in view normally, with an extension shelf and under a status); `breadcrumbOffset`; `fullscreenOpacity`; `origin`; `resizeFallback`. |
|
|
680
|
+
| `opacity` | The resting opacity of the card **banner** and **description**. A page's own `bannerOpacity` or description `style.opacity` still wins. |
|
|
681
|
+
| `drag` | The swipe-to-dismiss feel (`elastic`, `range`, `opacity`, `scale`) and `dismiss: { offset, velocity }`, the distance (px) or speed (px/s) past which a swipe closes the surface. |
|
|
682
|
+
| `skeleton` | The class of the loading skeletons. |
|
|
683
|
+
|
|
684
|
+
Start from `templates/motion/dock.ts`, the dock's original motion. Geometry and opacities are not times, so tempo and reduced motion leave them alone; only transitions and `timings` are stretched or collapsed.
|
|
685
|
+
|
|
686
|
+
**The phases and their order belong to the dock.** A surface always opens `dismissAction → expandBody → open` and closes `anticipation → collapseBody → restoreHeader`; the render code relies on that sequence. You decide how each phase looks and how long it holds, not which phases exist.
|
|
668
687
|
|
|
669
|
-
Stays in code:
|
|
688
|
+
Stays in code: what the layout measures (card, HUD and chrome heights, viewport margin), per-position z-index (`Z_INDEX.DOCK_CARD_STACK_BASE − position`), compositor hints, the swipe's direction and its non-draggable targets, and the focus trap. The theme and its motion are **required**: a missing theme throws `Theme "dock" is missing…` and a theme without motion throws `Motion for theme "dock" is missing…`. `DockProvider` on its own (no dock theme) still runs; its surface phases advance immediately.
|
|
670
689
|
|
|
671
690
|
Per-card overrides use `style` on the card (see [§6.1](#61-dockpageconfig-the-dock-key-of-usepage)). Custom buttons inside surfaces should use `useDockActionClass()` so they match.
|
|
672
691
|
|
|
@@ -719,8 +738,7 @@ Gotchas:
|
|
|
719
738
|
| `runtime/display.ts` | Item pipeline: registry entries → filtered items → active item (applies surface / status / media). |
|
|
720
739
|
| `runtime/pipeline.ts` | Item identity, active item detection, ancestor deduplication, loading item cleanup, and stacking reorder. |
|
|
721
740
|
| `runtime/commands.ts` | Context-action registry, `useDockContextActions`, `defineDockAction`. |
|
|
722
|
-
| `routing/navigation.ts` | `useDockNavigation
|
|
723
|
-
| `routing/transactions.ts` | Navigation transaction reducer and timeout. |
|
|
741
|
+
| `routing/navigation.ts` | `useDockNavigation` (on a navigation trip), route policy, topology, prefetch, location key, view connection. |
|
|
724
742
|
| `routing/guards.ts` | Guard registry, `useDockGuard`, guard confirmation. |
|
|
725
743
|
| `routing/continuity.ts` | Scroll/focus snapshots and return handoffs. |
|
|
726
744
|
| `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.
|