@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.
Files changed (37) hide show
  1. package/actions-core/index.d.ts +18 -2
  2. package/composite-core/index.d.ts +93 -15
  3. package/design-core/index.d.ts +566 -4
  4. package/fesm2022/vectoriox-iox-ui-actions-core.mjs +18 -1
  5. package/fesm2022/vectoriox-iox-ui-actions-core.mjs.map +1 -1
  6. package/fesm2022/vectoriox-iox-ui-composite-core.mjs +307 -39
  7. package/fesm2022/vectoriox-iox-ui-composite-core.mjs.map +1 -1
  8. package/fesm2022/vectoriox-iox-ui-design-core.mjs +680 -15
  9. package/fesm2022/vectoriox-iox-ui-design-core.mjs.map +1 -1
  10. package/fesm2022/vectoriox-iox-ui-generation-core.mjs +35 -4
  11. package/fesm2022/vectoriox-iox-ui-generation-core.mjs.map +1 -1
  12. package/fesm2022/vectoriox-iox-ui-scene-core.mjs +101 -5
  13. package/fesm2022/vectoriox-iox-ui-scene-core.mjs.map +1 -1
  14. package/fesm2022/vectoriox-iox-ui.mjs +285 -12
  15. package/fesm2022/vectoriox-iox-ui.mjs.map +1 -1
  16. package/generation-core/index.d.ts +13 -0
  17. package/index.d.ts +143 -42
  18. package/package.json +1 -1
  19. package/scene-core/index.d.ts +32 -4
  20. package/src/actions-core/dialog-motion.ts +25 -0
  21. package/src/composite-core/header.ts +25 -0
  22. package/src/composite-core/public-api.ts +1 -0
  23. package/src/composite-core/slider-runtime.ts +82 -0
  24. package/src/composite-core/slider.ts +194 -27
  25. package/src/composite-core/structure.ts +14 -9
  26. package/src/design-core/flavor-spec.generated.ts +596 -9
  27. package/src/design-core/header-behaviour.ts +76 -0
  28. package/src/design-core/public-api.ts +1 -0
  29. package/src/design-core/theme-color.ts +21 -5
  30. package/src/generation-core/compose-site.ts +8 -0
  31. package/src/generation-core/node-baselines.generated.ts +17 -3
  32. package/src/generation-core/patterns.ts +2 -0
  33. package/src/generation-core/site-spec.generated.ts +8 -0
  34. package/src/scene-core/scene.ts +18 -0
  35. package/src/scene-core/scroll-driver.ts +83 -6
  36. package/styles/iox-design.css +232 -1
  37. package/styles/iox-forms.css +5 -2
@@ -48,7 +48,25 @@ var SceneAttribute;
48
48
  SceneAttribute["Root"] = "data-iox-scene";
49
49
  SceneAttribute["Tracks"] = "data-iox-tracks";
50
50
  SceneAttribute["PhoneTracks"] = "data-iox-tracks-phone";
51
+ /** A root's stage that is not a Scene's `scene.stage` part: the slider a scroll plays (premium M4). */
52
+ SceneAttribute["Stage"] = "data-iox-scene-stage";
51
53
  })(SceneAttribute || (SceneAttribute = {}));
54
+ /**
55
+ * A SLIDER THE SCROLL PLAYS (premium components M4 — M1.3's held gallery). Its progress rests on each picture and changes
56
+ * only over the middle share of each step, so every picture arrives and stays a moment while the page holds it: `p` is
57
+ * the scroll's progress through the runway (0–1), `n` the slides; the answer is Swiper's progress (0–1).
58
+ */
59
+ const SLIDER_HOLD_SHARE = 0.2;
60
+ /** Page px of scroll per picture, as a share of the screen's height (the runway the renderers reserve). */
61
+ const SLIDER_SCROLL_STEP_SCREENS = 0.55;
62
+ function heldProgress(p, n) {
63
+ if (!(n >= 2) || !Number.isFinite(p))
64
+ return 0;
65
+ const s = clamp01(p) * (n - 1);
66
+ const i = Math.min(n - 2, Math.floor(s));
67
+ const f = clamp01((s - i - SLIDER_HOLD_SHARE) / (1 - 2 * SLIDER_HOLD_SHARE));
68
+ return round((i + smoothstep(f)) / (n - 1));
69
+ }
52
70
  /** At or under this stage width a layer plays its phone tracks. The `mobile` breakpoint's width. */
