@phis/ui 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/cms/phi-cms-layout-renderer.js +1 -1
- package/components/controls/phi-collection-header-control.js +1 -1
- package/components/controls/phi-collection-view-control.js +2 -1
- package/components/controls/phi-drawer-control-adapter.d.ts +1 -1
- package/components/controls/phi-drawer-control-adapter.js +2 -2
- package/components/controls/phi-image-control.d.ts +6 -1
- package/components/controls/phi-image-control.js +23 -4
- package/components/controls/phi-lightbox-image-control-adapter.d.ts +2 -0
- package/components/controls/phi-lightbox-image-control-adapter.js +11 -0
- package/components/controls/phi-lightbox-image-control.d.ts +3 -2
- package/components/controls/phi-lightbox-image-control.js +7 -3
- package/components/controls/phi-modal-control-adapter.d.ts +1 -1
- package/components/controls/phi-modal-control-adapter.js +5 -2
- package/components/controls/phi-overlay-control-contract.d.ts +5 -0
- package/components/controls/phi-pagination-control-adapter.d.ts +2 -0
- package/components/controls/phi-pagination-control-adapter.js +15 -0
- package/components/controls/phi-pagination-control.d.ts +2 -1
- package/components/controls/phi-pagination-control.js +11 -7
- package/components/controls/phi-table-control.d.ts +6 -1
- package/components/controls/phi-table-control.js +17 -5
- package/components/controls/phi-timeline-control-adapter.d.ts +2 -0
- package/components/controls/phi-timeline-control-adapter.js +28 -0
- package/components/controls/phi-timeline-control.d.ts +3 -1
- package/components/controls/phi-timeline-control.js +8 -22
- package/components/controls/phi-tour-control.js +25 -7
- package/components/controls/phi-tour-viewport.d.ts +36 -0
- package/components/controls/phi-tour-viewport.js +86 -0
- package/components/controls/phi-zoom-image-overlay.d.ts +17 -0
- package/components/controls/phi-zoom-image-overlay.js +58 -0
- package/components/layouts/layout-definitions.d.ts +2 -20
- package/components/layouts/layout-definitions.js +13 -2
- package/components/layouts/plugins/carousel-layout-plugin.js +2 -1
- package/components/media/aspect-ratios.d.ts +14 -0
- package/components/media/aspect-ratios.js +13 -0
- package/components/motion/phi-sequence-run.d.ts +27 -0
- package/components/motion/phi-sequence-run.js +87 -0
- package/components/motion/phi-sequence-viewport.d.ts +16 -0
- package/components/motion/phi-sequence-viewport.js +97 -34
- package/components/overlays/phi-overlay-container-client.js +5 -2
- package/components/runtime/runtime-visual-anchors.d.ts +13 -2
- package/components/runtime/runtime-visual-anchors.js +49 -3
- package/components/shell/use-phi-shell-sticky-top.d.ts +5 -0
- package/components/shell/use-phi-shell-sticky-top.js +78 -0
- package/components/widgets/config/image-variant-options.d.ts +3 -5
- package/components/widgets/config/image-variant-options.js +11 -11
- package/components/widgets/config/sequence-settings.d.ts +75 -0
- package/components/widgets/config/sequence-settings.js +173 -0
- package/components/widgets/helpers/semantic-html-translation.d.ts +3 -1
- package/components/widgets/helpers/semantic-html-translation.js +8 -3
- package/components/widgets/label-sets/content-entries.d.ts +9 -0
- package/components/widgets/label-sets/content-entries.js +17 -0
- package/components/widgets/label-types/gallery.d.ts +2 -10
- package/components/widgets/label-types/sequence.d.ts +18 -0
- package/components/widgets/label-types/sequence.js +1 -0
- package/components/widgets/navigation-descriptor-resolver.server.js +6 -1
- package/components/widgets/shared/markdown-render.js +15 -5
- package/helpers/cms-layout-defaults.js +9 -2
- package/helpers/cms-region-keys.d.ts +1 -1
- package/helpers/motion.d.ts +7 -0
- package/helpers/motion.js +7 -0
- package/helpers/text-source.d.ts +19 -0
- package/helpers/text-source.js +20 -0
- package/next/config.mjs +1 -1
- package/package.json +1 -1
- package/plugins/runtime-modules/asset/data-providers.d.ts +4 -0
- package/plugins/runtime-modules/asset/data-providers.js +1 -0
- package/plugins/runtime-modules/asset/definition.d.ts +4 -0
- package/plugins/runtime-modules/asset/presets.d.ts +53 -1
- package/plugins/runtime-modules/asset/presets.js +16 -12
- package/plugins/runtime-modules/asset/tour.d.ts +44 -0
- package/plugins/runtime-modules/asset/tour.js +53 -0
- package/plugins/runtime-modules/auth/trees/phi-default-pub-confirm-page-tree.js +3 -0
- package/plugins/runtime-modules/auth/trees/phi-default-pub-login-page-tree.js +2 -0
- package/plugins/runtime-modules/auth/trees/phi-default-pub-registration-page-tree.js +3 -0
- package/plugins/runtime-modules/auth/trees/phi-default-pub-reset-password-page-tree.js +3 -0
- package/plugins/runtime-modules/builder/clients/inspector-config-field.js +42 -18
- package/plugins/runtime-modules/builder/clients/inspector-image-field.js +2 -2
- package/plugins/runtime-modules/builder/clients/widget-inspector.js +9 -0
- package/plugins/runtime-modules/builder/clients/workspace-scope-boundary.js +10 -0
- package/plugins/runtime-modules/builder/controller/workspace-controller.js +18 -3
- package/plugins/runtime-modules/builder/plugin-meta-labels.js +8 -0
- package/plugins/runtime-modules/builder/presets.js +5 -3
- package/plugins/runtime-modules/builder/tour.d.ts +316 -0
- package/plugins/runtime-modules/builder/tour.js +337 -0
- package/plugins/runtime-modules/builder/trees/phi-builder-inspector-area-overlay-tree.server.js +8 -1
- package/plugins/runtime-modules/builder/trees/phi-default-builder-area-preset-tree.js +4 -0
- package/plugins/runtime-modules/builder/widgets/structure-region/built-in.js +3 -2
- package/plugins/runtime-modules/core/widgets/card/server.js +4 -2
- package/plugins/runtime-modules/core/widgets/collection-view/client.js +10 -0
- package/plugins/runtime-modules/core/widgets/description/config.d.ts +28 -1
- package/plugins/runtime-modules/core/widgets/description/config.js +2 -0
- package/plugins/runtime-modules/core/widgets/description/server.js +10 -12
- package/plugins/runtime-modules/core/widgets/entry-detail/config.d.ts +2 -0
- package/plugins/runtime-modules/core/widgets/entry-detail/config.js +8 -1
- package/plugins/runtime-modules/core/widgets/entry-header/config.d.ts +3 -0
- package/plugins/runtime-modules/core/widgets/entry-header/config.js +8 -1
- package/plugins/runtime-modules/core/widgets/entry-list/client.d.ts +4 -1
- package/plugins/runtime-modules/core/widgets/entry-list/client.js +3 -2
- package/plugins/runtime-modules/core/widgets/entry-list/config.d.ts +392 -93
- package/plugins/runtime-modules/core/widgets/entry-list/config.js +21 -3
- package/plugins/runtime-modules/core/widgets/entry-list/server.js +4 -2
- package/plugins/runtime-modules/core/widgets/gallery/authoring.js +62 -10
- package/plugins/runtime-modules/core/widgets/gallery/client.d.ts +6 -1
- package/plugins/runtime-modules/core/widgets/gallery/client.js +7 -79
- package/plugins/runtime-modules/core/widgets/gallery/config.d.ts +337 -144
- package/plugins/runtime-modules/core/widgets/gallery/config.js +52 -113
- package/plugins/runtime-modules/core/widgets/gallery/defaults.d.ts +15 -0
- package/plugins/runtime-modules/core/widgets/gallery/defaults.js +20 -0
- package/plugins/runtime-modules/core/widgets/html/server.js +1 -1
- package/plugins/runtime-modules/core/widgets/image/client.js +1 -1
- package/plugins/runtime-modules/core/widgets/image/config.d.ts +29 -4
- package/plugins/runtime-modules/core/widgets/image/config.js +19 -2
- package/plugins/runtime-modules/core/widgets/image/editor.js +1 -1
- package/plugins/runtime-modules/core/widgets/markdown-toc/server.js +7 -2
- package/plugins/runtime-modules/core/widgets/quick-links/config.d.ts +3 -0
- package/plugins/runtime-modules/core/widgets/quick-links/config.js +2 -0
- package/plugins/runtime-modules/core/widgets/quick-links/plugin.js +4 -2
- package/plugins/runtime-modules/core/widgets/quick-links/server.d.ts +5 -1
- package/plugins/runtime-modules/core/widgets/quick-links/server.js +4 -2
- package/plugins/runtime-modules/core/widgets/result/config.d.ts +25 -1
- package/plugins/runtime-modules/core/widgets/simple-text/config.d.ts +28 -1
- package/plugins/runtime-modules/core/widgets/simple-text/config.js +2 -0
- package/plugins/runtime-modules/core/widgets/simple-text/plugin.js +1 -1
- package/plugins/runtime-modules/core/widgets/simple-text/server.d.ts +4 -1
- package/plugins/runtime-modules/core/widgets/simple-text/server.js +3 -3
- package/plugins/runtime-modules/core/widgets/table/client.js +1 -1
- package/plugins/runtime-modules/core/widgets/table/config.d.ts +80 -3
- package/plugins/runtime-modules/core/widgets/table/config.js +3 -0
- package/plugins/runtime-modules/core/widgets/tour-button/client.js +1 -1
- package/plugins/runtime-modules/phi-default-site-area-preset-tree.js +12 -4
- package/plugins/runtime-modules/public/trees/phi-default-pub-contact-page-tree.js +2 -0
- package/plugins/runtime-modules/public/trees/phi-default-pub-unsubscribe-page-tree.js +3 -0
- package/plugins/runtime-modules/public/trees/phi-default-pub-welcome-page-tree.js +2 -1
- package/plugins/runtime-modules/revisions/presets.d.ts +53 -2
- package/plugins/runtime-modules/revisions/presets.js +16 -12
- package/plugins/runtime-modules/revisions/tour.d.ts +43 -0
- package/plugins/runtime-modules/revisions/tour.js +51 -0
- package/plugins/runtime-modules/revisions/trees/phi-default-builder-revisions-page-tree.js +4 -0
- package/plugins/runtime-modules/theme/presets.d.ts +36 -4
- package/plugins/runtime-modules/theme/tour.d.ts +45 -11
- package/plugins/runtime-modules/theme/tour.js +39 -8
- package/plugins/runtime-modules/tour/controller/address.js +2 -2
- package/plugins/runtime-modules/tour/controller/client.js +43 -2
- package/plugins/runtime-modules/tour/controller/definition.js +2 -1
- package/server-helpers/cms-request.js +1 -0
- package/server-helpers/page-tour.d.ts +10 -3
- package/server-helpers/page-tour.js +25 -12
- package/server-helpers/translate.d.ts +9 -0
- package/server-helpers/translate.js +14 -0
- package/styles/controls.css +15 -1
- package/styles/root.css +20 -19
- package/types/cms-config.d.ts +3 -1
- package/types/cms-config.js +3 -3
- package/types/cms-plugins.d.ts +31 -4
- package/types/core-widget-placements.d.ts +5 -0
- package/types/table-widget.d.ts +2 -0
- package/types/tour-controller.d.ts +10 -0
- package/types/tour-controller.js +13 -0
- package/types/tours.d.ts +27 -25
- package/types/tours.js +9 -23
- package/types/visual-anchors.d.ts +15 -0
- package/types/visual-anchors.js +12 -0
- package/types.d.ts +2 -0
- package/types.js +4 -1
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a Tour step's target is brought into view: below whatever stays stuck at the top of the window.
|
|
3
|
+
*
|
|
4
|
+
* Ant Design's Tour knows neither. It scrolls only when the target is not wholly inside the window, so a
|
|
5
|
+
* target taller than the window is scrolled on every step and every scroll event -- to the middle, its
|
|
6
|
+
* top under the Header -- and a target under a sticky Header counts as seen and is not scrolled at all.
|
|
7
|
+
* So the owning Controller sets the window's scroll padding to the cover before the step is presented
|
|
8
|
+
* (design/TOURS.md, Target readiness), `PhiTourControl` hands Ant Design the alignment that fits the
|
|
9
|
+
* target, and once the step stands it brings in a target Ant Design took for seen. Every scroll then
|
|
10
|
+
* aims at the same place, and the one Ant Design repeats on every scroll event moves nothing.
|
|
11
|
+
*
|
|
12
|
+
* Not while the previous step is still shown: Ant Design scrolls back to a shown target that leaves the
|
|
13
|
+
* window, so a scroll begun before the new step is presented is undone by the old one.
|
|
14
|
+
*
|
|
15
|
+
* The cover is found from the target outwards, not by searching the document: the Shell's Header bands
|
|
16
|
+
* and a Page's sticky band are siblings of something the target stands in, so the bands are found among
|
|
17
|
+
* the earlier siblings of the target's ancestors that are stuck, sit at the top, and stand over the target.
|
|
18
|
+
*/
|
|
19
|
+
/** Whether the viewer asked for less motion: the Tour then jumps rather than glides. */
|
|
20
|
+
function prefersPhiTourReducedMotion() {
|
|
21
|
+
return typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)").matches === true;
|
|
22
|
+
}
|
|
23
|
+
export function resolvePhiTourScrollBehavior() {
|
|
24
|
+
return prefersPhiTourReducedMotion() ? "auto" : "smooth";
|
|
25
|
+
}
|
|
26
|
+
/** Air between the cover and the target, so a highlighted edge is not drawn against the Header. */
|
|
27
|
+
const PHI_TOUR_VIEWPORT_GAP = 8;
|
|
28
|
+
function isStuck(element) {
|
|
29
|
+
const { position } = getComputedStyle(element);
|
|
30
|
+
return position === "sticky" || position === "fixed";
|
|
31
|
+
}
|
|
32
|
+
/** How far down from the window's top the bands that stay put over `target` reach, in pixels. */
|
|
33
|
+
function readPhiTourTopCover(target) {
|
|
34
|
+
const viewHeight = window.innerHeight;
|
|
35
|
+
const targetRect = target.getBoundingClientRect();
|
|
36
|
+
let cover = 0;
|
|
37
|
+
for (let node = target; node && node !== document.body; node = node.parentElement) {
|
|
38
|
+
for (let sibling = node.previousElementSibling; sibling; sibling = sibling.previousElementSibling) {
|
|
39
|
+
if (!isStuck(sibling))
|
|
40
|
+
continue;
|
|
41
|
+
const rect = sibling.getBoundingClientRect();
|
|
42
|
+
const atTop = rect.height > 0 && rect.top < viewHeight / 2 && rect.height < viewHeight / 2;
|
|
43
|
+
const overTarget = rect.left < targetRect.right && rect.right > targetRect.left;
|
|
44
|
+
if (atTop && overTarget)
|
|
45
|
+
cover = Math.max(cover, rect.bottom);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return Math.max(0, Math.round(cover));
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Centered where the target fits below the cover, top-aligned under it where it does not: a target
|
|
52
|
+
* taller than what is left of the window is read from its top.
|
|
53
|
+
*/
|
|
54
|
+
export function resolvePhiTourScrollBlock(target, cover = readPhiTourTopCover(target)) {
|
|
55
|
+
const free = window.innerHeight - cover - PHI_TOUR_VIEWPORT_GAP * 2;
|
|
56
|
+
return target.getBoundingClientRect().height <= free ? "center" : "start";
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Keeps the window's scroll padding at the cover over `target`, so every `scrollIntoView` -- Ant
|
|
60
|
+
* Design's included -- aligns below it. Returns the cover.
|
|
61
|
+
*/
|
|
62
|
+
export function holdPhiTourViewport(target) {
|
|
63
|
+
const cover = readPhiTourTopCover(target);
|
|
64
|
+
document.documentElement.style.scrollPaddingTop = `${cover + PHI_TOUR_VIEWPORT_GAP}px`;
|
|
65
|
+
return cover;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Brings `target` into the part of the window nothing covers. A target already wholly in that part --
|
|
69
|
+
* or, taller than it, already standing at its top -- is left where it is. Returns whether it scrolled.
|
|
70
|
+
*/
|
|
71
|
+
export function bringPhiTourTargetIntoView(target, behavior) {
|
|
72
|
+
const cover = holdPhiTourViewport(target);
|
|
73
|
+
const rect = target.getBoundingClientRect();
|
|
74
|
+
const block = resolvePhiTourScrollBlock(target, cover);
|
|
75
|
+
const inView = block === "center"
|
|
76
|
+
? rect.top >= cover && rect.bottom <= window.innerHeight
|
|
77
|
+
: Math.abs(rect.top - cover - PHI_TOUR_VIEWPORT_GAP) <= 1;
|
|
78
|
+
if (inView)
|
|
79
|
+
return false;
|
|
80
|
+
target.scrollIntoView({ block, inline: "nearest", behavior });
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
/** The window's scroll padding as it was before a Tour set it. */
|
|
84
|
+
export function releasePhiTourViewport() {
|
|
85
|
+
document.documentElement.style.scrollPaddingTop = "";
|
|
86
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type PhiZoomImageOverlayProps = {
|
|
2
|
+
open: boolean;
|
|
3
|
+
src: string;
|
|
4
|
+
alt: string;
|
|
5
|
+
title?: string;
|
|
6
|
+
onClose: () => void;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* A picture scaled to the viewport over the page, and nothing else: no toolbar, no zoom steps, no
|
|
10
|
+
* rotation -- that is the lightbox's (`PhiLightboxImageControl`). A click anywhere closes it, as does
|
|
11
|
+
* Escape, and the focus goes back to where it was.
|
|
12
|
+
*
|
|
13
|
+
* On `document.body`, at the layer a Modal stands on, for the reason `PhiFloatButtonPortal` gives: a
|
|
14
|
+
* picture's slot and Region open stacking contexts of their own, and an overlay left inside them would be
|
|
15
|
+
* painted under the Header. The server draws nothing; it opens only on a click.
|
|
16
|
+
*/
|
|
17
|
+
export declare function PhiZoomImageOverlay({ open, src, alt, title, onClose }: PhiZoomImageOverlayProps): import("react").ReactPortal | null;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useRef, useSyncExternalStore } from "react";
|
|
4
|
+
import { createPortal } from "react-dom";
|
|
5
|
+
import { usePhiConfig } from "../root/phi-config-provider";
|
|
6
|
+
const subscribeNever = () => () => { };
|
|
7
|
+
/** Ant Design's `zIndexPopupBase`, the layer its Modal stands on; the Phi token set does not carry it. */
|
|
8
|
+
const PHI_ZOOM_OVERLAY_Z_INDEX = 1000;
|
|
9
|
+
/**
|
|
10
|
+
* A picture scaled to the viewport over the page, and nothing else: no toolbar, no zoom steps, no
|
|
11
|
+
* rotation -- that is the lightbox's (`PhiLightboxImageControl`). A click anywhere closes it, as does
|
|
12
|
+
* Escape, and the focus goes back to where it was.
|
|
13
|
+
*
|
|
14
|
+
* On `document.body`, at the layer a Modal stands on, for the reason `PhiFloatButtonPortal` gives: a
|
|
15
|
+
* picture's slot and Region open stacking contexts of their own, and an overlay left inside them would be
|
|
16
|
+
* painted under the Header. The server draws nothing; it opens only on a click.
|
|
17
|
+
*/
|
|
18
|
+
export function PhiZoomImageOverlay({ open, src, alt, title, onClose }) {
|
|
19
|
+
const { token } = usePhiConfig();
|
|
20
|
+
const mounted = useSyncExternalStore(subscribeNever, () => true, () => false);
|
|
21
|
+
const overlayRef = useRef(null);
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (!open)
|
|
24
|
+
return undefined;
|
|
25
|
+
const previousFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
26
|
+
overlayRef.current?.focus();
|
|
27
|
+
const closeOnEscape = (event) => {
|
|
28
|
+
if (event.key === "Escape")
|
|
29
|
+
onClose();
|
|
30
|
+
};
|
|
31
|
+
document.addEventListener("keydown", closeOnEscape);
|
|
32
|
+
return () => {
|
|
33
|
+
document.removeEventListener("keydown", closeOnEscape);
|
|
34
|
+
previousFocus?.focus();
|
|
35
|
+
};
|
|
36
|
+
}, [onClose, open]);
|
|
37
|
+
if (!mounted || !open)
|
|
38
|
+
return null;
|
|
39
|
+
return createPortal(_jsx("div", { ref: overlayRef, role: "dialog", "aria-modal": "true", "aria-label": alt || title, tabIndex: -1,
|
|
40
|
+
/*
|
|
41
|
+
* The portal leaves the DOM, not the React tree: a click here would bubble on to the picture's box,
|
|
42
|
+
* which opens it again. It stops at the overlay.
|
|
43
|
+
*/
|
|
44
|
+
onClick: (event) => {
|
|
45
|
+
event.stopPropagation();
|
|
46
|
+
onClose();
|
|
47
|
+
}, onKeyDown: (event) => event.stopPropagation(), style: {
|
|
48
|
+
position: "fixed",
|
|
49
|
+
inset: 0,
|
|
50
|
+
zIndex: PHI_ZOOM_OVERLAY_Z_INDEX,
|
|
51
|
+
display: "flex",
|
|
52
|
+
alignItems: "center",
|
|
53
|
+
justifyContent: "center",
|
|
54
|
+
background: token.colorBgMask,
|
|
55
|
+
cursor: "zoom-out",
|
|
56
|
+
outline: "none",
|
|
57
|
+
}, children: _jsx("img", { alt: alt, src: src, title: title, style: { display: "block", width: "100vw", height: "100dvh", objectFit: "contain" } }) }), document.body);
|
|
58
|
+
}
|
|
@@ -429,15 +429,6 @@ export declare const PHI_CAROUSEL_LAYOUT_DEFINITION: {
|
|
|
429
429
|
step: number;
|
|
430
430
|
prefix: string;
|
|
431
431
|
options?: undefined;
|
|
432
|
-
} | {
|
|
433
|
-
key: string;
|
|
434
|
-
type: "string";
|
|
435
|
-
label: string;
|
|
436
|
-
min?: undefined;
|
|
437
|
-
max?: undefined;
|
|
438
|
-
options?: undefined;
|
|
439
|
-
step?: undefined;
|
|
440
|
-
prefix?: undefined;
|
|
441
432
|
} | {
|
|
442
433
|
key: string;
|
|
443
434
|
type: "boolean";
|
|
@@ -492,7 +483,7 @@ export declare const PHI_GRID_LAYOUT_DEFINITION: {
|
|
|
492
483
|
kind: "layout";
|
|
493
484
|
pluginKey: string;
|
|
494
485
|
typeKey: string;
|
|
495
|
-
slotSizePolicy: "fill";
|
|
486
|
+
slotSizePolicy: "fill-inline";
|
|
496
487
|
title: string;
|
|
497
488
|
description: string;
|
|
498
489
|
category: "structure";
|
|
@@ -1132,15 +1123,6 @@ export declare const PHI_CORE_LAYOUT_DEFINITIONS: readonly [{
|
|
|
1132
1123
|
step: number;
|
|
1133
1124
|
prefix: string;
|
|
1134
1125
|
options?: undefined;
|
|
1135
|
-
} | {
|
|
1136
|
-
key: string;
|
|
1137
|
-
type: "string";
|
|
1138
|
-
label: string;
|
|
1139
|
-
min?: undefined;
|
|
1140
|
-
max?: undefined;
|
|
1141
|
-
options?: undefined;
|
|
1142
|
-
step?: undefined;
|
|
1143
|
-
prefix?: undefined;
|
|
1144
1126
|
} | {
|
|
1145
1127
|
key: string;
|
|
1146
1128
|
type: "boolean";
|
|
@@ -1194,7 +1176,7 @@ export declare const PHI_CORE_LAYOUT_DEFINITIONS: readonly [{
|
|
|
1194
1176
|
kind: "layout";
|
|
1195
1177
|
pluginKey: string;
|
|
1196
1178
|
typeKey: string;
|
|
1197
|
-
slotSizePolicy: "fill";
|
|
1179
|
+
slotSizePolicy: "fill-inline";
|
|
1198
1180
|
title: string;
|
|
1199
1181
|
description: string;
|
|
1200
1182
|
category: "structure";
|
|
@@ -7,6 +7,7 @@ import { PHI_RENDERABLE_BLOCK_DEFAULT_ANCHOR } from "../../helpers/renderable-bl
|
|
|
7
7
|
import { PHI_LAYOUT_PADDING_FIELDS } from "../../helpers/layout-padding-field";
|
|
8
8
|
import { PHI_SIGNAL_VALUE_SCHEMAS } from "../../types/signals";
|
|
9
9
|
import { PHI_CORE_RUNTIME_DATA_PROVIDER_KEYS } from "../../constants/core-data-provider-keys";
|
|
10
|
+
import { PHI_SPACING_SCALE_KEYS } from "../widgets/config/spacing-options";
|
|
10
11
|
const PHI_COLLAPSIBLE_SLOT_OPTIONS = PHI_CMS_COLLAPSIBLE_LAYOUT_SLOTS.map((slot) => ({
|
|
11
12
|
value: slot.key,
|
|
12
13
|
label: slot.label,
|
|
@@ -358,7 +359,12 @@ export const PHI_CAROUSEL_LAYOUT_DEFINITION = {
|
|
|
358
359
|
label: "Easing",
|
|
359
360
|
options: [...PHI_MOTION_EASING_FIELD_OPTIONS],
|
|
360
361
|
},
|
|
361
|
-
{
|
|
362
|
+
{
|
|
363
|
+
key: "slotGap",
|
|
364
|
+
type: "choice",
|
|
365
|
+
label: "Slot Gap",
|
|
366
|
+
options: PHI_SPACING_SCALE_KEYS.map((key) => ({ value: key, label: key })),
|
|
367
|
+
},
|
|
362
368
|
{
|
|
363
369
|
key: "controls",
|
|
364
370
|
type: "choice",
|
|
@@ -409,7 +415,12 @@ export const PHI_GRID_LAYOUT_DEFINITION = {
|
|
|
409
415
|
kind: "layout",
|
|
410
416
|
pluginKey: resolvePhiCmsLayoutPluginKey("grid"),
|
|
411
417
|
typeKey: "grid",
|
|
412
|
-
|
|
418
|
+
/*
|
|
419
|
+
* As wide as its slot, as tall as its rows. A Grid is content in flow: filling the block axis as well
|
|
420
|
+
* let a vertical stack share its height out among the Grids in it, and each one, held to its share,
|
|
421
|
+
* spilled its rows over the next section.
|
|
422
|
+
*/
|
|
423
|
+
slotSizePolicy: "fill-inline",
|
|
413
424
|
title: "Grid",
|
|
414
425
|
description: "Slots in rows, as many to a row as the Grid's own width allows.",
|
|
415
426
|
category: "structure",
|
|
@@ -6,11 +6,12 @@ import { PhiCarouselLayout } from "../phi-carousel-layout";
|
|
|
6
6
|
import { definePhiLayoutRenderers } from "../layout-plugin-renderers";
|
|
7
7
|
import { resolvePhiLayoutSlotMeta } from "../layout-slot-meta";
|
|
8
8
|
import { PHI_CAROUSEL_LAYOUT_DEFINITION } from "../layout-definitions";
|
|
9
|
+
import { resolvePhiSpacingScaleValue } from "../../widgets/config/spacing-options";
|
|
9
10
|
function resolvePhiCarouselSlotKeys() {
|
|
10
11
|
return PHI_CMS_CAROUSEL_LAYOUT_SLOTS.map((slot) => slot.key);
|
|
11
12
|
}
|
|
12
13
|
export const PHI_CAROUSEL_LAYOUT_PLUGIN = {
|
|
13
14
|
...PHI_CAROUSEL_LAYOUT_DEFINITION,
|
|
14
15
|
parseConfig: parsePhiCmsCarouselLayoutConfig,
|
|
15
|
-
...definePhiLayoutRenderers(({ node, config, renderSequentialSlotChildren }, renderMode) => (_jsx(PhiCarouselLayout, { blockId: node.id, layoutKind: "carousel", slots: renderSequentialSlotChildren(node), slotKeys: resolvePhiCarouselSlotKeys(), slotMeta: resolvePhiLayoutSlotMeta(node, PHI_CMS_CAROUSEL_LAYOUT_SLOTS), renderMode: renderMode, defaultActiveSlotKey: config.defaultActiveSlotKey, visibleSlots: config.visibleSlots, windowAnchor: config.windowAnchor, transition: config.transition, transitionDurationMs: config.transitionDurationMs, transitionEasing: config.transitionEasing, slotGap: config.slotGap, controls: config.controls, loop: config.loop, autoplayMs: config.autoplayMs, lookahead: config.lookahead, slotAnchor: resolvePhiLayoutAnchor(config.anchor, PHI_CAROUSEL_LAYOUT_DEFINITION.defaultAnchor), editSlotAnchor: resolvePhiLayoutAnchor(config.anchor, PHI_CAROUSEL_LAYOUT_DEFINITION.defaultAnchor), zIndex: config.zIndex, surface: config.surface, padding: config.padding, paddingTop: config.paddingTop, paddingRight: config.paddingRight, paddingBottom: config.paddingBottom, paddingLeft: config.paddingLeft, labelEnd: config.labelEnd, size: config.size, minSize: config.minSize, maxSize: config.maxSize, collapsedSizeHint: config.collapsedSizeHint }, `layout-${node.id}`))),
|
|
16
|
+
...definePhiLayoutRenderers(({ node, config, renderSequentialSlotChildren }, renderMode) => (_jsx(PhiCarouselLayout, { blockId: node.id, layoutKind: "carousel", slots: renderSequentialSlotChildren(node), slotKeys: resolvePhiCarouselSlotKeys(), slotMeta: resolvePhiLayoutSlotMeta(node, PHI_CMS_CAROUSEL_LAYOUT_SLOTS), renderMode: renderMode, defaultActiveSlotKey: config.defaultActiveSlotKey, visibleSlots: config.visibleSlots, windowAnchor: config.windowAnchor, transition: config.transition, transitionDurationMs: config.transitionDurationMs, transitionEasing: config.transitionEasing, slotGap: resolvePhiSpacingScaleValue(config.slotGap ?? "none"), controls: config.controls, loop: config.loop, autoplayMs: config.autoplayMs, lookahead: config.lookahead, slotAnchor: resolvePhiLayoutAnchor(config.anchor, PHI_CAROUSEL_LAYOUT_DEFINITION.defaultAnchor), editSlotAnchor: resolvePhiLayoutAnchor(config.anchor, PHI_CAROUSEL_LAYOUT_DEFINITION.defaultAnchor), zIndex: config.zIndex, surface: config.surface, padding: config.padding, paddingTop: config.paddingTop, paddingRight: config.paddingRight, paddingBottom: config.paddingBottom, paddingLeft: config.paddingLeft, labelEnd: config.labelEnd, size: config.size, minSize: config.minSize, maxSize: config.maxSize, collapsedSizeHint: config.collapsedSizeHint }, `layout-${node.id}`))),
|
|
16
17
|
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The proportions a frame for pictures is offered in, stored as `width:height`.
|
|
3
|
+
*
|
|
4
|
+
* A closed list rather than a text box: a ratio somebody types is a ratio nobody checks, and the few a
|
|
5
|
+
* page actually needs are the ones a camera, a screen and a print already use.
|
|
6
|
+
*/
|
|
7
|
+
export declare const PHI_MEDIA_ASPECT_RATIOS: readonly ["16:9", "3:2", "4:3", "1:1", "3:4", "2:3", "9:16", "21:9"];
|
|
8
|
+
export type PhiMediaAspectRatio = (typeof PHI_MEDIA_ASPECT_RATIOS)[number];
|
|
9
|
+
export declare const PHI_MEDIA_ASPECT_RATIO_OPTIONS: {
|
|
10
|
+
value: "16:9" | "3:2" | "4:3" | "1:1" | "3:4" | "2:3" | "9:16" | "21:9";
|
|
11
|
+
label: "16:9" | "3:2" | "4:3" | "1:1" | "3:4" | "2:3" | "9:16" | "21:9";
|
|
12
|
+
}[];
|
|
13
|
+
/** The ratio as CSS `aspect-ratio` writes it. */
|
|
14
|
+
export declare function resolvePhiMediaAspectRatioCss(ratio: PhiMediaAspectRatio): string;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The proportions a frame for pictures is offered in, stored as `width:height`.
|
|
3
|
+
*
|
|
4
|
+
* A closed list rather than a text box: a ratio somebody types is a ratio nobody checks, and the few a
|
|
5
|
+
* page actually needs are the ones a camera, a screen and a print already use.
|
|
6
|
+
*/
|
|
7
|
+
export const PHI_MEDIA_ASPECT_RATIOS = ["16:9", "3:2", "4:3", "1:1", "3:4", "2:3", "9:16", "21:9"];
|
|
8
|
+
export const PHI_MEDIA_ASPECT_RATIO_OPTIONS = PHI_MEDIA_ASPECT_RATIOS.map((ratio) => ({ value: ratio, label: ratio }));
|
|
9
|
+
/** The ratio as CSS `aspect-ratio` writes it. */
|
|
10
|
+
export function resolvePhiMediaAspectRatioCss(ratio) {
|
|
11
|
+
const [width, height] = ratio.split(":");
|
|
12
|
+
return `${width} / ${height}`;
|
|
13
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type CSSProperties, type ReactNode } from "react";
|
|
2
|
+
import { type PhiSequenceSettings } from "../widgets/config/sequence-settings";
|
|
3
|
+
import type { PhiSequenceControlLabels } from "../widgets/label-types/sequence";
|
|
4
|
+
/** How many items stand in the window: never more than there are, never fewer than one. */
|
|
5
|
+
export declare function resolvePhiSequenceRunSpan(visibleCount: number | undefined, itemCount: number): number;
|
|
6
|
+
/**
|
|
7
|
+
* A run of items shown a few at a time, and the two things the browser will not do for us.
|
|
8
|
+
*
|
|
9
|
+
* The first is knowing which item is next. Native lazy loading decides from an element's distance to
|
|
10
|
+
* the viewport, so on a fade -- where every item sits in the same place -- it loads all of them at once,
|
|
11
|
+
* and on a slide it loads the next one as it arrives, during the transition. So the window says what
|
|
12
|
+
* loads: what is mounted is wanted, and an item that loads anything should load it eagerly.
|
|
13
|
+
*
|
|
14
|
+
* The second is stopping: `usePhiSequenceAutoplay` holds every way to decline movement nobody asked
|
|
15
|
+
* for, and the Pause button is the one a finger has, since a finger neither hovers nor tabs. Where
|
|
16
|
+
* the window stops -- the last window rather than the last multiple of the window -- is the Carousel
|
|
17
|
+
* Layout's list of stops (`phi-carousel-pages.ts`), read by the arrows, the dots and the timer alike.
|
|
18
|
+
*
|
|
19
|
+
* What the items are is the caller's: the Gallery's pictures, the Entries list's cards.
|
|
20
|
+
*/
|
|
21
|
+
export declare function PhiSequenceRun({ items, settings, labels, itemStyle, }: {
|
|
22
|
+
/** Keyed by the caller; the run neither wraps nor rekeys them. */
|
|
23
|
+
items: readonly ReactNode[];
|
|
24
|
+
settings: PhiSequenceSettings | undefined;
|
|
25
|
+
labels: PhiSequenceControlLabels;
|
|
26
|
+
itemStyle?: CSSProperties;
|
|
27
|
+
}): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useCallback, useMemo, useState } from "react";
|
|
4
|
+
import { usePhiConfig } from "../root/phi-config-provider";
|
|
5
|
+
import { PhiButtonControl } from "../controls/phi-button-control";
|
|
6
|
+
import { resolvePhiCarouselCurrentPage, resolvePhiCarouselPageStarts, resolvePhiCarouselStep, } from "../layouts/phi-carousel-pages";
|
|
7
|
+
import { PhiIcon } from "../shell/phi-icon";
|
|
8
|
+
import { resolvePhiSpacingScaleValue } from "../widgets/config/spacing-options";
|
|
9
|
+
import { PHI_SEQUENCE_SETTINGS_DEFAULTS, } from "../widgets/config/sequence-settings";
|
|
10
|
+
import { PhiSequenceViewport, resolvePhiSequenceWindowStart } from "./phi-sequence-viewport";
|
|
11
|
+
import { usePhiSequenceAutoplay } from "./use-phi-sequence-autoplay";
|
|
12
|
+
import { usePhiSequenceGestures } from "./use-phi-sequence-gestures";
|
|
13
|
+
/** The dots' label, `%1` the first and `%2` the last item a page shows. */
|
|
14
|
+
function formatPageLabel(template, from, to) {
|
|
15
|
+
return template.replace("%1", String(from)).replace("%2", String(to));
|
|
16
|
+
}
|
|
17
|
+
/** How many items stand in the window: never more than there are, never fewer than one. */
|
|
18
|
+
export function resolvePhiSequenceRunSpan(visibleCount, itemCount) {
|
|
19
|
+
return Math.min(visibleCount ?? PHI_SEQUENCE_SETTINGS_DEFAULTS.visibleCount, Math.max(itemCount, 1));
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* A run of items shown a few at a time, and the two things the browser will not do for us.
|
|
23
|
+
*
|
|
24
|
+
* The first is knowing which item is next. Native lazy loading decides from an element's distance to
|
|
25
|
+
* the viewport, so on a fade -- where every item sits in the same place -- it loads all of them at once,
|
|
26
|
+
* and on a slide it loads the next one as it arrives, during the transition. So the window says what
|
|
27
|
+
* loads: what is mounted is wanted, and an item that loads anything should load it eagerly.
|
|
28
|
+
*
|
|
29
|
+
* The second is stopping: `usePhiSequenceAutoplay` holds every way to decline movement nobody asked
|
|
30
|
+
* for, and the Pause button is the one a finger has, since a finger neither hovers nor tabs. Where
|
|
31
|
+
* the window stops -- the last window rather than the last multiple of the window -- is the Carousel
|
|
32
|
+
* Layout's list of stops (`phi-carousel-pages.ts`), read by the arrows, the dots and the timer alike.
|
|
33
|
+
*
|
|
34
|
+
* What the items are is the caller's: the Gallery's pictures, the Entries list's cards.
|
|
35
|
+
*/
|
|
36
|
+
export function PhiSequenceRun({ items, settings, labels, itemStyle, }) {
|
|
37
|
+
const { token } = usePhiConfig();
|
|
38
|
+
const defaults = PHI_SEQUENCE_SETTINGS_DEFAULTS;
|
|
39
|
+
const loop = settings?.loop ?? defaults.loop;
|
|
40
|
+
const controls = settings?.controls ?? defaults.controls;
|
|
41
|
+
const autoplayMs = settings?.autoplayMs ?? defaults.autoplayMs;
|
|
42
|
+
const itemCount = items.length;
|
|
43
|
+
const [activeIndex, setActiveIndex] = useState(0);
|
|
44
|
+
const span = resolvePhiSequenceRunSpan(settings?.visibleCount, itemCount);
|
|
45
|
+
const pageStarts = useMemo(() => resolvePhiCarouselPageStarts(itemCount, span), [itemCount, span]);
|
|
46
|
+
const lastStart = pageStarts[pageStarts.length - 1] ?? 0;
|
|
47
|
+
const step = useCallback((direction) => {
|
|
48
|
+
setActiveIndex((current) => resolvePhiCarouselStep(pageStarts, current, direction, loop));
|
|
49
|
+
}, [loop, pageStarts]);
|
|
50
|
+
const advance = useCallback(() => step(1), [step]);
|
|
51
|
+
const autoplay = usePhiSequenceAutoplay({ intervalMs: autoplayMs, active: itemCount > span, advance });
|
|
52
|
+
// A step somebody took: the next automatic one is a whole interval away again.
|
|
53
|
+
const stepByHand = useCallback((direction) => {
|
|
54
|
+
step(direction);
|
|
55
|
+
autoplay.restart();
|
|
56
|
+
}, [autoplay, step]);
|
|
57
|
+
const gestures = usePhiSequenceGestures({ step: stepByHand });
|
|
58
|
+
if (itemCount === 0) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
const windowStart = resolvePhiSequenceWindowStart({
|
|
62
|
+
activeIndex,
|
|
63
|
+
visibleCount: span,
|
|
64
|
+
itemCount,
|
|
65
|
+
anchor: "start",
|
|
66
|
+
});
|
|
67
|
+
const pageCount = pageStarts.length;
|
|
68
|
+
const currentPage = resolvePhiCarouselCurrentPage(pageStarts, windowStart);
|
|
69
|
+
const atStart = windowStart <= 0;
|
|
70
|
+
const atEnd = windowStart >= lastStart;
|
|
71
|
+
// Two disabled arrows over a single item say nothing; the dots already know to stay away.
|
|
72
|
+
const showArrows = (controls === "arrows" || controls === "both") && itemCount > span;
|
|
73
|
+
const showDots = (controls === "dots" || controls === "both") && pageCount > 1;
|
|
74
|
+
return (_jsxs("div", { role: "region", "aria-roledescription": "carousel", "aria-label": labels.region, style: { display: "flex", flexDirection: "column", gap: token.marginXS, width: "100%", minWidth: 0, ...gestures.style }, ...autoplay.holdHandlers, ...gestures.handlers, children: [_jsxs("div", { style: { position: "relative", minWidth: 0 }, "aria-live": autoplay.running ? "off" : "polite", children: [_jsx(PhiSequenceViewport, { items: [...items], activeIndex: activeIndex, visibleCount: span, anchor: "start", transition: settings?.transition ?? defaults.transition, ...(settings?.durationMs === undefined ? {} : { durationMs: settings.durationMs }), ...(settings?.easing === undefined ? {} : { easing: settings.easing }), lookahead: settings?.lookahead ?? defaults.lookahead, mountPolicy: settings?.mountPolicy ?? defaults.mountPolicy, gap: resolvePhiSpacingScaleValue(settings?.gap ?? defaults.gap), itemStyle: itemStyle }), showArrows ? (_jsxs(_Fragment, { children: [_jsx("div", { style: { position: "absolute", insetBlockStart: "50%", insetInlineStart: token.marginXS, transform: "translateY(-50%)" }, children: _jsx(PhiButtonControl, { icon: _jsx(PhiIcon, { name: "left", size: "inherit" }), ariaLabel: labels.previous, shape: "circle", disabled: atStart && !loop, onClick: () => stepByHand(-1) }) }), _jsx("div", { style: { position: "absolute", insetBlockStart: "50%", insetInlineEnd: token.marginXS, transform: "translateY(-50%)" }, children: _jsx(PhiButtonControl, { icon: _jsx(PhiIcon, { name: "right", size: "inherit" }), ariaLabel: labels.next, shape: "circle", disabled: atEnd && !loop, onClick: () => stepByHand(1) }) })] })) : null, autoplay.offered ? (_jsx("div", { style: { position: "absolute", insetBlockEnd: token.marginXS, insetInlineEnd: token.marginXS }, children: _jsx(PhiButtonControl, { icon: _jsx(PhiIcon, { name: autoplay.stopped ? "play-circle" : "pause-circle", size: "inherit" }), ariaLabel: autoplay.stopped ? labels.play : labels.pause, ariaPressed: autoplay.stopped, shape: "circle", onClick: autoplay.toggleStopped }) })) : null] }), showDots ? (_jsx("div", { role: "group", "aria-label": labels.pages, style: { display: "flex", justifyContent: "center", gap: token.marginXXS }, children: Array.from({ length: pageCount }, (_, page) => (_jsx("button", { type: "button", "aria-label": formatPageLabel(labels.page, (pageStarts[page] ?? 0) + 1, Math.min((pageStarts[page] ?? 0) + span, itemCount)), "aria-current": page === currentPage || undefined, onClick: () => {
|
|
75
|
+
setActiveIndex(pageStarts[page] ?? 0);
|
|
76
|
+
autoplay.restart();
|
|
77
|
+
}, style: {
|
|
78
|
+
width: page === currentPage ? token.controlHeightXS : token.marginXS,
|
|
79
|
+
height: token.marginXS,
|
|
80
|
+
padding: 0,
|
|
81
|
+
border: "none",
|
|
82
|
+
cursor: "pointer",
|
|
83
|
+
borderRadius: token.borderRadiusSM,
|
|
84
|
+
background: page === currentPage ? token.colorPrimary : token.colorFill,
|
|
85
|
+
transition: `width ${token.motionDurationMid}, background ${token.motionDurationMid}`,
|
|
86
|
+
} }, page))) })) : null] }));
|
|
87
|
+
}
|
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
import { type CSSProperties, type ReactNode } from "react";
|
|
2
2
|
import { type PhiMotionEasing, type PhiSequenceAnchor, type PhiSequenceTransition } from "../../helpers/motion";
|
|
3
3
|
import { type PhiCmsMountPolicy } from "../../types/cms-mount-policy";
|
|
4
|
+
/**
|
|
5
|
+
* A window onto a sequence, and the movement from one window to the next.
|
|
6
|
+
*
|
|
7
|
+
* This is the piece a Stack, a Carousel and an image gallery have in common, and it is deliberately
|
|
8
|
+
* the thinnest of the three layers: it takes already-rendered children, an index and a shape, and it
|
|
9
|
+
* knows nothing about slots, signals, the Builder or where the pictures came from. Whoever owns the
|
|
10
|
+
* index -- a signal channel, a pair of arrows, a timer -- owns it outside.
|
|
11
|
+
*
|
|
12
|
+
* Two geometries, chosen by the transition, because the two ways of showing a sequence are genuinely
|
|
13
|
+
* different acts. `slide` lays the items out side by side on a track and moves the track: the
|
|
14
|
+
* neighbours exist next to the window and arrive from the direction they were in. `fade`, `diagonal`
|
|
15
|
+
* and `none` keep the window where it is and exchange what stands in its places -- the Stack's way.
|
|
16
|
+
* A fade crosses the leaving items over into the coming ones; a diagonal leaves the old ones standing
|
|
17
|
+
* and slides each new one over its place from the corner it comes from, below and after it going
|
|
18
|
+
* forward, above and before it going back. Nothing travels sideways.
|
|
19
|
+
*/
|
|
4
20
|
export type PhiSequenceViewportProps = {
|
|
5
21
|
items: readonly ReactNode[];
|
|
6
22
|
/**
|
|
@@ -1,26 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
-
import { useEffect, useState } from "react";
|
|
4
|
-
import { clampPhiSequenceTransitionMs, } from "../../helpers/motion";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
4
|
+
import { clampPhiSequenceTransitionMs, PHI_SEQUENCE_DEFAULT_DURATION_MS, PHI_SEQUENCE_DEFAULT_EASING, } from "../../helpers/motion";
|
|
5
5
|
import { shouldPhiCmsContentStayMounted, } from "../../types/cms-mount-policy";
|
|
6
|
-
/**
|
|
7
|
-
* A window onto a sequence, and the movement from one window to the next.
|
|
8
|
-
*
|
|
9
|
-
* This is the piece a Stack, a Carousel and an image gallery have in common, and it is deliberately
|
|
10
|
-
* the thinnest of the three layers: it takes already-rendered children, an index and a shape, and it
|
|
11
|
-
* knows nothing about slots, signals, the Builder or where the pictures came from. Whoever owns the
|
|
12
|
-
* index -- a signal channel, a pair of arrows, a timer -- owns it outside.
|
|
13
|
-
*
|
|
14
|
-
* Two geometries, chosen by the transition, because the two ways of showing a sequence are genuinely
|
|
15
|
-
* different acts. `slide` and `diagonal` lay the items out side by side on a track and move the track:
|
|
16
|
-
* the neighbours exist next to the window and arrive from the direction they were in. `fade` and
|
|
17
|
-
* `none` put the window's items in flow and cross-fade the ones that are leaving over the top, which
|
|
18
|
-
* is what the Stack has always done -- the items do not travel, they exchange places.
|
|
19
|
-
*/
|
|
20
|
-
/** How far a diagonal leaves the horizontal, as a share of the travel it is already making. */
|
|
21
|
-
const DIAGONAL_RISE = 0.35;
|
|
22
|
-
/** What a caller who says nothing gets: long enough to read as movement, short enough to ignore. */
|
|
23
|
-
const DEFAULT_SEQUENCE_DURATION_MS = 320;
|
|
24
6
|
function toCssLength(value) {
|
|
25
7
|
if (value === undefined)
|
|
26
8
|
return "0px";
|
|
@@ -41,7 +23,7 @@ export function PhiSequenceViewport({ items, activeIndex, visibleCount = 1, anch
|
|
|
41
23
|
// whole number is a caller's mistake, and the type says so rather than this line rescuing it.
|
|
42
24
|
const span = Math.min(visibleCount, Math.max(itemCount, 1));
|
|
43
25
|
const start = resolvePhiSequenceWindowStart({ activeIndex, visibleCount: span, itemCount, anchor });
|
|
44
|
-
const onTrack = transition === "slide"
|
|
26
|
+
const onTrack = transition === "slide";
|
|
45
27
|
/*
|
|
46
28
|
* Which items have ever been inside the window, which is what `lazy-keep` keeps.
|
|
47
29
|
*
|
|
@@ -91,9 +73,9 @@ export function PhiSequenceViewport({ items, activeIndex, visibleCount = 1, anch
|
|
|
91
73
|
const resolvedDurationMs = reducedMotion || transition === "none"
|
|
92
74
|
? 0
|
|
93
75
|
: durationMs === undefined
|
|
94
|
-
?
|
|
76
|
+
? PHI_SEQUENCE_DEFAULT_DURATION_MS
|
|
95
77
|
: clampPhiSequenceTransitionMs(durationMs);
|
|
96
|
-
const resolvedEasing = easing ??
|
|
78
|
+
const resolvedEasing = easing ?? PHI_SEQUENCE_DEFAULT_EASING;
|
|
97
79
|
const gapLength = toCssLength(gap);
|
|
98
80
|
const slice = span === 1
|
|
99
81
|
? "100%"
|
|
@@ -112,9 +94,70 @@ export function PhiSequenceViewport({ items, activeIndex, visibleCount = 1, anch
|
|
|
112
94
|
mountedIndices.add(index);
|
|
113
95
|
}
|
|
114
96
|
}
|
|
115
|
-
const trackTransform = onTrack
|
|
116
|
-
|
|
117
|
-
|
|
97
|
+
const trackTransform = onTrack ? `translate3d(calc(-1 * ${start} * ${step}), 0px, 0)` : undefined;
|
|
98
|
+
/*
|
|
99
|
+
* The exchange between two windows off the track, played after the new window is laid out and before
|
|
100
|
+
* it is painted, so the first frame already starts where the movement does.
|
|
101
|
+
*
|
|
102
|
+
* At the tail the last window overlaps the one before it -- five pictures three at a time go one to
|
|
103
|
+
* three, then three to five -- so an item can be in both windows at another place. It cannot leave
|
|
104
|
+
* where it was and arrive where it goes as one element, and moving it at once read as the one picture
|
|
105
|
+
* that changed without a transition. So it arrives at its new place like any other, and a copy of it
|
|
106
|
+
* stands in its old place until the leaving window is let go.
|
|
107
|
+
*/
|
|
108
|
+
const leavingStart = onTrack ? null : fadeState.outgoingStart;
|
|
109
|
+
const movedIndices = leavingStart === null
|
|
110
|
+
? []
|
|
111
|
+
: Array.from({ length: span }, (_, offset) => leavingStart + offset)
|
|
112
|
+
// In the window it left and in the one it enters: a new start puts it at another place.
|
|
113
|
+
.filter((index) => index >= start && index < start + span);
|
|
114
|
+
const itemElements = useRef([]);
|
|
115
|
+
const trackElement = useRef(null);
|
|
116
|
+
const previousStart = useRef(start);
|
|
117
|
+
const runningAnimations = useRef([]);
|
|
118
|
+
useLayoutEffect(() => {
|
|
119
|
+
const from = previousStart.current;
|
|
120
|
+
previousStart.current = start;
|
|
121
|
+
if (from === start || onTrack || resolvedDurationMs === 0)
|
|
122
|
+
return;
|
|
123
|
+
/*
|
|
124
|
+
* A step taken while the last one is still moving -- a click on top of autoplay, an interval as
|
|
125
|
+
* short as the movement -- starts from a window that has arrived. Left running, the half-arrived
|
|
126
|
+
* items became the leaving ones over nothing, because what stood under them was let go.
|
|
127
|
+
*/
|
|
128
|
+
for (const animation of runningAnimations.current)
|
|
129
|
+
animation.finish();
|
|
130
|
+
runningAnimations.current = [];
|
|
131
|
+
const timing = { duration: resolvedDurationMs, easing: resolvedEasing };
|
|
132
|
+
const windowIndices = Array.from({ length: span }, (_, offset) => start + offset);
|
|
133
|
+
const moved = windowIndices.filter((index) => index >= from && index < from + span);
|
|
134
|
+
const copyOf = (index) => trackElement.current?.querySelector(`[data-phi-sequence-leaving="${index}"]`);
|
|
135
|
+
if (transition === "fade") {
|
|
136
|
+
for (const index of moved) {
|
|
137
|
+
// Not held at the end: the item's own opacity is 1 already, and a held animation would outlast
|
|
138
|
+
// the next fade that wants to take it away.
|
|
139
|
+
const arriving = itemElements.current[index]?.animate?.([{ opacity: 0 }, { opacity: 1 }], timing);
|
|
140
|
+
// The copy is held at nothing until it goes, which is when the leaving window is let go.
|
|
141
|
+
const leaving = copyOf(index)?.animate?.([{ opacity: 1 }, { opacity: 0 }], { ...timing, fill: "forwards" });
|
|
142
|
+
runningAnimations.current.push(...[arriving, leaving].filter((animation) => animation !== undefined));
|
|
143
|
+
}
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (transition === "diagonal") {
|
|
147
|
+
/*
|
|
148
|
+
* A whole place's width and height, as a percentage of the item itself: every new item starts one
|
|
149
|
+
* box off its place, so each comes in from outside the window's corner, whatever its proportion.
|
|
150
|
+
*/
|
|
151
|
+
const offset = start > from ? "100%" : "-100%";
|
|
152
|
+
const animations = windowIndices.flatMap((index) => {
|
|
153
|
+
const animation = itemElements.current[index]?.animate?.([{ translate: `${offset} ${offset}` }, { translate: "0 0" }], timing);
|
|
154
|
+
return animation ? [animation] : [];
|
|
155
|
+
});
|
|
156
|
+
runningAnimations.current = animations;
|
|
157
|
+
// Nothing fades here, so no transition ends to say so: the window is let go when the last item is in.
|
|
158
|
+
void Promise.all(animations.map((animation) => animation.finished)).then(() => setFadeState((current) => (current.start === start ? { ...current, outgoingStart: null } : current)), () => undefined);
|
|
159
|
+
}
|
|
160
|
+
}, [start, span, onTrack, transition, resolvedDurationMs, resolvedEasing]);
|
|
118
161
|
const renderedItems = items.map((item, index) => {
|
|
119
162
|
const visible = index >= start && index < start + span;
|
|
120
163
|
const outgoing = !visible && fadeState.outgoingStart !== null &&
|
|
@@ -129,7 +172,8 @@ export function PhiSequenceViewport({ items, activeIndex, visibleCount = 1, anch
|
|
|
129
172
|
? { flex: `0 0 ${slice}`, minWidth: 0 }
|
|
130
173
|
: {
|
|
131
174
|
...(visible
|
|
132
|
-
|
|
175
|
+
// Above what leaves, so a diagonal's new items cover the old ones they slide over.
|
|
176
|
+
? { flex: `0 0 ${slice}`, minWidth: 0, position: "relative", zIndex: 1 }
|
|
133
177
|
: {
|
|
134
178
|
position: "absolute",
|
|
135
179
|
insetBlockStart: 0,
|
|
@@ -139,17 +183,26 @@ export function PhiSequenceViewport({ items, activeIndex, visibleCount = 1, anch
|
|
|
139
183
|
: 0,
|
|
140
184
|
width: slice,
|
|
141
185
|
}),
|
|
142
|
-
|
|
143
|
-
|
|
186
|
+
// A diagonal's leaving items stand until the new ones cover them; a fade's go out.
|
|
187
|
+
opacity: visible || (outgoing && transition === "diagonal" && resolvedDurationMs > 0) ? 1 : 0,
|
|
188
|
+
transition: resolvedDurationMs > 0 && transition === "fade"
|
|
144
189
|
? `opacity ${resolvedDurationMs}ms ${resolvedEasing}`
|
|
145
190
|
: undefined,
|
|
146
191
|
};
|
|
192
|
+
const ref = (element) => {
|
|
193
|
+
itemElements.current[index] = element;
|
|
194
|
+
};
|
|
147
195
|
if (!mounted && !visible && !outgoing) {
|
|
148
|
-
return _jsx("div", { "aria-hidden": true, style: { ...box, pointerEvents: "none" } }, index);
|
|
196
|
+
return _jsx("div", { ref: ref, "aria-hidden": true, style: { ...box, pointerEvents: "none" } }, index);
|
|
149
197
|
}
|
|
150
|
-
return (_jsx("div", { "data-phi-sequence-item-state": visible ? "visible" : outgoing ? "outgoing" : "waiting", inert: !visible, "aria-hidden": !visible || undefined, style: { ...box, ...itemStyle }, children: mounted ? (renderItem ? renderItem(item, { index, visible }) : item) : null }, index));
|
|
198
|
+
return (_jsx("div", { ref: ref, "data-phi-sequence-item-state": visible ? "visible" : outgoing ? "outgoing" : "waiting", inert: !visible, "aria-hidden": !visible || undefined, style: { ...box, ...itemStyle }, children: mounted ? (renderItem ? renderItem(item, { index, visible }) : item) : null }, index));
|
|
151
199
|
});
|
|
152
|
-
return (_jsx("div", { className: className, "data-phi-sequence-transition": transition,
|
|
200
|
+
return (_jsx("div", { className: className, "data-phi-sequence-transition": transition,
|
|
201
|
+
/*
|
|
202
|
+
* Its own stacking context: the items it raises over the leaving ones are raised inside it, never
|
|
203
|
+
* over the arrows and buttons a caller lays over the viewport.
|
|
204
|
+
*/
|
|
205
|
+
style: { position: "relative", overflow: "hidden", isolation: "isolate", minWidth: 0, ...style }, children: _jsxs("div", { ref: trackElement, style: {
|
|
153
206
|
position: "relative",
|
|
154
207
|
display: "flex",
|
|
155
208
|
alignItems: "stretch",
|
|
@@ -177,5 +230,15 @@ export function PhiSequenceViewport({ items, activeIndex, visibleCount = 1, anch
|
|
|
177
230
|
: {}),
|
|
178
231
|
}, onTransitionEnd: fadeState.outgoingStart === null
|
|
179
232
|
? undefined
|
|
180
|
-
: () => setFadeState((current) => ({ ...current, outgoingStart: null })), children: renderedItems
|
|
233
|
+
: () => setFadeState((current) => ({ ...current, outgoingStart: null })), children: [renderedItems, leavingStart === null ? null : movedIndices.map((index) => (_jsx("div", { "data-phi-sequence-leaving": index, "aria-hidden": true, inert: true, style: {
|
|
234
|
+
position: "absolute",
|
|
235
|
+
insetBlockStart: 0,
|
|
236
|
+
insetBlockEnd: 0,
|
|
237
|
+
insetInlineStart: `calc(${index - leavingStart} * ${step})`,
|
|
238
|
+
width: slice,
|
|
239
|
+
// Under the window's own items, which arrive over it.
|
|
240
|
+
zIndex: 0,
|
|
241
|
+
pointerEvents: "none",
|
|
242
|
+
...itemStyle,
|
|
243
|
+
}, children: items[index] }, `leaving-${index}`)))] }) }));
|
|
181
244
|
}
|