@wildo-ai/presets-components-3d 1.1.6 → 1.1.8
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/esm/focal-plane/focal-plane-settings.d.ts +35 -0
- package/dist/esm/focal-plane/focal-plane-settings.d.ts.map +1 -0
- package/dist/esm/focal-plane/focal-plane-settings.js +68 -0
- package/dist/esm/focal-plane/focal-plane-settings.js.map +1 -0
- package/dist/esm/focal-plane/focal-plane-shell-container.d.ts +13 -0
- package/dist/esm/focal-plane/focal-plane-shell-container.d.ts.map +1 -0
- package/dist/esm/focal-plane/focal-plane-shell-container.js +147 -0
- package/dist/esm/focal-plane/focal-plane-shell-container.js.map +1 -0
- package/dist/esm/focal-plane/focal-plane-stylesheet.d.ts +14 -0
- package/dist/esm/focal-plane/focal-plane-stylesheet.d.ts.map +1 -0
- package/dist/esm/focal-plane/focal-plane-stylesheet.js +236 -0
- package/dist/esm/focal-plane/focal-plane-stylesheet.js.map +1 -0
- package/dist/esm/focal-plane/use-pane-depth.d.ts +20 -0
- package/dist/esm/focal-plane/use-pane-depth.d.ts.map +1 -0
- package/dist/esm/focal-plane/use-pane-depth.js +162 -0
- package/dist/esm/focal-plane/use-pane-depth.js.map +1 -0
- package/dist/esm/focal-plane/use-shared-vanishing-point.d.ts +35 -0
- package/dist/esm/focal-plane/use-shared-vanishing-point.d.ts.map +1 -0
- package/dist/esm/focal-plane/use-shared-vanishing-point.js +86 -0
- package/dist/esm/focal-plane/use-shared-vanishing-point.js.map +1 -0
- package/dist/esm/frame/frame-canvas.js +3 -3
- package/dist/esm/frame/frame-canvas.js.map +1 -1
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +1 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/layout/layout-3d-definition.d.ts +5 -6
- package/dist/esm/layout/layout-3d-definition.d.ts.map +1 -1
- package/dist/esm/layout/layout-3d-definition.js +13 -14
- package/dist/esm/layout/layout-3d-definition.js.map +1 -1
- package/dist/esm/layout/layout-3d-vocabulary.d.ts +13 -8
- package/dist/esm/layout/layout-3d-vocabulary.d.ts.map +1 -1
- package/dist/esm/layout/layout-3d-vocabulary.js +12 -7
- package/dist/esm/layout/layout-3d-vocabulary.js.map +1 -1
- package/dist/esm/layout-3d-main-container.d.ts +6 -0
- package/dist/esm/layout-3d-main-container.d.ts.map +1 -1
- package/dist/esm/layout-3d-main-container.js +8 -1
- package/dist/esm/layout-3d-main-container.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -0
- package/package.json +3 -3
- package/dist/esm/spatial/spatial-camera.d.ts +0 -106
- package/dist/esm/spatial/spatial-camera.d.ts.map +0 -1
- package/dist/esm/spatial/spatial-camera.js +0 -151
- package/dist/esm/spatial/spatial-camera.js.map +0 -1
- package/dist/esm/spatial/spatial-environment.d.ts +0 -6
- package/dist/esm/spatial/spatial-environment.d.ts.map +0 -1
- package/dist/esm/spatial/spatial-environment.js +0 -166
- package/dist/esm/spatial/spatial-environment.js.map +0 -1
- package/dist/esm/spatial/spatial-environment.lazy.d.ts +0 -10
- package/dist/esm/spatial/spatial-environment.lazy.d.ts.map +0 -1
- package/dist/esm/spatial/spatial-environment.lazy.js +0 -13
- package/dist/esm/spatial/spatial-environment.lazy.js.map +0 -1
- package/dist/esm/spatial/spatial-shell-container.d.ts +0 -21
- package/dist/esm/spatial/spatial-shell-container.d.ts.map +0 -1
- package/dist/esm/spatial/spatial-shell-container.js +0 -231
- package/dist/esm/spatial/spatial-shell-container.js.map +0 -1
- package/dist/esm/spatial/spatial-stage-store.d.ts +0 -27
- package/dist/esm/spatial/spatial-stage-store.d.ts.map +0 -1
- package/dist/esm/spatial/spatial-stage-store.js +0 -33
- package/dist/esm/spatial/spatial-stage-store.js.map +0 -1
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** @wildo_source:part:start presets.3d.focal-plane-settings facet:layer:frontend facet:family:layout-3d facet:audience:app-developer */
|
|
2
|
+
/**
|
|
3
|
+
* The focal plane's numbers (#2049). The defaults are the values Paul settled on in the prototype on 2026-09-30.
|
|
4
|
+
* They reach the stylesheet as CSS custom properties on the layout's root, so every object reads one source.
|
|
5
|
+
*/
|
|
6
|
+
export interface FocalPlaneSettings {
|
|
7
|
+
/** The still camera's distance from the focal plane, in CSS pixels: the `perspective()` every object is seen through. */
|
|
8
|
+
cameraDistancePx: number;
|
|
9
|
+
/** How far back one layer steps, in CSS pixels. A pane behind the focused one steps back part of it. */
|
|
10
|
+
layerDepthPx: number;
|
|
11
|
+
/** How long an object takes to travel, in milliseconds. */
|
|
12
|
+
motionMs: number;
|
|
13
|
+
/** How much a receding object dims per layer, 0-1. */
|
|
14
|
+
recedeDim: number;
|
|
15
|
+
/** How much a receding PANE blurs per layer, in CSS pixels (navigation; 0 keeps panes sharp). */
|
|
16
|
+
recedeBlurPx: number;
|
|
17
|
+
/** How much everything blurs behind a MODAL popup (dialog, sheet, command palette), in CSS pixels. */
|
|
18
|
+
modalBlurPx: number;
|
|
19
|
+
/** How opaque a glass window is, 0-1: the rest is the background blurred through it. */
|
|
20
|
+
windowOpacity: number;
|
|
21
|
+
/** How far a shell panel rises toward the camera under the pointer, in CSS pixels. */
|
|
22
|
+
hoverLiftPx: number;
|
|
23
|
+
}
|
|
24
|
+
export declare const FOCAL_PLANE_DEFAULTS: FocalPlaneSettings;
|
|
25
|
+
/**
|
|
26
|
+
* The application's overrides over the defaults, checked: an unknown key or a value outside its range is refused at
|
|
27
|
+
* registration, naming the setting, rather than drawn as something the application did not mean.
|
|
28
|
+
*/
|
|
29
|
+
export declare function resolveFocalPlaneSettings(overrides?: Partial<FocalPlaneSettings>): FocalPlaneSettings;
|
|
30
|
+
/** The resolved settings, provided by the 3D main container to its placement. Outside one, the defaults. */
|
|
31
|
+
export declare const FocalPlaneSettingsContext: import("react").Context<FocalPlaneSettings>;
|
|
32
|
+
export declare function useFocalPlaneSettings(): FocalPlaneSettings;
|
|
33
|
+
/** The settings as the custom properties the focal-plane stylesheet reads. Reduced motion zeroes the travel time. */
|
|
34
|
+
export declare function focalPlaneCssVariables(settings: FocalPlaneSettings, reducedMotion: boolean): Record<string, string>;
|
|
35
|
+
//# sourceMappingURL=focal-plane-settings.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"focal-plane-settings.d.ts","sourceRoot":"","sources":["../../../../src/focal-plane/focal-plane-settings.ts"],"names":[],"mappings":"AAEA,wIAAwI;AACxI;;;GAGG;AACH,MAAM,WAAW,kBAAkB;IACjC,yHAAyH;IACzH,gBAAgB,EAAE,MAAM,CAAC;IACzB,wGAAwG;IACxG,YAAY,EAAE,MAAM,CAAC;IACrB,2DAA2D;IAC3D,QAAQ,EAAE,MAAM,CAAC;IACjB,sDAAsD;IACtD,SAAS,EAAE,MAAM,CAAC;IAClB,iGAAiG;IACjG,YAAY,EAAE,MAAM,CAAC;IACrB,sGAAsG;IACtG,WAAW,EAAE,MAAM,CAAC;IACpB,wFAAwF;IACxF,aAAa,EAAE,MAAM,CAAC;IACtB,sFAAsF;IACtF,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,eAAO,MAAM,oBAAoB,EAAE,kBASlC,CAAC;AAeF;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,SAAS,GAAE,OAAO,CAAC,kBAAkB,CAAM,GAAG,kBAAkB,CAmBzG;AAED,4GAA4G;AAC5G,eAAO,MAAM,yBAAyB,6CAA0D,CAAC;AAEjG,wBAAgB,qBAAqB,IAAI,kBAAkB,CAE1D;AAED,qHAAqH;AACrH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,kBAAkB,EAAE,aAAa,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAYnH"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
export const FOCAL_PLANE_DEFAULTS = {
|
|
3
|
+
cameraDistancePx: 1000,
|
|
4
|
+
layerDepthPx: 240,
|
|
5
|
+
motionMs: 580,
|
|
6
|
+
recedeDim: 0.18,
|
|
7
|
+
recedeBlurPx: 0,
|
|
8
|
+
modalBlurPx: 2.5,
|
|
9
|
+
windowOpacity: 0.56,
|
|
10
|
+
hoverLiftPx: 16,
|
|
11
|
+
};
|
|
12
|
+
/** @wildo_source:part:end presets.3d.focal-plane-settings */
|
|
13
|
+
/** The range each setting accepts: outside it the layout would draw nonsense (a camera behind the plane, a negative dim). */
|
|
14
|
+
const SETTING_RANGES = {
|
|
15
|
+
cameraDistancePx: [200, 10000],
|
|
16
|
+
layerDepthPx: [0, 2000],
|
|
17
|
+
motionMs: [0, 5000],
|
|
18
|
+
recedeDim: [0, 0.9],
|
|
19
|
+
recedeBlurPx: [0, 20],
|
|
20
|
+
modalBlurPx: [0, 20],
|
|
21
|
+
windowOpacity: [0.05, 1],
|
|
22
|
+
hoverLiftPx: [0, 200],
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* The application's overrides over the defaults, checked: an unknown key or a value outside its range is refused at
|
|
26
|
+
* registration, naming the setting, rather than drawn as something the application did not mean.
|
|
27
|
+
*/
|
|
28
|
+
export function resolveFocalPlaneSettings(overrides = {}) {
|
|
29
|
+
const resolved = { ...FOCAL_PLANE_DEFAULTS };
|
|
30
|
+
for (const [key, value] of Object.entries(overrides)) {
|
|
31
|
+
if (!(key in SETTING_RANGES))
|
|
32
|
+
throw new Error(`register3DLayout(): the focal plane has no setting '${key}'.`);
|
|
33
|
+
const [min, max] = SETTING_RANGES[key];
|
|
34
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value < min || value > max) {
|
|
35
|
+
throw new Error(`register3DLayout(): focalPlane.${key} must be a number from ${min} to ${max}; got ${String(value)}.`);
|
|
36
|
+
}
|
|
37
|
+
resolved[key] = value;
|
|
38
|
+
}
|
|
39
|
+
// Each value alone can be in range and the combination still put an object at or behind the camera: the nearest any
|
|
40
|
+
// object comes is a sheet arriving at 0.9 of a layer in front of the plane, and a hovered panel at its lift.
|
|
41
|
+
if (resolved.layerDepthPx * 0.9 >= resolved.cameraDistancePx * 0.8) {
|
|
42
|
+
throw new Error(`register3DLayout(): focalPlane.layerDepthPx (${resolved.layerDepthPx}) is too deep for a camera ${resolved.cameraDistancePx}px away: a popup arriving from in front would reach the camera. Keep it under ${Math.floor((resolved.cameraDistancePx * 0.8) / 0.9)}.`);
|
|
43
|
+
}
|
|
44
|
+
if (resolved.hoverLiftPx >= resolved.cameraDistancePx * 0.5) {
|
|
45
|
+
throw new Error(`register3DLayout(): focalPlane.hoverLiftPx (${resolved.hoverLiftPx}) must stay under half the camera distance (${resolved.cameraDistancePx * 0.5}).`);
|
|
46
|
+
}
|
|
47
|
+
return resolved;
|
|
48
|
+
}
|
|
49
|
+
/** The resolved settings, provided by the 3D main container to its placement. Outside one, the defaults. */
|
|
50
|
+
export const FocalPlaneSettingsContext = createContext(FOCAL_PLANE_DEFAULTS);
|
|
51
|
+
export function useFocalPlaneSettings() {
|
|
52
|
+
return useContext(FocalPlaneSettingsContext);
|
|
53
|
+
}
|
|
54
|
+
/** The settings as the custom properties the focal-plane stylesheet reads. Reduced motion zeroes the travel time. */
|
|
55
|
+
export function focalPlaneCssVariables(settings, reducedMotion) {
|
|
56
|
+
return {
|
|
57
|
+
'--fp-distance': `${settings.cameraDistancePx}px`,
|
|
58
|
+
'--fp-step': `${settings.layerDepthPx}px`,
|
|
59
|
+
'--fp-motion': `${reducedMotion ? 0 : settings.motionMs}ms`,
|
|
60
|
+
'--fp-dim': String(settings.recedeDim),
|
|
61
|
+
'--fp-recede-blur': `${settings.recedeBlurPx}px`,
|
|
62
|
+
'--fp-modal-blur': `${settings.modalBlurPx}px`,
|
|
63
|
+
'--fp-glass': `${Math.round(settings.windowOpacity * 100)}%`,
|
|
64
|
+
'--fp-hover-lift': `${settings.hoverLiftPx}px`,
|
|
65
|
+
'--fp-ease': 'cubic-bezier(0.2, 0.9, 0.25, 1)',
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=focal-plane-settings.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"focal-plane-settings.js","sourceRoot":"","sources":["../../../../src/focal-plane/focal-plane-settings.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,OAAO,CAAC;AA0BlD,MAAM,CAAC,MAAM,oBAAoB,GAAuB;IACtD,gBAAgB,EAAE,IAAI;IACtB,YAAY,EAAE,GAAG;IACjB,QAAQ,EAAE,GAAG;IACb,SAAS,EAAE,IAAI;IACf,YAAY,EAAE,CAAC;IACf,WAAW,EAAE,GAAG;IAChB,aAAa,EAAE,IAAI;IACnB,WAAW,EAAE,EAAE;CAChB,CAAC;AACF,6DAA6D;AAE7D,6HAA6H;AAC7H,MAAM,cAAc,GAAgE;IAClF,gBAAgB,EAAE,CAAC,GAAG,EAAE,KAAK,CAAC;IAC9B,YAAY,EAAE,CAAC,CAAC,EAAE,IAAI,CAAC;IACvB,QAAQ,EAAE,CAAC,CAAC,EAAE,IAAI,CAAC;IACnB,SAAS,EAAE,CAAC,CAAC,EAAE,GAAG,CAAC;IACnB,YAAY,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC;IACrB,WAAW,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC;IACpB,aAAa,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC;IACxB,WAAW,EAAE,CAAC,CAAC,EAAE,GAAG,CAAC;CACtB,CAAC;AAEF;;;GAGG;AACH,MAAM,UAAU,yBAAyB,CAAC,YAAyC,EAAE;IACnF,MAAM,QAAQ,GAAuB,EAAE,GAAG,oBAAoB,EAAE,CAAC;IACjE,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,CAAC;QACrD,IAAI,CAAC,CAAC,GAAG,IAAI,cAAc,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,uDAAuD,GAAG,IAAI,CAAC,CAAC;QAC9G,MAAM,CAAC,GAAG,EAAE,GAAG,CAAC,GAAG,cAAc,CAAC,GAA+B,CAAC,CAAC;QACnE,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,GAAG,IAAI,KAAK,GAAG,GAAG,EAAE,CAAC;YACvF,MAAM,IAAI,KAAK,CAAC,kCAAkC,GAAG,0BAA0B,GAAG,OAAO,GAAG,SAAS,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACzH,CAAC;QACD,QAAQ,CAAC,GAA+B,CAAC,GAAG,KAAK,CAAC;IACpD,CAAC;IACD,oHAAoH;IACpH,6GAA6G;IAC7G,IAAI,QAAQ,CAAC,YAAY,GAAG,GAAG,IAAI,QAAQ,CAAC,gBAAgB,GAAG,GAAG,EAAE,CAAC;QACnE,MAAM,IAAI,KAAK,CAAC,gDAAgD,QAAQ,CAAC,YAAY,8BAA8B,QAAQ,CAAC,gBAAgB,iFAAiF,IAAI,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,gBAAgB,GAAG,GAAG,CAAC,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC;IACvR,CAAC;IACD,IAAI,QAAQ,CAAC,WAAW,IAAI,QAAQ,CAAC,gBAAgB,GAAG,GAAG,EAAE,CAAC;QAC5D,MAAM,IAAI,KAAK,CAAC,+CAA+C,QAAQ,CAAC,WAAW,+CAA+C,QAAQ,CAAC,gBAAgB,GAAG,GAAG,IAAI,CAAC,CAAC;IACzK,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,4GAA4G;AAC5G,MAAM,CAAC,MAAM,yBAAyB,GAAG,aAAa,CAAqB,oBAAoB,CAAC,CAAC;AAEjG,MAAM,UAAU,qBAAqB;IACnC,OAAO,UAAU,CAAC,yBAAyB,CAAC,CAAC;AAC/C,CAAC;AAED,qHAAqH;AACrH,MAAM,UAAU,sBAAsB,CAAC,QAA4B,EAAE,aAAsB;IACzF,OAAO;QACL,eAAe,EAAE,GAAG,QAAQ,CAAC,gBAAgB,IAAI;QACjD,WAAW,EAAE,GAAG,QAAQ,CAAC,YAAY,IAAI;QACzC,aAAa,EAAE,GAAG,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,QAAQ,IAAI;QAC3D,UAAU,EAAE,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC;QACtC,kBAAkB,EAAE,GAAG,QAAQ,CAAC,YAAY,IAAI;QAChD,iBAAiB,EAAE,GAAG,QAAQ,CAAC,WAAW,IAAI;QAC9C,YAAY,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,aAAa,GAAG,GAAG,CAAC,GAAG;QAC5D,iBAAiB,EAAE,GAAG,QAAQ,CAAC,WAAW,IAAI;QAC9C,WAAW,EAAE,iCAAiC;KAC/C,CAAC;AACJ,CAAC","sourcesContent":["import { createContext, useContext } from 'react';\n\n/** @wildo_source:part:start presets.3d.focal-plane-settings facet:layer:frontend facet:family:layout-3d facet:audience:app-developer */\n/**\n * The focal plane's numbers (#2049). The defaults are the values Paul settled on in the prototype on 2026-09-30.\n * They reach the stylesheet as CSS custom properties on the layout's root, so every object reads one source.\n */\nexport interface FocalPlaneSettings {\n /** The still camera's distance from the focal plane, in CSS pixels: the `perspective()` every object is seen through. */\n cameraDistancePx: number;\n /** How far back one layer steps, in CSS pixels. A pane behind the focused one steps back part of it. */\n layerDepthPx: number;\n /** How long an object takes to travel, in milliseconds. */\n motionMs: number;\n /** How much a receding object dims per layer, 0-1. */\n recedeDim: number;\n /** How much a receding PANE blurs per layer, in CSS pixels (navigation; 0 keeps panes sharp). */\n recedeBlurPx: number;\n /** How much everything blurs behind a MODAL popup (dialog, sheet, command palette), in CSS pixels. */\n modalBlurPx: number;\n /** How opaque a glass window is, 0-1: the rest is the background blurred through it. */\n windowOpacity: number;\n /** How far a shell panel rises toward the camera under the pointer, in CSS pixels. */\n hoverLiftPx: number;\n}\n\nexport const FOCAL_PLANE_DEFAULTS: FocalPlaneSettings = {\n cameraDistancePx: 1000,\n layerDepthPx: 240,\n motionMs: 580,\n recedeDim: 0.18,\n recedeBlurPx: 0,\n modalBlurPx: 2.5,\n windowOpacity: 0.56,\n hoverLiftPx: 16,\n};\n/** @wildo_source:part:end presets.3d.focal-plane-settings */\n\n/** The range each setting accepts: outside it the layout would draw nonsense (a camera behind the plane, a negative dim). */\nconst SETTING_RANGES: Record<keyof FocalPlaneSettings, readonly [number, number]> = {\n cameraDistancePx: [200, 10000],\n layerDepthPx: [0, 2000],\n motionMs: [0, 5000],\n recedeDim: [0, 0.9],\n recedeBlurPx: [0, 20],\n modalBlurPx: [0, 20],\n windowOpacity: [0.05, 1],\n hoverLiftPx: [0, 200],\n};\n\n/**\n * The application's overrides over the defaults, checked: an unknown key or a value outside its range is refused at\n * registration, naming the setting, rather than drawn as something the application did not mean.\n */\nexport function resolveFocalPlaneSettings(overrides: Partial<FocalPlaneSettings> = {}): FocalPlaneSettings {\n const resolved: FocalPlaneSettings = { ...FOCAL_PLANE_DEFAULTS };\n for (const [key, value] of Object.entries(overrides)) {\n if (!(key in SETTING_RANGES)) throw new Error(`register3DLayout(): the focal plane has no setting '${key}'.`);\n const [min, max] = SETTING_RANGES[key as keyof FocalPlaneSettings];\n if (typeof value !== 'number' || !Number.isFinite(value) || value < min || value > max) {\n throw new Error(`register3DLayout(): focalPlane.${key} must be a number from ${min} to ${max}; got ${String(value)}.`);\n }\n resolved[key as keyof FocalPlaneSettings] = value;\n }\n // Each value alone can be in range and the combination still put an object at or behind the camera: the nearest any\n // object comes is a sheet arriving at 0.9 of a layer in front of the plane, and a hovered panel at its lift.\n if (resolved.layerDepthPx * 0.9 >= resolved.cameraDistancePx * 0.8) {\n throw new Error(`register3DLayout(): focalPlane.layerDepthPx (${resolved.layerDepthPx}) is too deep for a camera ${resolved.cameraDistancePx}px away: a popup arriving from in front would reach the camera. Keep it under ${Math.floor((resolved.cameraDistancePx * 0.8) / 0.9)}.`);\n }\n if (resolved.hoverLiftPx >= resolved.cameraDistancePx * 0.5) {\n throw new Error(`register3DLayout(): focalPlane.hoverLiftPx (${resolved.hoverLiftPx}) must stay under half the camera distance (${resolved.cameraDistancePx * 0.5}).`);\n }\n return resolved;\n}\n\n/** The resolved settings, provided by the 3D main container to its placement. Outside one, the defaults. */\nexport const FocalPlaneSettingsContext = createContext<FocalPlaneSettings>(FOCAL_PLANE_DEFAULTS);\n\nexport function useFocalPlaneSettings(): FocalPlaneSettings {\n return useContext(FocalPlaneSettingsContext);\n}\n\n/** The settings as the custom properties the focal-plane stylesheet reads. Reduced motion zeroes the travel time. */\nexport function focalPlaneCssVariables(settings: FocalPlaneSettings, reducedMotion: boolean): Record<string, string> {\n return {\n '--fp-distance': `${settings.cameraDistancePx}px`,\n '--fp-step': `${settings.layerDepthPx}px`,\n '--fp-motion': `${reducedMotion ? 0 : settings.motionMs}ms`,\n '--fp-dim': String(settings.recedeDim),\n '--fp-recede-blur': `${settings.recedeBlurPx}px`,\n '--fp-modal-blur': `${settings.modalBlurPx}px`,\n '--fp-glass': `${Math.round(settings.windowOpacity * 100)}%`,\n '--fp-hover-lift': `${settings.hoverLiftPx}px`,\n '--fp-ease': 'cubic-bezier(0.2, 0.9, 0.25, 1)',\n };\n}\n"]}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { type AppLayoutPreset_MainContainer_DefaultProps } from '@wildo-ai/saas-frontend-lib';
|
|
3
|
+
/**
|
|
4
|
+
* The focal plane's placement (#2049). The regions are the application's own, rendered by the standard outlets with
|
|
5
|
+
* every context; this container arranges them as floating glass windows over an ambient background, and marks each
|
|
6
|
+
* as an object the still camera sees in depth. The navigation panes inside the content region are objects too, found
|
|
7
|
+
* by the engine's own markers (see `focal-plane-stylesheet.ts`).
|
|
8
|
+
*
|
|
9
|
+
* Overlays render in `AppShellOverlayPortalLayer`, outside every transformed object: a dialog inside a transformed pane
|
|
10
|
+
* would be confined to it, and a menu placed in the wrong coordinates.
|
|
11
|
+
*/
|
|
12
|
+
export declare function FocalPlaneShellContainer({ children }: AppLayoutPreset_MainContainer_DefaultProps): React.JSX.Element;
|
|
13
|
+
//# sourceMappingURL=focal-plane-shell-container.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"focal-plane-shell-container.d.ts","sourceRoot":"","sources":["../../../../src/focal-plane/focal-plane-shell-container.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAuD,MAAM,OAAO,CAAC;AAG5E,OAAO,EAcL,KAAK,0CAA0C,EAChD,MAAM,6BAA6B,CAAC;AAOrC;;;;;;;;GAQG;AACH,wBAAgB,wBAAwB,CAAC,EAAE,QAAQ,EAAE,EAAE,0CAA0C,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO,CASpH"}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
3
|
+
import { HorizontalPlacement, ShellRegion, SurfaceVariant, VerticalPlacement } from '@wildo-ai/presets-components-models';
|
|
4
|
+
import { AppShellContentOutlet, AppShellLoadingState, AppShellOverlayPortalLayer, AppShellOverlaysOutlet, AppShellProviders, AppShellRegionOutlet, NON_CANONICAL_SHELL_SURFACE_POLICY_OVERRIDE, Surface, useAppShellFirstReady, useAppShellLayout, NavigationZone, useNavigationState, useUI, } from '@wildo-ai/saas-frontend-lib';
|
|
5
|
+
import { focalPlaneCssVariables, useFocalPlaneSettings } from './focal-plane-settings.js';
|
|
6
|
+
import { installFocalPlaneStylesheet } from './focal-plane-stylesheet.js';
|
|
7
|
+
import { usePaneDepth } from './use-pane-depth.js';
|
|
8
|
+
import { useSharedVanishingPoint } from './use-shared-vanishing-point.js';
|
|
9
|
+
/**
|
|
10
|
+
* The focal plane's placement (#2049). The regions are the application's own, rendered by the standard outlets with
|
|
11
|
+
* every context; this container arranges them as floating glass windows over an ambient background, and marks each
|
|
12
|
+
* as an object the still camera sees in depth. The navigation panes inside the content region are objects too, found
|
|
13
|
+
* by the engine's own markers (see `focal-plane-stylesheet.ts`).
|
|
14
|
+
*
|
|
15
|
+
* Overlays render in `AppShellOverlayPortalLayer`, outside every transformed object: a dialog inside a transformed pane
|
|
16
|
+
* would be confined to it, and a menu placed in the wrong coordinates.
|
|
17
|
+
*/
|
|
18
|
+
export function FocalPlaneShellContainer({ children }) {
|
|
19
|
+
const layoutInfo = useAppShellLayout();
|
|
20
|
+
const hasLayoutBeenReady = useAppShellFirstReady(layoutInfo.isLayoutReady);
|
|
21
|
+
if (!layoutInfo.isLayoutReady && !hasLayoutBeenReady)
|
|
22
|
+
return _jsx(AppShellLoadingState, {});
|
|
23
|
+
return (_jsx(AppShellProviders, { layout: layoutInfo, children: _jsx(FocalPlaneStage, { children: children }) }));
|
|
24
|
+
}
|
|
25
|
+
function FocalPlaneStage({ children }) {
|
|
26
|
+
const { prefersReducedMotion } = useUI();
|
|
27
|
+
const rootRef = useRef(null);
|
|
28
|
+
const settings = useFocalPlaneSettings();
|
|
29
|
+
const navigation = useNavigationState();
|
|
30
|
+
useLayoutEffect(() => installFocalPlaneStylesheet(), []);
|
|
31
|
+
useSharedVanishingPoint(rootRef);
|
|
32
|
+
const { dip } = usePaneDepth(rootRef, settings, prefersReducedMotion);
|
|
33
|
+
const front = useFrontPane(rootRef, navigation.focusedZone);
|
|
34
|
+
useColumnDip(rootRef, settings, prefersReducedMotion);
|
|
35
|
+
usePlaceChanges(navigation, (zone, how) => {
|
|
36
|
+
if (how === PlaceChange.CHANGED)
|
|
37
|
+
dip(zone);
|
|
38
|
+
front.bringForward(zone);
|
|
39
|
+
});
|
|
40
|
+
const shell = (content) => _jsx("div", { "data-fp-object": "shell", children: content });
|
|
41
|
+
return (_jsx(Surface, { asChild: true, emitPolicyVars: false, variant: SurfaceVariant.SHELL, policyOverride: NON_CANONICAL_SHELL_SURFACE_POLICY_OVERRIDE, children: _jsxs("div", { ref: rootRef, "data-focal-plane": "", "data-fp-focus": front.zone, style: focalPlaneCssVariables(settings, prefersReducedMotion), children: [_jsx("div", { className: "fp-ghosts", "aria-hidden": "true" }), _jsxs("div", { className: "fp-backdrop", "aria-hidden": "true", children: [_jsx("i", { className: "fp-a" }), _jsx("i", { className: "fp-b" }), _jsx("i", { className: "fp-c" })] }), _jsxs("div", { className: "fp-grid", children: [shell(_jsx(AppShellRegionOutlet, { region: ShellRegion.STATUS_BAR, placement: VerticalPlacement.TOP })), shell(_jsx(AppShellRegionOutlet, { region: ShellRegion.MENUBAR })), _jsxs("div", { className: "fp-row fp-middle", children: [shell(_jsx(AppShellRegionOutlet, { region: ShellRegion.ACTIVITY_BAR })), shell(_jsx(AppShellRegionOutlet, { region: ShellRegion.SIDEBAR, placement: HorizontalPlacement.START })), shell(_jsx(AppShellRegionOutlet, { region: ShellRegion.UTILITY_PANEL, placement: HorizontalPlacement.START })), _jsxs("div", { className: "fp-column", children: [shell(_jsx(AppShellRegionOutlet, { region: ShellRegion.TOOLBAR })), _jsx("div", { className: "fp-content", children: _jsx(AppShellContentOutlet, { className: "flex-1 relative min-w-0 min-h-0", children: children }) })] }), shell(_jsx(AppShellRegionOutlet, { region: ShellRegion.UTILITY_PANEL, placement: HorizontalPlacement.END })), shell(_jsx(AppShellRegionOutlet, { region: ShellRegion.SIDEBAR, placement: HorizontalPlacement.END }))] }), shell(_jsx(AppShellRegionOutlet, { region: ShellRegion.STATUS_BAR, placement: VerticalPlacement.BOTTOM }))] }), _jsx("div", { className: "fp-ghosts fp-ghosts-front", "aria-hidden": "true" }), _jsx(AppShellOverlaysOutlet, {}), _jsx(AppShellOverlayPortalLayer, {})] }) }));
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Which pane is in front: the one you last pressed in or moved keyboard focus into, or the one that last opened or
|
|
45
|
+
* changed place. Kept by the layout rather than read from the navigation state's `focusedZone` alone, because that
|
|
46
|
+
* follows only interaction: opening a record beside the list leaves the LIST focused (the click happened there), and
|
|
47
|
+
* the prototype this layout follows brings the new pane forward. The engine's focus state is never written.
|
|
48
|
+
*/
|
|
49
|
+
function useFrontPane(rootRef, focusedZone) {
|
|
50
|
+
const [zone, setZone] = useState(focusedZone);
|
|
51
|
+
useEffect(() => setZone(focusedZone), [focusedZone]);
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
const root = rootRef.current;
|
|
54
|
+
if (!root)
|
|
55
|
+
return undefined;
|
|
56
|
+
const fromEvent = (event) => {
|
|
57
|
+
const target = event.target instanceof Element ? event.target : null;
|
|
58
|
+
const pane = target?.closest(`[data-component-area="${ShellRegion.CONTENT}"] :is([data-content-area], [data-panel-id^="nav-"])`);
|
|
59
|
+
const name = pane?.dataset.contentArea ?? pane?.dataset.panelId?.replace(/^nav-/, '');
|
|
60
|
+
if (name)
|
|
61
|
+
setZone(name);
|
|
62
|
+
};
|
|
63
|
+
root.addEventListener('pointerdown', fromEvent, true);
|
|
64
|
+
root.addEventListener('focusin', fromEvent, true);
|
|
65
|
+
return () => {
|
|
66
|
+
root.removeEventListener('pointerdown', fromEvent, true);
|
|
67
|
+
root.removeEventListener('focusin', fromEvent, true);
|
|
68
|
+
};
|
|
69
|
+
}, [rootRef]);
|
|
70
|
+
return { zone, bringForward: setZone };
|
|
71
|
+
}
|
|
72
|
+
const STRUCTURAL_ZONES = new Set([NavigationZone.PRIMARY, NavigationZone.SECONDARY, NavigationZone.COMPANION]);
|
|
73
|
+
/** How a navigation zone's place moved between two renders. */
|
|
74
|
+
var PlaceChange;
|
|
75
|
+
(function (PlaceChange) {
|
|
76
|
+
/** The zone held nothing and now holds a place: a pane opened. */
|
|
77
|
+
PlaceChange["APPEARED"] = "appeared";
|
|
78
|
+
/** The zone showed one place and now shows another, in the same pane. */
|
|
79
|
+
PlaceChange["CHANGED"] = "changed";
|
|
80
|
+
})(PlaceChange || (PlaceChange = {}));
|
|
81
|
+
/**
|
|
82
|
+
* A zone that APPEARS (it held nothing, now it holds a place) or CHANGES place (the same zone now shows another URL
|
|
83
|
+
* path) is reported: a change dips back and returns, and either way the pane comes forward. The arrival itself is the
|
|
84
|
+
* stylesheet's (a side pane arrives from depth when it mounts).
|
|
85
|
+
*/
|
|
86
|
+
function usePlaceChanges(navigation, onPlace) {
|
|
87
|
+
// The structural zones only: the overlay zone is a dialog, not a pane, and must never become the "front pane".
|
|
88
|
+
const zones = Object.keys(navigation.state.zones).filter((zone) => STRUCTURAL_ZONES.has(String(zone)));
|
|
89
|
+
const places = zones.map((zone) => {
|
|
90
|
+
const top = navigation.getTopEntry(zone);
|
|
91
|
+
return top ? (top.urlPath?.split(/[?#]/)[0] ?? top.id) : '';
|
|
92
|
+
});
|
|
93
|
+
const signature = zones.map((zone, index) => `${String(zone)}:${places[index]}`).join('|');
|
|
94
|
+
const previous = useRef(null);
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
const current = new Map(zones.map((zone, index) => [String(zone), places[index] ?? '']));
|
|
97
|
+
const before = previous.current;
|
|
98
|
+
previous.current = current;
|
|
99
|
+
if (!before)
|
|
100
|
+
return;
|
|
101
|
+
for (const [zone, place] of current) {
|
|
102
|
+
const was = before.get(zone) ?? '';
|
|
103
|
+
if (place === '' || was === place)
|
|
104
|
+
continue;
|
|
105
|
+
onPlace(zone, was === '' ? PlaceChange.APPEARED : PlaceChange.CHANGED);
|
|
106
|
+
}
|
|
107
|
+
// `signature` is the whole dependency: the zones and places are derived from the same render.
|
|
108
|
+
}, [signature]);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* A side column (the sidebar folding to its rail or unfolding) dips back and returns while the panes re-arrange. The
|
|
112
|
+
* width animates over many frames, so the dip plays once per settled change of more than a few pixels, not per frame.
|
|
113
|
+
* The dip runs on this layout's OWN wrapper, through the Web Animations API, so nothing is written on a region.
|
|
114
|
+
*/
|
|
115
|
+
function useColumnDip(rootRef, settings, reducedMotion) {
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
const root = rootRef.current;
|
|
118
|
+
if (!root || reducedMotion || typeof ResizeObserver === 'undefined')
|
|
119
|
+
return undefined;
|
|
120
|
+
const columns = [...root.querySelectorAll('[data-fp-object="shell"]')].filter((wrapper) => wrapper.querySelector(`:scope > [data-component-area="${ShellRegion.SIDEBAR}"], :scope > [data-component-area="${ShellRegion.UTILITY_PANEL}"]`));
|
|
121
|
+
const settled = new Map();
|
|
122
|
+
const lastDip = new Map();
|
|
123
|
+
const observer = new ResizeObserver((entries) => {
|
|
124
|
+
for (const entry of entries) {
|
|
125
|
+
const wrapper = entry.target;
|
|
126
|
+
const width = wrapper.offsetWidth;
|
|
127
|
+
const previous = settled.get(wrapper);
|
|
128
|
+
if (previous === undefined || width === 0) {
|
|
129
|
+
settled.set(wrapper, width);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (Math.abs(width - previous) <= 24)
|
|
133
|
+
continue;
|
|
134
|
+
settled.set(wrapper, width);
|
|
135
|
+
const now = performance.now();
|
|
136
|
+
if (now - (lastDip.get(wrapper) ?? 0) < settings.motionMs)
|
|
137
|
+
continue;
|
|
138
|
+
lastDip.set(wrapper, now);
|
|
139
|
+
wrapper.animate([{ offset: 0.5, transform: `perspective(${settings.cameraDistancePx}px) translate3d(0, 0, ${-settings.layerDepthPx * 0.3}px)` }], { duration: settings.motionMs, easing: 'cubic-bezier(0.2, 0.9, 0.25, 1)' });
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
for (const column of columns)
|
|
143
|
+
observer.observe(column);
|
|
144
|
+
return () => observer.disconnect();
|
|
145
|
+
}, [rootRef, settings, reducedMotion]);
|
|
146
|
+
}
|
|
147
|
+
//# sourceMappingURL=focal-plane-shell-container.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"focal-plane-shell-container.js","sourceRoot":"","sources":["../../../../src/focal-plane/focal-plane-shell-container.tsx"],"names":[],"mappings":"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","sourcesContent":["import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';\n\nimport { HorizontalPlacement, ShellRegion, SurfaceVariant, VerticalPlacement } from '@wildo-ai/presets-components-models';\nimport {\n AppShellContentOutlet,\n AppShellLoadingState,\n AppShellOverlayPortalLayer,\n AppShellOverlaysOutlet,\n AppShellProviders,\n AppShellRegionOutlet,\n NON_CANONICAL_SHELL_SURFACE_POLICY_OVERRIDE,\n Surface,\n useAppShellFirstReady,\n useAppShellLayout,\n NavigationZone,\n useNavigationState,\n useUI,\n type AppLayoutPreset_MainContainer_DefaultProps,\n} from '@wildo-ai/saas-frontend-lib';\n\nimport { focalPlaneCssVariables, useFocalPlaneSettings, type FocalPlaneSettings } from './focal-plane-settings';\nimport { installFocalPlaneStylesheet } from './focal-plane-stylesheet';\nimport { usePaneDepth } from './use-pane-depth';\nimport { useSharedVanishingPoint } from './use-shared-vanishing-point';\n\n/**\n * The focal plane's placement (#2049). The regions are the application's own, rendered by the standard outlets with\n * every context; this container arranges them as floating glass windows over an ambient background, and marks each\n * as an object the still camera sees in depth. The navigation panes inside the content region are objects too, found\n * by the engine's own markers (see `focal-plane-stylesheet.ts`).\n *\n * Overlays render in `AppShellOverlayPortalLayer`, outside every transformed object: a dialog inside a transformed pane\n * would be confined to it, and a menu placed in the wrong coordinates.\n */\nexport function FocalPlaneShellContainer({ children }: AppLayoutPreset_MainContainer_DefaultProps): React.JSX.Element {\n const layoutInfo = useAppShellLayout();\n const hasLayoutBeenReady = useAppShellFirstReady(layoutInfo.isLayoutReady);\n if (!layoutInfo.isLayoutReady && !hasLayoutBeenReady) return <AppShellLoadingState />;\n return (\n <AppShellProviders layout={layoutInfo}>\n <FocalPlaneStage>{children}</FocalPlaneStage>\n </AppShellProviders>\n );\n}\n\nfunction FocalPlaneStage({ children }: { children: React.ReactNode }): React.JSX.Element {\n const { prefersReducedMotion } = useUI();\n const rootRef = useRef<HTMLDivElement>(null);\n const settings = useFocalPlaneSettings();\n const navigation = useNavigationState();\n useLayoutEffect(() => installFocalPlaneStylesheet(), []);\n useSharedVanishingPoint(rootRef);\n const { dip } = usePaneDepth(rootRef, settings, prefersReducedMotion);\n const front = useFrontPane(rootRef, navigation.focusedZone);\n useColumnDip(rootRef, settings, prefersReducedMotion);\n usePlaceChanges(navigation, (zone, how) => {\n if (how === PlaceChange.CHANGED) dip(zone);\n front.bringForward(zone);\n });\n const shell = (content: React.ReactNode): React.JSX.Element => <div data-fp-object=\"shell\">{content}</div>;\n\n return (\n <Surface asChild emitPolicyVars={false} variant={SurfaceVariant.SHELL} policyOverride={NON_CANONICAL_SHELL_SURFACE_POLICY_OVERRIDE}>\n <div\n ref={rootRef}\n data-focal-plane=\"\"\n data-fp-focus={front.zone}\n style={focalPlaneCssVariables(settings, prefersReducedMotion) as React.CSSProperties}\n >\n <div className=\"fp-ghosts\" aria-hidden=\"true\" />\n <div className=\"fp-backdrop\" aria-hidden=\"true\">\n <i className=\"fp-a\" />\n <i className=\"fp-b\" />\n <i className=\"fp-c\" />\n </div>\n <div className=\"fp-grid\">\n {shell(<AppShellRegionOutlet region={ShellRegion.STATUS_BAR} placement={VerticalPlacement.TOP} />)}\n {shell(<AppShellRegionOutlet region={ShellRegion.MENUBAR} />)}\n <div className=\"fp-row fp-middle\">\n {shell(<AppShellRegionOutlet region={ShellRegion.ACTIVITY_BAR} />)}\n {shell(<AppShellRegionOutlet region={ShellRegion.SIDEBAR} placement={HorizontalPlacement.START} />)}\n {shell(<AppShellRegionOutlet region={ShellRegion.UTILITY_PANEL} placement={HorizontalPlacement.START} />)}\n <div className=\"fp-column\">\n {shell(<AppShellRegionOutlet region={ShellRegion.TOOLBAR} />)}\n <div className=\"fp-content\">\n <AppShellContentOutlet className=\"flex-1 relative min-w-0 min-h-0\">{children}</AppShellContentOutlet>\n </div>\n </div>\n {shell(<AppShellRegionOutlet region={ShellRegion.UTILITY_PANEL} placement={HorizontalPlacement.END} />)}\n {shell(<AppShellRegionOutlet region={ShellRegion.SIDEBAR} placement={HorizontalPlacement.END} />)}\n </div>\n {shell(<AppShellRegionOutlet region={ShellRegion.STATUS_BAR} placement={VerticalPlacement.BOTTOM} />)}\n </div>\n <div className=\"fp-ghosts fp-ghosts-front\" aria-hidden=\"true\" />\n <AppShellOverlaysOutlet />\n <AppShellOverlayPortalLayer />\n </div>\n </Surface>\n );\n}\n\n/**\n * Which pane is in front: the one you last pressed in or moved keyboard focus into, or the one that last opened or\n * changed place. Kept by the layout rather than read from the navigation state's `focusedZone` alone, because that\n * follows only interaction: opening a record beside the list leaves the LIST focused (the click happened there), and\n * the prototype this layout follows brings the new pane forward. The engine's focus state is never written.\n */\nfunction useFrontPane(rootRef: React.RefObject<HTMLElement | null>, focusedZone: string): { zone: string; bringForward: (zone: string) => void } {\n const [zone, setZone] = useState<string>(focusedZone);\n useEffect(() => setZone(focusedZone), [focusedZone]);\n useEffect(() => {\n const root = rootRef.current;\n if (!root) return undefined;\n const fromEvent = (event: Event): void => {\n const target = event.target instanceof Element ? event.target : null;\n const pane = target?.closest<HTMLElement>(`[data-component-area=\"${ShellRegion.CONTENT}\"] :is([data-content-area], [data-panel-id^=\"nav-\"])`);\n const name = pane?.dataset.contentArea ?? pane?.dataset.panelId?.replace(/^nav-/, '');\n if (name) setZone(name);\n };\n root.addEventListener('pointerdown', fromEvent, true);\n root.addEventListener('focusin', fromEvent, true);\n return () => {\n root.removeEventListener('pointerdown', fromEvent, true);\n root.removeEventListener('focusin', fromEvent, true);\n };\n }, [rootRef]);\n return { zone, bringForward: setZone };\n}\n\nconst STRUCTURAL_ZONES: ReadonlySet<string> = new Set([NavigationZone.PRIMARY, NavigationZone.SECONDARY, NavigationZone.COMPANION]);\n\n/** How a navigation zone's place moved between two renders. */\nenum PlaceChange {\n /** The zone held nothing and now holds a place: a pane opened. */\n APPEARED = 'appeared',\n /** The zone showed one place and now shows another, in the same pane. */\n CHANGED = 'changed',\n}\n\n/**\n * A zone that APPEARS (it held nothing, now it holds a place) or CHANGES place (the same zone now shows another URL\n * path) is reported: a change dips back and returns, and either way the pane comes forward. The arrival itself is the\n * stylesheet's (a side pane arrives from depth when it mounts).\n */\nfunction usePlaceChanges(navigation: ReturnType<typeof useNavigationState>, onPlace: (zone: string, how: PlaceChange) => void): void {\n // The structural zones only: the overlay zone is a dialog, not a pane, and must never become the \"front pane\".\n const zones = (Object.keys(navigation.state.zones) as Array<keyof typeof navigation.state.zones>).filter((zone) => STRUCTURAL_ZONES.has(String(zone)));\n const places = zones.map((zone) => {\n const top = navigation.getTopEntry(zone);\n return top ? (top.urlPath?.split(/[?#]/)[0] ?? top.id) : '';\n });\n const signature = zones.map((zone, index) => `${String(zone)}:${places[index]}`).join('|');\n const previous = useRef<Map<string, string> | null>(null);\n useEffect(() => {\n const current = new Map(zones.map((zone, index) => [String(zone), places[index] ?? '']));\n const before = previous.current;\n previous.current = current;\n if (!before) return;\n for (const [zone, place] of current) {\n const was = before.get(zone) ?? '';\n if (place === '' || was === place) continue;\n onPlace(zone, was === '' ? PlaceChange.APPEARED : PlaceChange.CHANGED);\n }\n // `signature` is the whole dependency: the zones and places are derived from the same render.\n }, [signature]);\n}\n\n/**\n * A side column (the sidebar folding to its rail or unfolding) dips back and returns while the panes re-arrange. The\n * width animates over many frames, so the dip plays once per settled change of more than a few pixels, not per frame.\n * The dip runs on this layout's OWN wrapper, through the Web Animations API, so nothing is written on a region.\n */\nfunction useColumnDip(rootRef: React.RefObject<HTMLElement | null>, settings: FocalPlaneSettings, reducedMotion: boolean): void {\n useEffect(() => {\n const root = rootRef.current;\n if (!root || reducedMotion || typeof ResizeObserver === 'undefined') return undefined;\n const columns = [...root.querySelectorAll<HTMLElement>('[data-fp-object=\"shell\"]')].filter((wrapper) =>\n wrapper.querySelector(`:scope > [data-component-area=\"${ShellRegion.SIDEBAR}\"], :scope > [data-component-area=\"${ShellRegion.UTILITY_PANEL}\"]`));\n const settled = new Map<HTMLElement, number>();\n const lastDip = new Map<HTMLElement, number>();\n const observer = new ResizeObserver((entries) => {\n for (const entry of entries) {\n const wrapper = entry.target as HTMLElement;\n const width = wrapper.offsetWidth;\n const previous = settled.get(wrapper);\n if (previous === undefined || width === 0) { settled.set(wrapper, width); continue; }\n if (Math.abs(width - previous) <= 24) continue;\n settled.set(wrapper, width);\n const now = performance.now();\n if (now - (lastDip.get(wrapper) ?? 0) < settings.motionMs) continue;\n lastDip.set(wrapper, now);\n wrapper.animate(\n [{ offset: 0.5, transform: `perspective(${settings.cameraDistancePx}px) translate3d(0, 0, ${-settings.layerDepthPx * 0.3}px)` }],\n { duration: settings.motionMs, easing: 'cubic-bezier(0.2, 0.9, 0.25, 1)' },\n );\n }\n });\n for (const column of columns) observer.observe(column);\n return () => observer.disconnect();\n }, [rootRef, settings, reducedMotion]);\n}\n"]}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The navigation panes, by the engine's markers: the zone itself in the static and single-zone branches, the panel in
|
|
3
|
+
* the resizable branch — never the zone INSIDE a panel, which would be moved twice.
|
|
4
|
+
*/
|
|
5
|
+
export declare const FOCAL_PLANE_PANE_SELECTOR = "[data-component-area=\"content\"] [data-content-area]:not([data-panel-id] *), [data-component-area=\"content\"] [data-panel-id^=\"nav-\"]";
|
|
6
|
+
/** The modal content slots, exported for the tests that pin what counts as modal. */
|
|
7
|
+
export declare const FOCAL_PLANE_MODAL_SLOTS: readonly string[];
|
|
8
|
+
export declare const FOCAL_PLANE_CSS: string;
|
|
9
|
+
/**
|
|
10
|
+
* Installs the stylesheet while at least one focal-plane layout is mounted, and removes it when the last one unmounts.
|
|
11
|
+
* Returns the release function. Falls back to a `<style>` element where constructable stylesheets are missing.
|
|
12
|
+
*/
|
|
13
|
+
export declare function installFocalPlaneStylesheet(doc?: Document): () => void;
|
|
14
|
+
//# sourceMappingURL=focal-plane-stylesheet.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"focal-plane-stylesheet.d.ts","sourceRoot":"","sources":["../../../../src/focal-plane/focal-plane-stylesheet.ts"],"names":[],"mappings":"AA8BA;;;GAGG;AACH,eAAO,MAAM,yBAAyB,8IAA8F,CAAC;AAqCrI,qFAAqF;AACrF,eAAO,MAAM,uBAAuB,EAAE,SAAS,MAAM,EAAgB,CAAC;AAEtE,eAAO,MAAM,eAAe,QAoI3B,CAAC;AAKF;;;GAGG;AACH,wBAAgB,2BAA2B,CAAC,GAAG,GAAE,QAAmB,GAAG,MAAM,IAAI,CAqBhF"}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { ShellRegion } from '@wildo-ai/presets-components-models';
|
|
2
|
+
import { NavigationZone } from '@wildo-ai/saas-frontend-lib';
|
|
3
|
+
/**
|
|
4
|
+
* The focal plane's stylesheet (#2049), scoped to `[data-focal-plane]`, so it touches nothing outside the layout.
|
|
5
|
+
*
|
|
6
|
+
* ## One still camera, without a nested 3D context
|
|
7
|
+
*
|
|
8
|
+
* Every object carries its OWN `perspective(d) translate3d(0, 0, z)`, with its `transform-origin` set (per element, by
|
|
9
|
+
* `useSharedVanishingPoint`) to the one vanishing point of the stage. That is the projection of a single still camera,
|
|
10
|
+
* and it needs no `transform-style: preserve-3d` ancestor — which matters, because a nested 3D context disables
|
|
11
|
+
* `backdrop-filter`, and the glass windows are made of it.
|
|
12
|
+
*
|
|
13
|
+
* ## What counts as an object
|
|
14
|
+
*
|
|
15
|
+
* - the shell panels this layout wraps (`[data-fp-object]`);
|
|
16
|
+
* - each navigation pane inside the content region, by the engine's own markers: `[data-content-area]` (the static and
|
|
17
|
+
* single-zone branches) and the resizable panel `[data-panel-id^="nav-"]` (NOT its inner `[data-content-area]`, which
|
|
18
|
+
* the panel's inline `overflow: hidden` would clip).
|
|
19
|
+
*
|
|
20
|
+
* Depth is a custom property (`--fp-z`); states set it, and the transform reads it, so rules compose instead of fighting
|
|
21
|
+
* over `transform`.
|
|
22
|
+
*
|
|
23
|
+
* ## Why a constructable stylesheet
|
|
24
|
+
*
|
|
25
|
+
* It is installed through `document.adoptedStyleSheets`, which a content security policy without `'unsafe-inline'`
|
|
26
|
+
* does not block, where a `<style>` element would be.
|
|
27
|
+
*/
|
|
28
|
+
const ROOT = '[data-focal-plane]';
|
|
29
|
+
const CONTENT = `[data-component-area="${ShellRegion.CONTENT}"]`;
|
|
30
|
+
/**
|
|
31
|
+
* The navigation panes, by the engine's markers: the zone itself in the static and single-zone branches, the panel in
|
|
32
|
+
* the resizable branch — never the zone INSIDE a panel, which would be moved twice.
|
|
33
|
+
*/
|
|
34
|
+
export const FOCAL_PLANE_PANE_SELECTOR = `${CONTENT} [data-content-area]:not([data-panel-id] *), ${CONTENT} [data-panel-id^="nav-"]`;
|
|
35
|
+
/** The zone names the navigation state focuses, and the markers each one is found by in either branch. */
|
|
36
|
+
const ZONES = [NavigationZone.PRIMARY, NavigationZone.SECONDARY, NavigationZone.COMPANION];
|
|
37
|
+
const zoneMarker = (zone) => `:is([data-content-area="${zone}"], [data-panel-id="nav-${zone}"])`;
|
|
38
|
+
const VISIBLE_PANE = `:is(${FOCAL_PLANE_PANE_SELECTOR}):not([data-zone-hidden])`;
|
|
39
|
+
/** Two panes or more are side by side (the other panes are siblings in both branches). */
|
|
40
|
+
const SPLIT = `${CONTENT}:has(${VISIBLE_PANE} ~ ${VISIBLE_PANE})`;
|
|
41
|
+
const PANES = `:is(${FOCAL_PLANE_PANE_SELECTOR})`;
|
|
42
|
+
const OBJECTS = `:is([data-fp-object], ${FOCAL_PLANE_PANE_SELECTOR})`;
|
|
43
|
+
/**
|
|
44
|
+
* The popups the focal plane moves, by the stable `data-slot` of their portalled content (Radix `data-state` on the
|
|
45
|
+
* same element). Lifting popups carry the CSS variable Radix publishes as their trigger-relative origin.
|
|
46
|
+
*/
|
|
47
|
+
const POPUPS = {
|
|
48
|
+
lift: [
|
|
49
|
+
['dropdown-menu-content', '--radix-dropdown-menu-content-transform-origin'],
|
|
50
|
+
['dropdown-menu-sub-content', '--radix-dropdown-menu-content-transform-origin'],
|
|
51
|
+
['context-menu-content', '--radix-context-menu-content-transform-origin'],
|
|
52
|
+
['context-menu-sub-content', '--radix-context-menu-content-transform-origin'],
|
|
53
|
+
['popover-content', '--radix-popover-content-transform-origin'],
|
|
54
|
+
['hover-card-content', '--radix-hover-card-content-transform-origin'],
|
|
55
|
+
['tooltip-content', '--radix-tooltip-content-transform-origin'],
|
|
56
|
+
['select-content', '--radix-select-content-transform-origin'],
|
|
57
|
+
['menubar-content', '--radix-menubar-content-transform-origin'],
|
|
58
|
+
['menubar-sub-content', '--radix-menubar-content-transform-origin'],
|
|
59
|
+
],
|
|
60
|
+
rise: ['dialog-content', 'alert-dialog-content'],
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* A MODAL popup is open: a dialog, an alert dialog, a sheet or a drawer, by their content SLOT. Not by `role="dialog"`:
|
|
64
|
+
* a Radix popover's content carries that role too (date pickers, linked-record pickers, the notification centre), and
|
|
65
|
+
* opening one must not step the form you are editing back. Not by `body[data-scroll-locked]` either, which dropdown
|
|
66
|
+
* and context menus also set.
|
|
67
|
+
*/
|
|
68
|
+
const MODAL_SLOTS = ['dialog-content', 'alert-dialog-content', 'sheet-content', 'drawer-content'];
|
|
69
|
+
const MODAL_OPEN = `:has([data-shell-overlay-layer] :is(${MODAL_SLOTS.map((slot) => `[data-slot="${slot}"]`).join(', ')})[data-state="open"])`;
|
|
70
|
+
/** The modal content slots, exported for the tests that pin what counts as modal. */
|
|
71
|
+
export const FOCAL_PLANE_MODAL_SLOTS = MODAL_SLOTS;
|
|
72
|
+
export const FOCAL_PLANE_CSS = `
|
|
73
|
+
/* Positioned, never isolated: an isolated root would be one stacking context, and anything positioned outside the
|
|
74
|
+
shell with a lower z-index (a consent prompt) would paint over the shell's own dialogs (#1830). */
|
|
75
|
+
${ROOT} { position: relative; height: 100vh; width: 100vw; overflow: hidden; background: var(--background); }
|
|
76
|
+
|
|
77
|
+
/* The ambient background: soft fields of the theme's own colours, drifting very slowly. */
|
|
78
|
+
${ROOT} .fp-backdrop { position: absolute; inset: -20%; z-index: 0; pointer-events: none; }
|
|
79
|
+
${ROOT} .fp-backdrop i { position: absolute; border-radius: 50%; filter: blur(70px); opacity: 0.8; }
|
|
80
|
+
${ROOT} .fp-backdrop .fp-a { width: 58%; height: 62%; left: 6%; top: 4%; background: radial-gradient(closest-side, color-mix(in oklab, var(--primary) 38%, var(--background)), transparent); animation: fp-drift-a 48s ease-in-out infinite alternate; }
|
|
81
|
+
${ROOT} .fp-backdrop .fp-b { width: 52%; height: 58%; right: 4%; top: 30%; background: radial-gradient(closest-side, color-mix(in oklab, var(--accent, var(--primary)) 30%, var(--background)), transparent); animation: fp-drift-b 61s ease-in-out infinite alternate; }
|
|
82
|
+
${ROOT} .fp-backdrop .fp-c { width: 46%; height: 50%; left: 34%; bottom: 0; background: radial-gradient(closest-side, color-mix(in oklab, var(--primary) 22%, var(--foreground) 8%), transparent); animation: fp-drift-c 55s ease-in-out infinite alternate; }
|
|
83
|
+
@keyframes fp-drift-a { to { transform: translate(9%, 7%) scale(1.08); } }
|
|
84
|
+
@keyframes fp-drift-b { to { transform: translate(-8%, -6%) scale(0.94); } }
|
|
85
|
+
@keyframes fp-drift-c { to { transform: translate(-6%, -9%) scale(1.1); } }
|
|
86
|
+
|
|
87
|
+
/* The arrangement: floating windows with a margin and gaps, never edge to edge. */
|
|
88
|
+
${ROOT} .fp-grid { position: absolute; inset: 0; z-index: 1; display: flex; flex-direction: column; gap: 12px; padding: 14px; }
|
|
89
|
+
${ROOT} .fp-row { display: flex; gap: 12px; min-height: 0; min-width: 0; }
|
|
90
|
+
${ROOT} .fp-middle { flex: 1; }
|
|
91
|
+
${ROOT} .fp-column { flex: 1; display: flex; flex-direction: column; gap: 12px; min-width: 0; min-height: 0; }
|
|
92
|
+
${ROOT} .fp-content { flex: 1; min-height: 0; min-width: 0; position: relative; display: flex; flex-direction: column; }
|
|
93
|
+
${ROOT} [data-fp-object]:empty { display: none; }
|
|
94
|
+
${ROOT} [data-fp-object] { position: relative; display: flex; min-height: 0; min-width: 0; }
|
|
95
|
+
${ROOT} [data-fp-object] > * { flex: 1; min-width: 0; min-height: 0; }
|
|
96
|
+
|
|
97
|
+
/* Every object: seen through the one still camera, at its depth. */
|
|
98
|
+
${ROOT} ${OBJECTS} {
|
|
99
|
+
transform-origin: var(--fp-ox, 50%) var(--fp-oy, 50%);
|
|
100
|
+
transform: perspective(var(--fp-distance)) translate3d(0, 0, var(--fp-z, 0px));
|
|
101
|
+
filter: var(--fp-filter, none);
|
|
102
|
+
transition: transform var(--fp-motion) var(--fp-ease), filter var(--fp-motion) var(--fp-ease), box-shadow var(--fp-motion) var(--fp-ease);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/* Glass: the theme's card colour, translucent, over the background blurred through it. */
|
|
106
|
+
${ROOT} :is([data-fp-object="shell"], ${FOCAL_PLANE_PANE_SELECTOR}) {
|
|
107
|
+
background: color-mix(in oklab, var(--card, var(--background)) var(--fp-glass), transparent);
|
|
108
|
+
-webkit-backdrop-filter: blur(26px) saturate(1.5);
|
|
109
|
+
backdrop-filter: blur(26px) saturate(1.5);
|
|
110
|
+
border: 1px solid color-mix(in oklab, var(--border) 55%, transparent);
|
|
111
|
+
border-radius: 14px;
|
|
112
|
+
box-shadow: 0 18px 44px -22px rgb(0 0 0 / 0.30), 0 1px 3px rgb(0 0 0 / 0.05);
|
|
113
|
+
}
|
|
114
|
+
${ROOT} ${CONTENT} { background: transparent; }
|
|
115
|
+
${ROOT} ${CONTENT} :is([data-content-area="${NavigationZone.SECONDARY}"], [data-content-area="${NavigationZone.COMPANION}"]):not([data-panel-id] *) { margin-left: 12px; }
|
|
116
|
+
|
|
117
|
+
/* Side by side, the pane you work in rests on the focal plane; every other pane steps back and dims. The focused
|
|
118
|
+
zone comes from the navigation state, written on the root as data-fp-focus. Clicking a pane focuses it (the
|
|
119
|
+
engine does that), which brings it forward. */
|
|
120
|
+
${ZONES.map((zone) => `${ROOT}[data-fp-focus="${zone}"] ${SPLIT} ${VISIBLE_PANE}:not(${zoneMarker(zone)})`).join(', ')} {
|
|
121
|
+
--fp-z: calc(var(--fp-step) * -0.55);
|
|
122
|
+
--fp-filter: brightness(calc(1 - var(--fp-dim) * 0.55)) saturate(calc(1 - var(--fp-dim) * 0.5)) blur(calc(var(--fp-recede-blur) * 0.55));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/* A side pane arrives from depth when it opens. */
|
|
126
|
+
${ROOT} ${CONTENT} :is(${zoneMarker(NavigationZone.SECONDARY)}, ${zoneMarker(NavigationZone.COMPANION)}):is(${FOCAL_PLANE_PANE_SELECTOR}) {
|
|
127
|
+
animation: fp-pane-arrive var(--fp-motion) var(--fp-ease);
|
|
128
|
+
}
|
|
129
|
+
@keyframes fp-pane-arrive {
|
|
130
|
+
from { transform: perspective(var(--fp-distance)) translate3d(0, 0, calc(var(--fp-step) * -1.2)); opacity: 0; }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/* Ghosts: a discarded pane flying away, and the still image of the panes an expanded pane stands in front of. */
|
|
134
|
+
${ROOT} .fp-ghosts { position: absolute; inset: 0; z-index: 0; pointer-events: none; }
|
|
135
|
+
${ROOT} .fp-ghosts-front { z-index: 2; }
|
|
136
|
+
${ROOT} .fp-ghost {
|
|
137
|
+
position: absolute; box-sizing: border-box; overflow: hidden;
|
|
138
|
+
transform-origin: var(--fp-ox) var(--fp-oy);
|
|
139
|
+
transform: perspective(var(--fp-distance)) translate3d(0, 0, var(--fp-z, 0px));
|
|
140
|
+
filter: var(--fp-filter, none);
|
|
141
|
+
transition: transform var(--fp-motion) var(--fp-ease), filter var(--fp-motion) var(--fp-ease), opacity var(--fp-motion) var(--fp-ease);
|
|
142
|
+
background: color-mix(in oklab, var(--card, var(--background)) var(--fp-glass), transparent);
|
|
143
|
+
border: 1px solid color-mix(in oklab, var(--border) 55%, transparent);
|
|
144
|
+
border-radius: 14px;
|
|
145
|
+
box-shadow: 0 18px 44px -22px rgb(0 0 0 / 0.30);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/* The shell panel under the pointer rises toward the camera; its items zoom a little. */
|
|
149
|
+
${ROOT} [data-fp-object="shell"]:hover { --fp-z: var(--fp-hover-lift); }
|
|
150
|
+
${ROOT} [data-fp-object="shell"] :is(button, a):not(:disabled) { transition: transform 180ms var(--fp-ease); }
|
|
151
|
+
${ROOT} [data-fp-object="shell"] :is(button, a):not(:disabled):hover { transform: scale(1.045); }
|
|
152
|
+
|
|
153
|
+
/* Behind a modal popup, everything steps back, dims and blurs lightly. A menu is not modal: it only lifts. */
|
|
154
|
+
${ROOT}${MODAL_OPEN} ${OBJECTS} {
|
|
155
|
+
--fp-z: calc(var(--fp-step) * -0.6);
|
|
156
|
+
--fp-filter: brightness(calc(1 - var(--fp-dim) * 0.6)) saturate(0.9) blur(var(--fp-modal-blur));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/* Popups are objects too. Their recipe animations (tw-animate's enter/exit) are replaced by depth KEYFRAMES —
|
|
160
|
+
keyframes, because the overlay library keeps a closing popup mounted only until its exit ANIMATION ends; a
|
|
161
|
+
transition would be cut off. Each popup rests at its true size: depth shows while it travels.
|
|
162
|
+
- A menu, popover or tooltip lifts off its trigger: the library publishes the trigger point as the transform
|
|
163
|
+
origin, and perspective() grows the popup out of it.
|
|
164
|
+
- A dialog (the command palette is one) rises from depth to the focal plane.
|
|
165
|
+
- A sheet arrives from in front of the plane and leaves back into depth.
|
|
166
|
+
Drawers and toasts keep their own motion: their libraries position and stack them with transforms. */
|
|
167
|
+
${POPUPS.lift.map(([slot, origin]) => `${ROOT} [data-shell-overlay-layer] [data-slot="${slot}"] { transform-origin: var(${origin}); }`).join('\n')}
|
|
168
|
+
${ROOT} [data-shell-overlay-layer] :is(${POPUPS.lift.map(([slot]) => `[data-slot="${slot}"]`).join(', ')}):is([data-state="open"], [data-state="delayed-open"], [data-state="instant-open"]) { animation: fp-lift-in calc(var(--fp-motion) * 0.6) var(--fp-ease) both; }
|
|
169
|
+
${ROOT} [data-shell-overlay-layer] :is(${POPUPS.lift.map(([slot]) => `[data-slot="${slot}"]`).join(', ')})[data-state="closed"] { animation: fp-lift-out calc(var(--fp-motion) * 0.45) var(--fp-ease) both; }
|
|
170
|
+
${ROOT} [data-shell-overlay-layer] :is(${POPUPS.rise.map((slot) => `[data-slot="${slot}"]`).join(', ')})[data-state="open"] { animation: fp-rise-in var(--fp-motion) var(--fp-ease) both; }
|
|
171
|
+
${ROOT} [data-shell-overlay-layer] :is(${POPUPS.rise.map((slot) => `[data-slot="${slot}"]`).join(', ')})[data-state="closed"] { animation: fp-rise-out calc(var(--fp-motion) * 0.7) var(--fp-ease) both; }
|
|
172
|
+
${ROOT} [data-shell-overlay-layer] [data-slot="sheet-content"][data-state="open"] { animation: fp-arrive-in var(--fp-motion) var(--fp-ease) both; }
|
|
173
|
+
${ROOT} [data-shell-overlay-layer] [data-slot="sheet-content"][data-state="closed"] { animation: fp-arrive-out calc(var(--fp-motion) * 0.7) var(--fp-ease) both; }
|
|
174
|
+
/* Popups are glass, a little more opaque than windows (they carry text you act on at once). Tooltips keep their own
|
|
175
|
+
inverse colours. */
|
|
176
|
+
${ROOT} [data-shell-overlay-layer] :is(${[...POPUPS.lift.filter(([slot]) => slot !== 'tooltip-content').map(([slot]) => slot), ...POPUPS.rise, 'sheet-content'].map((slot) => `[data-slot="${slot}"]`).join(', ')}) {
|
|
177
|
+
background: color-mix(in oklab, var(--popover, var(--card, var(--background))) calc(var(--fp-glass) + 26%), transparent);
|
|
178
|
+
-webkit-backdrop-filter: blur(24px) saturate(1.5);
|
|
179
|
+
backdrop-filter: blur(24px) saturate(1.5);
|
|
180
|
+
border-color: color-mix(in oklab, var(--border) 60%, transparent);
|
|
181
|
+
}
|
|
182
|
+
@keyframes fp-lift-in { from { transform: perspective(var(--fp-distance)) translate3d(0, 0, calc(var(--fp-step) * -0.35)); opacity: 0; } to { transform: none; opacity: 1; } }
|
|
183
|
+
@keyframes fp-lift-out { from { transform: none; opacity: 1; } to { transform: perspective(var(--fp-distance)) translate3d(0, 0, calc(var(--fp-step) * -0.35)); opacity: 0; } }
|
|
184
|
+
@keyframes fp-rise-in { from { transform: perspective(var(--fp-distance)) translate3d(0, 0, calc(var(--fp-step) * -1.4)); opacity: 0; } to { transform: none; opacity: 1; } }
|
|
185
|
+
@keyframes fp-rise-out { from { transform: none; opacity: 1; } to { transform: perspective(var(--fp-distance)) translate3d(0, 0, calc(var(--fp-step) * -1.4)); opacity: 0; } }
|
|
186
|
+
@keyframes fp-arrive-in { from { transform: perspective(var(--fp-distance)) translate3d(0, 0, calc(var(--fp-step) * 0.9)); opacity: 0; } to { transform: none; opacity: 1; } }
|
|
187
|
+
@keyframes fp-arrive-out { from { transform: none; opacity: 1; } to { transform: perspective(var(--fp-distance)) translate3d(0, 0, calc(var(--fp-step) * -0.5)); opacity: 0; } }
|
|
188
|
+
|
|
189
|
+
/* The modal scrim: in the focal plane, stepping back and the light blur above ARE the scrim, so the engine's dark
|
|
190
|
+
overlay (and its own backdrop blur) would double it into a heavy grey. Keep the overlay element (it still catches
|
|
191
|
+
the outside click), just transparent. */
|
|
192
|
+
${ROOT} [data-shell-overlay-layer] :is([data-slot="dialog-overlay"], [data-slot="alert-dialog-overlay"], [data-slot="sheet-overlay"], [data-slot="drawer-overlay"]) {
|
|
193
|
+
background: transparent;
|
|
194
|
+
-webkit-backdrop-filter: none;
|
|
195
|
+
backdrop-filter: none;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
@media (prefers-reduced-motion: reduce) {
|
|
199
|
+
/* !important: the motion rules above are more specific (the drifting fields, the item zoom), and the reader's
|
|
200
|
+
setting must win over every one of them. */
|
|
201
|
+
${ROOT} ${OBJECTS}, ${ROOT} .fp-ghost, ${ROOT} [data-fp-object="shell"] :is(button, a), ${ROOT} [data-shell-overlay-layer] [data-slot] { transition-duration: 0ms !important; animation: none !important; }
|
|
202
|
+
${ROOT} .fp-backdrop i { animation: none !important; }
|
|
203
|
+
}
|
|
204
|
+
`;
|
|
205
|
+
let installed = null;
|
|
206
|
+
let users = 0;
|
|
207
|
+
/**
|
|
208
|
+
* Installs the stylesheet while at least one focal-plane layout is mounted, and removes it when the last one unmounts.
|
|
209
|
+
* Returns the release function. Falls back to a `<style>` element where constructable stylesheets are missing.
|
|
210
|
+
*/
|
|
211
|
+
export function installFocalPlaneStylesheet(doc = document) {
|
|
212
|
+
users += 1;
|
|
213
|
+
if (users === 1) {
|
|
214
|
+
if ('adoptedStyleSheets' in doc && typeof CSSStyleSheet !== 'undefined' && 'replaceSync' in CSSStyleSheet.prototype) {
|
|
215
|
+
installed = new CSSStyleSheet();
|
|
216
|
+
installed.replaceSync(FOCAL_PLANE_CSS);
|
|
217
|
+
doc.adoptedStyleSheets = [...doc.adoptedStyleSheets, installed];
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
const style = doc.createElement('style');
|
|
221
|
+
style.dataset.focalPlane = '';
|
|
222
|
+
style.textContent = FOCAL_PLANE_CSS;
|
|
223
|
+
doc.head.appendChild(style);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return () => {
|
|
227
|
+
users -= 1;
|
|
228
|
+
if (users > 0)
|
|
229
|
+
return;
|
|
230
|
+
if (installed)
|
|
231
|
+
doc.adoptedStyleSheets = doc.adoptedStyleSheets.filter((sheet) => sheet !== installed);
|
|
232
|
+
doc.head.querySelector('style[data-focal-plane]')?.remove();
|
|
233
|
+
installed = null;
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
//# sourceMappingURL=focal-plane-stylesheet.js.map
|