@omerdlw/base-framework 1.0.5 → 2.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/dist/atoms/index.js +1 -103
- package/dist/atoms/index.js.map +1 -1
- package/dist/chunk-3BUP64Q6.js +105 -0
- package/dist/chunk-3BUP64Q6.js.map +1 -0
- package/dist/chunk-3MSK4XGK.js +9888 -0
- package/dist/chunk-3MSK4XGK.js.map +1 -0
- package/dist/chunk-4GMQKD6F.js +353 -0
- package/dist/chunk-4GMQKD6F.js.map +1 -0
- package/dist/chunk-65ZB4QTQ.js +888 -0
- package/dist/chunk-65ZB4QTQ.js.map +1 -0
- package/dist/chunk-6SG7V77J.js +577 -0
- package/dist/chunk-6SG7V77J.js.map +1 -0
- package/dist/chunk-7HQMXTNP.js +419 -0
- package/dist/chunk-7HQMXTNP.js.map +1 -0
- package/dist/chunk-7TAVYJ3A.js +371 -0
- package/dist/chunk-7TAVYJ3A.js.map +1 -0
- package/dist/chunk-AQ4BNXA2.js +349 -0
- package/dist/chunk-AQ4BNXA2.js.map +1 -0
- package/dist/chunk-CGHZSSFR.js +2166 -0
- package/dist/chunk-CGHZSSFR.js.map +1 -0
- package/dist/chunk-DGCQWQLZ.js +61 -0
- package/dist/chunk-DGCQWQLZ.js.map +1 -0
- package/dist/chunk-FIMPHODX.js +67 -0
- package/dist/chunk-FIMPHODX.js.map +1 -0
- package/dist/chunk-GF7TUX5I.js +1889 -0
- package/dist/chunk-GF7TUX5I.js.map +1 -0
- package/dist/chunk-JELG4JLT.js +454 -0
- package/dist/chunk-JELG4JLT.js.map +1 -0
- package/dist/chunk-K676GL7Z.js +51 -0
- package/dist/chunk-K676GL7Z.js.map +1 -0
- package/dist/chunk-KKQT46TP.js +26 -0
- package/dist/chunk-KKQT46TP.js.map +1 -0
- package/dist/chunk-QI3ALLIB.js +38 -0
- package/dist/chunk-QI3ALLIB.js.map +1 -0
- package/dist/chunk-QXR55VVS.js +1139 -0
- package/dist/chunk-QXR55VVS.js.map +1 -0
- package/dist/chunk-R2YGFJDJ.js +408 -0
- package/dist/chunk-R2YGFJDJ.js.map +1 -0
- package/dist/chunk-RFZQWLC5.js +27 -0
- package/dist/chunk-RFZQWLC5.js.map +1 -0
- package/dist/chunk-SYAVGTOX.js +521 -0
- package/dist/chunk-SYAVGTOX.js.map +1 -0
- package/dist/chunk-TV3WEF5G.js +784 -0
- package/dist/chunk-TV3WEF5G.js.map +1 -0
- package/dist/chunk-XASQT4CQ.js +280 -0
- package/dist/chunk-XASQT4CQ.js.map +1 -0
- package/dist/error/index.js +5 -682
- package/dist/error/index.js.map +1 -1
- package/dist/error/reporter.d.ts.map +1 -1
- package/dist/error/theme.d.ts +1 -1
- package/dist/error/theme.d.ts.map +1 -1
- package/dist/events.js +1 -7
- package/dist/events.js.map +1 -1
- package/dist/hooks/index.js +1 -358
- package/dist/hooks/index.js.map +1 -1
- package/dist/hooks/use-global-event.d.ts +1 -1
- package/dist/index.js +18 -20019
- package/dist/index.js.map +1 -1
- package/dist/kernel/index.js +2 -2035
- package/dist/kernel/index.js.map +1 -1
- package/dist/kernel/types.d.ts +1 -1
- package/dist/modules/ambient/analyze.d.ts +46 -0
- package/dist/modules/ambient/analyze.d.ts.map +1 -0
- package/dist/modules/ambient/constants.d.ts +1 -5
- package/dist/modules/ambient/constants.d.ts.map +1 -1
- package/dist/modules/ambient/context.d.ts.map +1 -1
- package/dist/modules/ambient/dom.d.ts +11 -12
- package/dist/modules/ambient/dom.d.ts.map +1 -1
- package/dist/modules/ambient/extractor.d.ts +12 -4
- package/dist/modules/ambient/extractor.d.ts.map +1 -1
- package/dist/modules/ambient/hooks.d.ts +1 -1
- package/dist/modules/ambient/hooks.d.ts.map +1 -1
- package/dist/modules/ambient/index.d.ts +6 -4
- package/dist/modules/ambient/index.d.ts.map +1 -1
- package/dist/modules/ambient/index.js +7 -753
- package/dist/modules/ambient/index.js.map +1 -1
- package/dist/modules/ambient/module.d.ts +1 -1
- package/dist/modules/ambient/module.d.ts.map +1 -1
- package/dist/modules/ambient/oklab.d.ts +36 -0
- package/dist/modules/ambient/oklab.d.ts.map +1 -0
- package/dist/modules/ambient/pipeline.d.ts +28 -0
- package/dist/modules/ambient/pipeline.d.ts.map +1 -0
- package/dist/modules/ambient/quantize.d.ts +22 -0
- package/dist/modules/ambient/quantize.d.ts.map +1 -0
- package/dist/modules/ambient/server/address.d.ts +8 -0
- package/dist/modules/ambient/server/address.d.ts.map +1 -0
- package/dist/modules/ambient/server/index.d.ts +53 -0
- package/dist/modules/ambient/server/index.d.ts.map +1 -0
- package/dist/modules/ambient/server/index.js +221 -0
- package/dist/modules/ambient/server/index.js.map +1 -0
- package/dist/modules/ambient/settings.d.ts +19 -0
- package/dist/modules/ambient/settings.d.ts.map +1 -0
- package/dist/modules/ambient/types.d.ts +105 -9
- package/dist/modules/ambient/types.d.ts.map +1 -1
- package/dist/modules/ambient/worker/index.d.ts +15 -0
- package/dist/modules/ambient/worker/index.d.ts.map +1 -0
- package/dist/modules/ambient/worker/index.js +114 -0
- package/dist/modules/ambient/worker/index.js.map +1 -0
- package/dist/modules/background/constants.d.ts +2 -2
- package/dist/modules/background/constants.d.ts.map +1 -1
- package/dist/modules/background/hooks.d.ts +1 -1
- package/dist/modules/background/index.js +6 -3078
- package/dist/modules/background/index.js.map +1 -1
- package/dist/modules/background/motion.d.ts +9 -7
- package/dist/modules/background/motion.d.ts.map +1 -1
- package/dist/modules/background/overlay.d.ts +3 -8
- package/dist/modules/background/overlay.d.ts.map +1 -1
- package/dist/modules/background/types.d.ts +16 -2
- package/dist/modules/background/types.d.ts.map +1 -1
- package/dist/modules/context-menu/constants.d.ts +2 -2
- package/dist/modules/context-menu/constants.d.ts.map +1 -1
- package/dist/modules/context-menu/hooks.d.ts +1 -1
- package/dist/modules/context-menu/index.js +10 -1896
- package/dist/modules/context-menu/index.js.map +1 -1
- package/dist/modules/context-menu/motion.d.ts +1 -8
- package/dist/modules/context-menu/motion.d.ts.map +1 -1
- package/dist/modules/context-menu/overlay.d.ts.map +1 -1
- package/dist/modules/context-menu/types.d.ts +16 -2
- package/dist/modules/context-menu/types.d.ts.map +1 -1
- package/dist/modules/controls/constants.d.ts +1 -1
- package/dist/modules/controls/constants.d.ts.map +1 -1
- package/dist/modules/controls/entries.d.ts +1 -1
- package/dist/modules/controls/hooks.d.ts +1 -1
- package/dist/modules/controls/index.js +4 -1110
- package/dist/modules/controls/index.js.map +1 -1
- package/dist/modules/controls/overlay.d.ts +1 -1
- package/dist/modules/controls/types.d.ts +2 -2
- package/dist/modules/dock/cards.d.ts +708 -24
- package/dist/modules/dock/cards.d.ts.map +1 -1
- package/dist/modules/dock/constants.d.ts +2 -2
- package/dist/modules/dock/constants.d.ts.map +1 -1
- package/dist/modules/dock/helpers.d.ts +2 -2
- package/dist/modules/dock/hooks.d.ts +2 -2
- package/dist/modules/dock/index.d.ts +1 -1
- package/dist/modules/dock/index.d.ts.map +1 -1
- package/dist/modules/dock/index.js +9 -12130
- package/dist/modules/dock/index.js.map +1 -1
- package/dist/modules/dock/module.d.ts +1 -1
- package/dist/modules/dock/motion.d.ts +41 -915
- package/dist/modules/dock/motion.d.ts.map +1 -1
- package/dist/modules/dock/overlay.d.ts.map +1 -1
- package/dist/modules/dock/runtime/dock.d.ts +1 -23
- package/dist/modules/dock/runtime/dock.d.ts.map +1 -1
- package/dist/modules/dock/styles.d.ts +4 -4
- package/dist/modules/dock/surface/context.d.ts.map +1 -1
- package/dist/modules/dock/surface/hooks.d.ts +10 -23
- package/dist/modules/dock/surface/hooks.d.ts.map +1 -1
- package/dist/modules/dock/surface/machine.d.ts +4 -3
- package/dist/modules/dock/surface/machine.d.ts.map +1 -1
- package/dist/modules/dock/surface/stack.d.ts.map +1 -1
- package/dist/modules/dock/types.d.ts +158 -3
- package/dist/modules/dock/types.d.ts.map +1 -1
- package/dist/modules/index.js +16 -17873
- package/dist/modules/index.js.map +1 -1
- package/dist/modules/loading/constants.d.ts +1 -1
- package/dist/modules/loading/constants.d.ts.map +1 -1
- package/dist/modules/loading/context.d.ts +1 -1
- package/dist/modules/loading/hooks.d.ts +1 -1
- package/dist/modules/loading/index.js +5 -1153
- package/dist/modules/loading/index.js.map +1 -1
- package/dist/modules/loading/types.d.ts +2 -2
- package/dist/modules/media/context.d.ts +1 -1
- package/dist/modules/media/index.js +3 -570
- package/dist/modules/media/index.js.map +1 -1
- package/dist/modules/media/types.d.ts +2 -2
- package/dist/modules/modal/constants.d.ts +2 -2
- package/dist/modules/modal/constants.d.ts.map +1 -1
- package/dist/modules/modal/context.d.ts +1 -1
- package/dist/modules/modal/hooks.d.ts +3 -3
- package/dist/modules/modal/index.js +8 -2355
- package/dist/modules/modal/index.js.map +1 -1
- package/dist/modules/modal/motion.d.ts +3 -8
- package/dist/modules/modal/motion.d.ts.map +1 -1
- package/dist/modules/modal/overlay.d.ts.map +1 -1
- package/dist/modules/modal/types.d.ts +21 -0
- package/dist/modules/modal/types.d.ts.map +1 -1
- package/dist/modules/notification/constants.d.ts +2 -2
- package/dist/modules/notification/constants.d.ts.map +1 -1
- package/dist/modules/notification/context.d.ts +1 -1
- package/dist/modules/notification/hooks.d.ts +1 -1
- package/dist/modules/notification/index.js +4 -828
- package/dist/modules/notification/index.js.map +1 -1
- package/dist/modules/notification/motion.d.ts +1 -7
- package/dist/modules/notification/motion.d.ts.map +1 -1
- package/dist/modules/notification/overlay.d.ts.map +1 -1
- package/dist/modules/notification/types.d.ts +9 -1
- package/dist/modules/notification/types.d.ts.map +1 -1
- package/dist/overlay-TZ6L52A5.js +9 -0
- package/dist/overlay-TZ6L52A5.js.map +1 -0
- package/dist/provider.d.ts +1 -1
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +6 -1968
- package/dist/provider.js.map +1 -1
- package/dist/result.js +2 -63
- package/dist/result.js.map +1 -1
- package/dist/theme.d.ts +45 -8
- package/dist/theme.d.ts.map +1 -1
- package/dist/theme.js +3 -41
- package/dist/theme.js.map +1 -1
- package/dist/tokens/index.d.ts +1 -0
- package/dist/tokens/index.d.ts.map +1 -1
- package/dist/tokens/index.js +126 -155
- package/dist/tokens/index.js.map +1 -1
- package/dist/tokens/motion-config.d.ts +44 -0
- package/dist/tokens/motion-config.d.ts.map +1 -0
- package/dist/tokens/motion.d.ts +23 -174
- package/dist/tokens/motion.d.ts.map +1 -1
- package/dist/utils/index.js +4 -82
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/report.d.ts.map +1 -1
- package/docs/README.md +1 -0
- package/docs/architecture-and-rules.md +4 -3
- package/docs/core-engine.md +7 -10
- package/docs/getting-started.md +4 -2
- package/docs/modules/ambient.md +194 -80
- package/docs/modules/background.md +3 -3
- package/docs/modules/context-menu.md +2 -2
- package/docs/modules/dock.md +8 -5
- package/docs/modules/modal.md +2 -2
- package/docs/modules/notification.md +2 -2
- package/docs/theming.md +81 -9
- package/package.json +22 -5
- package/templates/AGENTS.md +1 -1
- package/templates/motion/background.ts +17 -0
- package/templates/motion/context-menu.ts +63 -0
- package/templates/motion/dock.ts +327 -0
- package/templates/motion/modal.ts +79 -0
- package/templates/motion/notification.ts +27 -0
- package/templates/motion/tokens.ts +47 -0
- package/templates/tsconfig.json +7 -0
- package/dist/modules/ambient/color.d.ts +0 -16
- package/dist/modules/ambient/color.d.ts.map +0 -1
package/docs/modules/ambient.md
CHANGED
|
@@ -1,30 +1,41 @@
|
|
|
1
1
|
# Ambient — `@omerdlw/base-framework/modules/ambient`
|
|
2
2
|
|
|
3
|
-
> **Read this first if you need to:** tint a page from an image (cover, avatar, album art), set theme colors from code, scope a tint to one element, or know which CSS variables change.
|
|
3
|
+
> **Read this first if you need to:** tint a page from an image (cover, avatar, album art), compute a palette on the server and hand it to the client, use more than one colour from an image (roles, tonal ramps, region glows, text colours), set theme colors from code, scope a tint to one element, or know which CSS variables change.
|
|
4
4
|
|
|
5
5
|
## 1. At a glance
|
|
6
6
|
|
|
7
|
-
| |
|
|
8
|
-
| :----------------- |
|
|
9
|
-
| **Import** | `import { useAmbientTheme,
|
|
10
|
-
| **
|
|
11
|
-
| **
|
|
12
|
-
| **
|
|
13
|
-
| **
|
|
14
|
-
| **
|
|
15
|
-
| **
|
|
16
|
-
| **
|
|
17
|
-
| **
|
|
7
|
+
| | |
|
|
8
|
+
| :----------------- | :------------------------------------------------------------------------------------------------ |
|
|
9
|
+
| **Import** | `import { useAmbientTheme, configureAmbient, … } from "@omerdlw/base-framework/modules/ambient"` |
|
|
10
|
+
| **Server entry** | `import { extractAmbientPalette } from "@omerdlw/base-framework/ambient/server"` (Node; no React) |
|
|
11
|
+
| **Worker entry** | `import { serveAmbientWorker } from "@omerdlw/base-framework/ambient/worker"` |
|
|
12
|
+
| **Module id** | `ambient` (`ambientModule`, in `defaultModules`) |
|
|
13
|
+
| **Renders** | Nothing. It writes **CSS custom properties** on an element (default `<html>`). |
|
|
14
|
+
| **Registry type** | none (no `registry` definition; it only uses the page hook) |
|
|
15
|
+
| **Page slice** | `usePage({ ambient })` and the dock banner → `page.modules.ambient.set(...)` |
|
|
16
|
+
| **Peers (`uses`)** | `background` (reads `posterUrl` through `useModuleState`) |
|
|
17
|
+
| **Theme** | `ambientTheme` → one slot, `transition` |
|
|
18
|
+
| **HTTP route** | optional proxy, `/api/ambient/proxy?url=…` by default (`configureAmbient({ proxy })`) |
|
|
19
|
+
| **Tests** | `tests/modules/ambient.test.ts`, `tests/modules/ambient-palette.test.ts` |
|
|
18
20
|
|
|
19
21
|
## 2. Mental model
|
|
20
22
|
|
|
21
23
|
```
|
|
22
|
-
image URL
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
server (RSC) image URL ─fetch─▶ sharp ─▶ 128px sample ─┐
|
|
25
|
+
client image URL ─fetch─▶ createImageBitmap ─────┤ (worker or main thread)
|
|
26
|
+
▼
|
|
27
|
+
letterbox cut ─▶ OKLab k-means (coloured + grey apart) ─▶ clusters
|
|
28
|
+
▼
|
|
29
|
+
roles: dominant · vibrant · muted · accent (each: colour, `on`, 11-tone ramp)
|
|
30
|
+
regions: top · bottom · left · right · center
|
|
31
|
+
shadow: the deep tone of the image's shadows
|
|
32
|
+
▼
|
|
33
|
+
CSS variables on the target element ─▶ everything that reads them re-tints
|
|
25
34
|
```
|
|
26
35
|
|
|
27
|
-
Ambient is a **colour pipeline**, not a component: pick an image (or pass colors directly), and the variables
|
|
36
|
+
Ambient is a **colour pipeline**, not a component: pick an image (or pass colors directly), and the `--ambient-*` variables change. The app maps its own tokens onto them, e.g. `:root { --primary: var(--ambient-vibrant, #101010); --black: var(--ambient-shadow, #0b0b0b) }`; Tailwind classes such as `bg-primary` and anything using `var(--black)` then follow automatically, and the fallbacks apply whenever no palette is set.
|
|
37
|
+
|
|
38
|
+
A palette is plain JSON (~2.7 KB), so it can be computed once on the server, cached, and passed down as `initialPalette`. When its `source` is the same image URL, the client does not extract again. Client extraction is the fallback.
|
|
28
39
|
|
|
29
40
|
## 3. Quick start
|
|
30
41
|
|
|
@@ -42,24 +53,73 @@ useAmbientTheme({ image: backdropUrl || account?.avatarUrl || null });
|
|
|
42
53
|
// 4. Explicit colors, scoped to one element
|
|
43
54
|
const cardRef = useRef<HTMLDivElement>(null);
|
|
44
55
|
useAmbientTheme({
|
|
45
|
-
colors: {
|
|
56
|
+
colors: { "--ambient-vibrant": "#e5d3ff", "--ambient-shadow": "#140d1f" },
|
|
46
57
|
scope: cardRef,
|
|
47
58
|
});
|
|
48
59
|
```
|
|
49
60
|
|
|
61
|
+
### 3.1 Server palette → client (recommended)
|
|
62
|
+
|
|
63
|
+
```tsx
|
|
64
|
+
// app/movie/[id]/page.tsx (server component; `"use cache"` needs `cacheComponents: true`,
|
|
65
|
+
// otherwise wrap the call in `unstable_cache`)
|
|
66
|
+
import { cacheLife } from "next/cache";
|
|
67
|
+
import { extractAmbientPalette } from "@omerdlw/base-framework/ambient/server";
|
|
68
|
+
|
|
69
|
+
async function paletteOf(url: string) {
|
|
70
|
+
"use cache";
|
|
71
|
+
cacheLife("max");
|
|
72
|
+
return extractAmbientPalette(url, {
|
|
73
|
+
allow: (target) => target.hostname === "image.tmdb.org",
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export default async function Page({ params }) {
|
|
78
|
+
const movie = await getMovie((await params).id);
|
|
79
|
+
const backdrop = `https://image.tmdb.org/t/p/w780${movie.backdrop_path}`;
|
|
80
|
+
const palette = await paletteOf(backdrop);
|
|
81
|
+
return <MovieChrome backdrop={backdrop} palette={palette} />;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// movie-chrome.tsx ("use client")
|
|
85
|
+
useAmbientTheme({ image: backdrop, initialPalette: palette ?? undefined });
|
|
86
|
+
// palette.source === backdrop → applied at once, no fetch, no flash.
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Use the **same URL** on both sides (the palette is keyed by `source`). A palette for a different size of the same image works as `initialPalette` too, but the client then extracts its own.
|
|
90
|
+
|
|
91
|
+
### 3.2 Worker (optional)
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
// app/ambient.worker.ts
|
|
95
|
+
import { serveAmbientWorker } from "@omerdlw/base-framework/ambient/worker";
|
|
96
|
+
serveAmbientWorker();
|
|
97
|
+
|
|
98
|
+
// once, in a client module that runs at start-up
|
|
99
|
+
configureAmbient({
|
|
100
|
+
worker: () =>
|
|
101
|
+
new Worker(new URL("./ambient.worker.ts", import.meta.url), { type: "module" }),
|
|
102
|
+
});
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
The app builds the `Worker` itself because only its bundler can turn `new URL("./x.ts", import.meta.url)` into a worker chunk. Without a worker, decoding still happens off the main thread (`createImageBitmap`); only drawing a 128 px bitmap and the analysis (a few ms) run on it.
|
|
106
|
+
|
|
50
107
|
## 4. What gets written
|
|
51
108
|
|
|
52
|
-
|
|
53
|
-
| :----------------------------- | :------------------------------------------- | :----------------------------------------------------------- |
|
|
54
|
-
| `--primary` | an `image` is given and extracted | bright OKLCH colour (lightness 0.74, chroma 0.15–0.24) |
|
|
55
|
-
| `--color-ambient-glow` | same | same as `--primary` |
|
|
56
|
-
| `--color-primary` | same **and** `tintGlobals` is true (default) | same as `--primary` |
|
|
57
|
-
| `--black` | an `image` is given and extracted | deep, slightly tinted OKLCH (lightness 0.15, chroma ≤ 0.055) |
|
|
58
|
-
| any variable named in `colors` | `colors` is set | the literal value |
|
|
109
|
+
Default names; see §5.2 to rename them or turn groups off. Nothing is written until a palette exists.
|
|
59
110
|
|
|
60
|
-
|
|
111
|
+
| Variable | Value |
|
|
112
|
+
| :----------------------------------------- | :--------------------------------------------------------------------- |
|
|
113
|
+
| `--ambient-shadow` | deep tone (L ≈ 0.16) of the shadows' hue, at the chroma sRGB allows there: navy, burgundy, forest… (chroma 0 for grey images) |
|
|
114
|
+
| `--ambient-{role}` | the role's colour; roles: `dominant`, `vibrant`, `muted`, `accent` |
|
|
115
|
+
| `--ambient-on-{role}` | text colour on that role, ≥ 4.5:1 (WCAG 2) by default |
|
|
116
|
+
| `--ambient-{role}-{tone}` | ramp tones `10 15 20 30 40 50 60 70 80 90 95` (= OKLCH L × 100) |
|
|
117
|
+
| `--ambient-{region}` | regions: `top`, `bottom`, `left`, `right`, `center` |
|
|
118
|
+
| any variable named in `colors` | the literal value |
|
|
61
119
|
|
|
62
|
-
|
|
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
|
+
|
|
122
|
+
On cleanup the previous inline values (and priorities) are restored exactly. 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).
|
|
63
123
|
|
|
64
124
|
## 5. Public API
|
|
65
125
|
|
|
@@ -68,14 +128,19 @@ On cleanup the previous inline values (and priorities) are restored exactly. If
|
|
|
68
128
|
| `ambientModule` | module | `defineModule` definition (provider + `usePage({ ambient })`). |
|
|
69
129
|
| `AmbientProvider` | component | Holds the **current shared palette** and `isExtracting` in a store. Prop `initialPalette`. |
|
|
70
130
|
| `useAmbientTheme(config)` | hook | **Main entry.** `config` is an image URL string or `AmbientConfig`. Extracts, applies CSS variables, updates the shared palette. Returns `{ palette, isExtracting }`. |
|
|
71
|
-
| `useAmbientColor(image, options?)` | hook | Extract a palette **without** applying it.
|
|
131
|
+
| `useAmbientColor(image, options?)` | hook | Extract a palette **without** applying it. Uses `options.initialPalette` as is when its `source` is this image. |
|
|
72
132
|
| `useAmbient(configOrImage?)` | hook | The shared state `{ palette, isExtracting, setPalette, setIsExtracting }`; with an argument it also applies that theme and returns its palette. Throws outside `AmbientProvider`. |
|
|
73
133
|
| `defineAmbient(def)` | builder | Reusable theme; `image` / `colors` may be functions of props. Returns `{ id, config, create(props?, overrides?), use(props?, overrides?) }`. |
|
|
74
|
-
| `
|
|
75
|
-
| `
|
|
134
|
+
| `configureAmbient(settings)` | config | Proxy, CSS variable names, strategy, worker, analysis defaults (§5.2). Returns a function that restores the previous settings. `getAmbientSettings()` reads them. |
|
|
135
|
+
| `extractPaletteFromImage(src, options?)` | util | Async client extraction with cache and in-flight de-duplication; resolves `null` on any failure. |
|
|
136
|
+
| `primeAmbientPalette(src, palette)`, `getCachedAmbientPalette(src)` | utils | Seed / read the client cache (e.g. with server palettes). |
|
|
137
|
+
| `analyzeAmbientPixels(pixels, options?)` | util | The pure analysis on RGBA pixels (`{ data, width, height }`). Same function on server, worker and client. |
|
|
138
|
+
| `findContentBounds(pixels)` | util | The letterbox / pillarbox cut used by the analysis. |
|
|
139
|
+
| `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
|
+
| `paletteVarMap(palette, names?)`, `resolveAmbientVarMap(…)`, `resolveCssVarNames(overrides?)` | utils | The variable maps the hooks write. |
|
|
76
141
|
| `resolvePageAmbientTheme(slice, posterUrl)` | util | Maps the page's `ambient` + dock banner to a theme config. |
|
|
77
|
-
| `
|
|
78
|
-
| Types | | `
|
|
142
|
+
| `AMBIENT_TONES`, `AMBIENT_ROLES`, `AMBIENT_REGIONS`, `DEFAULT_ANALYSIS`, `DEFAULT_CSS_VAR_NAMES`, `ambientTheme`, `AmbientContext` | constants | |
|
|
143
|
+
| Types | | `AmbientPalette`, `AmbientSwatch`, `AmbientRegionColor`, `AmbientConfig`, `AmbientSettings`, `AmbientAnalysisOptions`, `AmbientCssVarNames`, `AmbientStrategy`, … |
|
|
79
144
|
|
|
80
145
|
### 5.1 `AmbientConfig`
|
|
81
146
|
|
|
@@ -83,72 +148,121 @@ On cleanup the previous inline values (and priorities) are restored exactly. If
|
|
|
83
148
|
| :--------------- | :---------------------------------- | :---------------------------------------------------------------------------------------------- |
|
|
84
149
|
| `image` | `null` | URL string or `{ src }`. |
|
|
85
150
|
| `colors` | `null` | Explicit variables (see §4). A config with only `colors` is active without extraction. |
|
|
151
|
+
| `cssVars` | `null` | Per-theme override of the variable names (same shape as `configureAmbient({ cssVars })`). |
|
|
86
152
|
| `scope` | `null` → `document.documentElement` | CSS selector string, element, or `ref`. A selector that matches nothing applies nothing. |
|
|
87
|
-
| `tintGlobals` | `true` | Also set `--color-primary`. Set `false` to tint only `--primary`/`--black`/glow inside a scope. |
|
|
88
153
|
| `transition` | `true` | Add the theme's transition classes to the target. |
|
|
89
|
-
| `initialPalette` |
|
|
90
|
-
| `options` | `null` | `{ initialPalette,
|
|
91
|
-
|
|
92
|
-
`
|
|
154
|
+
| `initialPalette` | `null` | Palette shown before extraction finishes (and if it fails); used as is (no extraction) when `source` equals `image`. |
|
|
155
|
+
| `options` | `null` | `{ initialPalette, strategy, proxy, analysis }` for this theme. |
|
|
156
|
+
|
|
157
|
+
Until a palette exists the shared state's `palette` is `null` and no variable is written, so the app's own CSS fallbacks apply.
|
|
158
|
+
|
|
159
|
+
### 5.2 `configureAmbient(settings)`
|
|
160
|
+
|
|
161
|
+
Settings live on `globalThis`, so every bundled copy of the module (one per package entry) shares them. Call it once at start-up, in a client module (and on the server too if you change `analysis`).
|
|
162
|
+
|
|
163
|
+
| Setting | Default | Meaning |
|
|
164
|
+
| :----------- | :---------------------------------------- | :------ |
|
|
165
|
+
| `proxy` | `"/api/ambient/proxy?url="` | Tried last, only for cross-origin images whose direct request (and its one cache-bypassing retry) failed. A string is a prefix the encoded URL is appended to, or a template with `{url}`; a function returns the URL; `null` turns it off. |
|
|
166
|
+
| `strategy` | `"auto"` | `"auto"`: the worker if one is configured, else the main thread. `"worker"`: worker only (no main-thread extraction). `"main"`: main thread only. `"none"`: never extract on the client (server palettes only). |
|
|
167
|
+
| `worker` | `null` | `() => Worker` running `serveAmbientWorker()` (§3.2). If it fails to start or errors, extraction falls back to the main thread (except under `"worker"`). |
|
|
168
|
+
| `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
|
+
| `analysis` | `DEFAULT_ANALYSIS` | `sampleSize` 128, `clusters` 7, `centerWeight` 0.5, `letterbox` true, `minHueDistance` 40, `minContrast` 4.5, `shadowLightness` 0.16. |
|
|
170
|
+
| `cacheLimit` | `100` | Client palettes kept (oldest evicted). |
|
|
171
|
+
|
|
172
|
+
### 5.3 `AmbientPalette`
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
{
|
|
176
|
+
source?: string | null; // the image URL it was made from
|
|
177
|
+
neutral: boolean; // the image has (almost) no colour
|
|
178
|
+
shadow: string; // deep tone of the shadows (oklch())
|
|
179
|
+
roles: Record<"dominant" | "vibrant" | "muted" | "accent", {
|
|
180
|
+
color: string; on: string; // oklch() strings
|
|
181
|
+
l: number; c: number; h: number;
|
|
182
|
+
population: number; // share of the image (centre-weighted)
|
|
183
|
+
fromImage: boolean; // false: derived (e.g. no second hue → accent = vibrant)
|
|
184
|
+
ramp: Record<10 | 15 | … | 95, string>;
|
|
185
|
+
}>;
|
|
186
|
+
regions: Record<"top" | "bottom" | "left" | "right" | "center", {
|
|
187
|
+
color: string; l: number; c: number; h: number;
|
|
188
|
+
strength: number; // 0 grey … 1 fully coloured
|
|
189
|
+
}>;
|
|
190
|
+
}
|
|
191
|
+
```
|
|
93
192
|
|
|
94
|
-
### 5.
|
|
193
|
+
### 5.4 Server entry: `@omerdlw/base-framework/ambient/server`
|
|
95
194
|
|
|
96
|
-
|
|
195
|
+
| Export | Description |
|
|
196
|
+
| :----- | :---------- |
|
|
197
|
+
| `extractAmbientPalette(url, options?)` | Fetches and analyzes; returns the palette (with `source = url`) or `null`. In-process cache (500) and in-flight sharing; wrap it in `"use cache"` / `unstable_cache` to keep palettes across requests. |
|
|
198
|
+
| `extractAmbientPaletteFromBytes(bytes, options?)` | Same, from bytes you already have. |
|
|
199
|
+
| `sharpDecoder` | The default decoder: `sharp` (optional peer dependency, `^0.35.0`), loaded on first use with a bundler-ignored dynamic import; Next.js already installs it. |
|
|
200
|
+
| `isPublicImageUrl(url)` | The default `allow`: http(s), no credentials, no localhost / `.local` / `.internal` / single-label names, no private, loopback, link-local, CGNAT or reserved IP literals in any spelling (`127.1`, `0x7f000001`, `[::ffff:127.0.0.1]`, …). `isPrivateAddress(ip)` and `isPrivateHostname(name)` are exported too. |
|
|
201
|
+
| `AmbientDecoderMissingError` | Thrown (not swallowed) when `sharp` is missing and no `decode` was passed. |
|
|
202
|
+
| `analyzeAmbientPixels`, `configureAmbient`, colour helpers, types | Re-exported. |
|
|
97
203
|
|
|
98
|
-
|
|
99
|
-
| :-------------- | :--------------------------------------------------------------------------------------------------- |
|
|
100
|
-
| `false` | No theme (also disables the banner tint). |
|
|
101
|
-
| `true` | `image` = banner, else the background video poster. |
|
|
102
|
-
| string | That image (`tintGlobals: true`). |
|
|
103
|
-
| object | `{ image: ambient.image ?? banner ?? poster, ...ambient }`. |
|
|
104
|
-
| not set | If the dock has a banner and the background has no poster → tint with the banner; otherwise nothing. |
|
|
204
|
+
Options: `allow(url)` (checked on every redirect hop), `resolve` (see below), `decode(bytes, size)`, `fetch`, `init` (e.g. `{ next: { revalidate } }`), `maxBytes` (12 MB), `timeoutMs` (8 s), `cache` (true), `analysis`.
|
|
105
205
|
|
|
106
|
-
`
|
|
206
|
+
Before every request, redirects included, the host is resolved (`resolve`, default `node:dns` lookup of all addresses) and the request is refused if **any** address is loopback, private, link-local, CGNAT, multicast or reserved, even when a custom `allow` let the URL through. `resolve: false` turns this off; only do that behind an `allow` list. The fetch itself resolves the name again, so a host that changes its answer in between (DNS rebinding) is only ruled out by an `allow` list of hosts you trust, e.g. `allow: (url) => url.hostname === "image.tmdb.org"`. Use one in production.
|
|
107
207
|
|
|
108
208
|
## 6. Extraction details
|
|
109
209
|
|
|
110
|
-
- **
|
|
111
|
-
- **The
|
|
112
|
-
- **
|
|
113
|
-
- **
|
|
114
|
-
- **
|
|
115
|
-
-
|
|
116
|
-
-
|
|
210
|
+
- **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
|
+
- **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.** Every role colour and ramp tone is `toneOf(h, L, rel)`: `rel` (the source colour's share of the available chroma, lifted per role) × the most chroma sRGB allows at that L and hue (`maxChroma`, a binary search on the gamut edge, × 0.96). So a dark tone of blue stays blue, and nothing is ever out of gamut.
|
|
215
|
+
- **`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
|
+
- **`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.
|
|
218
|
+
- **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
|
+
- **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.
|
|
221
|
+
- **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
|
+
- **Failure.** Client: `extractPaletteFromImage` resolves `null` and the hooks keep `initialPalette` (or nothing); it never throws. Server: `null`, except a missing decoder.
|
|
117
223
|
|
|
118
224
|
## 7. Recipes and gotchas
|
|
119
225
|
|
|
120
|
-
| I want to…
|
|
121
|
-
|
|
|
122
|
-
| Tint a page from its hero image
|
|
123
|
-
|
|
|
124
|
-
|
|
|
125
|
-
|
|
|
126
|
-
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
226
|
+
| I want to… | Do this |
|
|
227
|
+
| :----------------------------------- | :---------------------------------------------------------------------- |
|
|
228
|
+
| Tint a page from its hero image | `usePage({ ambient: heroUrl })` |
|
|
229
|
+
| No flash, no client work | Compute on the server (§3.1); add `configureAmbient({ strategy: "none" })` if every image has one |
|
|
230
|
+
| Glows that follow the picture | `radial-gradient(at 20% 0%, color-mix(in oklab, var(--ambient-top) 30%, transparent), transparent)` … |
|
|
231
|
+
| Text on a role colour | `background: var(--ambient-accent); color: var(--ambient-on-accent)` |
|
|
232
|
+
| A surface a few steps above the shadow | `var(--ambient-dominant-20)` / `-30` |
|
|
233
|
+
| Tint only one section | `useAmbientTheme({ image, scope: "#section" })` |
|
|
234
|
+
| Rename the variables | `configureAmbient({ cssVars: { shadow: "--bg", role: "--tint-{role}" } })` |
|
|
235
|
+
| Fewer variables on `<html>` | `configureAmbient({ cssVars: { tones: [20, 80], region: null } })` |
|
|
236
|
+
| Own proxy / no proxy | `configureAmbient({ proxy: "/img?u={url}" })` / `{ proxy: null }` |
|
|
237
|
+
| Disable tint on one page | `usePage({ ambient: false })` |
|
|
238
|
+
|
|
239
|
+
- The theme is global by default: two components calling `useAmbientTheme` without `scope` fight over the `--ambient-*` variables; the last effect wins.
|
|
240
|
+
- Cross-origin images that block CORS **and** the proxy cannot be sampled on the client; `initialPalette` (or the app's CSS fallbacks) stays. The server has no CORS limits.
|
|
130
241
|
- `useAmbient()` without arguments only _reads_ the shared palette; it does not apply anything.
|
|
131
|
-
- Components should read the variables
|
|
242
|
+
- Components should read the variables rather than the palette object, so they follow transitions for free.
|
|
132
243
|
|
|
133
244
|
## 8. File map
|
|
134
245
|
|
|
135
|
-
| File
|
|
136
|
-
|
|
|
137
|
-
| `index.ts`
|
|
138
|
-
| `types.ts`
|
|
139
|
-
| `
|
|
140
|
-
| `
|
|
141
|
-
| `
|
|
142
|
-
| `
|
|
143
|
-
| `
|
|
144
|
-
| `
|
|
145
|
-
| `
|
|
146
|
-
| `
|
|
147
|
-
|
|
148
|
-
|
|
246
|
+
| File | Responsibility |
|
|
247
|
+
| :------------------- | :--------------------------------------------------------------------------------------------- |
|
|
248
|
+
| `index.ts` | Public barrel export (client entry). |
|
|
249
|
+
| `types.ts` | Palette, swatch, region, settings, config, descriptor, page contracts. |
|
|
250
|
+
| `settings.ts` | Defaults, `configureAmbient` (on `globalThis`), proxy resolution. Pure. |
|
|
251
|
+
| `oklab.ts` | OKLab/OKLCH math, gamut room (`maxChroma`), tones, WCAG contrast, `on` colours. Pure. |
|
|
252
|
+
| `quantize.ts` | Deterministic weighted k-means. Pure. |
|
|
253
|
+
| `analyze.ts` | Letterbox cut, binning, clustering, roles, ramps, regions, `shadow`. Pure. |
|
|
254
|
+
| `pipeline.ts` | fetch → `createImageBitmap` → canvas pixels → analysis; runs in a window or a worker. |
|
|
255
|
+
| `extractor.ts` | Client cache, strategy, worker dispatch with main-thread fallback. |
|
|
256
|
+
| `worker/index.ts` | `serveAmbientWorker` (the `ambient/worker` entry). |
|
|
257
|
+
| `server/index.ts` | `extractAmbientPalette`, sharp decoder, URL guard, server cache (the `ambient/server` entry). |
|
|
258
|
+
| `dom.ts` | Target resolution, CSS variable maps, scoped style restoration. |
|
|
259
|
+
| `hooks.ts` | `useAmbientColor`, `useAmbientTheme`, `useAmbient`, `useStableObject`. |
|
|
260
|
+
| `context.tsx` | `AmbientContext` and `AmbientProvider` store. |
|
|
261
|
+
| `module.tsx` | `ambientModule`, `resolvePageAmbientTheme`, `defineAmbient`, kernel type augmentation. |
|
|
149
262
|
|
|
150
263
|
## 9. Dependencies
|
|
151
264
|
|
|
152
|
-
- **Uses:** `@omerdlw/base-framework/kernel` (`defineModule`, `useModuleState`), `@omerdlw/base-framework/hooks` (`useRequiredContext`, `useStore`), `@omerdlw/base-framework/utils` (`createStore`, `shallowEqual`), `@omerdlw/base-framework/theme
|
|
265
|
+
- **Uses:** `@omerdlw/base-framework/kernel` (`defineModule`, `useModuleState`), `@omerdlw/base-framework/hooks` (`useRequiredContext`, `useStore`), `@omerdlw/base-framework/utils` (`createStore`, `shallowEqual`), `@omerdlw/base-framework/theme`.
|
|
266
|
+
- **Server entry:** `sharp` `^0.35.0`, an **optional peer dependency** (Next.js already installs it), loaded on first use with a bundler-ignored dynamic import. Apps that never call the server entry, or pass their own `decode`, do not need it.
|
|
153
267
|
- **Peer:** `background` (poster fallback), read through `useModuleState("background", …, null)`, so it works when background is not installed.
|
|
154
|
-
- **Used by:** `
|
|
268
|
+
- **Used by:** `usePage({ ambient })`, and features such as the account layout (`useAmbientTheme`).
|
|
@@ -71,7 +71,7 @@ usePage({
|
|
|
71
71
|
| Edge fades | `fadeEdges` (`number`, `"20"`, `{ left, right }`, `false`), `leftGradient` / `rightGradient` (strength 1–n). A custom `width` implies a 20 % fade unless `fadeEdges: false`. |
|
|
72
72
|
| Film grain | `noiseStyle: { opacity, mixBlendMode }`. |
|
|
73
73
|
| Layout | `width` (number px or CSS length) places a video frame left/right/center via `position`; `fit` / `videoOptions.fit` (`cover contain fill none scale-down`); `bg-*` class tokens in `className`/`videoClassName` map to object-position slots. |
|
|
74
|
-
| Custom transition | `animation: { initial, animate, exit, transition, exitDurationFactor }
|
|
74
|
+
| Custom transition | `animation: { initial, animate, exit, transition, exitDurationFactor }`, field by field over the app's background motion (`motion: BackgroundMotion` in the theme; the starter fades opacity over 0.58 s, exit factor 0.6). It follows the app's tempo.|
|
|
75
75
|
| Programmatic control | `useBackgroundActions()` / `page.modules.background`. |
|
|
76
76
|
| Dock media controls | Automatic through the `media` module ([§8](#8-media-integration)). |
|
|
77
77
|
|
|
@@ -155,7 +155,7 @@ return <MediaSource src="/audio/city.mp3" follow={BACKGROUND_MEDIA_ID} />;
|
|
|
155
155
|
|
|
156
156
|
## 9. Theming
|
|
157
157
|
|
|
158
|
-
All classes come from `backgroundTheme` (`src/config/background.module.theme.ts`): layer slots (`root`, `image`, `solid`, `gradient`, `edge`, `noise`), direct video slots (`video`, `videoFrame`) and YouTube player slots (`youtube*`). Variants are `data-*` modifiers on the slot (`data-side`, `data-position`, `data-width`, `data-visible`). `fit` and `bg-*` tokens are translated to inline `object-fit` / `object-position` by the module, not by theme slots.
|
|
158
|
+
All classes come from `backgroundTheme` (`src/config/background.module.theme.ts`): layer slots (`root`, `image`, `solid`, `gradient`, `edge`, `noise`), direct video slots (`video`, `videoFrame`) and YouTube player slots (`youtube*`). Variants are `data-*` modifiers on the slot (`data-side`, `data-position`, `data-width`, `data-visible`). `fit` and `bg-*` tokens are translated to inline `object-fit` / `object-position` by the module, not by theme slots. Motion is the app's: `motion: BackgroundMotion` in the theme ([theming §5](../theming.md#5-motion-your-app-owns-every-animation), starter in `templates/motion/background.ts`) gives the default `initial`, `animate`, `exit` and `transition`, plus `exitDurationFactor` for a page exit without its own transition. Stays in code: compositor hints (`will-change`) and mask/gradient math (`generateBaseGradient`, `generateEdgeGradient`, `getEdgeFadeMask`). The module deliberately has **no presets** (a test bans `BACKGROUND_PRESETS`): pass a URL or a state object, or build your own with `defineBackground`.
|
|
159
159
|
|
|
160
160
|
## 10. Recipes and gotchas
|
|
161
161
|
|
|
@@ -183,7 +183,7 @@ All classes come from `backgroundTheme` (`src/config/background.module.theme.ts`
|
|
|
183
183
|
| `visual.ts` | Gradient generation, smoothstep edge-fade masks, and video styling/fit options. |
|
|
184
184
|
| `playback.ts` | Direct HTML video DOM controls: play/pause, volume/mute, loop, and playbackRate sync. |
|
|
185
185
|
| `overlay.tsx` | `BackgroundOverlay`, `NativeVideo`, and backdrop overlay layers (solid, noise, gradients). |
|
|
186
|
-
| `motion.ts` |
|
|
186
|
+
| `motion.ts` | `useBackgroundMotion`, how a page's `animation` merges over the app's motion, the exit fallback, and CSS duration/easing helpers.|
|
|
187
187
|
| `context.tsx` | `BackgroundProvider`: store, actions, `media` module bridge, and `useBackground*` hooks. |
|
|
188
188
|
| `module.tsx` | `backgroundModule` definition, `selectPageBackground`, `defineBackground`, and kernel type augmentation. |
|
|
189
189
|
| `youtube/parse.ts` | YouTube URL/ID parsing, time params, stream/thumbnail URLs, and video element proxy. |
|
|
@@ -208,7 +208,7 @@ Important: the global capture listener runs **before** React's element handler.
|
|
|
208
208
|
|
|
209
209
|
## 6. Theming
|
|
210
210
|
|
|
211
|
-
No classes or colors in the module, except the pointer-following `left/top/position: fixed`. Slots in `contextMenuTheme`: `backdrop`, `menu`, `header`, `headerIcon`, `headerText`, `headerEyebrow`, `headerTitle`, `headerDescription`, `separator`, `item`, `itemIcon`, `itemLabel`, `itemShortcut`, plus optional per-slot `styles` (the starter theme sets the backdrop/menu z-index there). A config's `classNames` (and an item's `className`) are appended on top of the theme. Active items get `data-active="true"`, handy for theme selectors. Motion (`
|
|
211
|
+
No classes or colors in the module, except the pointer-following `left/top/position: fixed`. Slots in `contextMenuTheme`: `backdrop`, `menu`, `header`, `headerIcon`, `headerText`, `headerEyebrow`, `headerTitle`, `headerDescription`, `separator`, `item`, `itemIcon`, `itemLabel`, `itemShortcut`, plus optional per-slot `styles` (the starter theme sets the backdrop/menu z-index there). A config's `classNames` (and an item's `className`) are appended on top of the theme. Active items get `data-active="true"`, handy for theme selectors. Motion comes from the theme too (`motion: ContextMenuMotion`, [theming §5](../theming.md#5-motion-your-app-owns-every-animation)): `pop` (the surface), `content` (the header), `item` (rows; `visible` may take the row index for a stagger), `itemTap` and `itemTransition`. Start from `templates/motion/context-menu.ts`.
|
|
212
212
|
|
|
213
213
|
## 7. Recipes and gotchas
|
|
214
214
|
|
|
@@ -238,7 +238,7 @@ No classes or colors in the module, except the pointer-following `left/top/posit
|
|
|
238
238
|
| `resolver.ts` | Candidate normalization, path/target/route scoring, context building, `resolveContextMenu`, `prepareMenu`. |
|
|
239
239
|
| `context.tsx` | `ContextMenuProvider` (store, `openMenu`, `closeMenu`, `bind`), `useContextMenu*` consumer hooks. |
|
|
240
240
|
| `overlay.tsx` | `ContextMenuGlobal`, portalled renderer, content host, header, item components, and interaction listeners. |
|
|
241
|
-
| `motion.ts` |
|
|
241
|
+
| `motion.ts` | `useContextMenuMotion`. The values come from the app's context-menu theme. |
|
|
242
242
|
| `module.tsx` | `contextMenuModule`, page API adapter, `defineContextMenu`, kernel type augmentation. |
|
|
243
243
|
|
|
244
244
|
## 9. Dependencies
|
package/docs/modules/dock.md
CHANGED
|
@@ -220,7 +220,7 @@ Everything below is exported from `@omerdlw/base-framework/modules/dock`. Anythi
|
|
|
220
220
|
| `DockCardHeader`, `DockCardBanner`, `DockIcon`, `DockTitle`, `DockDescription` | The same pieces the route card uses. |
|
|
221
221
|
| `useDockActionClass()` | Returns `(options?) ⇒ className` for buttons that must look like dock actions (`variant`, `className`, `tone`). Needs the theme. |
|
|
222
222
|
| `isValidBannerUrl(value)` | Banner URL guard (rejects unsafe/empty values). |
|
|
223
|
-
| `
|
|
223
|
+
| `useDockMotion()` | The app's dock motion (retimed by its tempo, or reduced), for surface content that should move like the dock: `useDockMotion().variants.fade`, `.transitions.text`, … |
|
|
224
224
|
| `dockTheme` | The theme spec (for `defineTheme`). |
|
|
225
225
|
| Types | `DockItem`, `DockPageConfig`, `DockPageApi`, `DockActions`, `DockState`, `DockActionDescriptor`, `SurfaceDescriptor` / `SurfaceEntry`, `SurfaceStep`, `SurfaceResult`, `SurfaceFlowDefinitionInput`, `DockHudDescriptor`, `DockOperation`, `DockGuardDefinition`, `BreadcrumbConfig`, … (`export type * from "./types"`). |
|
|
226
226
|
|
|
@@ -409,12 +409,13 @@ Imperative equivalents on `DockActions`: `openSurfaceFlow(def, input)`, `restore
|
|
|
409
409
|
|
|
410
410
|
```tsx
|
|
411
411
|
function InviteForm({ close }) {
|
|
412
|
+
const dockMotion = useDockMotion();
|
|
412
413
|
return (
|
|
413
414
|
<>
|
|
414
415
|
<DockSurfaceHeaderButton icon="solar:question-circle-bold" onClick={openHelp} />
|
|
415
416
|
<DockSurfaceExtension id="invite-tabs" align="center"><Tabs … /></DockSurfaceExtension>
|
|
416
417
|
<DockSurfaceAction><Button onClick={() => close({ success: true })}>Send</Button></DockSurfaceAction>
|
|
417
|
-
<motion.ul variants={
|
|
418
|
+
<motion.ul variants={dockMotion.variants.fade} initial="hidden" animate="visible">…</motion.ul>
|
|
418
419
|
</>
|
|
419
420
|
);
|
|
420
421
|
}
|
|
@@ -663,7 +664,9 @@ Reserve space with `useDockHeight().padding` or `padding-bottom: var(--dock-h)`.
|
|
|
663
664
|
|
|
664
665
|
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).
|
|
665
666
|
|
|
666
|
-
|
|
667
|
+
The theme also carries the dock's **motion** (`motion: DockMotion`, [theming §5](../theming.md#5-motion-your-app-owns-every-animation)): every transition and variant, the deck's elevation, compression, peek and card exit, the swipe-to-dismiss feel, the skeleton class, and `timings`, which hold how long each surface phase lasts (`dismissAction`, `anticipation`, `expandBody`, `collapseBody`, `restoreHeader`) plus the card staggers, in seconds. The surface state machine schedules its phases from those timings, so they follow the app's tempo with everything else. Start from `templates/motion/dock.ts`, the dock's original motion.
|
|
668
|
+
|
|
669
|
+
Stays in code: measured geometry and stack layout (card offsets and scales per position, the breadcrumb lift), per-position z-index (`Z_INDEX.DOCK_CARD_STACK_BASE − position`), compositor hints, swipe thresholds, and which of the app's transitions each state uses. 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.
|
|
667
670
|
|
|
668
671
|
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.
|
|
669
672
|
|
|
@@ -687,7 +690,7 @@ Gotchas:
|
|
|
687
690
|
- A flow `snapshot` must be a plain object; anything else becomes `null`.
|
|
688
691
|
- Status events are global: remember to emit `DOCK_STATUS_CLEAR` for sticky (`duration: 0`) statuses.
|
|
689
692
|
- Guards registered by an unmounted page disappear with it; `clearDockGuards()` drops every guard, including other pages'.
|
|
690
|
-
- No arbitrary durations or `z-*` classes in surface content: use
|
|
693
|
+
- No arbitrary durations or `z-*` classes in surface content: use your app's motion (`useDockMotion()`, or your own tokens) and `Z_INDEX` ([Rules 4 and 5](../architecture-and-rules.md)).
|
|
691
694
|
|
|
692
695
|
## 19. File map
|
|
693
696
|
|
|
@@ -711,7 +714,7 @@ Gotchas:
|
|
|
711
714
|
| `overlay.tsx` | `Dock` and every rendered piece: cards, header, banner, command bar, surface shell/controls/extensions, HUD view, media controls and scrubber, breadcrumbs card, `ErrorActions`, `GuardActions`. |
|
|
712
715
|
| `cards.tsx` | View models for the card components (banner, header, command bar, item, media controls/scrubber), `applyMediaAction`, action-slot resolution. |
|
|
713
716
|
| `hud.ts` | HUD descriptors, registry hook, `defineHud`, `useHud`, `useDockHud`, HUD lifecycle and view model, operation HUD. |
|
|
714
|
-
| `motion.ts` |
|
|
717
|
+
| `motion.ts` | `useDockMotion`, `useDockSurfaceTimings`, and helpers that pick from the app's dock motion by state (stack, cards, media, backdrop). No motion values. |
|
|
715
718
|
| `runtime/dock.tsx` | `useDock` (view controller), `useDockModel`, layout ordering, expansion, operation state, route bridge. |
|
|
716
719
|
| `runtime/display.ts` | Item pipeline: registry entries → filtered items → active item (applies surface / status / media). |
|
|
717
720
|
| `runtime/pipeline.ts` | Item identity, active item detection, ancestor deduplication, loading item cleanup, and stacking reorder. |
|
package/docs/modules/modal.md
CHANGED
|
@@ -191,7 +191,7 @@ No classes or colors in the module except the two z-indexes. Slots in `modalThem
|
|
|
191
191
|
|
|
192
192
|
`data-layout` is one of `center`, `top`, `bottom`, `left`, `right`, `top-mobile`, `bottom-mobile`, `side-mobile` (`getModalLayout`), so each position/viewport combination is a single selector.
|
|
193
193
|
|
|
194
|
-
Motion (`motion.
|
|
194
|
+
Motion comes from the theme too (`motion: ModalMotion`, [theming §5](../theming.md#5-motion-your-app-owns-every-animation)): the `backdrop`, and the `content`, `header` and `footer` rows of a `ModalContainer`, plus `positions`, the dialog's variants per position (`center`, `top`, `right`, `bottom`, `left`) with an optional own `transition` (the starter gives `center` a spring). A theme without motion throws `Motion for theme "modal" is missing…`. Start from `templates/motion/modal.ts`. Use `Z_INDEX` for anything layered above or below a modal ([Rule 5](../architecture-and-rules.md)).
|
|
195
195
|
|
|
196
196
|
## 8. Recipes and gotchas
|
|
197
197
|
|
|
@@ -223,7 +223,7 @@ Motion (`motion.ts`: backdrop, per-position panel variants/transitions, header/b
|
|
|
223
223
|
| `context.tsx` | `ModalProvider` (stack, pending promises, actions), `useModal*` hooks, registry lookup. |
|
|
224
224
|
| `hooks.ts` | View models: `useModalModel` (visibility, viewport, scroll lock), `useModalLayerModel` (focus, Esc), `useModalContainerModel`. |
|
|
225
225
|
| `overlay.tsx` | `Modal`, `ModalLayer`, `ModalContainer`, switcher, close button. |
|
|
226
|
-
| `motion.ts` |
|
|
226
|
+
| `motion.ts` | `useModalMotion`, `getModalPositionMotion`, compositor hints. The values come from the app's modal theme. |
|
|
227
227
|
| `module.tsx` | `modalModule`, lazy renderer, page API, `defineModal`, kernel type augmentation. |
|
|
228
228
|
|
|
229
229
|
## 10. Dependencies
|
|
@@ -116,7 +116,7 @@ String messages are trimmed and **trailing periods/whitespace are removed** (`"S
|
|
|
116
116
|
|
|
117
117
|
## 7. Theming
|
|
118
118
|
|
|
119
|
-
No classes or inline styles in the module. `notificationTheme` slots: `layer`, `toast`, `toastDocked`, `toastMessage` (Tailwind classes, all required) plus optional inline `styles` per slot. A missing theme throws a descriptive error.
|
|
119
|
+
No classes or inline styles in the module. `notificationTheme` slots: `layer`, `toast`, `toastDocked`, `toastMessage` (Tailwind classes, all required) plus optional inline `styles` per slot. A missing theme throws a descriptive error. The toast's motion comes from the theme as well (`motion: NotificationMotion`, [theming §5](../theming.md#5-motion-your-app-owns-every-animation)): `toast` variants (`hidden` → `visible` → `exit`) and its `transition`. Start from `templates/motion/notification.ts`.
|
|
120
120
|
|
|
121
121
|
## 8. Writing toast copy
|
|
122
122
|
|
|
@@ -150,7 +150,7 @@ No classes or inline styles in the module. `notificationTheme` slots: `layer`, `
|
|
|
150
150
|
| `context.tsx` | `NotificationProvider` (store, timers, show/dismiss actions), `NotificationContext`. |
|
|
151
151
|
| `hooks.ts` | Public hooks (`useToast`, `useNotification*`), `NotificationListener`, container view model. |
|
|
152
152
|
| `overlay.tsx` | `Toast`, `NotificationContainer` (dock/floating portal), `NotificationLayer`. |
|
|
153
|
-
| `motion.ts` |
|
|
153
|
+
| `motion.ts` | `useNotificationMotion`, compositor hints. The values come from the app's notification theme.|
|
|
154
154
|
| `module.tsx` | `notificationModule` and the page API. |
|
|
155
155
|
|
|
156
156
|
`builder.ts` is absent: toasts are imperative ([README](./README.md#42-why-some-optional-slots-are-absent)).
|