@vectoriox/iox-ui 5.37.0 → 5.39.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 (45) hide show
  1. package/actions-core/index.d.ts +6 -1
  2. package/composite-core/index.d.ts +684 -28
  3. package/design-core/index.d.ts +452 -6
  4. package/fesm2022/vectoriox-iox-ui-actions-core.mjs +12 -4
  5. package/fesm2022/vectoriox-iox-ui-actions-core.mjs.map +1 -1
  6. package/fesm2022/vectoriox-iox-ui-composite-core.mjs +1761 -143
  7. package/fesm2022/vectoriox-iox-ui-composite-core.mjs.map +1 -1
  8. package/fesm2022/vectoriox-iox-ui-design-core.mjs +555 -6
  9. package/fesm2022/vectoriox-iox-ui-design-core.mjs.map +1 -1
  10. package/fesm2022/vectoriox-iox-ui-generation-core.mjs +534 -12
  11. package/fesm2022/vectoriox-iox-ui-generation-core.mjs.map +1 -1
  12. package/fesm2022/vectoriox-iox-ui-media-core.mjs +571 -0
  13. package/fesm2022/vectoriox-iox-ui-media-core.mjs.map +1 -0
  14. package/fesm2022/vectoriox-iox-ui.mjs +494 -127
  15. package/fesm2022/vectoriox-iox-ui.mjs.map +1 -1
  16. package/generation-core/index.d.ts +210 -4
  17. package/index.d.ts +217 -30
  18. package/media-core/index.d.ts +277 -0
  19. package/media-core/package.json +3 -0
  20. package/package.json +5 -1
  21. package/src/actions-core/element-actions.ts +12 -3
  22. package/src/composite-core/accordion-runtime.ts +3 -15
  23. package/src/composite-core/accordion.ts +4 -2
  24. package/src/composite-core/composite-kinds.ts +121 -7
  25. package/src/composite-core/composite-node.ts +4 -0
  26. package/src/composite-core/composite-runtime.ts +68 -0
  27. package/src/composite-core/exclusive-runtime.ts +358 -0
  28. package/src/composite-core/lightbox.ts +192 -0
  29. package/src/composite-core/public-api.ts +8 -2
  30. package/src/composite-core/slider-runtime.ts +180 -0
  31. package/src/composite-core/slider.ts +378 -0
  32. package/src/composite-core/structure.ts +38 -5
  33. package/src/composite-core/tabs.ts +628 -0
  34. package/src/design-core/contrast-audit.ts +5 -0
  35. package/src/design-core/element-styles.ts +34 -3
  36. package/src/design-core/flavor-spec.generated.ts +517 -2
  37. package/src/generation-core/baseline-roles.ts +2 -0
  38. package/src/generation-core/compose-site.ts +147 -4
  39. package/src/generation-core/node-baselines.generated.ts +156 -6
  40. package/src/generation-core/patterns.ts +20 -0
  41. package/src/generation-core/site-spec.generated.ts +210 -0
  42. package/src/media-core/public-api.ts +9 -0
  43. package/src/media-core/video-runtime.ts +239 -0
  44. package/src/media-core/video.ts +443 -0
  45. package/styles/iox-design.css +112 -0
@@ -2,12 +2,14 @@ import { DesignChoices, LinkType, resolveDesignChoices } from '@vectoriox/iox-ui
2
2
  import { FormPresetDef, FormPresetField, formPresetById } from '@vectoriox/iox-ui/forms-core';
3
3
  import {
4
4
  ACCORDION_BIND_PROP, ACCORDION_CONTENT_LIMIT, ACCORDION_WORDS, accordionAnswerOf, accordionItems, accordionQuestionOf, AccordionRole,
5
- buildAccordionRecipe, buildCardRecipe, CardLayout, CardPart, CardRole, CARD_LAYOUTS, CARD_PARTS, CompositeNode, CompositeNodeSpec, findByRole,
6
- pickAccordionAutoBinds,
5
+ buildAccordionRecipe, buildCardRecipe, buildCarouselRecipe, buildLightboxRecipe, CardLayout, CardPart, CardRole, CARD_LAYOUTS, CARD_PARTS, CompositeNode,
6
+ CompositeNodeSpec, findByRole, LIGHTBOX_LABEL, pickAccordionAutoBinds, pickTabsAutoBinds, SLIDE_TO_ROW, SLIDE_TYPE, buildTabsRecipe, tabLabelNodeOf,
7
+ tabPanelImageOf, tabPanelTextOf, tabsListOf, tabsPanels, tabsPanelsOf, tabsTriggers, TABS_CONTENT_LIMIT, TABS_IMAGE_BIND_PROP, TABS_LABEL_BIND_PROP,
8
+ TABS_TEXT_BIND_PROP, TABS_WORDS,
7
9
  } from '@vectoriox/iox-ui/composite-core';