53
71
  const SCENE_PHONE_MAX_WIDTH = FRAMES_PHONE_MAX_WIDTH;
54
72
  const DECIMALS = 10000;
@@ -779,8 +797,26 @@ const SETTLED_PX = 0.25;
779
797
  const MAX_FRAME_SECONDS = 0.1;
780
798
  /** The first frame after a sleep has no previous one to measure from: it counts as one frame, or a scroll would lose it. */
781
799
  const WAKING_FRAME_SECONDS = 1 / 60;
782
- const STAGE_SELECTOR = `.iox-role-${SceneRole.Stage.replace(/\./g, '-')}`;
800
+ const STAGE_SELECTOR = `.iox-role-${SceneRole.Stage.replace(/\./g, '-')}, [${SceneAttribute.Stage}]`;
783
801
  const TRANSFORM_WILL_CHANGE = 'transform, opacity';
802
+ /**
803
+ * Dispatched (bubbling) by something the driver plays that arrived after the driver last looked — a Swiper booted on a
804
+ * slider's stage. Never a DOM `scroll`: nothing that follows the driver may dispatch one (SC2).
805
+ */
806
+ const SCENE_WAKE_EVENT = 'iox-scene-wake';
807
+ /**
808
+ * ONE driver per root (premium M4 — §0 "To verify first"). The live site attaches a driver per runtime instance (the
809
+ * page's and the globals'); an instance that scanned a container holding the other's Scenes would drive them a second
810
+ * time, two damped values fighting over one layer. A root is held by the first driver that adds it until that driver
811
+ * lets it go.
812
+ */
813
+ const HELD = new WeakMap();
814
+ function sliderOf(stage) {
815
+ // The stage is the runway's child: the `.swiper` itself (both engine paths), or the canvas's shell around it.
816
+ const holder = (stage?.classList.contains('swiper') ? stage : stage?.querySelector('.swiper'));
817
+ const swiper = holder?.swiper;
818
+ return swiper && typeof swiper.setProgress === 'function' && !swiper.destroyed ? swiper : null;
819
+ }
784
820
  /** One frame of the damping: frame-rate independent, so 60 Hz and 120 Hz settle in the same time. */
