@vectoriox/iox-ui 5.83.1 → 5.85.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/actions-core/index.d.ts +18 -2
- package/composite-core/index.d.ts +93 -15
- package/design-core/index.d.ts +566 -4
- package/fesm2022/vectoriox-iox-ui-actions-core.mjs +18 -1
- package/fesm2022/vectoriox-iox-ui-actions-core.mjs.map +1 -1
- package/fesm2022/vectoriox-iox-ui-composite-core.mjs +307 -39
- package/fesm2022/vectoriox-iox-ui-composite-core.mjs.map +1 -1
- package/fesm2022/vectoriox-iox-ui-design-core.mjs +680 -15
- package/fesm2022/vectoriox-iox-ui-design-core.mjs.map +1 -1
- package/fesm2022/vectoriox-iox-ui-generation-core.mjs +665 -24
- package/fesm2022/vectoriox-iox-ui-generation-core.mjs.map +1 -1
- package/fesm2022/vectoriox-iox-ui-harness-core.mjs +39 -1
- package/fesm2022/vectoriox-iox-ui-harness-core.mjs.map +1 -1
- package/fesm2022/vectoriox-iox-ui-scene-core.mjs +101 -5
- package/fesm2022/vectoriox-iox-ui-scene-core.mjs.map +1 -1
- package/fesm2022/vectoriox-iox-ui.mjs +285 -12
- package/fesm2022/vectoriox-iox-ui.mjs.map +1 -1
- package/generation-core/index.d.ts +414 -8
- package/harness-core/index.d.ts +81 -3
- package/index.d.ts +143 -42
- package/package.json +1 -1
- package/scene-core/index.d.ts +32 -4
- package/src/actions-core/dialog-motion.ts +25 -0
- package/src/composite-core/header.ts +25 -0
- package/src/composite-core/public-api.ts +1 -0
- package/src/composite-core/slider-runtime.ts +82 -0
- package/src/composite-core/slider.ts +194 -27
- package/src/composite-core/structure.ts +14 -9
- package/src/design-core/flavor-spec.generated.ts +596 -9
- package/src/design-core/header-behaviour.ts +76 -0
- package/src/design-core/public-api.ts +1 -0
- package/src/design-core/theme-color.ts +21 -5
- package/src/generation-core/ai-run.ts +10 -2
- package/src/generation-core/art-direction.ts +122 -0
- package/src/generation-core/audit-site.ts +14 -0
- package/src/generation-core/compose-site.ts +8 -0
- package/src/generation-core/node-baselines.generated.ts +17 -3
- package/src/generation-core/patterns.ts +2 -0
- package/src/generation-core/pictures.ts +464 -0
- package/src/generation-core/plan-wire.ts +2 -0
- package/src/generation-core/public-api.ts +2 -0
- package/src/generation-core/site-build-run.ts +3 -0
- package/src/generation-core/site-plan.model.ts +4 -0
- package/src/generation-core/site-spec.generated.ts +126 -1
- package/src/generation-core/stock-photos.ts +40 -10
- package/src/generation-core/validate-site-plan.ts +14 -0
- package/src/harness-core/business-types.ts +3 -1
- package/src/harness-core/generation-view.ts +50 -1
- package/src/harness-core/harness-spec.generated.ts +30 -0
- package/src/scene-core/scene.ts +18 -0
- package/src/scene-core/scroll-driver.ts +83 -6
- package/styles/iox-design.css +232 -1
- package/styles/iox-forms.css +5 -2
package/package.json
CHANGED
package/scene-core/index.d.ts
CHANGED
|
@@ -70,8 +70,19 @@ interface SceneFrame {
|
|
|
70
70
|
declare enum SceneAttribute {
|
|
71
71
|
Root = "data-iox-scene",
|
|
72
72
|
Tracks = "data-iox-tracks",
|
|
73
|
-
PhoneTracks = "data-iox-tracks-phone"
|
|
73
|
+
PhoneTracks = "data-iox-tracks-phone",
|
|
74
|
+
/** A root's stage that is not a Scene's `scene.stage` part: the slider a scroll plays (premium M4). */
|
|
75
|
+
Stage = "data-iox-scene-stage"
|
|
74
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* A SLIDER THE SCROLL PLAYS (premium components M4 — M1.3's held gallery). Its progress rests on each picture and changes
|
|
79
|
+
* only over the middle share of each step, so every picture arrives and stays a moment while the page holds it: `p` is
|
|
80
|
+
* the scroll's progress through the runway (0–1), `n` the slides; the answer is Swiper's progress (0–1).
|
|
81
|
+
*/
|
|
82
|
+
declare const SLIDER_HOLD_SHARE = 0.2;
|
|
83
|
+
/** Page px of scroll per picture, as a share of the screen's height (the runway the renderers reserve). */
|
|
84
|
+
declare const SLIDER_SCROLL_STEP_SCREENS = 0.55;
|
|
85
|
+
declare function heldProgress(p: number, n: number): number;
|
|
75
86
|
/** At or under this stage width a layer plays its phone tracks. The `mobile` breakpoint's width. */
|
|
76
87
|
declare const SCENE_PHONE_MAX_WIDTH = 767;
|
|
77
88
|
/**
|
|
@@ -287,6 +298,11 @@ declare function buildSceneRecipe(create: CompositeNodeFactory, words?: Partial<
|
|
|
287
298
|
* - Reads come first in a frame and writes after, and a write only ever touches compositor properties.
|
|
288
299
|
*/
|
|
289
300
|
declare const SCENE_DAMPING = 9;
|
|
301
|
+
/**
|
|
302
|
+
* Dispatched (bubbling) by something the driver plays that arrived after the driver last looked — a Swiper booted on a
|
|
303
|
+
* slider's stage. Never a DOM `scroll`: nothing that follows the driver may dispatch one (SC2).
|
|
304
|
+
*/
|
|
305
|
+
declare const SCENE_WAKE_EVENT = "iox-scene-wake";
|
|
290
306
|
/** One frame of the damping: frame-rate independent, so 60 Hz and 120 Hz settle in the same time. */
|
|
291
307
|
declare function dampToward(current: number, target: number, seconds: number, rate?: number): number;
|
|
292
308
|
/** Where a root stands: scrolled `local` page px into its `travel`. `zoom` undoes the canvas's scale. */
|
|
@@ -321,9 +337,12 @@ declare class ScrollDriver {
|
|
|
321
337
|
private frame;
|
|
322
338
|
private lastTime;
|
|
323
339
|
private readonly wakeUp;
|
|
340
|
+
private readonly onWakeEvent;
|
|
324
341
|
constructor(options?: ScrollDriverOptions);
|
|
325
|
-
/** Drive a Scene's root. Idempotent: adding a root again re-reads it. */
|
|
326
|
-
add(root: HTMLElement):
|
|
342
|
+
/** Drive a Scene's root. Idempotent: adding a root again re-reads it. False when another driver holds it. */
|
|
343
|
+
add(root: HTMLElement): boolean;
|
|
344
|
+
/** True when this driver holds `root`. */
|
|
345
|
+
holds(root: HTMLElement): boolean;
|
|
327
346
|
remove(root: HTMLElement): void;
|
|
328
347
|
/** Read a root's layers again now, keeping where it stands: an editor changed their tracks. */
|
|
329
348
|
reread(root: HTMLElement): void;
|
|
@@ -366,6 +385,15 @@ declare class ScrollDriver {
|
|
|
366
385
|
/** A page's Scenes, driven by one driver; null when it holds none, or when the visitor asked for less motion (the
|
|
367
386
|
* page's own CSS then lets each Scene flow). The live site's door. */
|
|
368
387
|
declare function attachScenes(container: HTMLElement, options?: ScrollDriverOptions): ScrollDriver | null;
|
|
388
|
+
/**
|
|
389
|
+
* The Scene roots that belong to `elements` (each, and what it holds) — what one runtime instance owns: the page's nodes,
|
|
390
|
+
* or the globals'. Never a whole container that also holds another instance's Scenes (premium M4: the globals' instance
|
|
391
|
+
* scanned the app root and drove the page's Scenes a second time).
|
|
392
|
+
*/
|
|
393
|
+
declare function sceneRootsIn(elements: Iterable<Element>): HTMLElement[];
|
|
394
|
+
/** The live site's door for a runtime that owns some nodes: their Scenes on one driver (null when none, or under reduced
|
|
395
|
+
* motion — the page's own CSS then lets each Scene flow). A root another driver holds stays with it. */
|
|
396
|
+
declare function attachSceneRoots(roots: ReadonlyArray<HTMLElement>, options?: ScrollDriverOptions): ScrollDriver | null;
|
|
369
397
|
|
|
370
398
|
/**
|
|
371
399
|
* Text into pieces, in the browser only (premium sites SC3, Motion v2 §3c). A saved node keeps its plain text and the
|
|
@@ -416,5 +444,5 @@ declare function revealStagger(groups: number, by: TextRevealBy): number;
|
|
|
416
444
|
*/
|
|
417
445
|
declare function playTextReveal(el: HTMLElement, by: TextRevealBy, timing: KeyframeAnimationOptions): Animation | null;
|
|
418
446
|
|
|
419
|
-
export { LIGHT_UP_PIECE_CAP, REVEAL_PIECE_CAP, SCENE_BUDGET, SCENE_DAMPING, SCENE_FILM_SMOOTH, SCENE_FLOW_RULES, SCENE_LENGTH, SCENE_MAX_SCREENS, SCENE_MIN_SCREENS, SCENE_PHONE_LENGTH, SCENE_PHONE_MAX_WIDTH, SCENE_REDUCED_MOTION_CSS, SCENE_REVEAL_DEFAULT, SCENE_STAGE_HEIGHT, SCENE_TRACK_DEFS, SCENE_WORDS, SCENE_WORD_DIM, SceneAttribute, SceneProp, SceneRevealFrom, ScrollDriver, TextRevealBy, applySceneFirstFrame, attachScenes, buildSceneRecipe, colourAt, dampToward, lineIndexes, movedSceneKey, normalizeTracks, playTextReveal, revealClipPath, revealStagger, sceneBudget, sceneColourKeyAt, sceneDisplayValue, sceneFilmDensity, sceneFirstFrame, sceneFrame, sceneFrameDeclarations, sceneHasKeyAt, sceneHeightForScreens, sceneKeyPosition, sceneLayersOf, sceneNodeAttributes, scenePlayed, sceneRevealFromOf, sceneRootOnPath, sceneScreensOf, sceneScrollState, sceneTrackDef, sceneTrackSpan, sceneWordOpacity, sceneWordsLit, scenesIn, splitText, syncSceneAttributes, textPieces, valueAt, withSceneKey, withScenePlayback, withSceneRevealFrom, withoutSceneKey, withoutScenePlayback };
|
|
447
|
+
export { LIGHT_UP_PIECE_CAP, REVEAL_PIECE_CAP, SCENE_BUDGET, SCENE_DAMPING, SCENE_FILM_SMOOTH, SCENE_FLOW_RULES, SCENE_LENGTH, SCENE_MAX_SCREENS, SCENE_MIN_SCREENS, SCENE_PHONE_LENGTH, SCENE_PHONE_MAX_WIDTH, SCENE_REDUCED_MOTION_CSS, SCENE_REVEAL_DEFAULT, SCENE_STAGE_HEIGHT, SCENE_TRACK_DEFS, SCENE_WAKE_EVENT, SCENE_WORDS, SCENE_WORD_DIM, SLIDER_HOLD_SHARE, SLIDER_SCROLL_STEP_SCREENS, SceneAttribute, SceneProp, SceneRevealFrom, ScrollDriver, TextRevealBy, applySceneFirstFrame, attachSceneRoots, attachScenes, buildSceneRecipe, colourAt, dampToward, heldProgress, lineIndexes, movedSceneKey, normalizeTracks, playTextReveal, revealClipPath, revealStagger, sceneBudget, sceneColourKeyAt, sceneDisplayValue, sceneFilmDensity, sceneFirstFrame, sceneFrame, sceneFrameDeclarations, sceneHasKeyAt, sceneHeightForScreens, sceneKeyPosition, sceneLayersOf, sceneNodeAttributes, scenePlayed, sceneRevealFromOf, sceneRootOnPath, sceneRootsIn, sceneScreensOf, sceneScrollState, sceneTrackDef, sceneTrackSpan, sceneWordOpacity, sceneWordsLit, scenesIn, splitText, syncSceneAttributes, textPieces, valueAt, withSceneKey, withScenePlayback, withSceneRevealFrom, withoutSceneKey, withoutScenePlayback };
|
|
420
448
|
export type { SceneBudget, SceneFilmDensity, SceneFrame, SceneFrameOptions, SceneFrameState, SceneKey, SceneLayerNode, SceneNodeSource, SceneTrack, SceneTrackDef, SceneTreeNode, SceneWords, ScrollDriverOptions, SplitText, SplitTextOptions, TextPiece };
|
|
@@ -103,3 +103,28 @@ export function closeDialogAnimated(el: DialogElementLike, options: ExitMotionOp
|
|
|
103
103
|
export function dialogTogglesClosed(el: Pick<DialogElementLike, 'open' | 'hasAttribute'>): boolean {
|
|
104
104
|
return el.open && !el.hasAttribute(DIALOG_CLOSING_ATTRIBUTE);
|
|
105
105
|
}
|
|
106
|
+
|
|
107
|
+
/** The element-style marker a styled dialog carries (design-core ELEMENT_STYLE_MARKER_CLASS — actions-core does not
|
|
108
|
+
* import design-core). */
|
|
109
|
+
const STYLED_DIALOG_CLASS = 'iox-es';
|
|
110
|
+
|
|
111
|
+
export interface FocusableSheet {
|
|
112
|
+
classList: { contains(token: string): boolean };
|
|
113
|
+
querySelector(selector: string): unknown;
|
|
114
|
+
hasAttribute(name: string): boolean;
|
|
115
|
+
setAttribute(name: string, value: string): void;
|
|
116
|
+
focus(options?: FocusOptions): void;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* A styled dialog a visitor just opened takes focus on its SHEET (premium components M4 — M1.1's carry-over). A bare
|
|
121
|
+
* `showModal()` focuses its first control, and a phone can ring it after a tap; the sheet never rings (iox-design.css) and
|
|
122
|
+
* the first Tab moves to that control, with its ring. A dialog with an author's `[autofocus]` keeps it; a dialog saved
|
|
123
|
+
* before element styles keeps the browser's behaviour. True when the sheet took focus.
|
|
124
|
+
*/
|
|
125
|
+
export function focusDialogSheet(dialog: FocusableSheet | null | undefined): boolean {
|
|
126
|
+
if (!dialog || !dialog.classList.contains(STYLED_DIALOG_CLASS) || dialog.querySelector('[autofocus]')) return false;
|
|
127
|
+
if (!dialog.hasAttribute('tabindex')) dialog.setAttribute('tabindex', '-1');
|
|
128
|
+
dialog.focus({ preventScroll: true });
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { HeaderBehaviour, HeaderEdge, headerBehaviourOf, headerEdgeOf, headerStaysAtTop, HeaderTrait } from '@vectoriox/iox-ui/design-core';
|
|
2
|
+
|
|
3
|
+
import { CompositeNode, readProp, readStyle, writeProp, writeStyles } from './composite-node';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Set how a header behaves as the page scrolls (premium components T2, M4) — ONE edit for the builder's Header section
|
|
7
|
+
* and the composer (generation-core), on either node shape. A header that stays at the top is the containing block of its
|
|
8
|
+
* edge: the live site makes it sticky (iox-design.css), and a header stored `static` becomes `relative` — what the canvas,
|
|
9
|
+
* which forces a node's own position, draws it with. Nothing else of the author's is touched.
|
|
10
|
+
*/
|
|
11
|
+
export function setHeaderBehaviour(node: CompositeNode, behaviour: HeaderBehaviour | `${HeaderBehaviour}`, edge: HeaderEdge | `${HeaderEdge}` | null): void {
|
|
12
|
+
const b = headerBehaviourOf(behaviour);
|
|
13
|
+
writeProp(node, HeaderTrait.Behaviour, b);
|
|
14
|
+
writeProp(node, HeaderTrait.Edge, headerEdgeOf(edge) ?? '');
|
|
15
|
+
const position = readStyle(node, 'position');
|
|
16
|
+
if (headerStaysAtTop(b) && (position === undefined || position === null || position === '' || position === 'static')) writeStyles(node, { position: 'relative' });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** The header's behaviour and edge as stored (Scrolls / none for a header that never chose). */
|
|
20
|
+
export function headerBehaviourOfNode(node: CompositeNode | null | undefined): { behaviour: HeaderBehaviour; edge: HeaderEdge | null } {
|
|
21
|
+
return {
|
|
22
|
+
behaviour: headerBehaviourOf(node ? readProp(node, HeaderTrait.Behaviour) : undefined),
|
|
23
|
+
edge: headerEdgeOf(node ? readProp(node, HeaderTrait.Edge) : undefined),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -178,3 +178,85 @@ export function attachCarouselA11y(swiper: CarouselSwiper, options: CarouselA11y
|
|
|
178
178
|
carouselOf(swiper.el)?.destroy();
|
|
179
179
|
return new CarouselA11y(swiper, options);
|
|
180
180
|
}
|
|
181
|
+
|
|
182
|
+
// ── The bar's words (premium components M4, M1.3) ──────────────────────────────────────────────────────
|
|
183
|
+
|
|
184
|
+
/** Where the Caption and the Count parts get their words: generated content (`attr()`), never the element's text — so
|
|
185
|
+
* the canvas's Text component and the runtime never write the same node. */
|
|
186
|
+
export const SLIDER_TEXT_ATTRIBUTE = 'data-iox-text';
|
|
187
|
+
/** A slide's caption, written by every renderer (iox-ui `nodeA11yAttributes`, SLIDE_CAPTION_ATTRIBUTE). */
|
|
188
|
+
const CAPTION_ATTRIBUTE = 'data-iox-caption';
|
|
189
|
+
/** Held for a frame while the caption changes, so its new words fade in (iox-design.css). */
|
|
190
|
+
export const SLIDER_SWAPPING_ATTRIBUTE = 'data-iox-swapping';
|
|
191
|
+
const BAR_RUNTIME_KEY = '__ioxSliderBar';
|
|
192
|
+
|
|
193
|
+
/** What the caption says for a slide: its own caption, else its picture's description, else nothing. */
|
|
194
|
+
export function sliderCaptionOf(slide: Element | null | undefined): string {
|
|
195
|
+
if (!slide) return '';
|
|
196
|
+
const holder = slide.hasAttribute(CAPTION_ATTRIBUTE) ? slide : slide.querySelector(`[${CAPTION_ATTRIBUTE}]`);
|
|
197
|
+
const own = holder?.getAttribute(CAPTION_ATTRIBUTE)?.trim();
|
|
198
|
+
if (own) return own;
|
|
199
|
+
return slide.querySelector('img[alt]')?.getAttribute('alt')?.trim() ?? '';
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** "2 / 5" — the slide in view and how many there are; nothing for an empty slider. */
|
|
203
|
+
export function sliderCountText(index: number, total: number): string {
|
|
204
|
+
if (!(total > 0)) return '';
|
|
205
|
+
const i = Math.min(total - 1, Math.max(0, Math.floor(Number.isFinite(index) ? index : 0)));
|
|
206
|
+
return `${i + 1} / ${total}`;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** The parts of a running Swiper the bar's runtime reads. */
|
|
210
|
+
export interface BarSwiper {
|
|
211
|
+
el: HTMLElement;
|
|
212
|
+
slides: HTMLElement[];
|
|
213
|
+
activeIndex: number;
|
|
214
|
+
realIndex?: number;
|
|
215
|
+
destroyed?: boolean;
|
|
216
|
+
on(event: string, handler: (...args: unknown[]) => void): void;
|
|
217
|
+
off(event: string, handler: (...args: unknown[]) => void): void;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Keep the bar's Caption and Count on the slide in view — the canvas, Preview and both engine paths (bootSlider). The
|
|
222
|
+
* caption changes behind a short fade (the first one does not); the count reads left to right in every language.
|
|
223
|
+
* Returns the stop; a slider with neither part gets a no-op.
|
|
224
|
+
*/
|
|
225
|
+
export function attachSliderBar(swiper: BarSwiper): () => void {
|
|
226
|
+
((swiper.el as unknown as Record<string, () => void>)[BAR_RUNTIME_KEY])?.();
|
|
227
|
+
const caption = sliderPartElement(swiper.el, SliderRole.Caption);
|
|
228
|
+
const count = sliderPartElement(swiper.el, SliderRole.Count);
|
|
229
|
+
if (!caption && !count) return () => { /* nothing to keep */ };
|
|
230
|
+
const view = swiper.el.ownerDocument?.defaultView;
|
|
231
|
+
const raf = (fn: () => void) => (view?.requestAnimationFrame ? view.requestAnimationFrame(() => view.requestAnimationFrame(fn)) : setTimeout(fn, 32));
|
|
232
|
+
let first = true;
|
|
233
|
+
const sync = () => {
|
|
234
|
+
if (swiper.destroyed) return;
|
|
235
|
+
const slides = swiper.slides ?? [];
|
|
236
|
+
if (count) {
|
|
237
|
+
count.setAttribute('dir', 'ltr');
|
|
238
|
+
count.setAttribute(SLIDER_TEXT_ATTRIBUTE, sliderCountText(swiper.realIndex ?? swiper.activeIndex, slides.length));
|
|
239
|
+
}
|
|
240
|
+
if (caption) {
|
|
241
|
+
const words = sliderCaptionOf(slides[swiper.activeIndex]);
|
|
242
|
+
if (caption.getAttribute(SLIDER_TEXT_ATTRIBUTE) !== words) {
|
|
243
|
+
if (!first) {
|
|
244
|
+
caption.setAttribute(SLIDER_SWAPPING_ATTRIBUTE, '');
|
|
245
|
+
raf(() => caption.removeAttribute(SLIDER_SWAPPING_ATTRIBUTE));
|
|
246
|
+
}
|
|
247
|
+
caption.setAttribute(SLIDER_TEXT_ATTRIBUTE, words);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
first = false;
|
|
251
|
+
};
|
|
252
|
+
swiper.on('slideChange', sync);
|
|
253
|
+
swiper.on('slidesLengthChange', sync);
|
|
254
|
+
sync();
|
|
255
|
+
const stop = () => {
|
|
256
|
+
swiper.off('slideChange', sync);
|
|
257
|
+
swiper.off('slidesLengthChange', sync);
|
|
258
|
+
delete (swiper.el as unknown as Record<string, unknown>)[BAR_RUNTIME_KEY];
|
|
259
|
+
};
|
|
260
|
+
(swiper.el as unknown as Record<string, () => void>)[BAR_RUNTIME_KEY] = stop;
|
|
261
|
+
return stop;
|
|
262
|
+
}
|
|
@@ -27,10 +27,37 @@ export enum SliderRole {
|
|
|
27
27
|
Prev = 'slider.prev',
|
|
28
28
|
Next = 'slider.next',
|
|
29
29
|
Pagination = 'slider.pagination',
|
|
30
|
+
/** Premium M4 (M1.3): the bar UNDER the picture that holds the controls — caption · count · arrows. */
|
|
31
|
+
Bar = 'slider.bar',
|
|
32
|
+
/** The caption of the slide in view (its `caption`, else its picture's description) — drawn by the runtime. */
|
|
33
|
+
Caption = 'slider.caption',
|
|
34
|
+
/** "1 / 3", drawn by the runtime, left to right in every language. */
|
|
35
|
+
Count = 'slider.count',
|
|
30
36
|
}
|
|
31
37
|
|
|
32
38
|
/** Parts in the order they are stamped: the pause control first (APG: the first focus stop), the dots last. */
|
|
33
|
-
export const SLIDER_PART_ROLES: ReadonlyArray<SliderRole> = [
|
|
39
|
+
export const SLIDER_PART_ROLES: ReadonlyArray<SliderRole> = [
|
|
40
|
+
SliderRole.Pause, SliderRole.Prev, SliderRole.Next, SliderRole.Pagination, SliderRole.Bar, SliderRole.Caption, SliderRole.Count,
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
/** Where the controls sit (premium M4): over the picture (CC3, every slider saved before M4) or in the bar under it. */
|
|
44
|
+
export enum SliderPlacement {
|
|
45
|
+
Over = 'over',
|
|
46
|
+
Under = 'under',
|
|
47
|
+
}
|
|
48
|
+
export const SLIDER_PLACEMENT_TRAIT = 'controlsPlacement';
|
|
49
|
+
/** The bar's parts in reading order: the pause control, the caption at the reading start, the count, the dots, the
|
|
50
|
+
* arrows at the end (M1.3). */
|
|
51
|
+
export const SLIDER_BAR_ORDER: ReadonlyArray<SliderRole> = [
|
|
52
|
+
SliderRole.Pause, SliderRole.Caption, SliderRole.Count, SliderRole.Pagination, SliderRole.Prev, SliderRole.Next,
|
|
53
|
+
];
|
|
54
|
+
/** Parts that exist only in the bar. */
|
|
55
|
+
const BAR_ONLY: ReadonlyArray<SliderRole> = [SliderRole.Caption, SliderRole.Count];
|
|
56
|
+
|
|
57
|
+
/** Premium M4: the trait that has the SCROLL play the slider (held mid-screen, resting on each picture). */
|
|
58
|
+
export const SLIDER_SCROLL_PLAYS_TRAIT = 'scrollPlays';
|
|
59
|
+
/** Premium M4: a slide's caption (SliderSlide ▸ Caption) — the bar's Caption part shows the one in view. */
|
|
60
|
+
export const SLIDE_CAPTION_TRAIT = 'caption';
|
|
34
61
|
|
|
35
62
|
export const SLIDER_TYPE = 'SliderContainer';
|
|
36
63
|
export const SLIDE_TYPE = 'SliderSlide';
|
|
@@ -40,12 +67,18 @@ export enum SliderFlag {
|
|
|
40
67
|
Navigation = 'enableNavigation',
|
|
41
68
|
Pagination = 'enablePagination',
|
|
42
69
|
Autoplay = 'enableAutoplay',
|
|
70
|
+
/** Premium M4: a caption under the picture (the bar only). */
|
|
71
|
+
Caption = 'enableCaption',
|
|
72
|
+
/** Premium M4: "1 / 3" under the picture (the bar only). */
|
|
73
|
+
Count = 'enableCount',
|
|
43
74
|
}
|
|
44
75
|
|
|
45
76
|
const FLAG_PARTS: ReadonlyArray<readonly [SliderFlag, ReadonlyArray<SliderRole>]> = [
|
|
46
77
|
[SliderFlag.Autoplay, [SliderRole.Pause]],
|
|
47
78
|
[SliderFlag.Navigation, [SliderRole.Prev, SliderRole.Next]],
|
|
48
79
|
[SliderFlag.Pagination, [SliderRole.Pagination]],
|
|
80
|
+
[SliderFlag.Caption, [SliderRole.Caption]],
|
|
81
|
+
[SliderFlag.Count, [SliderRole.Count]],
|
|
49
82
|
];
|
|
50
83
|
|
|
51
84
|
/** The trait that names the carousel (Slider ▸ Accessibility ▸ Carousel name) — the A11y trait every named part uses. */
|
|
@@ -61,9 +94,25 @@ export function isSlider(node: RoleLike | null | undefined): boolean {
|
|
|
61
94
|
return node?.type === SLIDER_TYPE;
|
|
62
95
|
}
|
|
63
96
|
|
|
64
|
-
/** The slider's own part playing `role`
|
|
65
|
-
export function sliderPart<T extends RoleLike>(slider: { children?: T[] | null }, role: SliderRole): T | undefined {
|
|
66
|
-
|
|
97
|
+
/** The slider's own part playing `role` — a direct child, or one in its bar — or undefined. */
|
|
98
|
+
export function sliderPart<T extends RoleLike & { children?: T[] | null }>(slider: { children?: T[] | null }, role: SliderRole): T | undefined {
|
|
99
|
+
const direct = (slider.children ?? []).find(c => c?.role === role);
|
|
100
|
+
if (direct) return direct;
|
|
101
|
+
const bar = (slider.children ?? []).find(c => c?.role === SliderRole.Bar);
|
|
102
|
+
return (bar?.children ?? []).find(c => c?.role === role);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Where the slider's controls sit. The STRUCTURE decides once there are controls — a bar means under the picture, a
|
|
107
|
+
* control of its own means over it (every slider saved before M4) — so a stored trait can never disagree with what is
|
|
108
|
+
* drawn. A slider with no controls yet takes its `controlsPlacement` (the builder's new Slider: under the picture).
|
|
109
|
+
*/
|
|
110
|
+
export function sliderPlacementOf(slider: CompositeNode | null | undefined): SliderPlacement {
|
|
111
|
+
if (!slider) return SliderPlacement.Over;
|
|
112
|
+
const children = slider.children ?? [];
|
|
113
|
+
if (children.some(c => c?.role === SliderRole.Bar)) return SliderPlacement.Under;
|
|
114
|
+
if (children.some(isSliderPart)) return SliderPlacement.Over;
|
|
115
|
+
return readProp(slider, SLIDER_PLACEMENT_TRAIT) === SliderPlacement.Under ? SliderPlacement.Under : SliderPlacement.Over;
|
|
67
116
|
}
|
|
68
117
|
|
|
69
118
|
/** True when the slider's controls are parts (it was created or migrated in CC3) — Swiper then draws none of its own. */
|
|
@@ -95,15 +144,18 @@ enum Type {
|
|
|
95
144
|
ButtonBlock = 'ButtonBlock',
|
|
96
145
|
Icon = 'Icon',
|
|
97
146
|
Container = 'Container',
|
|
147
|
+
Text = 'Text',
|
|
98
148
|
}
|
|
99
149
|
|
|
100
150
|
enum Style {
|
|
101
151
|
Control = 'button-secondary',
|
|
102
152
|
Icon = 'icon',
|
|
153
|
+
/** The caption and the count read as small print. */
|
|
154
|
+
Words = 'small',
|
|
103
155
|
}
|
|
104
156
|
|
|
105
157
|
/** Each part's glyph (Phosphor) and its default accessible name. */
|
|
106
|
-
export const SLIDER_PART_GLYPH: Readonly<Record<
|
|
158
|
+
export const SLIDER_PART_GLYPH: Readonly<Record<SliderRole.Pause | SliderRole.Prev | SliderRole.Next, string>> = {
|
|
107
159
|
[SliderRole.Pause]: 'ph ph-pause',
|
|
108
160
|
[SliderRole.Prev]: 'ph ph-caret-left',
|
|
109
161
|
[SliderRole.Next]: 'ph ph-caret-right',
|
|
@@ -115,9 +167,30 @@ const PART_LABEL: Readonly<Record<SliderRole, string>> = {
|
|
|
115
167
|
[SliderRole.Prev]: 'Previous',
|
|
116
168
|
[SliderRole.Next]: 'Next',
|
|
117
169
|
[SliderRole.Pagination]: 'Pagination',
|
|
170
|
+
[SliderRole.Bar]: 'Controls',
|
|
171
|
+
[SliderRole.Caption]: 'Caption',
|
|
172
|
+
[SliderRole.Count]: 'Count',
|
|
118
173
|
};
|
|
119
174
|
|
|
120
175
|
const ZERO = '0px 0px 0px 0px';
|
|
176
|
+
/** A control in the bar: in flow, its own size, no offsets (the bar spaces its parts). */
|
|
177
|
+
const BAR_PART_LAYOUT: Readonly<Record<string, string>> = { position: 'static', top: 'auto', margin: ZERO };
|
|
178
|
+
/**
|
|
179
|
+
* The bar (M1.3): a row under the picture. Its inline padding keeps the PAGE's margin when the slider bleeds to the
|
|
180
|
+
* screen's edge — the gutter less however far the slider is already inset from the screen (`100%` is the slider's
|
|
181
|
+
* width, the bar's containing block) — so in a padded section it is 0 and at full bleed it is the gutter.
|
|
182
|
+
*/
|
|
183
|
+
const BAR_INLINE_PAD = 'max(0px, calc(var(--iox-space-gutter, 24px) - (100vw - 100%) / 2))';
|
|
184
|
+
const BAR_BASE: Readonly<Record<string, string>> = {
|
|
185
|
+
position: 'relative', display: 'flex', flexDirection: 'row', flexWrap: 'nowrap', alignItems: 'center', justifyContent: 'flex-start',
|
|
186
|
+
gap: '12px', width: '100%', height: 'auto', minHeight: '44px', margin: ZERO, padding: `12px ${BAR_INLINE_PAD} 0px ${BAR_INLINE_PAD}`, zIndex: '10',
|
|
187
|
+
};
|
|
188
|
+
/** The caption takes the room the controls leave; its words end in an ellipsis rather than push them off. */
|
|
189
|
+
const CAPTION_BASE: Readonly<Record<string, string>> = {
|
|
190
|
+
flexGrow: '1', flexShrink: '1', flexBasis: '0%', minWidth: '0px', width: 'auto', minHeight: 'auto', margin: ZERO, padding: ZERO,
|
|
191
|
+
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
|
|
192
|
+
};
|
|
193
|
+
const COUNT_BASE: Readonly<Record<string, string>> = { flexGrow: '0', flexShrink: '0', width: 'auto', minHeight: 'auto', margin: ZERO, padding: ZERO };
|
|
121
194
|
/** A round 44px control (board 6 `.st-ctl`), over the slides (z above `.swiper-wrapper`). The PLACE (which edge) is the
|
|
122
195
|
* shared CSS's, in logical insets, so a carousel reads the same way in RTL. */
|
|
123
196
|
const CONTROL_BASE: Readonly<Record<string, string>> = {
|
|
@@ -158,6 +231,17 @@ function legacyLookOf(slider: CompositeNode): LegacySliderLook {
|
|
|
158
231
|
|
|
159
232
|
/** One part, from the recipe (board 6: `button-secondary` controls, token dots) or in a migrated slider's legacy look. */
|
|
160
233
|
export function buildSliderPart(create: CompositeNodeFactory, role: SliderRole, legacy?: LegacySliderLook): CompositeNode {
|
|
234
|
+
if (role === SliderRole.Bar) {
|
|
235
|
+
const bar = makeCompositePart(create, { key: 'bar', type: Type.Container, props: {}, elementStyle: NO_ELEMENT_STYLE }, role, PART_LABEL[role], BAR_BASE);
|
|
236
|
+
bar.children = [];
|
|
237
|
+
return bar;
|
|
238
|
+
}
|
|
239
|
+
if (role === SliderRole.Caption || role === SliderRole.Count) {
|
|
240
|
+
// Their words are the runtime's (the slide in view), drawn as the part's generated content: the node's own
|
|
241
|
+
// content stays empty, so no renderer ever fights the runtime over the element's text.
|
|
242
|
+
return makeCompositePart(create, { key: role.split('.')[1], type: Type.Text, props: { content: '', tag: 'p' }, elementStyle: Style.Words },
|
|
243
|
+
role, PART_LABEL[role], role === SliderRole.Caption ? CAPTION_BASE : COUNT_BASE);
|
|
244
|
+
}
|
|
161
245
|
if (role === SliderRole.Pagination) {
|
|
162
246
|
const dots = makeCompositePart(create, { key: 'pagination', type: Type.Container, props: {}, elementStyle: NO_ELEMENT_STYLE }, role, PART_LABEL[role],
|
|
163
247
|
legacy?.paginationColor ? { ...PAGINATION_BASE, color: legacy.paginationColor } : PAGINATION_BASE);
|
|
@@ -186,50 +270,131 @@ export function buildSliderPart(create: CompositeNodeFactory, role: SliderRole,
|
|
|
186
270
|
|
|
187
271
|
const truthy = (v: unknown): boolean => v === true || v === 'true';
|
|
188
272
|
|
|
189
|
-
/** The parts the slider's switches call for, in stamping order
|
|
190
|
-
|
|
191
|
-
|
|
273
|
+
/** The parts the slider's switches call for, in stamping order — and its bar, while the controls sit under the
|
|
274
|
+
* picture. A caption and a count live in the bar only. */
|
|
275
|
+
export function wantedSliderParts(slider: CompositeNode, placement: SliderPlacement = sliderPlacementOf(slider)): SliderRole[] {
|
|
276
|
+
const under = placement === SliderPlacement.Under;
|
|
277
|
+
const roles = FLAG_PARTS.filter(([flag]) => truthy(readProp(slider, flag))).flatMap(([, r]) => r).filter(r => under || !BAR_ONLY.includes(r));
|
|
278
|
+
// The bar exists while it holds something: an empty band under the picture is never drawn.
|
|
279
|
+
return under && roles.length ? [...roles, SliderRole.Bar] : roles;
|
|
192
280
|
}
|
|
193
281
|
|
|
282
|
+
/** The slider's parts wherever they are: its direct children and those in its bar. */
|
|
283
|
+
function allSliderParts(slider: CompositeNode): CompositeNode[] {
|
|
284
|
+
const direct = (slider.children ?? []).filter(isSliderPart);
|
|
285
|
+
const bar = direct.find(c => c.role === SliderRole.Bar);
|
|
286
|
+
return [...direct, ...(bar?.children ?? []).filter(isSliderPart)];
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const barIndex = (role: string | undefined): number => SLIDER_BAR_ORDER.indexOf(role as SliderRole);
|
|
290
|
+
|
|
194
291
|
export interface SliderPartsChange {
|
|
195
292
|
added: CompositeNode[];
|
|
196
293
|
removed: CompositeNode[];
|
|
197
294
|
}
|
|
198
295
|
|
|
199
296
|
/**
|
|
200
|
-
* Make the slider's parts match its switches — ONE edit (a palette switch, the migration, the composer
|
|
201
|
-
* parts are stamped from the recipe (or, `legacy`, in the migrated look) at their place: the
|
|
202
|
-
* and Next before the first slide
|
|
203
|
-
*
|
|
297
|
+
* Make the slider's parts match its switches — ONE edit (a palette switch, the migration, the composer, a change of
|
|
298
|
+
* placement). Missing parts are stamped from the recipe (or, `legacy`, in the migrated look) at their place: over the
|
|
299
|
+
* picture, the pause control, Previous and Next before the first slide and the pagination after the last; under it
|
|
300
|
+
* (premium M4), every control in the bar in its reading order, the bar after the slides. Parts whose switch is off are
|
|
301
|
+
* removed wherever they are. Everything else (the slides, the author's edits to an existing part) is kept.
|
|
204
302
|
*/
|
|
205
303
|
export function syncSliderParts(slider: CompositeNode, create: CompositeNodeFactory, legacy?: LegacySliderLook): SliderPartsChange {
|
|
206
|
-
|
|
207
|
-
const
|
|
208
|
-
const
|
|
209
|
-
const
|
|
304
|
+
// A migrated slider keeps the place Swiper drew its controls in: over the picture.
|
|
305
|
+
const wanted = new Set(wantedSliderParts(slider, legacy ? SliderPlacement.Over : sliderPlacementOf(slider)));
|
|
306
|
+
const under = wanted.has(SliderRole.Bar);
|
|
307
|
+
const removed = allSliderParts(slider).filter(c => !wanted.has(c.role as SliderRole));
|
|
210
308
|
const added: CompositeNode[] = [];
|
|
309
|
+
let kept = [...(slider.children ?? [])].filter(c => !removed.includes(c));
|
|
310
|
+
let bar = kept.find(c => c.role === SliderRole.Bar) ?? null;
|
|
311
|
+
if (bar) bar.children = (bar.children ?? []).filter(c => !removed.includes(c));
|
|
312
|
+
if (under && !bar) {
|
|
313
|
+
bar = buildSliderPart(create, SliderRole.Bar);
|
|
314
|
+
added.push(bar);
|
|
315
|
+
kept.push(bar);
|
|
316
|
+
}
|
|
211
317
|
for (const role of SLIDER_PART_ROLES) {
|
|
212
|
-
if (!wanted.has(role) || kept.some(c => c.role === role)) continue;
|
|
318
|
+
if (role === SliderRole.Bar || !wanted.has(role) || allSliderPartsOf(kept, bar).some(c => c.role === role)) continue;
|
|
213
319
|
const part = buildSliderPart(create, role, legacy);
|
|
214
320
|
added.push(part);
|
|
215
|
-
if (
|
|
321
|
+
if (under && bar) {
|
|
322
|
+
if (role !== SliderRole.Caption && role !== SliderRole.Count) writeStyles(part, BAR_PART_LAYOUT);
|
|
323
|
+
placeInBar(bar, part);
|
|
324
|
+
} else if (role === SliderRole.Pagination) {
|
|
216
325
|
kept.push(part);
|
|
217
326
|
} else {
|
|
218
|
-
|
|
219
|
-
const order = SLIDER_PART_ROLES.indexOf(role);
|
|
220
|
-
const firstSlide = kept.findIndex(c => !isSliderPart(c));
|
|
221
|
-
let at = firstSlide < 0 ? kept.length : firstSlide;
|
|
222
|
-
for (let i = 0; i < kept.length; i++) {
|
|
223
|
-
const r = kept[i].role as SliderRole;
|
|
224
|
-
if (isSliderPart(kept[i]) && SLIDER_PART_ROLES.indexOf(r) > order && i < at) { at = i; break; }
|
|
225
|
-
}
|
|
226
|
-
kept.splice(at, 0, part);
|
|
327
|
+
kept = placeBeforeSlides(kept, part, role);
|
|
227
328
|
}
|
|
228
329
|
}
|
|
229
330
|
if (added.length || removed.length) slider.children = kept;
|
|
230
331
|
return { added, removed };
|
|
231
332
|
}
|
|
232
333
|
|
|
334
|
+
function allSliderPartsOf(children: ReadonlyArray<CompositeNode>, bar: CompositeNode | null): CompositeNode[] {
|
|
335
|
+
return [...children.filter(isSliderPart), ...(bar?.children ?? []).filter(isSliderPart)];
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** Into the bar at its reading-order place (after the parts that come before it). */
|
|
339
|
+
function placeInBar(bar: CompositeNode, part: CompositeNode): void {
|
|
340
|
+
const children = [...(bar.children ?? [])];
|
|
341
|
+
const order = barIndex(part.role);
|
|
342
|
+
const at = children.findIndex(c => barIndex(c.role) > order);
|
|
343
|
+
children.splice(at < 0 ? children.length : at, 0, part);
|
|
344
|
+
bar.children = children;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/** Before the first slide, after the parts that come before it in stamping order. */
|
|
348
|
+
function placeBeforeSlides(kept: CompositeNode[], part: CompositeNode, role: SliderRole): CompositeNode[] {
|
|
349
|
+
const out = [...kept];
|
|
350
|
+
const order = SLIDER_PART_ROLES.indexOf(role);
|
|
351
|
+
const firstSlide = out.findIndex(c => !isSliderPart(c));
|
|
352
|
+
let at = firstSlide < 0 ? out.length : firstSlide;
|
|
353
|
+
for (let i = 0; i < out.length; i++) {
|
|
354
|
+
const r = out[i].role as SliderRole;
|
|
355
|
+
if (isSliderPart(out[i]) && SLIDER_PART_ROLES.indexOf(r) > order && i < at) { at = i; break; }
|
|
356
|
+
}
|
|
357
|
+
out.splice(at, 0, part);
|
|
358
|
+
return out;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/** The layout a control has over the picture (its recipe's place: absolute, the arrows at the vertical middle). */
|
|
362
|
+
function overLayoutFor(role: SliderRole): Readonly<Record<string, string>> {
|
|
363
|
+
if (role === SliderRole.Pagination) return { position: 'absolute', margin: ZERO };
|
|
364
|
+
return role === SliderRole.Pause ? { position: 'absolute', top: 'auto', margin: ZERO } : { position: 'absolute', top: ARROW_TOP, margin: ZERO };
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Move the controls over the picture or into the bar under it (premium M4, M1.3 — Slider ▸ Controls) — ONE edit and
|
|
369
|
+
* one undo step. The parts keep their identity and the author's edits; only their place changes (in flow in the bar,
|
|
370
|
+
* absolute over the picture). A caption and a count exist only in the bar. True when anything changed.
|
|
371
|
+
*/
|
|
372
|
+
export function setSliderPlacement(slider: CompositeNode, placement: SliderPlacement, create: CompositeNodeFactory): boolean {
|
|
373
|
+
const was = sliderPlacementOf(slider);
|
|
374
|
+
writeProp(slider, SLIDER_PLACEMENT_TRAIT, placement);
|
|
375
|
+
if (was === placement) return false;
|
|
376
|
+
const children = [...(slider.children ?? [])];
|
|
377
|
+
const bar = children.find(c => c.role === SliderRole.Bar) ?? null;
|
|
378
|
+
if (placement === SliderPlacement.Under) {
|
|
379
|
+
const controls = children.filter(c => isSliderPart(c) && c.role !== SliderRole.Bar);
|
|
380
|
+
const rest = children.filter(c => !controls.includes(c));
|
|
381
|
+
const target = bar ?? buildSliderPart(create, SliderRole.Bar);
|
|
382
|
+
for (const part of controls) { writeStyles(part, BAR_PART_LAYOUT); placeInBar(target, part); }
|
|
383
|
+
slider.children = bar ? rest : [...rest, target];
|
|
384
|
+
} else {
|
|
385
|
+
const inBar = (bar?.children ?? []).filter(isSliderPart);
|
|
386
|
+
let rest = children.filter(c => c !== bar);
|
|
387
|
+
for (const part of inBar) {
|
|
388
|
+
if (BAR_ONLY.includes(part.role as SliderRole)) continue;
|
|
389
|
+
writeStyles(part, overLayoutFor(part.role as SliderRole));
|
|
390
|
+
rest = part.role === SliderRole.Pagination ? [...rest, part] : placeBeforeSlides(rest, part, part.role as SliderRole);
|
|
391
|
+
}
|
|
392
|
+
slider.children = rest;
|
|
393
|
+
}
|
|
394
|
+
syncSliderParts(slider, create);
|
|
395
|
+
return true;
|
|
396
|
+
}
|
|
397
|
+
|
|
233
398
|
/** True when a saved slider still relies on Swiper-drawn controls: a switch is on and no part exists yet. */
|
|
234
399
|
export function needsSliderMigration(slider: CompositeNode | null | undefined): boolean {
|
|
235
400
|
return !!slider && isSlider(slider) && !hasSliderParts(slider) && wantedSliderParts(slider).length > 0;
|
|
@@ -354,6 +519,8 @@ export function buildCarouselRecipe(create: CompositeNodeFactory, options: Carou
|
|
|
354
519
|
// Rewind: Previous and Next are always there to press (board 6) — neither greys out at an end.
|
|
355
520
|
slidesPerView: 1, spaceBetween: 0, loop: 'rewind', enableNavigation: true, enablePagination: true, enableAutoplay: true, autoplayDelay: 5000,
|
|
356
521
|
autoplayPauseOnMouseEnter: true, autoHeight: true, [SLIDER_NAME_TRAIT]: options.name ?? 'What clients say',
|
|
522
|
+
// Board 6 draws its controls in a row above the quotes — its own place, not the M4 bar under a picture.
|
|
523
|
+
[SLIDER_PLACEMENT_TRAIT]: SliderPlacement.Over,
|
|
357
524
|
})) writeProp(slider, name, value);
|
|
358
525
|
slider.children = (options.quotes ?? CAROUSEL_QUOTES).map((q, i) => {
|
|
359
526
|
const slide = makeCompositePart(create, { key: `slide-${i + 1}`, type: SLIDE_TYPE, props: {}, elementStyle: 'card' }, null, 'Slide', QUOTE_SLIDE_BASE);
|
|
@@ -11,7 +11,8 @@ import { SLIDER_TYPE, SliderRole } from './slider';
|
|
|
11
11
|
export interface CompositeRoleSpec {
|
|
12
12
|
/** The role of the part's parent; null for a composite root. */
|
|
13
13
|
readonly parent: string | null;
|
|
14
|
-
/** The TYPE of the part's parent, for a part whose parent carries no role (a Slider's controls).
|
|
14
|
+
/** The TYPE of the part's parent, for a part whose parent carries no role (a Slider's controls). With `parent`
|
|
15
|
+
* also set, either parent places it (a Slider's control lives in the slider or in its bar). */
|
|
15
16
|
readonly parentType?: string;
|
|
16
17
|
readonly locked?: boolean;
|
|
17
18
|
/** May be deleted, but never moved out of its parent (a Slider's controls work only as its direct children). */
|
|
@@ -59,13 +60,17 @@ export const COMPOSITE_ROLE_SPECS: Readonly<Record<string, CompositeRoleSpec>> =
|
|
|
59
60
|
// A Scene always keeps its stage: everything it pins and clips lives there.
|
|
60
61
|
[SceneRole.Scene]: { parent: null },
|
|
61
62
|
[SceneRole.Stage]: { parent: SceneRole.Scene, locked: true },
|
|
62
|
-
// CC3 — a Slider's controls are its own direct children (Swiper is handed them)
|
|
63
|
-
// Autoplay is on (WCAG 2.2.2): the Autoplay switch removes it,
|
|
64
|
-
// their switches and may be deleted
|
|
65
|
-
|
|
66
|
-
[SliderRole.
|
|
67
|
-
[SliderRole.
|
|
68
|
-
[SliderRole.
|
|
63
|
+
// CC3 — a Slider's controls are its own direct children (Swiper is handed them) — or, premium M4, children of its
|
|
64
|
+
// bar under the picture. The pause control stays while Autoplay is on (WCAG 2.2.2): the Autoplay switch removes it,
|
|
65
|
+
// never Delete. The arrows, the dots, the caption and the count follow their switches and may be deleted; the bar
|
|
66
|
+
// follows Slider ▸ Controls.
|
|
67
|
+
[SliderRole.Pause]: { parent: SliderRole.Bar, parentType: SLIDER_TYPE, locked: true },
|
|
68
|
+
[SliderRole.Prev]: { parent: SliderRole.Bar, parentType: SLIDER_TYPE, pinned: true },
|
|
69
|
+
[SliderRole.Next]: { parent: SliderRole.Bar, parentType: SLIDER_TYPE, pinned: true },
|
|
70
|
+
[SliderRole.Pagination]: { parent: SliderRole.Bar, parentType: SLIDER_TYPE, pinned: true },
|
|
71
|
+
[SliderRole.Bar]: { parent: null, parentType: SLIDER_TYPE, pinned: true },
|
|
72
|
+
[SliderRole.Caption]: { parent: SliderRole.Bar, pinned: true },
|
|
73
|
+
[SliderRole.Count]: { parent: SliderRole.Bar, pinned: true },
|
|
69
74
|
};
|
|
70
75
|
|
|
71
76
|
export enum StructureEdit {
|
|
@@ -95,7 +100,7 @@ export function structureRefusal(node: RoleLike | null | undefined, edit: Struct
|
|
|
95
100
|
if (edit === StructureEdit.Move && acceptsRefusal(node, parent)) return StructureRefusal.NotAccepted;
|
|
96
101
|
if (!spec?.locked && !spec?.pinned) return null;
|
|
97
102
|
if (edit === StructureEdit.Delete) return spec.locked ? StructureRefusal.LockedPart : null;
|
|
98
|
-
const placed = spec.parentType
|
|
103
|
+
const placed = (!!spec.parentType && parent?.type === spec.parentType) || (spec.parent !== null && parent?.role === spec.parent);
|
|
99
104
|
return placed ? null : StructureRefusal.LockedPlacement;
|
|
100
105
|
}
|
|
101
106
|
|