@vectoriox/iox-ui 5.77.0 → 5.79.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.
Files changed (33) hide show
  1. package/design-core/index.d.ts +34 -4
  2. package/fesm2022/vectoriox-iox-ui-design-core.mjs +34 -4
  3. package/fesm2022/vectoriox-iox-ui-design-core.mjs.map +1 -1
  4. package/fesm2022/vectoriox-iox-ui-generation-core.mjs +950 -15
  5. package/fesm2022/vectoriox-iox-ui-generation-core.mjs.map +1 -1
  6. package/fesm2022/vectoriox-iox-ui-harness-core.mjs +38 -3
  7. package/fesm2022/vectoriox-iox-ui-harness-core.mjs.map +1 -1
  8. package/fesm2022/vectoriox-iox-ui-media-core.mjs +23 -1
  9. package/fesm2022/vectoriox-iox-ui-media-core.mjs.map +1 -1
  10. package/fesm2022/vectoriox-iox-ui-scene-core.mjs +3 -6
  11. package/fesm2022/vectoriox-iox-ui-scene-core.mjs.map +1 -1
  12. package/generation-core/index.d.ts +585 -8
  13. package/harness-core/index.d.ts +40 -2
  14. package/media-core/index.d.ts +19 -1
  15. package/package.json +1 -1
  16. package/scene-core/index.d.ts +1 -4
  17. package/src/design-core/flavor-spec.generated.ts +34 -4
  18. package/src/generation-core/compose-site.ts +25 -2
  19. package/src/generation-core/film.ts +91 -0
  20. package/src/generation-core/patterns.ts +41 -4
  21. package/src/generation-core/plan-wire.ts +4 -2
  22. package/src/generation-core/public-api.ts +1 -0
  23. package/src/generation-core/serialized-node.ts +3 -0
  24. package/src/generation-core/showcase.ts +32 -7
  25. package/src/generation-core/site-plan.model.ts +8 -0
  26. package/src/generation-core/site-spec.generated.ts +738 -1
  27. package/src/generation-core/validate-site-plan.ts +57 -2
  28. package/src/harness-core/business-types.ts +3 -0
  29. package/src/harness-core/generation-view.ts +3 -1
  30. package/src/harness-core/harness-spec.generated.ts +32 -2
  31. package/src/harness-core/motion.ts +8 -1
  32. package/src/media-core/frames.ts +23 -0
  33. package/src/scene-core/scene-edit.ts +3 -5
@@ -1,6 +1,7 @@
1
1
  import { MotionId, MoodId, TintId, DensityId, DesignMotion, DesignChoices, DesignMode, StylePreset, ShapeId, AnimateId } from '@vectoriox/iox-ui/design-core';
2
2
  import { FactKey, PathIssue, PlainObject, JsonSchema, SiteTemplate, ShapeEntry, AiGenerationStage, AiStep, SitePlan, PlanEdit, PlanChange, SiteAudit } from '@vectoriox/iox-ui/generation-core';
3
3
  export { JsonSchema } from '@vectoriox/iox-ui/generation-core';
4
+ import { FrameSequence } from '@vectoriox/iox-ui/media-core';
4
5
 