785
821
  function dampToward(current, target, seconds, rate = SCENE_DAMPING) {
786
822
  return current + (target - current) * (1 - Math.exp(-seconds * rate));
@@ -809,6 +845,13 @@ class ScrollDriver {
809
845
  this.frame = 0;
810
846
  this.lastTime = 0;
811
847
  this.wakeUp = () => this.wake();
848
+ this.onWakeEvent = (e) => {
849
+ const scene = [...this.scenes.values()].find(s => s.root.contains(e.target));
850
+ if (scene) {
851
+ scene.fresh = true;
852
+ this.wake();
853
+ }
854
+ };
812
855
  this.tick = (now) => {
813
856
  this.frame = 0;
814
857
  const seconds = this.lastTime ? Math.min(MAX_FRAME_SECONDS, (now - this.lastTime) / 1000) : WAKING_FRAME_SECONDS;
@@ -837,13 +880,18 @@ class ScrollDriver {
837
880
  // In capture: a scroll does not bubble, and the canvas's Lenis scrolls an element, not the window.
838
881
  this.host.addEventListener('scroll', this.wakeUp, { capture: true, passive: true });
839
882
  this.host.addEventListener('resize', this.wakeUp, { passive: true });
883
+ this.host.addEventListener(SCENE_WAKE_EVENT, this.onWakeEvent, { capture: true });
840
884
  const Observer = this.host.ResizeObserver;
841
885
  this.resize = Observer ? new Observer(() => this.remeasureAll()) : null;
842
886
  }
843
- /** Drive a Scene's root. Idempotent: adding a root again re-reads it. */
887
+ /** Drive a Scene's root. Idempotent: adding a root again re-reads it. False when another driver holds it. */
844
888
  add(root) {
889
+ const holder = HELD.get(root);
890
+ if (holder && holder !== this)
891
+ return false;
845
892
  this.remove(root);
846
- const scene = { root, stage: null, layers: [], progress: 0, target: 0, height: 0, travel: 0, phone: false, inlineSign: 1, fresh: true, mutations: null };
893
+ HELD.set(root, this);
894
+ const scene = { root, stage: null, layers: [], progress: 0, target: 0, height: 0, travel: 0, stick: 0, phone: false, inlineSign: 1, fresh: true, mutations: null };
847
895
  this.scenes.set(root, scene);
848
896
  this.resize?.observe(root);
849
897
  if (this.options.observe) {
@@ -854,11 +902,18 @@ class ScrollDriver {
854
902
  }
855
903
  }
856
904
  this.readScene(scene);
905
+ return true;
906
+ }
907
+ /** True when this driver holds `root`. */
908
+ holds(root) {
909
+ return this.scenes.has(root);
857
910
  }
858
911
  remove(root) {
859
912
  const scene = this.scenes.get(root);
860
913
  if (!scene)
861
914
  return;
915
+ if (HELD.get(root) === this)
916
+ HELD.delete(root);
862
917
  scene.mutations?.disconnect();
863
918
  this.resize?.unobserve(root);
864
919
  if (scene.stage)
@@ -922,6 +977,7 @@ class ScrollDriver {
922
977
  this.frame = 0;
923
978
  this.host.removeEventListener('scroll', this.wakeUp, { capture: true });
924
979
  this.host.removeEventListener('resize', this.wakeUp);
980
+ this.host.removeEventListener(SCENE_WAKE_EVENT, this.onWakeEvent, { capture: true });
925
981
  for (const root of [...this.scenes.keys()])
926
982
  this.remove(root);
927
983
  this.resize?.disconnect();
@@ -971,6 +1027,9 @@ class ScrollDriver {
971
1027
  measure(scene) {
972
1028
  scene.height = scene.root.offsetHeight;
973
1029
  scene.travel = Math.max(0, scene.height - (scene.stage?.offsetHeight ?? scene.height));
1030
+ // Where the stage holds: its sticky top (a played slider holds mid-screen; a Scene's stage at 0). Read when
1031
+ // something resizes, never per frame.
1032
+ scene.stick = scene.stage ? (parseFloat(this.host.getComputedStyle(scene.stage).top) || 0) : 0;
974
1033
  scene.phone = (scene.stage?.offsetWidth ?? Infinity) <= SCENE_PHONE_MAX_WIDTH;
975
1034
  scene.inlineSign = scene.stage && this.host.getComputedStyle(scene.stage).direction === 'rtl' ? -1 : 1;
976
1035
  for (const layer of scene.layers)
@@ -980,10 +1039,14 @@ class ScrollDriver {
980
1039
  const rect = scene.root.getBoundingClientRect();
981
1040
  const origin = this.options.originOf?.(scene.root)?.getBoundingClientRect().top ?? 0;
982
1041
  const zoom = scene.height ? rect.height / scene.height : 1;
983
- const { local, progress } = sceneScrollState(rect.top, origin, zoom, scene.travel);
1042
+ const { local, progress } = sceneScrollState(rect.top, origin + scene.stick * zoom, zoom, scene.travel);
984
1043
  return { scene, local, target: progress };
985
1044
  }
986
1045
  write(scene) {
1046
+ // A slider the scroll plays (M4): the damped moment, held on each picture.
1047
+ const slider = sliderOf(scene.stage);
1048
+ if (slider)
1049
+ slider.setProgress(heldProgress(scene.progress, slider.slides?.length ?? 0), 0);
987
1050
  for (const layer of scene.layers) {
988
1051
  const tracks = scene.phone && layer.phoneTracks.length ? layer.phoneTracks : layer.tracks;
989
1052
  this.writeWords(layer, sceneWordsLit(tracks, scene.progress));
@@ -1062,6 +1125,39 @@ function attachScenes(container, options = {}) {
1062
1125
  roots.forEach(root => driver.add(root));
1063
1126
  return driver;
1064
1127
  }
1128
+ /**
1129
+ * The Scene roots that belong to `elements` (each, and what it holds) — what one runtime instance owns: the page's nodes,
1130
+ * or the globals'. Never a whole container that also holds another instance's Scenes (premium M4: the globals' instance
1131
+ * scanned the app root and drove the page's Scenes a second time).
1132
+ */
1133
+ function sceneRootsIn(elements) {
1134
+ const out = new Set();
1135
+ for (const el of elements) {
1136
+ if (el.hasAttribute?.(SceneAttribute.Root))
1137
+ out.add(el);
1138
+ // A wrapper the node renders around itself (a played slider's runway around its `.swiper`) is the node's own.
1139
+ const parent = el.parentElement;
1140
+ if (parent?.hasAttribute(SceneAttribute.Root) && parent.children.length === 1)
1141
+ out.add(parent);
1142
+ el.querySelectorAll?.(`[${SceneAttribute.Root}]`).forEach(root => out.add(root));
1143
+ }
1144
+ return [...out];
1145
+ }
1146
+ /** The live site's door for a runtime that owns some nodes: their Scenes on one driver (null when none, or under reduced
1147
+ * motion — the page's own CSS then lets each Scene flow). A root another driver holds stays with it. */
1148
+ function attachSceneRoots(roots, options = {}) {
1149
+ if (!roots.length)
1150
+ return null;
1151
+ const host = options.host ?? window;
1152
+ if (host.matchMedia?.('(prefers-reduced-motion: reduce)').matches)
1153
+ return null;
1154
+ const driver = new ScrollDriver(options);
1155
+ const held = roots.filter(root => driver.add(root));
1156
+ if (held.length)
1157
+ return driver;
1158
+ driver.destroy();
1159
+ return null;
1160
+ }
1065
1161
 
1066
1162
  /**
1067
1163
  * @vectoriox/iox-ui/scene-core — a Scene: a stage that holds still while the page scrolls past it, its layers moving on
@@ -1074,5 +1170,5 @@ function attachScenes(container, options = {}) {
1074
1170
  * Generated bundle index. Do not edit.
1075
1171
  */
1076
1172
 
1077
- 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 };
1173
+ export { LIGHT_UP_PIECE_CAP, REVEAL_PIECE_CAP, SCENE_BUDGET, SCENE_DAMPING, SCENE_FILM_SMOOTH, SCENE_FLOW_RULES, SCENE_LENGTH, SCENE_MAX_SCREENS, SCENE_MIN_SCREENS, SCENE_PHONE_LENGTH, SCENE_PHONE_MAX_WIDTH, SCENE_REDUCED_MOTION_CSS, SCENE_REVEAL_DEFAULT, SCENE_STAGE_HEIGHT, SCENE_TRACK_DEFS, SCENE_WAKE_EVENT, SCENE_WORDS, SCENE_WORD_DIM, SLIDER_HOLD_SHARE, SLIDER_SCROLL_STEP_SCREENS, SceneAttribute, SceneProp, SceneRevealFrom, ScrollDriver, TextRevealBy, applySceneFirstFrame, attachSceneRoots, attachScenes, buildSceneRecipe, colourAt, dampToward, heldProgress, lineIndexes, movedSceneKey, normalizeTracks, playTextReveal, revealClipPath, revealStagger, sceneBudget, sceneColourKeyAt, sceneDisplayValue, sceneFilmDensity, sceneFirstFrame, sceneFrame, sceneFrameDeclarations, sceneHasKeyAt, sceneHeightForScreens, sceneKeyPosition, sceneLayersOf, sceneNodeAttributes, scenePlayed, sceneRevealFromOf, sceneRootOnPath, sceneRootsIn, sceneScreensOf, sceneScrollState, sceneTrackDef, sceneTrackSpan, sceneWordOpacity, sceneWordsLit, scenesIn, splitText, syncSceneAttributes, textPieces, valueAt, withSceneKey, withScenePlayback, withSceneRevealFrom, withoutSceneKey, withoutScenePlayback };
1078
1174
  //# sourceMappingURL=vectoriox-iox-ui-scene-core.mjs.map