@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.
@@ -417,6 +417,568 @@ export const SPEC_SITE_ARCHETYPES = {
417
417
  } as const;
418
418
 
419
419
  export const SPEC_SITE_PATTERNS = {
420
+ "hero.scene-depth": {
421
+ "family": "hero",
422
+ "scene": true,
423
+ "label": "Hero — a picture that opens as you scroll",
424
+ "intent": "One strong picture of the work that opens and settles while the page holds still — a first screen that moves, for a studio with a picture worth it.",
425
+ "node": {
426
+ "t": "Section",
427
+ "es": "section",
428
+ "scheme": "inverse",
429
+ "role": "scene",
430
+ "lay": {
431
+ "position": "relative",
432
+ "display": "block",
433
+ "width": "100%",
434
+ "height": "300vh",
435
+ "padding": "0px"
436
+ },
437
+ "c": [
438
+ {
439
+ "t": "Container",
440
+ "role": "scene.stage",
441
+ "lay": {
442
+ "position": "sticky",
443
+ "top": "0px",
444
+ "display": "block",
445
+ "width": "100%",
446
+ "height": "100svh",
447
+ "overflow": "hidden",
448
+ "padding": "0px"
449
+ },
450
+ "c": [
451
+ {
452
+ "t": "Image",
453
+ "es": "media",
454
+ "ph": "image",
455
+ "lay": {
456
+ "position": "absolute",
457
+ "top": "0px",
458
+ "left": "0px",
459
+ "width": "100%",
460
+ "height": "100%",
461
+ "aspectRatio": "auto",
462
+ "borderRadius": "0px",
463
+ "objectFit": "cover"
464
+ },
465
+ "tracks": [
466
+ {
467
+ "prop": "scale",
468
+ "keys": [
469
+ [
470
+ 0,
471
+ 1.3
472
+ ],
473
+ [
474
+ 1,
475
+ 1
476
+ ]
477
+ ]
478
+ },
479
+ {
480
+ "prop": "clip",
481
+ "from": "bottom",
482
+ "keys": [
483
+ [
484
+ 0,
485
+ 38
486
+ ],
487
+ [
488
+ 0.42,
489
+ 100
490
+ ]
491
+ ]
492
+ }
493
+ ]
494
+ },
495
+ {
496
+ "t": "Container",
497
+ "es": "media-scrim",
498
+ "lay": {
499
+ "display": "flex",
500
+ "flexDirection": "column",
501
+ "gap": "14px",
502
+ "position": "absolute",
503
+ "left": "0px",
504
+ "bottom": "0px",
505
+ "width": "100%",
506
+ "padding": "48px 32px 56px 32px"
507
+ },
508
+ "c": [
509
+ {
510
+ "t": "Heading",
511
+ "es": "display",
512
+ "level": "h1",
513
+ "slot": "headline",
514
+ "max": 56,
515
+ "fromBusiness": "name",
516
+ "tracks": [
517
+ {
518
+ "prop": "y",
519
+ "keys": [
520
+ [
521
+ 0,
522
+ 0
523
+ ],
524
+ [
525
+ 1,
526
+ -6
527
+ ]
528
+ ]
529
+ }
530
+ ]
531
+ },
532
+ {
533
+ "t": "Text",
534
+ "es": "lead",
535
+ "slot": "lead",
536
+ "max": 180,
537
+ "fromBusiness": "tagline",
538
+ "ph": "copy",
539
+ "tracks": [
540
+ {
541
+ "prop": "opacity",
542
+ "keys": [
543
+ [
544
+ 0.45,
545
+ 0
546
+ ],
547
+ [
548
+ 0.6,
549
+ 1
550
+ ]
551
+ ]
552
+ }
553
+ ]
554
+ },
555
+ {
556
+ "t": "ButtonBlock",
557
+ "es": "button-primary",
558
+ "href": "/contact",
559
+ "slot": "cta",
560
+ "max": 24,
561
+ "text": "Get in touch",
562
+ "tracks": [
563
+ {
564
+ "prop": "opacity",
565
+ "keys": [
566
+ [
567
+ 0.5,
568
+ 0
569
+ ],
570
+ [
571
+ 0.65,
572
+ 1
573
+ ]
574
+ ]
575
+ }
576
+ ]
577
+ }
578
+ ]
579
+ }
580
+ ]
581
+ }
582
+ ]
583
+ }
584
+ },
585
+ "story.scene-chapters": {
586
+ "family": "story",
587
+ "scene": true,
588
+ "label": "Story — three chapters on one held stage",
589
+ "intent": "How you work in three short chapters that take turns over one picture while the page holds still — a process worth slowing down for.",
590
+ "node": {
591
+ "t": "Section",
592
+ "es": "section",
593
+ "scheme": "inverse",
594
+ "role": "scene",
595
+ "lay": {
596
+ "position": "relative",
597
+ "display": "block",
598
+ "width": "100%",
599
+ "height": "400vh",
600
+ "padding": "0px"
601
+ },
602
+ "c": [
603
+ {
604
+ "t": "Container",
605
+ "role": "scene.stage",
606
+ "lay": {
607
+ "position": "sticky",
608
+ "top": "0px",
609
+ "display": "block",
610
+ "width": "100%",
611
+ "height": "100svh",
612
+ "overflow": "hidden",
613
+ "padding": "0px"
614
+ },
615
+ "c": [
616
+ {
617
+ "t": "Image",
618
+ "es": "media",
619
+ "ph": "image",
620
+ "lay": {
621
+ "position": "absolute",
622
+ "top": "0px",
623
+ "left": "0px",
624
+ "width": "100%",
625
+ "height": "100%",
626
+ "aspectRatio": "auto",
627
+ "borderRadius": "0px",
628
+ "objectFit": "cover"
629
+ },
630
+ "tracks": [
631
+ {
632
+ "prop": "scale",
633
+ "keys": [
634
+ [
635
+ 0,
636
+ 1.12
637
+ ],
638
+ [
639
+ 1,
640
+ 1
641
+ ]
642
+ ]
643
+ }
644
+ ]
645
+ },
646
+ {
647
+ "t": "Container",
648
+ "es": "media-scrim",
649
+ "lay": {
650
+ "position": "absolute",
651
+ "left": "0px",
652
+ "bottom": "0px",
653
+ "width": "100%",
654
+ "height": "62%",
655
+ "padding": "0px"
656
+ },
657
+ "c": [
658
+ {
659
+ "t": "Container",
660
+ "es": "box",
661
+ "lay": {
662
+ "display": "flex",
663
+ "flexDirection": "column",
664
+ "gap": "12px",
665
+ "position": "absolute",
666
+ "left": "32px",
667
+ "right": "32px",
668
+ "bottom": "56px",
669
+ "maxWidth": "640px"
670
+ },
671
+ "tracks": [
672
+ {
673
+ "prop": "opacity",
674
+ "keys": [
675
+ [
676
+ 0,
677
+ 1
678
+ ],
679
+ [
680
+ 0.26,
681
+ 1
682
+ ],
683
+ [
684
+ 0.33,
685
+ 0
686
+ ]
687
+ ]
688
+ },
689
+ {
690
+ "prop": "y",
691
+ "keys": [
692
+ [
693
+ 0.26,
694
+ 0
695
+ ],
696
+ [
697
+ 0.33,
698
+ -6
699
+ ]
700
+ ]
701
+ }
702
+ ],
703
+ "c": [
704
+ {
705
+ "t": "Text",
706
+ "es": "eyebrow",
707
+ "slot": "eyebrow",
708
+ "max": 32,
709
+ "ph": "copy",
710
+ "text": "How we work"
711
+ },
712
+ {
713
+ "t": "Heading",
714
+ "es": "h2",
715
+ "level": "h2",
716
+ "slot": "title",
717
+ "max": 52,
718
+ "ph": "copy",
719
+ "text": "It starts with a conversation"
720
+ },
721
+ {
722
+ "t": "Text",
723
+ "es": "lead",
724
+ "slot": "text",
725
+ "max": 180,
726
+ "ph": "copy",
727
+ "text": "What the first step is, in a sentence or two."
728
+ }
729
+ ]
730
+ },
731
+ {
732
+ "t": "Container",
733
+ "es": "box",
734
+ "lay": {
735
+ "display": "flex",
736
+ "flexDirection": "column",
737
+ "gap": "12px",
738
+ "position": "absolute",
739
+ "left": "32px",
740
+ "right": "32px",
741
+ "bottom": "56px",
742
+ "maxWidth": "640px"
743
+ },
744
+ "tracks": [
745
+ {
746
+ "prop": "opacity",
747
+ "keys": [
748
+ [
749
+ 0.33,
750
+ 0
751
+ ],
752
+ [
753
+ 0.4,
754
+ 1
755
+ ],
756
+ [
757
+ 0.6,
758
+ 1
759
+ ],
760
+ [
761
+ 0.67,
762
+ 0
763
+ ]
764
+ ]
765
+ },
766
+ {
767
+ "prop": "y",
768
+ "keys": [
769
+ [
770
+ 0.33,
771
+ 6
772
+ ],
773
+ [
774
+ 0.4,
775
+ 0
776
+ ],
777
+ [
778
+ 0.6,
779
+ 0
780
+ ],
781
+ [
782
+ 0.67,
783
+ -6
784
+ ]
785
+ ]
786
+ }
787
+ ],
788
+ "c": [
789
+ {
790
+ "t": "Heading",
791
+ "es": "h2",
792
+ "level": "h2",
793
+ "slot": "title2",
794
+ "max": 52,
795
+ "ph": "copy",
796
+ "text": "Then we make it"
797
+ },
798
+ {
799
+ "t": "Text",
800
+ "es": "lead",
801
+ "slot": "text2",
802
+ "max": 180,
803
+ "ph": "copy",
804
+ "text": "What the middle of the work is, in a sentence or two."
805
+ }
806
+ ]
807
+ },
808
+ {
809
+ "t": "Container",
810
+ "es": "box",
811
+ "lay": {
812
+ "display": "flex",
813
+ "flexDirection": "column",
814
+ "gap": "12px",
815
+ "position": "absolute",
816
+ "left": "32px",
817
+ "right": "32px",
818
+ "bottom": "56px",
819
+ "maxWidth": "640px"
820
+ },
821
+ "tracks": [
822
+ {
823
+ "prop": "opacity",
824
+ "keys": [
825
+ [
826
+ 0.67,
827
+ 0
828
+ ],
829
+ [
830
+ 0.74,
831
+ 1
832
+ ],
833
+ [
834
+ 1,
835
+ 1
836
+ ]
837
+ ]
838
+ },
839
+ {
840
+ "prop": "y",
841
+ "keys": [
842
+ [
843
+ 0.67,
844
+ 6
845
+ ],
846
+ [
847
+ 0.74,
848
+ 0
849
+ ]
850
+ ]
851
+ }
852
+ ],
853
+ "c": [
854
+ {
855
+ "t": "Heading",
856
+ "es": "h2",
857
+ "level": "h2",
858
+ "slot": "title3",
859
+ "max": 52,
860
+ "ph": "copy",
861
+ "text": "And it is yours"
862
+ },
863
+ {
864
+ "t": "Text",
865
+ "es": "lead",
866
+ "slot": "text3",
867
+ "max": 180,
868
+ "ph": "copy",
869
+ "text": "What the client leaves with, in a sentence or two."
870
+ }
871
+ ]
872
+ }
873
+ ]
874
+ }
875
+ ]
876
+ }
877
+ ]
878
+ }
879
+ },
880
+ "proof.scene-words": {
881
+ "family": "proof",
882
+ "scene": true,
883
+ "label": "A testimonial that lights word by word",
884
+ "intent": "One client's words, lit one word at a time while the page holds still — only once the studio has a real quote worth slowing down for.",
885
+ "node": {
886
+ "t": "Section",
887
+ "es": "section",
888
+ "role": "scene",
889
+ "lay": {
890
+ "position": "relative",
891
+ "display": "block",
892
+ "width": "100%",
893
+ "height": "250vh",
894
+ "padding": "0px"
895
+ },
896
+ "c": [
897
+ {
898
+ "t": "Container",
899
+ "role": "scene.stage",
900
+ "lay": {
901
+ "position": "sticky",
902
+ "top": "0px",
903
+ "display": "flex",
904
+ "width": "100%",
905
+ "height": "100svh",
906
+ "overflow": "hidden",
907
+ "padding": "32px 32px 32px 32px",
908
+ "alignItems": "center",
909
+ "justifyContent": "center"
910
+ },
911
+ "c": [
912
+ {
913
+ "t": "Container",
914
+ "es": "box",
915
+ "lay": {
916
+ "display": "flex",
917
+ "flexDirection": "column",
918
+ "gap": "20px",
919
+ "maxWidth": "900px",
920
+ "width": "100%",
921
+ "margin": "0px auto 0px auto"
922
+ },
923
+ "c": [
924
+ {
925
+ "t": "Text",
926
+ "es": "eyebrow",
927
+ "slot": "eyebrow",
928
+ "max": 32,
929
+ "ph": "copy",
930
+ "text": "What clients say"
931
+ },
932
+ {
933
+ "t": "Heading",
934
+ "es": "h2",
935
+ "level": "h2",
936
+ "ph": "copy",
937
+ "text": "A sentence a client actually said, in their own words.",
938
+ "tracks": [
939
+ {
940
+ "prop": "words",
941
+ "keys": [
942
+ [
943
+ 0.08,
944
+ 0
945
+ ],
946
+ [
947
+ 0.7,
948
+ 100
949
+ ]
950
+ ]
951
+ }
952
+ ]
953
+ },
954
+ {
955
+ "t": "Text",
956
+ "es": "small",
957
+ "ph": "copy",
958
+ "text": "[Client name] · [Project, city]",
959
+ "tracks": [
960
+ {
961
+ "prop": "opacity",
962
+ "keys": [
963
+ [
964
+ 0.7,
965
+ 0
966
+ ],
967
+ [
968
+ 0.82,
969
+ 1
970
+ ]
971
+ ]
972
+ }
973
+ ]
974
+ }
975
+ ]
976
+ }
977
+ ]
978
+ }
979
+ ]
980
+ }
981
+ },
420
982
  "nav.inline-cta": {
421
983
  "family": "nav",
422
984
  "label": "Navigation bar",
@@ -4924,6 +5486,11 @@ export const SPEC_SITE_LIBRARY = [
4924
5486
  "family": "hero",
4925
5487
  "composed": true
4926
5488
  },
5489
+ {
5490
+ "id": "hero.scene-depth",
5491
+ "family": "hero",
5492
+ "composed": true
5493
+ },
4927
5494
  {
4928
5495
  "id": "proof.logo-strip",
4929
5496
  "family": "proof",
@@ -4944,6 +5511,11 @@ export const SPEC_SITE_LIBRARY = [
4944
5511
  "family": "proof",
4945
5512
  "composed": true
4946
5513
  },
5514
+ {
5515
+ "id": "proof.scene-words",
5516
+ "family": "proof",
5517
+ "composed": true
5518
+ },
4947
5519
  {
4948
5520
  "id": "feature.triad",
4949
5521
  "family": "feature",
@@ -4999,6 +5571,11 @@ export const SPEC_SITE_LIBRARY = [
4999
5571
  "family": "story",
5000
5572
  "composed": true
5001
5573
  },
5574
+ {
5575
+ "id": "story.scene-chapters",
5576
+ "family": "story",
5577
+ "composed": true
5578
+ },
5002
5579
  {
5003
5580
  "id": "cta.banner",
5004
5581
  "family": "cta",
@@ -4,8 +4,7 @@ import { SEO_DESCRIPTION_MAX, SEO_TITLE_MAX } from '@vectoriox/iox-ui/seo-core';
4
4
 
5
5
  import {
6
6
  BindScope, patternById, patternHasForm, patternLinkSlots, patternNeedsBinding, patternOptionalParts, patternRoleNeeds, patternTakesBinding, patternScopes,
7
- patternSlots, PatternGlobalPosition, slotBudget,
8
- } from './patterns';
7
+ patternSlots, PatternGlobalPosition, slotBudget, SCENE_LEVEL, SCENE_OTHER_PAGES } from './patterns';
9
8
  import { ContentFieldControl, FIELD_TYPE_OF_CONTROL,
10
9
  FACT_KEYS, GlobalSlot, PAGE_ARCHETYPES, SITE_NEED_IDS, PlanDataSourceMode, PlanDataSourceType, RelativeOp, REQUIRED_CONTENT_ROLES, SitePlan, SitePlanSourceKind,
11
10
  SITE_LOCALES, SITE_PLAN_VERSION,
@@ -70,6 +69,9 @@ export enum SitePlanIssueCode {
70
69
  UnknownNeed = 'unknown-need',
71
70
  /** A page key that is a global slot's name (`header`, `footer`): its sections' ids would be the global's. */
72
71
  ReservedPageKey = 'reserved-page-key',
72
+ /** SC6: a Scene below How animated Expressive, a second Scene on a page, or Scenes on more than the home page and
73
+ * one other — the pages answer by choosing another pattern. */
74
+ SceneBudget = 'scene-budget',
73
75
  }
74
76
 
75
77
  export type SitePlanIssue = PathIssue<SitePlanIssueCode>;
@@ -455,6 +457,25 @@ function validatePages(c: Collector, list: unknown[], types: Map<string, Content
455
457
  }
456
458
 
457
459
 
460
+ /** SC6 (premium-sites plan, APPROVED 2026-11-05): who may hold the page — Scenes only at Expressive, one per page, on the
461
+ * home page and at most one other. A Scene past that is an issue at its pattern, so the pages choose another. */
462
+ function validateScenes(c: Collector, pages: unknown[], animate: string | null): void {
463
+ let otherPages = 0;
464
+ pages.forEach((raw, i) => {
465
+ if (!isObject(raw) || !Array.isArray(raw['sections'])) return;
466
+ let onPage = 0;
467
+ const counted = raw['isHome'] === true;
468
+ raw['sections'].forEach((s, j) => {
469
+ const pattern = isObject(s) && typeof s['pattern'] === 'string' ? patternById(s['pattern']) : null;
470
+ if (!pattern?.scene) return;
471
+ const at = `pages[${i}].sections[${j}].pattern`;
472
+ if (animate !== SCENE_LEVEL) return c.add(SitePlanIssueCode.SceneBudget, at, `"${s['pattern']}" holds the page as it scrolls — only on a site whose How animated is Expressive (this one is ${animate ?? 'unset'})`);
473
+ if (++onPage > 1) return c.add(SitePlanIssueCode.SceneBudget, at, 'one Scene per page — choose a section that does not hold the page');
474
+ if (!counted && onPage === 1 && ++otherPages > SCENE_OTHER_PAGES) c.add(SitePlanIssueCode.SceneBudget, at, `Scenes on the home page and at most ${SCENE_OTHER_PAGES} other page — choose a section that does not hold the page`);
475
+ });
476
+ });
477
+ }
478
+
458
479
  function validatePageSeo(c: Collector, seo: unknown, path: string): void {
459
480
  if (seo === undefined) return;
460
481
  if (!isObject(seo)) return c.add(SitePlanIssueCode.Shape, path, 'must be an object');
@@ -501,6 +522,7 @@ export function validateSitePlan(plan: unknown): SitePlanValidation {
501
522
  const design = isObject(plan['design']) ? plan['design'] : {};
502
523
  validatePages(c, c.array(plan, 'pages', ''), types, sources, typeof design['density'] === 'string' ? design['density'] : null);
503
524
  validateGlobals(c, plan['globals']);
525
+ validateScenes(c, c.array(plan, 'pages', ''), typeof design['animate'] === 'string' ? design['animate'] : null);
504
526
  return { valid: c.issues.length === 0, issues: c.issues };
505
527
  }
506
528
 
@@ -111,6 +111,9 @@ export interface BusinessType {
111
111
  readonly motion: MotionId;
112
112
  /** How its site usually scrolls, how animated it is and what plays between pages (rev 3.1). */
113
113
  readonly moves: Readonly<DesignMotion>;
114
+ /** SC6: the most How animated a GENERATED site of this type may start at (`motionFor`). A Scene needs Expressive,
115
+ * so a type capped below it never gets one. Binds generated output only — the owner may choose any level. */
116
+ readonly ceiling: DesignMotion['animate'];
114
117
  readonly imagery: string;
115
118
  readonly pages: ReadonlyArray<string>;
116
119
  /** The needs its page set implies, in page order (derived from `pages` at contract build). */