5
6
  declare const SPEC_HARNESS_SOURCE: {
6
7
  readonly repo: "nextlevelbuilder/ui-ux-pro-max-skill";
@@ -155,6 +156,7 @@ declare const SPEC_BUSINESS_TYPES: readonly [{
155
156
  readonly animate: "subtle";
156
157
  readonly between: "cinematic";
157
158
  };
159
+ readonly ceiling: "expressive";
158
160
  readonly imagery: "Your own photos only, large and uncropped; never stock.";
159
161
  readonly pages: readonly ["home", "work (collection)", "project (detail)", "about", "contact"];
160
162
  readonly needs: readonly ["collection", "detail", "about", "contact"];
@@ -217,6 +219,7 @@ declare const SPEC_BUSINESS_TYPES: readonly [{
217
219
  readonly animate: "subtle";
218
220
  readonly between: "cinematic";
219
221
  };
222
+ readonly ceiling: "expressive";
220
223
  readonly imagery: "Wide, bright room shots; details of materials.";
221
224
  readonly pages: readonly ["home", "projects (collection)", "project (detail)", "services", "about", "contact"];
222
225
  readonly needs: readonly ["collection", "detail", "services", "about", "contact"];
@@ -280,6 +283,7 @@ declare const SPEC_BUSINESS_TYPES: readonly [{
280
283
  readonly animate: "subtle";
281
284
  readonly between: "fade";
282
285
  };
286
+ readonly ceiling: "expressive";
283
287
  readonly imagery: "Large project photos and drawings.";
284
288
  readonly pages: readonly ["home", "projects (collection)", "project (detail)", "about", "contact"];
285
289
  readonly needs: readonly ["collection", "detail", "about", "contact"];
@@ -341,9 +345,10 @@ declare const SPEC_BUSINESS_TYPES: readonly [{
341
345
  readonly motion: "standard";
342
346
  readonly moves: {
343
347
  readonly scroll: "smooth";
344
- readonly animate: "lively";
348
+ readonly animate: "expressive";
345
349
  readonly between: "slide";
346
350
  };
351
+ readonly ceiling: "expressive";
347
352
  readonly imagery: "Case-study images of your own work.";
348
353
  readonly pages: readonly ["home", "work (collection)", "case study (detail)", "services", "about", "contact"];
349
354
  readonly needs: readonly ["collection", "detail", "services", "about", "contact"];
@@ -398,6 +403,7 @@ declare const SPEC_BUSINESS_TYPES: readonly [{
398
403
  readonly animate: "standard";
399
404
  readonly between: "fade";
400
405
  };
406
+ readonly ceiling: "expressive";
401
407
  readonly imagery: "Your pieces, photographed flat and true to colour.";
402
408
  readonly pages: readonly ["home", "works (collection)", "work (detail)", "about", "contact"];
403
409
  readonly needs: readonly ["collection", "about", "contact"];
@@ -451,9 +457,10 @@ declare const SPEC_BUSINESS_TYPES: readonly [{
451
457
  readonly motion: "calm";
452
458
  readonly moves: {
453
459
  readonly scroll: "normal";
454
- readonly animate: "lively";
460
+ readonly animate: "expressive";
455
461
  readonly between: "slide";
456
462
  };
463
+ readonly ceiling: "expressive";
457
464
  readonly imagery: "Your pieces and hands at work; natural light.";
458
465
  readonly pages: readonly ["home", "pieces (collection)", "piece (detail)", "workshops", "about", "contact"];
459
466
  readonly needs: readonly ["collection", "detail", "about", "contact"];
@@ -509,6 +516,7 @@ declare const SPEC_BUSINESS_TYPES: readonly [{
509
516
  readonly animate: "subtle";
510
517
  readonly between: "cinematic";
511
518
  };
519
+ readonly ceiling: "expressive";
512
520
  readonly imagery: "Close-ups on a plain ground.";
513
521
  readonly pages: readonly ["home", "collection", "piece (detail)", "about", "contact"];
514
522
  readonly needs: readonly ["collection", "detail", "about", "contact"];
@@ -565,6 +573,7 @@ declare const SPEC_BUSINESS_TYPES: readonly [{
565
573
  readonly animate: "standard";
566
574
  readonly between: "slide";
567
575
  };
576
+ readonly ceiling: "expressive";
568
577
  readonly imagery: "Healed work, clean photos.";
569
578
  readonly pages: readonly ["home", "work (collection)", "artists", "faq", "contact"];
570
579
  readonly needs: readonly ["collection", "faq", "contact"];
@@ -620,6 +629,7 @@ declare const SPEC_BUSINESS_TYPES: readonly [{
620
629
  readonly animate: "subtle";
621
630
  readonly between: "fade";
622
631
  };
632
+ readonly ceiling: "standard";
623
633
  readonly imagery: "Rooms, textures, hands; no faces needed.";
624
634
  readonly pages: readonly ["home", "treatments (services)", "prices", "about", "contact"];
625
635
  readonly needs: readonly ["services", "prices", "about", "contact"];
@@ -685,6 +695,7 @@ declare const SPEC_BUSINESS_TYPES: readonly [{
685
695
  readonly animate: "standard";
686
696
  readonly between: "fade";
687
697
  };
698
+ readonly ceiling: "lively";
688
699
  readonly imagery: "Your own before-and-after photos.";
689
700
  readonly pages: readonly ["home", "services", "prices", "gallery", "contact"];
690
701
  readonly needs: readonly ["services", "prices", "gallery", "contact"];
@@ -744,6 +755,7 @@ declare const SPEC_BUSINESS_TYPES: readonly [{
744
755
  readonly animate: "subtle";
745
756
  readonly between: "fade";
746
757
  };
758
+ readonly ceiling: "lively";
747
759
  readonly imagery: "The room, real classes, daylight.";
748
760
  readonly pages: readonly ["home", "classes", "schedule", "teachers", "prices", "contact"];
749
761
  readonly needs: readonly ["prices", "contact"];
@@ -804,6 +816,7 @@ declare const SPEC_BUSINESS_TYPES: readonly [{
804
816
  readonly animate: "lively";
805
817
  readonly between: "slide";
806
818
  };
819
+ readonly ceiling: "lively";
807
820
  readonly imagery: "Real people training; movement.";
808
821
  readonly pages: readonly ["home", "classes", "prices", "coaches", "contact"];
809
822
  readonly needs: readonly ["prices", "contact"];
@@ -860,6 +873,7 @@ declare const SPEC_BUSINESS_TYPES: readonly [{
860
873
  readonly animate: "subtle";
861
874
  readonly between: "none";
862
875
  };
876
+ readonly ceiling: "subtle";
863
877
  readonly imagery: "A calm room, daylight; your portrait.";
864
878
  readonly pages: readonly ["home", "how I work", "about", "faq", "contact"];
865
879
  readonly needs: readonly ["collection", "about", "faq", "contact"];
@@ -920,6 +934,7 @@ declare const SPEC_BUSINESS_TYPES: readonly [{
920
934
  readonly animate: "subtle";
921
935
  readonly between: "fade";
922
936
  };
937
+ readonly ceiling: "subtle";
923
938
  readonly imagery: "Your team and rooms; no stock doctors.";
924
939
  readonly pages: readonly ["home", "treatments (services)", "team", "faq", "contact"];
925
940
  readonly needs: readonly ["services", "team", "faq", "contact"];
@@ -981,6 +996,7 @@ declare const SPEC_BUSINESS_TYPES: readonly [{
981
996
  readonly animate: "subtle";
982
997
  readonly between: "fade";
983
998
  };
999
+ readonly ceiling: "subtle";
984
1000
  readonly imagery: "Your clinic and team.";
985
1001
  readonly pages: readonly ["home", "treatments", "team", "faq", "contact"];
986
1002
  readonly needs: readonly ["services", "team", "faq", "contact"];
@@ -1040,6 +1056,7 @@ declare const SPEC_BUSINESS_TYPES: readonly [{
1040
1056
  readonly animate: "standard";
1041
1057
  readonly between: "fade";
1042
1058
  };
1059
+ readonly ceiling: "standard";
1043
1060
  readonly imagery: "Real animals you treat (with permission).";
1044
1061
  readonly pages: readonly ["home", "services", "team", "faq", "contact"];
1045
1062
  readonly needs: readonly ["services", "team", "faq", "contact"];
@@ -1095,6 +1112,7 @@ declare const SPEC_BUSINESS_TYPES: readonly [{
1095
1112
  readonly animate: "standard";
1096
1113
  readonly between: "fade";
1097
1114
  };
1115
+ readonly ceiling: "expressive";
1098
1116
  readonly imagery: "Your own food and room photos.";
1099
1117
  readonly pages: readonly ["home", "menu", "about", "contact"];
1100
1118
  readonly needs: readonly ["menu", "about", "contact"];
@@ -1157,6 +1175,7 @@ declare const SPEC_BUSINESS_TYPES: readonly [{
1157
1175
  readonly animate: "standard";
1158
1176
  readonly between: "slide";
1159
1177
  };
1178
+ readonly ceiling: "lively";
1160
1179
  readonly imagery: "Your bakes and drinks, close up.";
1161
1180
  readonly pages: readonly ["home", "menu", "about", "contact"];
1162
1181
  readonly needs: readonly ["menu", "about", "contact"];
@@ -1216,6 +1235,7 @@ declare const SPEC_BUSINESS_TYPES: readonly [{
1216
1235
  readonly animate: "subtle";
1217
1236
  readonly between: "cinematic";
1218
1237
  };
1238
+ readonly ceiling: "expressive";
1219
1239
  readonly imagery: "The cellar, the bar, the bottle.";
1220
1240
  readonly pages: readonly ["home", "menu or wines", "our story", "visit", "contact"];
1221
1241
  readonly needs: readonly ["menu", "visit", "contact"];
@@ -1275,6 +1295,7 @@ declare const SPEC_BUSINESS_TYPES: readonly [{
1275
1295
  readonly animate: "standard";
1276
1296
  readonly between: "slide";
1277
1297
  };
1298
+ readonly ceiling: "expressive";
1278
1299
  readonly imagery: "Product photos on plain grounds; real people wearing them.";
1279
1300
  readonly pages: readonly ["home", "collection", "product (detail)", "about", "contact"];
1280
1301
  readonly needs: readonly ["collection", "detail", "about", "contact"];
@@ -1329,6 +1350,7 @@ declare const SPEC_BUSINESS_TYPES: readonly [{
1329
1350
  readonly animate: "standard";
1330
1351
  readonly between: "fade";
1331
1352
  };
1353
+ readonly ceiling: "lively";
1332
1354
  readonly imagery: "Your own arrangements in daylight.";
1333
1355
  readonly pages: readonly ["home", "flowers (collection)", "weddings and events", "about", "contact"];
1334
1356
  readonly needs: readonly ["collection", "about", "contact"];
@@ -1384,6 +1406,7 @@ declare const SPEC_BUSINESS_TYPES: readonly [{
1384
1406
  readonly animate: "subtle";
1385
1407
  readonly between: "fade";
1386
1408
  };
1409
+ readonly ceiling: "expressive";
1387
1410
  readonly imagery: "Pieces in real rooms and on plain grounds.";
1388
1411
  readonly pages: readonly ["home", "collection", "product (detail)", "about", "contact"];
1389
1412
  readonly needs: readonly ["collection", "detail", "about", "contact"];
@@ -1439,6 +1462,7 @@ declare const SPEC_BUSINESS_TYPES: readonly [{
1439
1462
  readonly animate: "subtle";
1440
1463
  readonly between: "fade";
1441
1464
  };
1465
+ readonly ceiling: "standard";
1442
1466
  readonly imagery: "Your portrait; diagrams over stock.";
1443
1467
  readonly pages: readonly ["home", "services", "case studies", "about", "contact"];
1444
1468
  readonly needs: readonly ["services", "about", "contact"];
@@ -1496,6 +1520,7 @@ declare const SPEC_BUSINESS_TYPES: readonly [{
1496
1520
  readonly animate: "standard";
1497
1521
  readonly between: "fade";
1498
1522
  };
1523
+ readonly ceiling: "standard";
1499
1524
  readonly imagery: "Your portrait, working with people.";
1500
1525
  readonly pages: readonly ["home", "how it works", "programmes", "about", "contact"];
1501
1526
  readonly needs: readonly ["collection", "about", "contact"];
@@ -1547,6 +1572,7 @@ declare const SPEC_BUSINESS_TYPES: readonly [{
1547
1572
  readonly animate: "still";
1548
1573
  readonly between: "none";
1549
1574
  };
1575
+ readonly ceiling: "subtle";
1550
1576
  readonly imagery: "Your team; your office.";
1551
1577
  readonly pages: readonly ["home", "practice areas (services)", "team", "about", "contact"];
1552
1578
  readonly needs: readonly ["services", "team", "about", "contact"];
@@ -1609,6 +1635,7 @@ declare const SPEC_BUSINESS_TYPES: readonly [{
1609
1635
  readonly animate: "standard";
1610
1636
  readonly between: "slide";
1611
1637
  };
1638
+ readonly ceiling: "lively";
1612
1639
  readonly imagery: "Your own property photos; never stock.";
1613
1640
  readonly pages: readonly ["home", "properties (collection)", "property (detail)", "about", "contact"];
1614
1641
  readonly needs: readonly ["collection", "detail", "about", "contact"];
@@ -1667,6 +1694,7 @@ declare const SPEC_BUSINESS_TYPES: readonly [{
1667
1694
  readonly animate: "standard";
1668
1695
  readonly between: "fade";
1669
1696
  };
1697
+ readonly ceiling: "standard";
1670
1698
  readonly imagery: "Real people you help (with permission).";
1671
1699
  readonly pages: readonly ["home", "what we do", "stories", "get involved", "contact"];
1672
1700
  readonly needs: readonly ["contact"];
@@ -1718,6 +1746,7 @@ declare const SPEC_BUSINESS_TYPES: readonly [{
1718
1746
  readonly animate: "standard";
1719
1747
  readonly between: "cinematic";
1720
1748
  };
1749
+ readonly ceiling: "expressive";
1721
1750
  readonly imagery: "Real events at your venue.";
1722
1751
  readonly pages: readonly ["home", "spaces", "gallery", "faq", "contact"];
1723
1752
  readonly needs: readonly ["gallery", "faq", "contact"];
@@ -1776,6 +1805,7 @@ declare const SPEC_BUSINESS_TYPES: readonly [{
1776
1805
  readonly animate: "subtle";
1777
1806
  readonly between: "cinematic";
1778
1807
  };
1808
+ readonly ceiling: "expressive";
1779
1809
  readonly imagery: "Rooms, views, breakfast; your own photos.";
1780
1810
  readonly pages: readonly ["home", "rooms (collection)", "room (detail)", "about", "contact"];
1781
1811
  readonly needs: readonly ["collection", "detail", "about", "contact"];
@@ -1834,6 +1864,7 @@ declare const SPEC_BUSINESS_TYPES: readonly [{
1834
1864
  readonly animate: "lively";
1835
1865
  readonly between: "slide";
1836
1866
  };
1867
+ readonly ceiling: "lively";
1837
1868
  readonly imagery: "Real classes (with permission).";
1838
1869
  readonly pages: readonly ["home", "classes", "prices", "about", "contact"];
1839
1870
  readonly needs: readonly ["prices", "about", "contact"];
@@ -2238,6 +2269,9 @@ interface BusinessType {
2238
2269
  readonly motion: MotionId;
2239
2270
  /** How its site usually scrolls, how animated it is and what plays between pages (rev 3.1). */
2240
2271
  readonly moves: Readonly<DesignMotion>;
2272
+ /** SC6: the most How animated a GENERATED site of this type may start at (`motionFor`). A Scene needs Expressive,
2273
+ * so a type capped below it never gets one. Binds generated output only — the owner may choose any level. */
2274
+ readonly ceiling: DesignMotion['animate'];
2241
2275
  readonly imagery: string;
2242
2276
  readonly pages: ReadonlyArray<string>;
2243
2277
  /** The needs its page set implies, in page order (derived from `pages` at contract build). */
@@ -2436,6 +2470,8 @@ interface MotionBasis {
2436
2470
  references: boolean;
2437
2471
  /** The owner asked for little movement: all three were capped. */
2438
2472
  littleMovement: boolean;
2473
+ /** SC6: How animated was held at the type's ceiling (a law firm stays calm whatever the style or references). */
2474
+ ceiling: boolean;
2439
2475
  }
2440
2476
  interface MotionStart extends DesignMotion {
2441
2477
  /** Why, in English — one line per step the start took. */
@@ -2913,11 +2949,13 @@ interface AiGenerationView {
2913
2949
  stage: `${AiGenerationStage}`;
2914
2950
  /** The model step the runner is working on; null = the owner's move. */
2915
2951
  task: `${AiStep}` | null;
2952
+ /** `film`: the owner's film as pictures (SC6b), told by the wizard at Build — `placeFilm` places it. */
2916
2953
  intake: {
2917
2954
  prompt: string;
2918
2955
  files: GenerationFileView[];
2919
2956
  addresses: string[];
2920
2957
  auto: boolean;
2958
+ film?: FrameSequence;
2921
2959
  };
2922
2960
  readers: GenerationReadersView;
2923
2961
  reads: {
@@ -111,6 +111,24 @@ declare const PHONE_LONG_SIDE = 1100;
111
111
  declare function fittedFrameSize(width: number, height: number, longSide: number): FrameSizes;
112
112
  /** How many pictures a video of `seconds` becomes. */
113
113
  declare function videoFrameCount(seconds: number): number;
114
+ /**
115
+ * Pictures per screen of scrolling at which a played film stays smooth as a glide slows to a stop. Measured on Doron's
116
+ * iPhone (2026-11-05): Studio Alma's film at about 11 a screen stepped, at about 55 it did not.
117
+ */
118
+ declare const FILM_SMOOTH_PER_SCREEN = 40;
119
+ /** The fewest and most screens a film the build places holds the page for (SC6b): long enough to feel, short enough to pass. */
120
+ declare const FILM_MIN_SCREENS = 1;
121
+ declare const FILM_MAX_SCREENS = 4;
122
+ /** What the build prepares a film to: about 60 pictures a screen at the longest hold — denser than smooth, never wasted. */
123
+ declare const FILM_PREPARED_PICTURES: number;
124
+ /** The fewest pictures a placed film may have: smooth over the shortest hold. */
125
+ declare const FILM_MIN_PICTURES: number;
126
+ /**
127
+ * The screens a film of `count` pictures holds the page for (SC6b, the composer's density rule): the stretches between
128
+ * pictures are what scroll, so `(count − 1) ÷ 40`, rounded DOWN to a tenth (150 pictures over 3.75 screens measure 39.7 a
129
+ * screen, thin), within the hold's limits. Null: too few pictures to play smoothly over the shortest hold.
130
+ */
131
+ declare function filmScreensFor(count: number): number | null;
114
132
  /** Which of `sourceCount` pictures make a set of `count`, spread evenly, first and last always in. */
115
133
  declare function sampledFrameIndexes(sourceCount: number, count: number): number[];
116
134
  /** Rendered frames in their real order: `frame2` before `frame10`. */
@@ -473,5 +491,5 @@ declare function attachVideos(container: ParentNode, options?: VideoRuntimeOptio
473
491
  /** The runtime already running at `el`, if any. */
474
492
  declare function videoRuntimeOf(el: Element | null | undefined): VideoRuntime | null;
475
493
 
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 };
494
+ export { CONSENT_GRANT_EVENT, CONSENT_PREFERENCES_EVENT, FILM_MAX_SCREENS, FILM_MIN_PICTURES, FILM_MIN_SCREENS, FILM_PREPARED_PICTURES, FILM_SMOOTH_PER_SCREEN, 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, filmScreensFor, 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 };
477
495
  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.77.0",
3
+ "version": "5.79.0",
4
4
  "peerDependencies": {
5
5
  "@angular/common": ">=20.0.0",
6
6
  "@angular/core": ">=20.0.0",
@@ -218,10 +218,7 @@ 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
- */
221
+ /** Pictures per screen of scrolling at which a played film stays smooth (media-core's, which the composer sizes by). */
225
222
  declare const SCENE_FILM_SMOOTH = 40;
226
223
  interface SceneFilmDensity {
227
224
  /** Pictures per screen of scrolling over most of the play: half its scroll holds at least this many. */
@@ -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": [
@@ -10,9 +10,10 @@ import {
10
10
  } from '@vectoriox/iox-ui/composite-core';
11
11
 
12
12
  import { EntranceMark, EntrancePart, planEntrances } from './entrances';
13
+ import { filmOfPlan } from './film';
13
14
  import { NODE_BASELINES } from './node-baselines.generated';
14
15
  import {
15
- BindScope, CardBindTarget, parsePatternPath, PatternDirective, PatternNode, PatternOpens, PatternPlaceholder, PatternRecipe, SitePattern, patternById,
16
+ BindScope, CardBindTarget, parsePatternPath, PatternDirective, PatternNode, PatternOpens, PatternPlaceholder, PatternRecipe, PatternTrack, SitePattern, patternById,
16
17
  } from './patterns';
17
18
  import { recipeListBinds } from './recipe-fields';
18
19
  import { assertStyleSlots, ComposeError, createSerializedNode, SerializedBinding, SerializedNode, setStyleProps, walkSerialized } from './serialized-node';
@@ -206,6 +207,8 @@ const IMAGE = 'Image';
206
207
  const VIDEO = 'Video';
207
208
  /** A Video's source prop (media-core `VideoTrait.Source` — not `source`, which names a data source). */
208
209
  const VIDEO_SOURCE_PROP = 'videoSource';
210
+ /** A Video's pictures the scroll plays (media-core `VideoTrait.Frames`). */
211
+ const VIDEO_FRAMES_PROP = 'frames';
209
212
  const DIALOG = 'Dialog';
210
213
  const SUBMIT_BUTTON = 'SubmitButton';
211
214
  const FORM_SUCCESS = 'FormSuccess';
@@ -575,6 +578,8 @@ function composeNode(given: PatternNode, id: string, repeater: RepeaterScope | n
575
578
  // Props first: a Heading with no `es` wears its LEVEL's default style.
576
579
  const node = create(id, type, props, pn.es ?? null);
577
580
  if (pn.role) node.role = pn.role;
581
+ if (pn.tracks?.length) node.sceneTracks = pn.tracks.map(cloneTrack);
582
+ if (pn.phoneTracks?.length) node.scenePhoneTracks = pn.phoneTracks.map(cloneTrack);
578
583
  markEntrance(pn, id, repeater, ctx);
579
584
 
580
585
  // Words.
@@ -600,6 +605,13 @@ function composeNode(given: PatternNode, id: string, repeater: RepeaterScope | n
600
605
  setProp(node, VIDEO_SOURCE_PROP, 'file');
601
606
  setProp(node, 'mode', pn.video);
602
607
  }
608
+ // SC6b: a film pattern's Video plays the owner's pictures (the build places the pattern only with a film).
609
+ if (pn.film) {
610
+ if (type !== VIDEO) throw new ComposeError(`${id}: "film" belongs on a Video`);
611
+ const film = filmOfPlan(ctx.plan);
612
+ if (!film) throw new ComposeError(`${id}: a film's pattern on a plan with no film`);
613
+ setProp(node, VIDEO_FRAMES_PROP, film.sequence);
614
+ }
603
615
  if (pn.ph === PatternPlaceholder.Video) {
604
616
  setProp(node, 'src', '');
605
617
  ctx.placeholders.push({ nodeId: id, owner: ctx.owner, kind: PlaceholderKind.Video, text: '' });
@@ -990,6 +1002,13 @@ function wireOpeners(root: SerializedNode, openers: ReadonlyArray<string>): void
990
1002
  }
991
1003
 
992
1004
  /** How a composed section moves in (M1-6b): the look's How animated (or the section's own) on the page's direction. */
1005
+ /** SC6: a track's own copy, so a composed page never shares an array with the contract. */
1006
+ const cloneTrack = (track: PatternTrack): Record<string, unknown> => ({
1007
+ prop: track.prop,
1008
+ keys: track.keys.map(([at, value]) => [at, value]),
1009
+ ...(track.from ? { from: track.from } : {}),
1010
+ });
1011
+
993
1012
  interface SectionMotion {
994
1013
  level: AnimateId | string | null;
995
1014
  rtl: boolean;
@@ -1002,7 +1021,11 @@ function composePattern(pattern: SitePattern, id: string, ctx: Ctx, motion: Sect
1002
1021
  if (!node) throw new ComposeError(`${id}: a pattern's root cannot be optional or a link`);
1003
1022
  wireOpeners(node, own.openers ?? []);
1004
1023
  wireShownWhen(node, own.shownWhen ?? []);
1005
- if (motion && !pattern.global) stampEntrances(node, pattern.family, own.entrances ?? [], motion);
1024
+ // A Scene moves by the scroll alone: the look's entrances would fade its pinned stage in on arrival (SC6).
1025
+ if (motion && !pattern.global && !pattern.scene) stampEntrances(node, pattern.family, own.entrances ?? [], motion);
1026
+ // SC6b, the density rule: a film's Scene holds the page as long as its pictures stay smooth, and no longer.
1027
+ const film = pattern.film ? filmOfPlan(ctx.plan) : null;
1028
+ if (film) setStyleProps(node, { height: `${film.heightVh}vh` });
1006
1029
  node.label = pattern.label;
1007
1030
  return node;
1008
1031
  }
@@ -0,0 +1,91 @@
1
+ import { filmScreensFor, FrameSequence, normalizeFrameSequence } from '@vectoriox/iox-ui/media-core';
2
+
3
+ import { patternById, patternLinkSlots, patternSlots, SCENE_LEVEL, SITE_PATTERNS } from './patterns';
4
+ import { PageSectionPlan, SitePlan } from './site-plan.model';
5
+
6
+ /**
7
+ * The owner's film on a built site (premium sites SC6b). The owner gives a video in Analysis; the wizard's browser makes
8
+ * it pictures (cms-client `FramesPrepareService`); the build places it. A model never picks the film's pattern — the
9
+ * manifest and the plan wire leave it out — so ONE pure placement decides where it goes, run by the wizard to draw the
10
+ * Plan and by the server to build: they cannot disagree.
11
+ */
12
+
13
+ /** The pattern a film plays in: the one whose Video is marked `film` in the contract. */
14
+ export const FILM_PATTERN: string = Object.entries(SITE_PATTERNS).find(([, p]) => p.film)?.[0] ?? '';
15
+
16
+ /** Why a film stays off the site. Each is said to the owner on the Plan step. */
17
+ export enum FilmUnplaced {
18
+ /** How animated is below Expressive: no section holds the page there. */
19
+ NotExpressive = 'notExpressive',
20
+ /** The home page does not open with a hero to play it in. */
21
+ NoHero = 'noHero',
22
+ /** The home page already holds the page elsewhere (one Scene per page). */
23
+ AnotherScene = 'anotherScene',
24
+ /** Too few pictures to play smoothly over the shortest hold (media-core `FILM_MIN_PICTURES`). */
25
+ TooShort = 'tooShort',
26
+ }
27
+
28
+ /** Where the film plays: the home page's first section when it is a hero. */
29
+ export interface FilmTarget {
30
+ readonly page: number;
31
+ readonly section: number;
32
+ }
33
+
34
+ export type FilmPlacement = { readonly target: FilmTarget; readonly unplaced?: undefined } | { readonly target?: undefined; readonly unplaced: FilmUnplaced };
35
+
36
+ /** Where a film WOULD play on this plan (the Plan step asks before the pictures exist). */
37
+ export function filmPlacement(plan: Pick<SitePlan, 'design' | 'pages'>): FilmPlacement {
38
+ if ((plan.design as { animate?: string }).animate !== SCENE_LEVEL) return { unplaced: FilmUnplaced.NotExpressive };
39
+ const page = plan.pages.findIndex(p => p.isHome);
40
+ const sections = page >= 0 ? plan.pages[page].sections : [];
41
+ const first = patternById(sections[0]?.pattern);
42
+ if (!first || first.family !== FILM_FAMILY) return { unplaced: FilmUnplaced.NoHero };
43
+ if (sections.slice(1).some(s => patternById(s.pattern)?.scene)) return { unplaced: FilmUnplaced.AnotherScene };
44
+ return { target: { page, section: 0 } };
45
+ }
46
+
47
+ const FILM_FAMILY = SITE_PATTERNS[FILM_PATTERN]?.family ?? 'hero';
48
+
49
+ export interface PlacedFilm {
50
+ readonly plan: SitePlan;
51
+ readonly unplaced?: FilmUnplaced;
52
+ }
53
+
54
+ /**
55
+ * The plan with the owner's film in it: `film` set, and the home page's hero becomes the film's pattern, keeping the
56
+ * hero's words and links that the film's pattern has. A film that cannot play (unreadable, too few pictures) or a plan
57
+ * with no place for it leaves the plan as it was — the hero keeps its picture, and the builder can add the film later.
58
+ */
59
+ export function placeFilm(plan: SitePlan, sequence: unknown): PlacedFilm {
60
+ const film = normalizeFrameSequence(sequence);
61
+ if (!film || filmScreensFor(film.wide.count) === null) return { plan, unplaced: FilmUnplaced.TooShort };
62
+ const placement = filmPlacement(plan);
63
+ if (!placement.target) return { plan, unplaced: placement.unplaced };
64
+ const { page, section } = placement.target;
65
+ const pages = plan.pages.map((p, i) => i !== page ? p : {
66
+ ...p,
67
+ sections: p.sections.map((s, j) => (j === section ? filmSection(s) : s)),
68
+ });
69
+ return { plan: { ...plan, pages, film: { sequence: film } } };
70
+ }
71
+
72
+ function filmSection(hero: PageSectionPlan): PageSectionPlan {
73
+ const pattern = SITE_PATTERNS[FILM_PATTERN];
74
+ const slots = new Set(patternSlots(pattern));
75
+ const links = new Set(patternLinkSlots(pattern));
76
+ const keep = (values: Readonly<Record<string, string>> | undefined, allowed: Set<string>) => {
77
+ const kept = Object.entries(values ?? {}).filter(([key]) => allowed.has(key));
78
+ return kept.length ? Object.fromEntries(kept) : undefined;
79
+ };
80
+ const keptSlots = keep(hero.slots, slots);
81
+ const keptLinks = keep(hero.links, links);
82
+ return { pattern: FILM_PATTERN, ...(keptSlots ? { slots: keptSlots } : {}), ...(keptLinks ? { links: keptLinks } : {}) };
83
+ }
84
+
85
+ /** The film's pictures as the composer stamps them, and the height its Scene holds the page for. */
86
+ export function filmOfPlan(plan: Pick<SitePlan, 'film'>): { sequence: FrameSequence; heightVh: number } | null {
87
+ const sequence = normalizeFrameSequence(plan.film?.sequence);
88
+ const screens = sequence ? filmScreensFor(sequence.wide.count) : null;
89
+ // A Scene's root holds the page for its height less the one screen its stage fills.
90
+ return sequence && screens !== null ? { sequence, heightVh: Math.round((screens + 1) * 100) } : null;
91
+ }
@@ -90,6 +90,21 @@ 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
+ /** SC6b: the Video a film pattern plays the owner's film in (its `frames`). */
97
+ readonly film?: boolean;
98
+ }
99
+
100
+ /**
101
+ * SC6: one scene-core track as the contract writes it — `{ prop, keys: [[progress, value]], from? }`. Typed here, not
102
+ * imported: generation-core stamps it verbatim and scene-core reads (and normalizes) it where it plays.
103
+ */
104
+ export interface PatternTrack {
105
+ readonly prop: string;
106
+ readonly keys: ReadonlyArray<readonly [number, number | string]>;
107
+ readonly from?: string;
93
108
  }
94
109
 
95
110
 
@@ -138,6 +153,11 @@ export interface SitePattern {
138
153
  readonly global?: `${PatternGlobalPosition}`;
139
154
  /** A navigation pattern's phone shape (CC1): `planNavShape` adds it after the pattern is stamped. */
140
155
  readonly phoneMenu?: 'drawer' | 'bar' | 'both';
156
+ /** SC6: a Scene section — held while the page scrolls, moved by the scroll. Only at How animated Expressive, at
157
+ * most one per page and on at most two pages (`validateSitePlan`); it never takes the look's entrances. */
158
+ readonly scene?: boolean;
159
+ /** SC6b: a Scene that plays the owner's film. Placed by `placeFilm`, never chosen: no manifest or wire lists it. */
160
+ readonly film?: boolean;
141
161
  readonly node: PatternNode;
142
162
  }
143
163
 
@@ -295,7 +315,12 @@ export function patternHasForm(pattern: SitePattern): boolean {
295
315
  return patternNodes(pattern).some(n => !!n.preset);
296
316
  }
297
317
 
298
- /** The library's 31 sections (contract `site.library`), each with whether it is composed. */
318
+ /** SC6: the level a Scene needs — the site's How animated, never a section's own `enter` (a Scene takes no entrance). */
319
+ export const SCENE_LEVEL = 'expressive';
320
+ /** SC6: Scenes on the home page and at most this many other pages, one per page (`validateSitePlan`). */
321
+ export const SCENE_OTHER_PAGES = 1;
322
+
323
+ /** The library's sections (contract `site.library`), each with whether it is composed. */
299
324
  export const SITE_LIBRARY: ReadonlyArray<{ readonly id: string; readonly family: string; readonly composed: boolean }> = SPEC_SITE_LIBRARY;
300
325
 
301
326
  /** 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 +341,22 @@ export interface PatternManifestEntry {
316
341
  roles: string[];
317
342
  optionalParts: string[];
318
343
  form: boolean;
344
+ /** SC6: it holds the page as it scrolls — one per page, the home page and at most one other. */
345
+ scene?: boolean;
319
346
  }
320
347
 
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]) => ({
348
+ export interface PatternManifestOptions {
349
+ /** The site's How animated: below `SCENE_LEVEL` the Scenes are left out — a model cannot choose what it never sees. */
350
+ animate?: string | null;
351
+ }
352
+
353
+ /**
354
+ * The composed library as the model reads it — every pattern, its budgets under `density` (M1-3's manifest). Given the
355
+ * site's How animated (SC6), the Scenes are listed only at `SCENE_LEVEL`, marked `scene`; without it, all of them.
356
+ */
357
+ export function patternManifest(density?: string | null, options: PatternManifestOptions = {}): PatternManifestEntry[] {
358
+ const scenes = !('animate' in options) || options.animate === SCENE_LEVEL;
359
+ return Object.entries(SITE_PATTERNS).filter(([, p]) => !p.film && (scenes || !p.scene)).map(([id, p]) => ({
324
360
  id, family: p.family, label: p.label, intent: p.intent, ...(p.global ? { global: p.global } : {}),
325
361
  slots: Object.fromEntries(Object.keys(patternSlotBudgets(p)).map(slot => [slot, slotBudget(p, slot, density) as number])),
326
362
  links: patternLinkSlots(p),
@@ -328,6 +364,7 @@ export function patternManifest(density?: string | null): PatternManifestEntry[]
328
364
  roles: [...new Set(patternRoleNeeds(p).map(r => `${r.scope}.@${r.role}${r.optional ? OPTIONAL_MARK : ''}`))],
329
365
  optionalParts: patternOptionalParts(p),
330
366
  form: patternHasForm(p),
367
+ ...(p.scene ? { scene: true } : {}),
331
368
  }));
332
369
  }
333
370