@vectoriox/iox-ui 5.76.1 → 5.78.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.
@@ -155,6 +155,7 @@ declare const SPEC_BUSINESS_TYPES: readonly [{
155
155
  readonly animate: "subtle";
156
156
  readonly between: "cinematic";
157
157
  };
158
+ readonly ceiling: "expressive";
158
159
  readonly imagery: "Your own photos only, large and uncropped; never stock.";
159
160
  readonly pages: readonly ["home", "work (collection)", "project (detail)", "about", "contact"];
160
161
  readonly needs: readonly ["collection", "detail", "about", "contact"];
@@ -217,6 +218,7 @@ declare const SPEC_BUSINESS_TYPES: readonly [{
217
218
  readonly animate: "subtle";
218
219
  readonly between: "cinematic";
219
220
  };
221
+ readonly ceiling: "expressive";
220
222
  readonly imagery: "Wide, bright room shots; details of materials.";
221
223
  readonly pages: readonly ["home", "projects (collection)", "project (detail)", "services", "about", "contact"];
222
224
  readonly needs: readonly ["collection", "detail", "services", "about", "contact"];
@@ -280,6 +282,7 @@ declare const SPEC_BUSINESS_TYPES: readonly [{
280
282
  readonly animate: "subtle";
281
283
  readonly between: "fade";
282
284
  };
285
+ readonly ceiling: "expressive";
283
286
  readonly imagery: "Large project photos and drawings.";
284
287
  readonly pages: readonly ["home", "projects (collection)", "project (detail)", "about", "contact"];
285
288
  readonly needs: readonly ["collection", "detail", "about", "contact"];
@@ -341,9 +344,10 @@ declare const SPEC_BUSINESS_TYPES: readonly [{
341
344
  readonly motion: "standard";
342
345
  readonly moves: {
343
346
  readonly scroll: "smooth";
344
- readonly animate: "lively";
347
+ readonly animate: "expressive";
345
348
  readonly between: "slide";
346
349
  };
350
+ readonly ceiling: "expressive";
347
351
  readonly imagery: "Case-study images of your own work.";
348
352
  readonly pages: readonly ["home", "work (collection)", "case study (detail)", "services", "about", "contact"];
349
353
  readonly needs: readonly ["collection", "detail", "services", "about", "contact"];
@@ -398,6 +402,7 @@ declare const SPEC_BUSINESS_TYPES: readonly [{
398
402
  readonly animate: "standard";
399
403
  readonly between: "fade";
400
404
  };
405
+ readonly ceiling: "expressive";
401
406
  readonly imagery: "Your pieces, photographed flat and true to colour.";
402
407
  readonly pages: readonly ["home", "works (collection)", "work (detail)", "about", "contact"];
403
408
  readonly needs: readonly ["collection", "about", "contact"];
@@ -451,9 +456,10 @@ declare const SPEC_BUSINESS_TYPES: readonly [{
451
456
  readonly motion: "calm";
452
457
  readonly moves: {
453
458
  readonly scroll: "normal";
454
- readonly animate: "lively";
459
+ readonly animate: "expressive";
455
460
  readonly between: "slide";
456
461
  };
462
+ readonly ceiling: "expressive";
457
463
  readonly imagery: "Your pieces and hands at work; natural light.";
458
464
  readonly pages: readonly ["home", "pieces (collection)", "piece (detail)", "workshops", "about", "contact"];
459
465
  readonly needs: readonly ["collection", "detail", "about", "contact"];
@@ -509,6 +515,7 @@ declare const SPEC_BUSINESS_TYPES: readonly [{
509
515
  readonly animate: "subtle";
510
516
  readonly between: "cinematic";
511
517
  };
518
+ readonly ceiling: "expressive";
512
519
  readonly imagery: "Close-ups on a plain ground.";
513
520
  readonly pages: readonly ["home", "collection", "piece (detail)", "about", "contact"];
514
521
  readonly needs: readonly ["collection", "detail", "about", "contact"];
@@ -565,6 +572,7 @@ declare const SPEC_BUSINESS_TYPES: readonly [{
565
572
  readonly animate: "standard";
566
573
  readonly between: "slide";
567
574
  };
575
+ readonly ceiling: "expressive";
568
576
  readonly imagery: "Healed work, clean photos.";
569
577
  readonly pages: readonly ["home", "work (collection)", "artists", "faq", "contact"];
570
578
  readonly needs: readonly ["collection", "faq", "contact"];
@@ -620,6 +628,7 @@ declare const SPEC_BUSINESS_TYPES: readonly [{
620
628
  readonly animate: "subtle";
621
629
  readonly between: "fade";
622
630
  };
631
+ readonly ceiling: "standard";
623
632
  readonly imagery: "Rooms, textures, hands; no faces needed.";
624
633
  readonly pages: readonly ["home", "treatments (services)", "prices", "about", "contact"];
625
634
  readonly needs: readonly ["services", "prices", "about", "contact"];
@@ -685,6 +694,7 @@ declare const SPEC_BUSINESS_TYPES: readonly [{
685
694
  readonly animate: "standard";
686
695
  readonly between: "fade";
687
696
  };
697
+ readonly ceiling: "lively";
688
698
  readonly imagery: "Your own before-and-after photos.";
689
699
  readonly pages: readonly ["home", "services", "prices", "gallery", "contact"];
690
700
  readonly needs: readonly ["services", "prices", "gallery", "contact"];
@@ -744,6 +754,7 @@ declare const SPEC_BUSINESS_TYPES: readonly [{
744
754
  readonly animate: "subtle";
745
755
  readonly between: "fade";
746
756
  };
757
+ readonly ceiling: "lively";
747
758
  readonly imagery: "The room, real classes, daylight.";
748
759
  readonly pages: readonly ["home", "classes", "schedule", "teachers", "prices", "contact"];
749
760
  readonly needs: readonly ["prices", "contact"];
@@ -804,6 +815,7 @@ declare const SPEC_BUSINESS_TYPES: readonly [{
804
815
  readonly animate: "lively";
805
816
  readonly between: "slide";
806
817
  };
818
+ readonly ceiling: "lively";
807
819
  readonly imagery: "Real people training; movement.";
808
820
  readonly pages: readonly ["home", "classes", "prices", "coaches", "contact"];
809
821
  readonly needs: readonly ["prices", "contact"];
@@ -860,6 +872,7 @@ declare const SPEC_BUSINESS_TYPES: readonly [{
860
872
  readonly animate: "subtle";
861
873
  readonly between: "none";
862
874
  };
875
+ readonly ceiling: "subtle";
863
876
  readonly imagery: "A calm room, daylight; your portrait.";
864
877
  readonly pages: readonly ["home", "how I work", "about", "faq", "contact"];
865
878
  readonly needs: readonly ["collection", "about", "faq", "contact"];
@@ -920,6 +933,7 @@ declare const SPEC_BUSINESS_TYPES: readonly [{
920
933
  readonly animate: "subtle";
921
934
  readonly between: "fade";
922
935
  };
936
+ readonly ceiling: "subtle";
923
937
  readonly imagery: "Your team and rooms; no stock doctors.";
924
938
  readonly pages: readonly ["home", "treatments (services)", "team", "faq", "contact"];
925
939
  readonly needs: readonly ["services", "team", "faq", "contact"];
@@ -981,6 +995,7 @@ declare const SPEC_BUSINESS_TYPES: readonly [{
981
995
  readonly animate: "subtle";
982
996
  readonly between: "fade";
983
997
  };
998
+ readonly ceiling: "subtle";
984
999
  readonly imagery: "Your clinic and team.";
985
1000
  readonly pages: readonly ["home", "treatments", "team", "faq", "contact"];
986
1001
  readonly needs: readonly ["services", "team", "faq", "contact"];
@@ -1040,6 +1055,7 @@ declare const SPEC_BUSINESS_TYPES: readonly [{
1040
1055
  readonly animate: "standard";
1041
1056
  readonly between: "fade";
1042
1057
  };
1058
+ readonly ceiling: "standard";
1043
1059
  readonly imagery: "Real animals you treat (with permission).";
1044
1060
  readonly pages: readonly ["home", "services", "team", "faq", "contact"];
1045
1061
  readonly needs: readonly ["services", "team", "faq", "contact"];
@@ -1095,6 +1111,7 @@ declare const SPEC_BUSINESS_TYPES: readonly [{
1095
1111
  readonly animate: "standard";
1096
1112
  readonly between: "fade";
1097
1113
  };
1114
+ readonly ceiling: "expressive";
1098
1115
  readonly imagery: "Your own food and room photos.";
1099
1116
  readonly pages: readonly ["home", "menu", "about", "contact"];
1100
1117
  readonly needs: readonly ["menu", "about", "contact"];
@@ -1157,6 +1174,7 @@ declare const SPEC_BUSINESS_TYPES: readonly [{
1157
1174
  readonly animate: "standard";
1158
1175
  readonly between: "slide";
1159
1176
  };
1177
+ readonly ceiling: "lively";
1160
1178
  readonly imagery: "Your bakes and drinks, close up.";
1161
1179
  readonly pages: readonly ["home", "menu", "about", "contact"];
1162
1180
  readonly needs: readonly ["menu", "about", "contact"];
@@ -1216,6 +1234,7 @@ declare const SPEC_BUSINESS_TYPES: readonly [{
1216
1234
  readonly animate: "subtle";
1217
1235
  readonly between: "cinematic";
1218
1236
  };
1237
+ readonly ceiling: "expressive";
1219
1238
  readonly imagery: "The cellar, the bar, the bottle.";
1220
1239
  readonly pages: readonly ["home", "menu or wines", "our story", "visit", "contact"];
1221
1240
  readonly needs: readonly ["menu", "visit", "contact"];
@@ -1275,6 +1294,7 @@ declare const SPEC_BUSINESS_TYPES: readonly [{
1275
1294
  readonly animate: "standard";
1276
1295
  readonly between: "slide";
1277
1296
  };
1297
+ readonly ceiling: "expressive";
1278
1298
  readonly imagery: "Product photos on plain grounds; real people wearing them.";
1279
1299
  readonly pages: readonly ["home", "collection", "product (detail)", "about", "contact"];
1280
1300
  readonly needs: readonly ["collection", "detail", "about", "contact"];
@@ -1329,6 +1349,7 @@ declare const SPEC_BUSINESS_TYPES: readonly [{
1329
1349
  readonly animate: "standard";
1330
1350
  readonly between: "fade";
1331
1351
  };
1352
+ readonly ceiling: "lively";
1332
1353
  readonly imagery: "Your own arrangements in daylight.";
1333
1354
  readonly pages: readonly ["home", "flowers (collection)", "weddings and events", "about", "contact"];
1334
1355
  readonly needs: readonly ["collection", "about", "contact"];
@@ -1384,6 +1405,7 @@ declare const SPEC_BUSINESS_TYPES: readonly [{
1384
1405
  readonly animate: "subtle";
1385
1406
  readonly between: "fade";
1386
1407
  };
1408
+ readonly ceiling: "expressive";
1387
1409
  readonly imagery: "Pieces in real rooms and on plain grounds.";
1388
1410
  readonly pages: readonly ["home", "collection", "product (detail)", "about", "contact"];
1389
1411
  readonly needs: readonly ["collection", "detail", "about", "contact"];
@@ -1439,6 +1461,7 @@ declare const SPEC_BUSINESS_TYPES: readonly [{
1439
1461
  readonly animate: "subtle";
1440
1462
  readonly between: "fade";
1441
1463
  };
1464
+ readonly ceiling: "standard";
1442
1465
  readonly imagery: "Your portrait; diagrams over stock.";
1443
1466
  readonly pages: readonly ["home", "services", "case studies", "about", "contact"];
1444
1467
  readonly needs: readonly ["services", "about", "contact"];
@@ -1496,6 +1519,7 @@ declare const SPEC_BUSINESS_TYPES: readonly [{
1496
1519
  readonly animate: "standard";
1497
1520
  readonly between: "fade";
1498
1521
  };
1522
+ readonly ceiling: "standard";
1499
1523
  readonly imagery: "Your portrait, working with people.";
1500
1524
  readonly pages: readonly ["home", "how it works", "programmes", "about", "contact"];
1501
1525
  readonly needs: readonly ["collection", "about", "contact"];
@@ -1547,6 +1571,7 @@ declare const SPEC_BUSINESS_TYPES: readonly [{
1547
1571
  readonly animate: "still";
1548
1572
  readonly between: "none";
1549
1573
  };
1574
+ readonly ceiling: "subtle";
1550
1575
  readonly imagery: "Your team; your office.";
1551
1576
  readonly pages: readonly ["home", "practice areas (services)", "team", "about", "contact"];
1552
1577
  readonly needs: readonly ["services", "team", "about", "contact"];
@@ -1609,6 +1634,7 @@ declare const SPEC_BUSINESS_TYPES: readonly [{
1609
1634
  readonly animate: "standard";
1610
1635
  readonly between: "slide";
1611
1636
  };
1637
+ readonly ceiling: "lively";
1612
1638
  readonly imagery: "Your own property photos; never stock.";
1613
1639
  readonly pages: readonly ["home", "properties (collection)", "property (detail)", "about", "contact"];
1614
1640
  readonly needs: readonly ["collection", "detail", "about", "contact"];
@@ -1667,6 +1693,7 @@ declare const SPEC_BUSINESS_TYPES: readonly [{
1667
1693
  readonly animate: "standard";
1668
1694
  readonly between: "fade";
1669
1695
  };
1696
+ readonly ceiling: "standard";
1670
1697
  readonly imagery: "Real people you help (with permission).";
1671
1698
  readonly pages: readonly ["home", "what we do", "stories", "get involved", "contact"];
1672
1699
  readonly needs: readonly ["contact"];
@@ -1718,6 +1745,7 @@ declare const SPEC_BUSINESS_TYPES: readonly [{
1718
1745
  readonly animate: "standard";
1719
1746
  readonly between: "cinematic";
1720
1747
  };
1748
+ readonly ceiling: "expressive";
1721
1749
  readonly imagery: "Real events at your venue.";
1722
1750
  readonly pages: readonly ["home", "spaces", "gallery", "faq", "contact"];
1723
1751
  readonly needs: readonly ["gallery", "faq", "contact"];
@@ -1776,6 +1804,7 @@ declare const SPEC_BUSINESS_TYPES: readonly [{
1776
1804
  readonly animate: "subtle";
1777
1805
  readonly between: "cinematic";
1778
1806
  };
1807
+ readonly ceiling: "expressive";
1779
1808
  readonly imagery: "Rooms, views, breakfast; your own photos.";
1780
1809
  readonly pages: readonly ["home", "rooms (collection)", "room (detail)", "about", "contact"];
1781
1810
  readonly needs: readonly ["collection", "detail", "about", "contact"];
@@ -1834,6 +1863,7 @@ declare const SPEC_BUSINESS_TYPES: readonly [{
1834
1863
  readonly animate: "lively";
1835
1864
  readonly between: "slide";
1836
1865
  };
1866
+ readonly ceiling: "lively";
1837
1867
  readonly imagery: "Real classes (with permission).";
1838
1868
  readonly pages: readonly ["home", "classes", "prices", "about", "contact"];
1839
1869
  readonly needs: readonly ["prices", "about", "contact"];
@@ -2238,6 +2268,9 @@ interface BusinessType {
2238
2268
  readonly motion: MotionId;
2239
2269
  /** How its site usually scrolls, how animated it is and what plays between pages (rev 3.1). */
2240
2270
  readonly moves: Readonly<DesignMotion>;
2271
+ /** SC6: the most How animated a GENERATED site of this type may start at (`motionFor`). A Scene needs Expressive,
2272
+ * so a type capped below it never gets one. Binds generated output only — the owner may choose any level. */
2273
+ readonly ceiling: DesignMotion['animate'];
2241
2274
  readonly imagery: string;
2242
2275
  readonly pages: ReadonlyArray<string>;
2243
2276
  /** The needs its page set implies, in page order (derived from `pages` at contract build). */
@@ -2436,6 +2469,8 @@ interface MotionBasis {
2436
2469
  references: boolean;
2437
2470
  /** The owner asked for little movement: all three were capped. */
2438
2471
  littleMovement: boolean;
2472
+ /** SC6: How animated was held at the type's ceiling (a law firm stays calm whatever the style or references). */
2473
+ ceiling: boolean;
2439
2474
  }
2440
2475
  interface MotionStart extends DesignMotion {
2441
2476
  /** Why, in English — one line per step the start took. */
@@ -38,8 +38,12 @@ declare enum FrameExtension {
38
38
  Webp = "webp",
39
39
  Jpeg = "jpg"
40
40
  }
41
- /** The most pictures one set may hold. Past this a phone runs out of memory before the visitor runs out of scroll. */
42
- declare const MAX_FRAMES = 300;
41
+ /**
42
+ * The most pictures one set may hold. A film's smoothness is its density, pictures per screen of scrolling: 210 over 18
43
+ * screens stepped on Doron's iPhone as a glide slowed, 1050 did not (2026-11-05). Memory is not what limits it: the player
44
+ * keeps the downloaded files (about 10 kB a phone picture) and only a window of them unpacked (`unpackedFrameCount`).
45
+ */
46
+ declare const MAX_FRAMES = 1200;
43
47
  /** The most one picture may weigh when uploaded. */
44
48
  declare const MAX_FRAME_BYTES: number;
45
49
  /** A sequence's name in storage: what the browser generates and what the server accepts in a key. */
@@ -109,8 +113,6 @@ declare function fittedFrameSize(width: number, height: number, longSide: number
109
113
  declare function videoFrameCount(seconds: number): number;
110
114
  /** Which of `sourceCount` pictures make a set of `count`, spread evenly, first and last always in. */
111
115
  declare function sampledFrameIndexes(sourceCount: number, count: number): number[];
112
- /** A phone plays half the pictures: it has less memory, and half is plenty on a small screen. */
113
- declare function phoneFrameCount(wideCount: number): number;
114
116
  /** Rendered frames in their real order: `frame2` before `frame10`. */
115
117
  declare function sortFrameFiles<T extends {
116
118
  name: string;
@@ -471,5 +473,5 @@ declare function attachVideos(container: ParentNode, options?: VideoRuntimeOptio
471
473
  /** The runtime already running at `el`, if any. */
472
474
  declare function videoRuntimeOf(el: Element | null | undefined): VideoRuntime | null;
473
475
 
474
- export { CONSENT_GRANT_EVENT, CONSENT_PREFERENCES_EVENT, FRAMES_PHONE_MAX_WIDTH, FRAMES_SHOWING_ATTRIBUTE, FRAMES_UNPACKED_BYTES, FRAME_BLEND_STEPS, FRAME_SEQUENCE_ID, FrameExtension, FramePlayer, FrameSet, FramesClass, MAX_FRAMES, MAX_FRAME_BYTES, PHONE_LONG_SIDE, VIDEO_EMBED_ORIGINS, VIDEO_NODE_TYPE, VIDEO_PROVIDER_COMPANY, VIDEO_PROVIDER_LABEL, VIDEO_WORDS, VideoAttribute, VideoClass, VideoIcon, VideoKind, VideoMode, VideoRuntime, VideoSource, VideoTrait, WIDE_LONG_SIDE, attachVideoAt, attachVideos, documentConsentTransport, fittedFrameSize, frameBlendAt, frameFileName, frameIndexAt, frameLoadOrder, framePosition, frameSetFor, frameSummary, frameUrl, framesToHold, isThirdPartyPoster, nearestLoadedFrame, normalizeFrameSequence, parseVideoUrl, phoneFrameCount, phoneFrameSrcset, planVideo, readConsentRecord, sampledFrameIndexes, serializeVideoAttributes, sortFrameFiles, unpackedFrameCount, videoConsentCategory, videoEmbedUrl, videoFrameCount, videoInnerHtml, videoPlayLabel, videoPosterFileName, videoProp, videoRefOf, videoRootAttributes, videoRuntimeOf, videoSourceOf, videoThumbnailRequest, videoWords };
476
+ export { CONSENT_GRANT_EVENT, CONSENT_PREFERENCES_EVENT, FRAMES_PHONE_MAX_WIDTH, FRAMES_SHOWING_ATTRIBUTE, FRAMES_UNPACKED_BYTES, FRAME_BLEND_STEPS, FRAME_SEQUENCE_ID, FrameExtension, FramePlayer, FrameSet, FramesClass, MAX_FRAMES, MAX_FRAME_BYTES, PHONE_LONG_SIDE, VIDEO_EMBED_ORIGINS, VIDEO_NODE_TYPE, VIDEO_PROVIDER_COMPANY, VIDEO_PROVIDER_LABEL, VIDEO_WORDS, VideoAttribute, VideoClass, VideoIcon, VideoKind, VideoMode, VideoRuntime, VideoSource, VideoTrait, WIDE_LONG_SIDE, attachVideoAt, attachVideos, documentConsentTransport, fittedFrameSize, frameBlendAt, frameFileName, frameIndexAt, frameLoadOrder, framePosition, frameSetFor, frameSummary, frameUrl, framesToHold, isThirdPartyPoster, nearestLoadedFrame, normalizeFrameSequence, parseVideoUrl, phoneFrameSrcset, planVideo, readConsentRecord, sampledFrameIndexes, serializeVideoAttributes, sortFrameFiles, unpackedFrameCount, videoConsentCategory, videoEmbedUrl, videoFrameCount, videoInnerHtml, videoPlayLabel, videoPosterFileName, videoProp, videoRefOf, videoRootAttributes, videoRuntimeOf, videoSourceOf, videoThumbnailRequest, videoWords };
475
477
  export type { ConsentGrantDetail, ConsentTransport, FrameBlend, FramePlayerOptions, FrameSequence, FrameSetInfo, FrameSizes, VideoMarkupOptions, VideoNodeLike, VideoPlan, VideoRef, VideoRuntimeOptions, VideoThumbnailRequest, VideoWords };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vectoriox/iox-ui",
3
- "version": "5.76.1",
3
+ "version": "5.78.0",
4
4
  "peerDependencies": {
5
5
  "@angular/common": ">=20.0.0",
6
6
  "@angular/core": ">=20.0.0",
@@ -218,6 +218,23 @@ declare const SCENE_MAX_SCREENS = 20;
218
218
  */
219
219
  declare function sceneScreensOf(height: string | null | undefined): number | null;
220
220
  declare function sceneHeightForScreens(screens: number): string;
221
+ /**
222
+ * Pictures per screen of scrolling at which a played film stays smooth as a glide slows to a stop. Measured on Doron's
223
+ * iPhone (2026-11-05): Studio Alma's film at about 11 a screen stepped, at about 55 it did not.
224
+ */
225
+ declare const SCENE_FILM_SMOOTH = 40;
226
+ interface SceneFilmDensity {
227
+ /** Pictures per screen of scrolling over most of the play: half its scroll holds at least this many. */
228
+ perScreen: number;
229
+ /** The screens of scrolling the film plays over; where it holds still is not counted. */
230
+ screens: number;
231
+ smooth: boolean;
232
+ }
233
+ /**
234
+ * How densely a film of `count` pictures plays on a Scene of `screens`, by its Play track; null when nothing plays. A
235
+ * film's smoothness is its density, never the drawing: spread thin, a slowing glide holds each picture for many refreshes.
236
+ */
237
+ declare function sceneFilmDensity(tracks: ReadonlyArray<SceneTrack> | null | undefined, screens: number | null, count: number): SceneFilmDensity | null;
221
238
  /** Motion v2 §3d's limits for scroll-linked work on one page. */
222
239
  declare const SCENE_BUDGET: {
223
240
  readonly heldSections: 1;
@@ -402,5 +419,5 @@ declare function revealStagger(groups: number, by: TextRevealBy): number;
402
419
  */
403
420
  declare function playTextReveal(el: HTMLElement, by: TextRevealBy, timing: KeyframeAnimationOptions): Animation | null;
404
421
 
405
- export { LIGHT_UP_PIECE_CAP, REVEAL_PIECE_CAP, SCENE_BUDGET, SCENE_DAMPING, 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, 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 };
406
- export type { SceneBudget, SceneFrame, SceneFrameOptions, SceneFrameState, SceneKey, SceneLayerNode, SceneNodeSource, SceneTrack, SceneTrackDef, SceneTreeNode, SceneWords, ScrollDriverOptions, SplitText, SplitTextOptions, TextPiece };
422
+ 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 };
423
+ export type { SceneBudget, SceneFilmDensity, SceneFrame, SceneFrameOptions, SceneFrameState, SceneKey, SceneLayerNode, SceneNodeSource, SceneTrack, SceneTrackDef, SceneTreeNode, SceneWords, ScrollDriverOptions, SplitText, SplitTextOptions, TextPiece };
@@ -1533,6 +1533,36 @@ export const SPEC_AXES = {
1533
1533
  "duration": 600
1534
1534
  }
1535
1535
  }
1536
+ },
1537
+ "expressive": {
1538
+ "label": "Expressive",
1539
+ "he": "נועזת",
1540
+ "hint": "Headings rise line by line and pictures open as they arrive; one section may hold the page and move as you scroll.",
1541
+ "parts": {
1542
+ "heroText": {
1543
+ "type": "fadeIn",
1544
+ "duration": 500
1545
+ },
1546
+ "heading": {
1547
+ "type": "revealText",
1548
+ "duration": 700
1549
+ },
1550
+ "items": {
1551
+ "type": "moveUp",
1552
+ "distance": 32,
1553
+ "duration": 700,
1554
+ "stagger": 110,
1555
+ "cap": 8
1556
+ },
1557
+ "media": {
1558
+ "type": "clipReveal",
1559
+ "duration": 800
1560
+ },
1561
+ "rowMedia": {
1562
+ "type": "clipReveal",
1563
+ "duration": 800
1564
+ }
1565
+ }
1536
1566
  }
1537
1567
  },
1538
1568
  "animationFamilies": {
@@ -1937,7 +1967,7 @@ export const SPEC_STYLE_PRESETS = [
1937
1967
  "pillButtons": false,
1938
1968
  "moves": {
1939
1969
  "scroll": "smooth",
1940
- "animate": "lively",
1970
+ "animate": "expressive",
1941
1971
  "between": "slide"
1942
1972
  },
1943
1973
  "modes": [
@@ -2034,7 +2064,7 @@ export const SPEC_STYLE_PRESETS = [
2034
2064
  "pillButtons": false,
2035
2065
  "moves": {
2036
2066
  "scroll": "normal",
2037
- "animate": "standard",
2067
+ "animate": "expressive",
2038
2068
  "between": "fade"
2039
2069
  },
2040
2070
  "modes": [
@@ -2320,7 +2350,7 @@ export const SPEC_STYLE_PRESETS = [
2320
2350
  "pillButtons": false,
2321
2351
  "moves": {
2322
2352
  "scroll": "normal",
2323
- "animate": "lively",
2353
+ "animate": "expressive",
2324
2354
  "between": "slide"
2325
2355
  },
2326
2356
  "modes": [
@@ -2385,7 +2415,7 @@ export const SPEC_STYLE_PRESETS = [
2385
2415
  "pillButtons": false,
2386
2416
  "moves": {
2387
2417
  "scroll": "normal",
2388
- "animate": "standard",
2418
+ "animate": "expressive",
2389
2419
  "between": "slide"
2390
2420
  },
2391
2421
  "modes": [
@@ -12,7 +12,7 @@ import {
12
12
  import { EntranceMark, EntrancePart, planEntrances } from './entrances';
13
13
  import { NODE_BASELINES } from './node-baselines.generated';
14
14
  import {
15
- BindScope, CardBindTarget, parsePatternPath, PatternDirective, PatternNode, PatternOpens, PatternPlaceholder, PatternRecipe, SitePattern, patternById,
15
+ BindScope, CardBindTarget, parsePatternPath, PatternDirective, PatternNode, PatternOpens, PatternPlaceholder, PatternRecipe, PatternTrack, SitePattern, patternById,
16
16
  } from './patterns';
17
17
  import { recipeListBinds } from './recipe-fields';
18
18
  import { assertStyleSlots, ComposeError, createSerializedNode, SerializedBinding, SerializedNode, setStyleProps, walkSerialized } from './serialized-node';
@@ -575,6 +575,8 @@ function composeNode(given: PatternNode, id: string, repeater: RepeaterScope | n
575
575
  // Props first: a Heading with no `es` wears its LEVEL's default style.
576
576
  const node = create(id, type, props, pn.es ?? null);
577
577
  if (pn.role) node.role = pn.role;
578
+ if (pn.tracks?.length) node.sceneTracks = pn.tracks.map(cloneTrack);
579
+ if (pn.phoneTracks?.length) node.scenePhoneTracks = pn.phoneTracks.map(cloneTrack);
578
580
  markEntrance(pn, id, repeater, ctx);
579
581
 
580
582
  // Words.
@@ -990,6 +992,13 @@ function wireOpeners(root: SerializedNode, openers: ReadonlyArray<string>): void
990
992
  }
991
993
 
992
994
  /** How a composed section moves in (M1-6b): the look's How animated (or the section's own) on the page's direction. */
995
+ /** SC6: a track's own copy, so a composed page never shares an array with the contract. */
996
+ const cloneTrack = (track: PatternTrack): Record<string, unknown> => ({
997
+ prop: track.prop,
998
+ keys: track.keys.map(([at, value]) => [at, value]),
999
+ ...(track.from ? { from: track.from } : {}),
1000
+ });
1001
+
993
1002
  interface SectionMotion {
994
1003
  level: AnimateId | string | null;
995
1004
  rtl: boolean;
@@ -1002,7 +1011,8 @@ function composePattern(pattern: SitePattern, id: string, ctx: Ctx, motion: Sect
1002
1011
  if (!node) throw new ComposeError(`${id}: a pattern's root cannot be optional or a link`);
1003
1012
  wireOpeners(node, own.openers ?? []);
1004
1013
  wireShownWhen(node, own.shownWhen ?? []);
1005
- if (motion && !pattern.global) stampEntrances(node, pattern.family, own.entrances ?? [], motion);
1014
+ // A Scene moves by the scroll alone: the look's entrances would fade its pinned stage in on arrival (SC6).
1015
+ if (motion && !pattern.global && !pattern.scene) stampEntrances(node, pattern.family, own.entrances ?? [], motion);
1006
1016
  node.label = pattern.label;
1007
1017
  return node;
1008
1018
  }
@@ -90,6 +90,19 @@ export interface PatternNode {
90
90
  readonly side?: `${EntranceSide}`;
91
91
  /** L0-3: a link to the page that shows this scope's item (the plan's template page of its type). */
92
92
  readonly detail?: `${BindScope.Item}` | `${BindScope.Next}`;
93
+ /** SC6: a Scene layer's scene-core tracks, written to the node's `sceneTracks` (and `scenePhoneTracks`). */
94
+ readonly tracks?: ReadonlyArray<PatternTrack>;
95
+ readonly phoneTracks?: ReadonlyArray<PatternTrack>;
96
+ }
97
+
98
+ /**
99
+ * SC6: one scene-core track as the contract writes it — `{ prop, keys: [[progress, value]], from? }`. Typed here, not
100
+ * imported: generation-core stamps it verbatim and scene-core reads (and normalizes) it where it plays.
101
+ */
102
+ export interface PatternTrack {
103
+ readonly prop: string;
104
+ readonly keys: ReadonlyArray<readonly [number, number | string]>;
105
+ readonly from?: string;
93
106
  }
94
107
 
95
108
 
@@ -138,6 +151,9 @@ export interface SitePattern {
138
151
  readonly global?: `${PatternGlobalPosition}`;
139
152
  /** A navigation pattern's phone shape (CC1): `planNavShape` adds it after the pattern is stamped. */
140
153
  readonly phoneMenu?: 'drawer' | 'bar' | 'both';
154
+ /** SC6: a Scene section — held while the page scrolls, moved by the scroll. Only at How animated Expressive, at
155
+ * most one per page and on at most two pages (`validateSitePlan`); it never takes the look's entrances. */
156
+ readonly scene?: boolean;
141
157
  readonly node: PatternNode;
142
158
  }
143
159
 
@@ -295,7 +311,12 @@ export function patternHasForm(pattern: SitePattern): boolean {
295
311
  return patternNodes(pattern).some(n => !!n.preset);
296
312
  }
297
313
 
298
- /** The library's 31 sections (contract `site.library`), each with whether it is composed. */
314
+ /** SC6: the level a Scene needs — the site's How animated, never a section's own `enter` (a Scene takes no entrance). */
315
+ export const SCENE_LEVEL = 'expressive';
316
+ /** SC6: Scenes on the home page and at most this many other pages, one per page (`validateSitePlan`). */
317
+ export const SCENE_OTHER_PAGES = 1;
318
+
319
+ /** The library's sections (contract `site.library`), each with whether it is composed. */
299
320
  export const SITE_LIBRARY: ReadonlyArray<{ readonly id: string; readonly family: string; readonly composed: boolean }> = SPEC_SITE_LIBRARY;
300
321
 
301
322
  /** One pattern as the model reads it (L0-2): what it is for, what it may write and how long, and what it binds. */
@@ -316,11 +337,22 @@ export interface PatternManifestEntry {
316
337
  roles: string[];
317
338
  optionalParts: string[];
318
339
  form: boolean;
340
+ /** SC6: it holds the page as it scrolls — one per page, the home page and at most one other. */
341
+ scene?: boolean;
319
342
  }
320
343
 
321
- /** The composed library as the model reads it — every pattern, its budgets under `density` (M1-3's manifest). */
322
- export function patternManifest(density?: string | null): PatternManifestEntry[] {
323
- return Object.entries(SITE_PATTERNS).map(([id, p]) => ({
344
+ export interface PatternManifestOptions {
345
+ /** The site's How animated: below `SCENE_LEVEL` the Scenes are left out — a model cannot choose what it never sees. */
346
+ animate?: string | null;
347
+ }
348
+
349
+ /**
350
+ * The composed library as the model reads it — every pattern, its budgets under `density` (M1-3's manifest). Given the
351
+ * site's How animated (SC6), the Scenes are listed only at `SCENE_LEVEL`, marked `scene`; without it, all of them.
352
+ */
353
+ export function patternManifest(density?: string | null, options: PatternManifestOptions = {}): PatternManifestEntry[] {
354
+ const scenes = !('animate' in options) || options.animate === SCENE_LEVEL;
355
+ return Object.entries(SITE_PATTERNS).filter(([, p]) => scenes || !p.scene).map(([id, p]) => ({
324
356
  id, family: p.family, label: p.label, intent: p.intent, ...(p.global ? { global: p.global } : {}),
325
357
  slots: Object.fromEntries(Object.keys(patternSlotBudgets(p)).map(slot => [slot, slotBudget(p, slot, density) as number])),
326
358
  links: patternLinkSlots(p),
@@ -328,6 +360,7 @@ export function patternManifest(density?: string | null): PatternManifestEntry[]
328
360
  roles: [...new Set(patternRoleNeeds(p).map(r => `${r.scope}.@${r.role}${r.optional ? OPTIONAL_MARK : ''}`))],
329
361
  optionalParts: patternOptionalParts(p),
330
362
  form: patternHasForm(p),
363
+ ...(p.scene ? { scene: true } : {}),
331
364
  }));
332
365
  }
333
366
 
@@ -34,6 +34,9 @@ export interface SerializedNode {
34
34
  itemKey?: string;
35
35
  /** A targeted state's driving node (CC2: an icon's `trigger-on` → its item). */
36
36
  stateTriggers?: Partial<Record<string, string>>;
37
+ /** SC6: a Scene layer's tracks (scene-core `SceneTrack`), and its phone's own. Additive. */
38
+ sceneTracks?: unknown[];
39
+ scenePhoneTracks?: unknown[];
37
40
  bindings: SerializedBinding[];
38
41
  interactions: unknown[];
39
42
  isGlobal?: boolean;
@@ -1,6 +1,6 @@
1
1
  import type { DesignChoices } from '@vectoriox/iox-ui/design-core';
2
2
 
3
- import { BindScope, patternNeedsBinding, PatternGlobalPosition, patternScopes, SITE_PATTERNS } from './patterns';
3
+ import { BindScope, patternNeedsBinding, PatternGlobalPosition, patternScopes, SCENE_LEVEL, SITE_PATTERNS } from './patterns';
4
4
  import { GlobalSlot, PagePlan, PageSectionPlan, SiteBusiness, SiteLocale, SitePlan, SitePlanSourceKind, SITE_PLAN_VERSION } from './site-plan.model';
5
5
  import { STUDIO_TEMPLATE } from './templates';
6
6
 
@@ -39,7 +39,7 @@ export function showcasePages(): PagePlan[] {
39
39
  const byFamily = new Map<string, string[]>();
40
40
  const onItemPage: string[] = [];
41
41
  for (const [id, p] of Object.entries(SITE_PATTERNS)) {
42
- if (p.global) continue;
42
+ if (p.global || p.scene) continue;
43
43
  if (readsRouteItem(id)) { onItemPage.push(id); continue; }
44
44
  byFamily.set(p.family, [...(byFamily.get(p.family) ?? []), id]);
45
45
  }
@@ -62,20 +62,31 @@ export function showcaseGlobals(): { headers: string[]; footers: string[] } {
62
62
  return { headers: of(PatternGlobalPosition.Before), footers: of(PatternGlobalPosition.After) };
63
63
  }
64
64
 
65
- /** The showcase as Site Plans — as many as it takes for every header and every footer to be composed once. */
65
+ /** SC6: the Scene patterns — each shown alone on the home page of its own plan, as a site may hold one (validateScenes). */
66
+ export function showcaseScenes(): string[] {
67
+ return Object.entries(SITE_PATTERNS).filter(([, p]) => p.scene && !p.global).map(([id]) => id);
68
+ }
69
+
70
+ /**
71
+ * The showcase as Site Plans — as many as it takes for every header and every footer to be composed once, then one per
72
+ * Scene pattern (SC6): its home page holds that Scene and nothing else, at How animated Expressive, the only level a
73
+ * Scene is composed at.
74
+ */
66
75
  export function showcasePlans(input: ShowcaseInput): SitePlan[] {
67
76
  const { headers, footers } = showcaseGlobals();
68
77
  const count = Math.max(headers.length, footers.length, 1);
69
78
  const pages = showcasePages();
70
- return Array.from({ length: count }, (_, i): SitePlan => ({
79
+ const plan = (i: number, planPages: PagePlan[], design: DesignChoices): SitePlan => ({
71
80
  version: SITE_PLAN_VERSION,
72
81
  source: { kind: SitePlanSourceKind.Template, templateId: 'showcase', templateVersion: 1 },
73
82
  business: JSON.parse(JSON.stringify(input.business)),
74
83
  ...(input.locale ? { locale: input.locale } : {}),
75
- design: JSON.parse(JSON.stringify(input.design)),
84
+ design: JSON.parse(JSON.stringify(design)),
76
85
  contentTypes: JSON.parse(JSON.stringify(STUDIO_TEMPLATE.contentTypes)),
77
86
  dataSources: JSON.parse(JSON.stringify(STUDIO_TEMPLATE.dataSources)),
78
- pages: JSON.parse(JSON.stringify(pages)),
87
+ pages: JSON.parse(JSON.stringify(planPages)),
79
88
  globals: { [GlobalSlot.Header]: headers[i % headers.length], [GlobalSlot.Footer]: footers[i % footers.length] } as SitePlan['globals'],
80
- }));
89
+ });
90
+ const scenes = showcaseScenes().map((id, i) => plan(i, [{ key: 'home', name: 'Home', route: '/', isHome: true, inNav: false, sections: [sectionFor(id)] }], { ...input.design, animate: SCENE_LEVEL }));
91
+ return [...Array.from({ length: count }, (_, i) => plan(i, pages, input.design)), ...scenes];
81
92
  }