@vectoriox/iox-ui 5.84.0 → 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 +35 -4
- package/fesm2022/vectoriox-iox-ui-generation-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 +13 -0
- 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/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/site-spec.generated.ts +8 -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
|
@@ -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
|
|