8
10
 
9
11
  import { NODE_BASELINES } from './node-baselines.generated';
10
- import { BindScope, CardBindTarget, PatternDirective, PatternNode, PatternPlaceholder, PatternRecipe, SitePattern, patternById } from './patterns';
12
+ import { BindScope, CardBindTarget, PatternDirective, PatternNode, PatternOpens, PatternPlaceholder, PatternRecipe, SitePattern, patternById } from './patterns';
11
13
  import { assertStyleSlots, ComposeError, createSerializedNode, SerializedBinding, SerializedNode, setStyleProps } from './serialized-node';
12
14
  import {
13
15
  ContentTypePlan, DataSourcePlan, FactKey, FACT_KEYS, GlobalSlot, PagePlan, PageSectionPlan, PlanDataSourceMode, PlanDataSourceType,
@@ -37,6 +39,8 @@ export enum PlaceholderKind {
37
39
  Copy = 'copy',
38
40
  /** An image slot with no image: the empty-image placeholder (DS3). */
39
41
  Image = 'image',
42
+ /** CC3: a video slot with no clip — the studio uploads its own (the poster blank meanwhile). */
43
+ Video = 'video',
40
44
  }
41
45
 
42
46
  export interface ComposedPlaceholder {
@@ -174,6 +178,10 @@ const BUTTON_BLOCK = 'ButtonBlock';
174
178
  const FORM = 'Form';
175
179
  const FIELD = 'Field';
176
180
  const IMAGE = 'Image';
181
+ const VIDEO = 'Video';
182
+ /** A Video's source prop (media-core `VideoTrait.Source` — not `source`, which names a data source). */
183
+ const VIDEO_SOURCE_PROP = 'videoSource';
184
+ const DIALOG = 'Dialog';
177
185
  const SUBMIT_BUTTON = 'SubmitButton';
178
186
  const FORM_SUCCESS = 'FormSuccess';
179
187
  const FORM_ERROR = 'FormError';
@@ -231,6 +239,8 @@ interface Ctx {
231
239
  placeholders: ComposedPlaceholder[];
232
240
  issues: ComposeIssue[];
233
241
  routes: ReadonlySet<string>;
242
+ /** CC3: nodes whose click opens the pattern's lightbox (`opens`), wired once the pattern is stamped. */
243
+ openers?: string[];
234
244
  }
235
245
 
236
246
  function nonEmpty(v: unknown): v is string {
@@ -480,6 +490,22 @@ function composeNode(pn: PatternNode, id: string, repeater: RepeaterScope | null
480
490
  }
481
491
  }
482
492
 
493
+ // CC3: a Video in background mode is an uploaded file's; its clip is the studio's own (a placeholder until then).
494
+ if (pn.video) {
495
+ if (type !== VIDEO) throw new ComposeError(`${id}: "video" belongs on a Video`);
496
+ setProp(node, VIDEO_SOURCE_PROP, 'file');
497
+ setProp(node, 'mode', pn.video);
498
+ }
499
+ if (pn.ph === PatternPlaceholder.Video) {
500
+ setProp(node, 'src', '');
501
+ ctx.placeholders.push({ nodeId: id, owner: ctx.owner, kind: PlaceholderKind.Video, text: '' });
502
+ }
503
+ if (pn.name) setProp(node, 'ariaLabel', pn.name);
504
+ if (pn.opens) {
505
+ if (pn.opens !== PatternOpens.Lightbox) throw new ComposeError(`${id}: "opens" names the pattern's lightbox`);
506
+ (ctx.openers ??= []).push(id);
507
+ }
508
+
483
509
  // Images: an empty slot, or a bound one — both fall back to the DS3 empty image, never a stock URL.
484
510
  if (pn.ph === PatternPlaceholder.Image) {
485
511
  setProp(node, 'src', '');
@@ -562,6 +588,9 @@ const CARD_BINDS: Readonly<Record<CardBindTarget, { role: CardRole; prop: string
562
588
  */
563
589
  function composeRecipe(pn: PatternNode, id: string, repeater: RepeaterScope | null, ctx: Ctx): SerializedNode {
564
590
  if (pn.recipe === PatternRecipe.Accordion) return composeAccordion(pn, id, ctx);
591
+ if (pn.recipe === PatternRecipe.Tabs) return composeTabs(pn, id, ctx);
592
+ if (pn.recipe === PatternRecipe.Carousel) return composeCarousel(pn, id, ctx);
593
+ if (pn.recipe === PatternRecipe.Lightbox) return composeLightbox(pn, id, repeater, ctx);
565
594
  if (pn.recipe !== PatternRecipe.Card) throw new ComposeError(`${id}: no recipe "${pn.recipe}" in composite-core`);
566
595
  const layout = (pn.layout ?? CardLayout.Top) as CardLayout;
567
596
  if (!CARD_LAYOUTS.includes(layout)) throw new ComposeError(`${id}: a card has no layout "${pn.layout}"`);
@@ -641,10 +670,124 @@ function composeAccordion(pn: PatternNode, id: string, ctx: Ctx): SerializedNode
641
670
  return root;
642
671
  }
643
672
 
673
+ /** The content type a section binding lists, through its data source. */
674
+ function boundContentType(binding: string, ctx: Ctx): ContentTypePlan | undefined {
675
+ const source = ctx.plan.dataSources.find(d => d.alias === binding);
676
+ return source && 'contentType' in source ? ctx.plan.contentTypes.find(t => t.name === source.contentType) : undefined;
677
+ }
678
+
679
+ const TABS_ROOT_KEY = 'tabs';
680
+ const TABS_HOLDER_KEYS: ReadonlySet<string> = new Set(['list', 'panels']);
681
+
682
+ /**
683
+ * Tabs (CC4a) — the composite-core recipe stamped with this composer's factory. Manual (no section binding): three
684
+ * tabs whose words are placeholders the studio rewrites (`ph: 'copy'`) and whose pictures it supplies — never invented.
685
+ * From content (the section names a list `binding`, R3's AI path): the tab list and the panels holder are BOTH
686
+ * Repeaters over that list, each holding its one template (one row → its tab AND its panel, paired by the row's id at
687
+ * render), the tab's words, the panel's words and its picture bound to the type's first short-text, long-text and image
688
+ * fields (`pickTabsAutoBinds`, the builder switch's rule), limit 6. The tab list is named by the pattern's `name`.
689
+ */
690
+ function composeTabs(pn: PatternNode, id: string, ctx: Ctx): SerializedNode {
691
+ const binding = pn.src === PatternDirective.OptionalBinding || pn.src === PatternDirective.Binding ? ctx.section?.binding ?? null : null;
692
+ if (pn.src === PatternDirective.Binding && !binding) throw new ComposeError(`${id}: "$binding" with no section binding`);
693
+ const factory = (spec: CompositeNodeSpec): CompositeNode => {
694
+ const type = binding && TABS_HOLDER_KEYS.has(spec.key) ? REPEATER : spec.type;
695
+ return create(spec.key === TABS_ROOT_KEY ? id : `${id}-${spec.key}`, type, { ...(spec.props ?? {}) }, spec.elementStyle ?? null) as CompositeNode;
696
+ };
697
+ const root = buildTabsRecipe(factory, { tabs: binding ? TABS_WORDS.slice(0, 1) : undefined, label: pn.name }) as SerializedNode;
698
+ const tree = root as CompositeNode;
699
+ if (!binding) {
700
+ if (pn.ph === PatternPlaceholder.Copy) {
701
+ for (const tab of tabsTriggers(tree)) {
702
+ const words = tabLabelNodeOf(tab);
703
+ if (words) ctx.placeholders.push({ nodeId: words.id!, owner: ctx.owner, kind: PlaceholderKind.Copy, text: String(words.props?.[TABS_LABEL_BIND_PROP] ?? '') });
704
+ }
705
+ for (const panel of tabsPanels(tree)) {
706
+ const text = tabPanelTextOf(panel);
707
+ if (text) ctx.placeholders.push({ nodeId: text.id!, owner: ctx.owner, kind: PlaceholderKind.Copy, text: String(text.props?.[TABS_TEXT_BIND_PROP] ?? '') });
708
+ const image = tabPanelImageOf(panel);
709
+ if (image) ctx.placeholders.push({ nodeId: image.id!, owner: ctx.owner, kind: PlaceholderKind.Image, text: '' });
710
+ }
711
+ }
712
+ return root;
713
+ }
714
+ const type = boundContentType(binding, ctx);
715
+ const picks = pickTabsAutoBinds((type?.fields ?? []).map(f => ({ name: f.name, type: f.type, controller: f.controller })));
716
+ if (!picks.label || !picks.text) {
717
+ throw new ComposeError(`${id}: "${binding}" has no short-text field for the tab and long-text field for its panel`);
718
+ }
719
+ for (const holder of [tabsListOf(tree), tabsPanelsOf(tree)] as SerializedNode[]) {
720
+ addBinding(holder, { prop: 'source', source: binding, path: '' });
721
+ setProp(holder, 'limit', TABS_CONTENT_LIMIT);
722
+ }
723
+ const [tab] = tabsTriggers(tree);
724
+ const [panel] = tabsPanels(tree);
725
+ delete tab.itemKey;
726
+ delete panel.itemKey;
727
+ addBinding(tabLabelNodeOf(tab) as SerializedNode, { prop: TABS_LABEL_BIND_PROP, source: binding, path: picks.label });
728
+ addBinding(tabPanelTextOf(panel) as SerializedNode, { prop: TABS_TEXT_BIND_PROP, source: binding, path: picks.text });
729
+ const image = tabPanelImageOf(panel) as SerializedNode | undefined;
730
+ if (image && picks.image) {
731
+ addBinding(image, { prop: TABS_IMAGE_BIND_PROP, source: binding, path: picks.image });
732
+ addBinding(image, { prop: 'alt', source: binding, path: picks.label });
733
+ }
734
+ return root;
735
+ }
736
+
737
+ /** A recipe's node factory for this composer: ids derive from the recipe's keys (`<id>-<key>`, the root `<id>`). */
738
+ function recipeFactory(id: string, rootKey: string): (spec: CompositeNodeSpec) => CompositeNode {
739
+ return spec => create(spec.key === rootKey ? id : `${id}-${spec.key}`, spec.type, { ...(spec.props ?? {}) }, spec.elementStyle ?? null) as CompositeNode;
740
+ }
741
+
742
+ /**
743
+ * CC3: the testimonial carousel — composite-core's recipe (the Slider with its control parts), stamped with this
744
+ * composer's factory. Every quote and every "client · project" line is a placeholder the studio rewrites (`ph: copy`):
745
+ * testimonials are facts, never invented.
746
+ */
747
+ function composeCarousel(pn: PatternNode, id: string, ctx: Ctx): SerializedNode {
748
+ const slider = buildCarouselRecipe(recipeFactory(id, 'carousel')) as SerializedNode;
749
+ if (pn.ph === PatternPlaceholder.Copy) {
750
+ for (const slide of (slider.children ?? []).filter(c => c.type === SLIDE_TYPE)) {
751
+ for (const text of slide.children ?? []) ctx.placeholders.push({ nodeId: text.id, owner: ctx.owner, kind: PlaceholderKind.Copy, text: String(text.props?.['content'] ?? '') });
752
+ }
753
+ }
754
+ return slider;
755
+ }
756
+
757
+ /**
758
+ * CC3: the Lightbox — a Dialog holding a Slider bound to the node's `src` (the gallery's own source): one template
759
+ * slide whose image reads the row (a bare URL row: decorative alt), its arrows hidden on a phone.
760
+ */
761
+ function composeLightbox(pn: PatternNode, id: string, repeater: RepeaterScope | null, ctx: Ctx): SerializedNode {
762
+ if (!pn.src) throw new ComposeError(`${id}: a lightbox needs the gallery's source (src)`);
763
+ const scoped = resolveScoped(pn.src, repeater, ctx, id);
764
+ const sourcePath = scoped.path === ITEM_VALUE_PATH ? '' : scoped.path;
765
+ return buildLightboxRecipe(recipeFactory(id, 'lightbox'), { source: scoped.source, sourcePath, imageField: 'item' }) as SerializedNode;
766
+ }
767
+
768
+ /** CC3: every `opens: lightbox` node of a stamped pattern opens the pattern's lightbox at its own row. */
769
+ function wireOpeners(root: SerializedNode, openers: ReadonlyArray<string>): void {
770
+ if (!openers.length) return;
771
+ let lightbox: SerializedNode | null = null;
772
+ const find = (n: SerializedNode) => { if (!lightbox && n.type === DIALOG && n.label === LIGHTBOX_LABEL) lightbox = n; (n.children ?? []).forEach(find); };
773
+ find(root);
774
+ if (!lightbox) throw new ComposeError(`${root.id}: "opens: lightbox" with no lightbox in the pattern`);
775
+ const target = (lightbox as SerializedNode).id;
776
+ const visit = (n: SerializedNode) => {
777
+ if (openers.includes(n.id)) {
778
+ n.interactions = [{ id: `${n.id}-opens`, trigger: 'click', actions: [{ type: 'openDialog', target, params: { slideTo: SLIDE_TO_ROW }, duration: 0, delay: 0, easing: 'ease' }] }];
779
+ }
780
+ (n.children ?? []).forEach(visit);
781
+ };
782
+ visit(root);
783
+ }
784
+
644
785
  function composePattern(pattern: SitePattern, id: string, ctx: Ctx): SerializedNode {
645
786
  if (pattern.node.each) throw new ComposeError(`${id}: a pattern's root cannot be stamped per page`);
646
- const node = composeNode(pattern.node, id, null, ctx);
787
+ const own: Ctx = { ...ctx, openers: [] };
788
+ const node = composeNode(pattern.node, id, null, own);
647
789
  if (!node) throw new ComposeError(`${id}: a pattern's root cannot be optional or a link`);
790
+ wireOpeners(node, own.openers ?? []);
648
791
  node.label = pattern.label;
649
792
  return node;
650
793
  }
@@ -667,7 +667,10 @@ export const NODE_BASELINES: Readonly<Record<string, NodeBaseline>> = {
667
667
  "htmlTag",
668
668
  "colorScheme",
669
669
  "multiple",
670
- "defaultOn"
670
+ "defaultOn",
671
+ "activateOn",
672
+ "orientation",
673
+ "ariaLabel"
671
674
  ],
672
675
  "styleTraits": [
673
676
  "position",
@@ -2900,7 +2903,10 @@ export const NODE_BASELINES: Readonly<Record<string, NodeBaseline>> = {
2900
2903
  "source",
2901
2904
  "limit",
2902
2905
  "multiple",
2903
- "defaultOn"
2906
+ "defaultOn",
2907
+ "activateOn",
2908
+ "orientation",
2909
+ "ariaLabel"
2904
2910
  ],
2905
2911
  "styleTraits": [
2906
2912
  "position",
@@ -3032,7 +3038,8 @@ export const NODE_BASELINES: Readonly<Record<string, NodeBaseline>> = {
3032
3038
  "enableParallax": false,
3033
3039
  "enableGrid": false,
3034
3040
  "gridRows": 2,
3035
- "breakpoints": ""
3041
+ "breakpoints": "",
3042
+ "ariaLabel": ""
3036
3043
  },
3037
3044
  "styleProps": {
3038
3045
  "position": "static",
@@ -3142,7 +3149,9 @@ export const NODE_BASELINES: Readonly<Record<string, NodeBaseline>> = {
3142
3149
  "enableGrid",
3143
3150
  "gridRows",
3144
3151
  "__responsiveLabel",
3145
- "breakpoints"
3152
+ "breakpoints",
3153
+ "__accessibilityLabel",
3154
+ "ariaLabel"
3146
3155
  ],
3147
3156
  "styleTraits": [
3148
3157
  "position",
@@ -4660,7 +4669,8 @@ export const NODE_BASELINES: Readonly<Record<string, NodeBaseline>> = {
4660
4669
  "showCloseButton": true,
4661
4670
  "consentRole": false,
4662
4671
  "closeAbove": "",
4663
- "ariaLabel": ""
4672
+ "ariaLabel": "",
4673
+ "colorScheme": "default"
4664
4674
  },
4665
4675
  "styleProps": {
4666
4676
  "position": "static",
@@ -4753,7 +4763,8 @@ export const NODE_BASELINES: Readonly<Record<string, NodeBaseline>> = {
4753
4763
  "__responsiveLabel",
4754
4764
  "closeAbove",
4755
4765
  "__accessibilityLabel",
4756
- "ariaLabel"
4766
+ "ariaLabel",
4767
+ "colorScheme"
4757
4768
  ],
4758
4769
  "styleTraits": [
4759
4770
  "position",
@@ -4847,6 +4858,145 @@ export const NODE_BASELINES: Readonly<Record<string, NodeBaseline>> = {
4847
4858
  "transitionTimingFunction"
4848
4859
  ],
4849
4860
  "container": true
4861
+ },
4862
+ "Video": {
4863
+ "props": {
4864
+ "videoSource": "youtube",
4865
+ "src": "",
4866
+ "poster": "",
4867
+ "mode": "inline",
4868
+ "autoplay": false,
4869
+ "title": "",
4870
+ "captions": "",
4871
+ "captionsLang": "en",
4872
+ "consentCategory": "marketing"
4873
+ },
4874
+ "styleProps": {
4875
+ "position": "static",
4876
+ "top": "auto",
4877
+ "right": "auto",
4878
+ "bottom": "auto",
4879
+ "left": "auto",
4880
+ "zIndex": "0",
4881
+ "width": "100%",
4882
+ "height": "auto",
4883
+ "minWidth": "auto",
4884
+ "minHeight": "auto",
4885
+ "maxWidth": "auto",
4886
+ "maxHeight": "auto",
4887
+ "aspectRatio": "16 / 9",
4888
+ "objectFit": "cover",
4889
+ "overflow": "visible",
4890
+ "margin": "0px 0px 0px 0px",
4891
+ "padding": "0px 0px 0px 0px",
4892
+ "borderWidth": "0px",
4893
+ "borderStyle": "solid",
4894
+ "borderColor": "#CCCCCC",
4895
+ "borderRadius": "0px",
4896
+ "opacity": "1",
4897
+ "mixBlendMode": "normal",
4898
+ "cursor": "auto",
4899
+ "boxShadow": "",
4900
+ "textShadow": "",
4901
+ "translateX": "0px",
4902
+ "translateY": "0px",
4903
+ "scaleX": "1",
4904
+ "scaleY": "1",
4905
+ "rotate": "0deg",
4906
+ "skewX": "0deg",
4907
+ "skewY": "0deg",
4908
+ "filterBlur": "0px",
4909
+ "filterHueRotate": "0deg",
4910
+ "filterBrightness": "1",
4911
+ "filterContrast": "1",
4912
+ "filterSaturate": "1",
4913
+ "filterGrayscale": "0",
4914
+ "filterSepia": "0",
4915
+ "filterInvert": "0",
4916
+ "backdropFilter": "none",
4917
+ "backdropBlur": "0px",
4918
+ "backdropBrightness": "1",
4919
+ "backdropContrast": "1",
4920
+ "backdropSaturate": "1",
4921
+ "clipPath": "none",
4922
+ "transitionDuration": "0ms",
4923
+ "transitionDelay": "0ms",
4924
+ "transitionTimingFunction": "ease"
4925
+ },
4926
+ "traits": [
4927
+ "__mediaLabel",
4928
+ "videoSource",
4929
+ "src",
4930
+ "poster",
4931
+ "__behaviourLabel",
4932
+ "mode",
4933
+ "autoplay",
4934
+ "__accessibilityLabel",
4935
+ "title",
4936
+ "captions",
4937
+ "captionsLang",
4938
+ "__privacyLabel",
4939
+ "consentCategory"
4940
+ ],
4941
+ "styleTraits": [
4942
+ "position",
4943
+ "top",
4944
+ "right",
4945
+ "bottom",
4946
+ "left",
4947
+ "zIndex",
4948
+ "width",
4949
+ "height",
4950
+ "minWidth",
4951
+ "minHeight",
4952
+ "maxWidth",
4953
+ "maxHeight",
4954
+ "aspectRatio",
4955
+ "objectFit",
4956
+ "overflow",
4957
+ "margin",
4958
+ "padding",
4959
+ "borderWidth",
4960
+ "borderStyle",
4961
+ "borderColor",
4962
+ "borderRadius",
4963
+ "opacity",
4964
+ "mixBlendMode",
4965
+ "cursor",
4966
+ "_sep_shadow",
4967
+ "boxShadow",
4968
+ "textShadow",
4969
+ "_sep_transform",
4970
+ "translateX",
4971
+ "translateY",
4972
+ "scaleX",
4973
+ "scaleY",
4974
+ "rotate",
4975
+ "skewX",
4976
+ "skewY",
4977
+ "_sep_filter",
4978
+ "filterBlur",
4979
+ "filterHueRotate",
4980
+ "filterBrightness",
4981
+ "filterContrast",
4982
+ "filterSaturate",
4983
+ "filterGrayscale",
4984
+ "filterSepia",
4985
+ "filterInvert",
4986
+ "_sep_backdrop",
4987
+ "backdropFilter",
4988
+ "backdropBlur",
4989
+ "backdropBrightness",
4990
+ "backdropContrast",
4991
+ "backdropSaturate",
4992
+ "_sep_clip",
4993
+ "clipPath",
4994
+ "_sep_transition",
4995
+ "transitionDuration",
4996
+ "transitionDelay",
4997
+ "transitionTimingFunction"
4998
+ ],
4999
+ "container": false
4850
5000
  }
4851
5001
  };
4852
5002
 
@@ -53,6 +53,12 @@ export interface PatternNode {
53
53
  readonly layout?: string;
54
54
  readonly parts?: Readonly<Record<string, boolean>>;
55
55
  readonly binds?: Readonly<Record<string, string>>;
56
+ /** CC3: a Video's mode — `background` (an uploaded file, a muted loop). */
57
+ readonly video?: string;
58
+ /** CC3: a click on this node opens the pattern's lightbox at the clicked row (`lightbox`). */
59
+ readonly opens?: string;
60
+ /** CC3: a control's accessible name (`ariaLabel`). */
61
+ readonly name?: string;
56
62
  }
57
63
 
58
64
  /** The composite-core recipes a pattern may stamp (`recipe`). */
@@ -61,6 +67,13 @@ export enum PatternRecipe {
61
67
  /** CC2: the Accordion — Manual (three placeholder items), or From content when the section names a list
62
68
  * `binding` (`src: '$binding?'`): one template item bound to the type's question / answer fields. */
63
69
  Accordion = 'accordion',
70
+ /** CC4a: Tabs — Manual (three placeholder tabs), or From content when the section names a list `binding`: the tab
71
+ * list and the panels are both Repeaters over it, one row giving its tab AND its panel. */
72
+ Tabs = 'tabs',
73
+ /** CC3: the accessible carousel — a named Slider whose controls are parts, one quote per slide (placeholders). */
74
+ Carousel = 'carousel',
75
+ /** CC3: the Lightbox — a Dialog holding a Slider over the node's `src` (the gallery's own source). */
76
+ Lightbox = 'lightbox',
64
77
  }
65
78
 
66
79
  /** A recipe node's `binds` keys → the part each binds (Card v2). */
@@ -97,6 +110,13 @@ export const SITE_PATTERNS: Readonly<Record<string, SitePattern>> = SPEC_SITE_PA
97
110
  export enum PatternPlaceholder {
98
111
  Copy = 'copy',
99
112
  Image = 'image',
113
+ /** CC3: a clip the studio uploads (a Video's file): the poster blank until then, publish-blocking. */
114
+ Video = 'video',
115
+ }
116
+
117
+ /** CC3: what an `opens` node opens. */
118
+ export enum PatternOpens {
119
+ Lightbox = 'lightbox',
100
120
  }
101
121
 
102
122
  /** The composer directives a pattern writes where a plan supplies the value. */
@@ -978,6 +978,216 @@ export const SPEC_SITE_PATTERNS = {
978
978
  }
979
979
  ]
980
980
  }
981
+ },
982
+ "feature.tabbed": {
983
+ "family": "feature",
984
+ "label": "Features — tabs that switch",
985
+ "node": {
986
+ "t": "Section",
987
+ "es": "section",
988
+ "c": [
989
+ {
990
+ "t": "Container",
991
+ "es": "box",
992
+ "lay": {
993
+ "display": "flex",
994
+ "flexDirection": "column",
995
+ "gap": "14px",
996
+ "maxWidth": "1100px",
997
+ "width": "100%",
998
+ "margin": "0px auto 0px auto"
999
+ },
1000
+ "c": [
1001
+ {
1002
+ "t": "Text",
1003
+ "es": "eyebrow",
1004
+ "slot": "eyebrow",
1005
+ "ph": "copy",
1006
+ "text": "What we offer"
1007
+ },
1008
+ {
1009
+ "t": "Heading",
1010
+ "es": "h2",
1011
+ "level": "h2",
1012
+ "slot": "title",
1013
+ "ph": "copy",
1014
+ "text": "What we design"
1015
+ },
1016
+ {
1017
+ "t": "Container",
1018
+ "recipe": "tabs",
1019
+ "src": "$binding?",
1020
+ "ph": "copy",
1021
+ "name": "What we design"
1022
+ }
1023
+ ]
1024
+ }
1025
+ ]
1026
+ }
1027
+ },
1028
+ "hero.full-bleed-media": {
1029
+ "family": "hero",
1030
+ "label": "Hero — words over a background video",
1031
+ "node": {
1032
+ "t": "Section",
1033
+ "es": "section",
1034
+ "scheme": "inverse",
1035
+ "lay": {
1036
+ "position": "relative",
1037
+ "overflow": "hidden",
1038
+ "display": "flex",
1039
+ "flexDirection": "column",
1040
+ "justifyContent": "flex-end",
1041
+ "minHeight": "70vh"
1042
+ },
1043
+ "c": [
1044
+ {
1045
+ "t": "Video",
1046
+ "es": "media",
1047
+ "video": "background",
1048
+ "ph": "video",
1049
+ "lay": {
1050
+ "position": "absolute",
1051
+ "top": "0px",
1052
+ "left": "0px",
1053
+ "width": "100%",
1054
+ "height": "100%",
1055
+ "aspectRatio": "auto",
1056
+ "borderRadius": "0px",
1057
+ "zIndex": "0"
1058
+ }
1059
+ },
1060
+ {
1061
+ "t": "Container",
1062
+ "es": "media-scrim",
1063
+ "lay": {
1064
+ "display": "flex",
1065
+ "flexDirection": "column",
1066
+ "gap": "14px",
1067
+ "position": "relative",
1068
+ "zIndex": "1",
1069
+ "padding": "32px 32px 32px 32px",
1070
+ "maxWidth": "760px"
1071
+ },
1072
+ "c": [
1073
+ {
1074
+ "t": "Heading",
1075
+ "es": "display",
1076
+ "level": "h1",
1077
+ "slot": "title",
1078
+ "ph": "copy",
1079
+ "text": "Rooms that hold the light"
1080
+ },
1081
+ {
1082
+ "t": "Text",
1083
+ "es": "lead",
1084
+ "slot": "lead",
1085
+ "fromBusiness": "tagline",
1086
+ "ph": "copy"
1087
+ },
1088
+ {
1089
+ "t": "ButtonBlock",
1090
+ "es": "button-primary",
1091
+ "href": "/contact",
1092
+ "slot": "cta",
1093
+ "text": "Get in touch"
1094
+ }
1095
+ ]
1096
+ }
1097
+ ]
1098
+ }
1099
+ },
1100
+ "proof.testimonial-carousel": {
1101
+ "family": "proof",
1102
+ "label": "Testimonials that rotate",
1103
+ "node": {
1104
+ "t": "Section",
1105
+ "es": "section",
1106
+ "c": [
1107
+ {
1108
+ "t": "Container",
1109
+ "es": "box",
1110
+ "lay": {
1111
+ "display": "flex",
1112
+ "flexDirection": "column",
1113
+ "gap": "14px",
1114
+ "maxWidth": "860px",
1115
+ "width": "100%",
1116
+ "margin": "0px auto 0px auto"
1117
+ },
1118
+ "c": [
1119
+ {
1120
+ "t": "Text",
1121
+ "es": "eyebrow",
1122
+ "slot": "eyebrow",
1123
+ "ph": "copy",
1124
+ "text": "What clients say"
1125
+ },
1126
+ {
1127
+ "t": "SliderContainer",
1128
+ "recipe": "carousel",
1129
+ "ph": "copy"
1130
+ }
1131
+ ]
1132
+ }
1133
+ ]
1134
+ }
1135
+ },
1136
+ "collection.masonry-gallery": {
1137
+ "family": "collection",
1138
+ "label": "Gallery that opens full screen",
1139
+ "node": {
1140
+ "t": "Section",
1141
+ "es": "section",
1142
+ "c": [
1143
+ {
1144
+ "t": "Repeater",
1145
+ "src": "route.gallery",
1146
+ "lay": {
1147
+ "display": "grid",
1148
+ "gridTemplateColumns": "repeat(3, minmax(0, 1fr))",
1149
+ "gap": "var(--iox-space-gap)"
1150
+ },
1151
+ "resp": {
1152
+ "mobile": {
1153
+ "gridTemplateColumns": "repeat(2, minmax(0, 1fr))"
1154
+ }
1155
+ },
1156
+ "c": [
1157
+ {
1158
+ "t": "ButtonBlock",
1159
+ "es": "button-ghost",
1160
+ "opens": "lightbox",
1161
+ "name": "Open the image",
1162
+ "lay": {
1163
+ "display": "block",
1164
+ "width": "100%",
1165
+ "padding": "0px 0px 0px 0px"
1166
+ },
1167
+ "c": [
1168
+ {
1169
+ "t": "Image",
1170
+ "es": "media",
1171
+ "bind": "item",
1172
+ "alt": "",
1173
+ "aspect": "4 / 3"
1174
+ }
1175
+ ]
1176
+ }
1177
+ ]
1178
+ },
1179
+ {
1180
+ "t": "Dialog",
1181
+ "recipe": "lightbox",
1182
+ "src": "route.gallery"
1183
+ }
1184
+ ],
1185
+ "lay": {
1186
+ "display": "flex",
1187
+ "flexDirection": "column",
1188
+ "gap": "32px"
1189
+ }
1190
+ }
981
1191
  }
982
1192
  } as const;
983
1193
 
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @vectoriox/iox-ui/media-core — media decisions shared by the builder, both engine paths and iox-cms (CC3).
3
+ * FRAMEWORK-FREE: `planVideo` is the one place that knows a YouTube / Vimeo address, the player's host and the consent
4
+ * category a facade waits for; the runtime is DOM-only. iox-cms reads `parseVideoUrl` / `videoThumbnailRequest` to
5
+ * fetch and store an embed's poster (the visitor never asks the provider for it).
6
+ * ⚠️ Consumers wire the usual trio — ng-package raw-ts assets, jest moduleNameMapper, tsconfig paths.
7
+ */
8
+ export * from './video';
9
+ export * from './video-runtime';