@vectoriox/iox-ui 5.47.0 → 5.49.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.
@@ -1,4 +1,4 @@
1
- import { DesignChoices } from '@vectoriox/iox-ui/design-core';
1
+ import { DesignChoices, AnimateId } from '@vectoriox/iox-ui/design-core';
2
2
 
3
3
  declare const SPEC_SITE_PLAN_SHAPE: {
4
4
  readonly plan: {
@@ -2709,5 +2709,111 @@ declare function removeBarItem(globals: ReadonlyArray<SerializedNode>, itemId: s
2709
2709
  /** The bar's Items list drag-to-reorder (board 2): item `from` moves to `to`. Input not mutated. */
2710
2710
  declare function moveBarItem(globals: ReadonlyArray<SerializedNode>, from: number, to: number): BarItemsEditResult;
2711
2711
 
2712
- export { BAR_ITEMS_MAX, BAR_ITEMS_MIN, BASELINE_ROLES, BaselineRole, BindScope, CardBindTarget, ComposeError, ComposeIssueCode, DANGLING_LINK_CODES, DEFAULT_PHONE_MENU_BREAKPOINT, FACT_KEYS, GlobalSlot, NAV_PART_LABELS, NAV_SHAPE_COPY, NEUTRAL_STYLE_PROPS, NODE_BASELINES, NavPagesSource, NavRole, NavShape, NavShapeError, PRICING_PATTERN, PUBLISH_BLOCKING_PLACEHOLDERS, PathIssueCollector, PatternDirective, PatternGlobalPosition, PatternOpens, PatternPlaceholder, PatternRecipe, PhoneMenuBreakpoint, PlaceholderKind, PlanDataSourceMode, PlanDataSourceType, RelativeOp, SAMPLE_IMAGE_REF, SITE_FACT_LABELS, SITE_HEADER_PATTERN, SITE_PATTERNS, SITE_PLAN_VERSION, SITE_TEMPLATES, SPEC_SITE_FACTS, SPEC_SITE_LIBRARY, SPEC_SITE_PATTERNS, SPEC_SITE_PLAN_SHAPE, SPEC_SITE_TEMPLATES, STUDIO_TEMPLATE, SitePlanError, SitePlanIssueCode, SitePlanSourceKind, SiteTemplateError, SiteTemplateId, addBarItem, altSourceScopes, assertStyleSlots, assertValidSitePlan, barIconForRoute, blocksPublish, composeNavHeader, composeSection, composeSite, createSerializedNode, findNavRoot, isNonEmptyString, isPlainObject, moveBarItem, navPages, navShapeBreakpoint, navShapeOf, nextSourceOf, nodeBindPaths, parseSampleImageRef, pathAt, patternById, patternNeedsBinding, patternNodes, patternOptionalParts, patternScopes, patternSlots, patternTakesBinding, phoneMenuPages, planFromTemplate, planNavShape, releasedLayoutDefaults, removeBarItem, requiredShapeKeys, routeParams, sampleImageRef, setStyleProps, shapeKeys, siteNavPages, validateSitePlan, walkSerialized };
2713
- export type { AddBarItemResult, BarItemsEditResult, CmsDataSourcePlan, ComposeContext, ComposeIssue, ComposedDataSource, ComposedItem, ComposedNavHeader, ComposedPage, ComposedPlaceholder, ComposedRoute, ComposedSection, ComposedSite, ContentFieldPlan, ContentSeedPlan, ContentTypePlan, CreateNodeOptions, DataSourcePlan, FactKey, NavHeaderInput, NavHeaderOptions, NavPage, NavShapeOptions, NavShapeResult, NodeBaseline, PagePlan, PageSectionPlan, PathIssue, PatternNode, PlainObject, RelativeDataSourcePlan, SectionInput, SectionOptions, SerializedBinding, SerializedNode, SerializedResponsive, ShapeEntry, ShapeIssueCodes, SiteBusiness, SitePageRef, SitePattern, SitePlan, SitePlanIssue, SitePlanSource, SitePlanValidation, SiteTemplate, TemplatePlanInput };
2712
+ /**
2713
+ * How animated (AI plan rev 3.1 §3p) — the ONE reading of the contract's `axes.animations` (what each level
2714
+ * moves, how) and `axes.animationFamilies` (which parts a section family lets move). The pieces every stamper
2715
+ * shares: the Design step's business specimen (H4-4) today, `planEntrances` in the composer (M1-6b) next. Pure.
2716
+ *
2717
+ * A level with `content` moves a section's content together, once; otherwise it names parts (heading, items,
2718
+ * media…) and each part enters on its own, items one after another. Navigation, footers and globals are a family
2719
+ * with no parts, so nothing there ever moves.
2720
+ */
2721
+ declare enum EntrancePart {
2722
+ Content = "content",
2723
+ Heading = "heading",
2724
+ Items = "items",
2725
+ Media = "media",
2726
+ RowMedia = "rowMedia",
2727
+ HeroText = "heroText"
2728
+ }
2729
+ /** What an entrance stamp is marked with — a later restamp touches only what the look made (H4-5). */
2730
+ declare const ENTRANCE_ORIGIN_LOOK = "look";
2731
+ /** The trigger every entrance plays on. */
2732
+ declare const ENTRANCE_TRIGGER = "viewportEnter";
2733
+ /** The easing every entrance plays with (only `opacity` and `transform` move). */
2734
+ declare const ENTRANCE_EASING = "ease-out";
2735
+ /** One entrance as the contract states it. */
2736
+ interface EntranceSpec {
2737
+ readonly type: string;
2738
+ readonly duration: number;
2739
+ readonly distance?: number;
2740
+ /** Items: ms added per item, up to `cap` items. */
2741
+ readonly stagger?: number;
2742
+ readonly cap?: number;
2743
+ /** The type a right-to-left page plays instead (a side-by-side row slides in from ITS side). */
2744
+ readonly mirror?: string;
2745
+ }
2746
+ /** What one section family moves at one level. `content` set = the whole content together, and no parts. */
2747
+ interface SectionEntrances {
2748
+ readonly content: EntranceSpec | null;
2749
+ readonly parts: Readonly<Partial<Record<EntrancePart, EntranceSpec>>>;
2750
+ }
2751
+ /** The interaction action a stamp carries — the shape both interaction runtimes play. */
2752
+ interface EntranceAction {
2753
+ type: string;
2754
+ target: 'self';
2755
+ duration: number;
2756
+ delay: number;
2757
+ easing: typeof ENTRANCE_EASING;
2758
+ once: true;
2759
+ params?: {
2760
+ distance: number;
2761
+ };
2762
+ }
2763
+ interface EntranceInteraction {
2764
+ id: string;
2765
+ trigger: typeof ENTRANCE_TRIGGER;
2766
+ actions: [EntranceAction];
2767
+ origin: typeof ENTRANCE_ORIGIN_LOOK;
2768
+ }
2769
+ /** What a section of `family` moves at `level` — nothing for an unknown level or family. */
2770
+ declare function sectionEntrances(level: AnimateId | string | null | undefined, family: string): SectionEntrances;
2771
+ /** The literal delay of the `index`-th item: `stagger` ms each, the last `cap` waiting no longer than the one before. */
2772
+ declare function staggerDelay(spec: EntranceSpec, index?: number): number;
2773
+ /** The action a node plays: the spec's type (mirrored on a right-to-left page), its distance and its delay. */
2774
+ declare function entranceAction(spec: EntranceSpec, opts?: {
2775
+ index?: number;
2776
+ rtl?: boolean;
2777
+ }): EntranceAction;
2778
+ /** The stamp on a node: `viewportEnter`, once, on itself (a duplicated node needs no remap), from the look. */
2779
+ declare function entranceInteraction(nodeId: string, action: EntranceAction): EntranceInteraction;
2780
+
2781
+ /**
2782
+ * What each page of a plan shows and where its data comes from (AI plan rev 3 §3d, `describePlanData`) — the
2783
+ * Plan step's data lines, in the vocabulary the builder already uses. Pure and STRUCTURED: the screen words
2784
+ * each kind in its own language ("Lists every Project", "One Project, found by its address /work/:slug").
2785
+ */
2786
+ declare enum PageDataKind {
2787
+ /** A section repeats every item of a content type. */
2788
+ List = "list",
2789
+ /** A section repeats the newest `count` items. */
2790
+ Latest = "latest",
2791
+ /** A template page: one item, found by its address — one page per item. */
2792
+ One = "one",
2793
+ /** The next (or previous) item beside the page's own. */
2794
+ Siblings = "siblings",
2795
+ /** A form: what it sends, to the studio's inbox (+ an email when Email is connected). */
2796
+ Form = "form",
2797
+ /** Words and pictures, no data. */
2798
+ Static = "static"
2799
+ }
2800
+ interface PageDataLine {
2801
+ kind: PageDataKind;
2802
+ /** The content type's label ("Projects"), for every kind that reads one. */
2803
+ contentType?: string;
2804
+ /** Latest: how many. */
2805
+ count?: number;
2806
+ /** One: the page's own address, with its parameter. */
2807
+ route?: string;
2808
+ /** Form: the form preset (`contact`). */
2809
+ form?: string;
2810
+ }
2811
+ interface PageData {
2812
+ pageKey: string;
2813
+ lines: PageDataLine[];
2814
+ }
2815
+ /** One entry per page of the plan, in plan order. */
2816
+ declare function describePlanData(plan: SitePlan): PageData[];
2817
+
2818
+ export { BAR_ITEMS_MAX, BAR_ITEMS_MIN, BASELINE_ROLES, BaselineRole, BindScope, CardBindTarget, ComposeError, ComposeIssueCode, DANGLING_LINK_CODES, DEFAULT_PHONE_MENU_BREAKPOINT, ENTRANCE_EASING, ENTRANCE_ORIGIN_LOOK, ENTRANCE_TRIGGER, EntrancePart, FACT_KEYS, GlobalSlot, NAV_PART_LABELS, NAV_SHAPE_COPY, NEUTRAL_STYLE_PROPS, NODE_BASELINES, NavPagesSource, NavRole, NavShape, NavShapeError, PRICING_PATTERN, PUBLISH_BLOCKING_PLACEHOLDERS, PageDataKind, PathIssueCollector, PatternDirective, PatternGlobalPosition, PatternOpens, PatternPlaceholder, PatternRecipe, PhoneMenuBreakpoint, PlaceholderKind, PlanDataSourceMode, PlanDataSourceType, RelativeOp, SAMPLE_IMAGE_REF, SITE_FACT_LABELS, SITE_HEADER_PATTERN, SITE_PATTERNS, SITE_PLAN_VERSION, SITE_TEMPLATES, SPEC_SITE_FACTS, SPEC_SITE_LIBRARY, SPEC_SITE_PATTERNS, SPEC_SITE_PLAN_SHAPE, SPEC_SITE_TEMPLATES, STUDIO_TEMPLATE, SitePlanError, SitePlanIssueCode, SitePlanSourceKind, SiteTemplateError, SiteTemplateId, addBarItem, altSourceScopes, assertStyleSlots, assertValidSitePlan, barIconForRoute, blocksPublish, composeNavHeader, composeSection, composeSite, createSerializedNode, describePlanData, entranceAction, entranceInteraction, findNavRoot, isNonEmptyString, isPlainObject, moveBarItem, navPages, navShapeBreakpoint, navShapeOf, nextSourceOf, nodeBindPaths, parseSampleImageRef, pathAt, patternById, patternNeedsBinding, patternNodes, patternOptionalParts, patternScopes, patternSlots, patternTakesBinding, phoneMenuPages, planFromTemplate, planNavShape, releasedLayoutDefaults, removeBarItem, requiredShapeKeys, routeParams, sampleImageRef, sectionEntrances, setStyleProps, shapeKeys, siteNavPages, staggerDelay, validateSitePlan, walkSerialized };
2819
+ export type { AddBarItemResult, BarItemsEditResult, CmsDataSourcePlan, ComposeContext, ComposeIssue, ComposedDataSource, ComposedItem, ComposedNavHeader, ComposedPage, ComposedPlaceholder, ComposedRoute, ComposedSection, ComposedSite, ContentFieldPlan, ContentSeedPlan, ContentTypePlan, CreateNodeOptions, DataSourcePlan, EntranceAction, EntranceInteraction, EntranceSpec, FactKey, NavHeaderInput, NavHeaderOptions, NavPage, NavShapeOptions, NavShapeResult, NodeBaseline, PageData, PageDataLine, PagePlan, PageSectionPlan, PathIssue, PatternNode, PlainObject, RelativeDataSourcePlan, SectionEntrances, SectionInput, SectionOptions, SerializedBinding, SerializedNode, SerializedResponsive, ShapeEntry, ShapeIssueCodes, SiteBusiness, SitePageRef, SitePattern, SitePlan, SitePlanIssue, SitePlanSource, SitePlanValidation, SiteTemplate, TemplatePlanInput };
@@ -2279,9 +2279,21 @@ interface MotionInput {
2279
2279
  businessType?: string | null;
2280
2280
  avoid?: ReadonlyArray<string>;
2281
2281
  }
2282
+ /** Where the start came from, structured (the screen words it in its own language). */
2283
+ interface MotionBasis {
2284
+ /** The kind of business it started from (null: no type matched, the style's own). */
2285
+ type: string | null;
2286
+ /** The style made How animated calmer than the business (or the references) wanted. */
2287
+ calmerStyle: boolean;
2288
+ /** The references set How animated. */
2289
+ references: boolean;
2290
+ /** The owner asked for little movement: all three were capped. */
2291
+ littleMovement: boolean;
2292
+ }
2282
2293
  interface MotionStart extends DesignMotion {
2283
2294
  /** Why, in English — one line per step the start took. */
2284
2295
  why: string[];
2296
+ basis: MotionBasis;
2285
2297
  }
2286
2298
  /** The owner asked for little movement (`USER_AVOID_IDS`). */
2287
2299
  declare const LITTLE_MOVEMENT_AVOID = "lots-of-motion";
@@ -2345,6 +2357,13 @@ declare function oklchHue(hex: string): {
2345
2357
  chroma: number;
2346
2358
  } | null;
2347
2359
  declare function lintDesign(site: LintSite, typeId: string, types?: ReadonlyArray<BusinessType>): LintFinding[];
2360
+ /**
2361
+ * Rev 3.1: two levels above what this kind of business usually wears (Subtle → Lively) is advice, never a block.
2362
+ * Structured, so Design ▸ Motion says it in the studio's language: `usual` = the level the type wears.
2363
+ */
2364
+ declare function motionAdvice(typeId: string | null | undefined, animate: AnimateId | string | null | undefined, types?: ReadonlyArray<BusinessType>): {
2365
+ usual: AnimateId;
2366
+ } | null;
2348
2367
 
2349
2368
  /**
2350
2369
  * What the reading of a prompt returns (AI plan rev 3 §3d): one of the 30 kinds of business (and the two
@@ -2475,5 +2494,24 @@ interface PromptContextOptions {
2475
2494
  }
2476
2495
  declare function analysisPromptContext(analysis: SiteAnalysis, opts?: PromptContextOptions): PromptContext;
2477
2496
 
2478
- export { ANIMATE_ADVICE_STEPS, AnalysisIssueCode, AnalysisLocale, AvoidCheck, BUSINESS_TYPES, BUSINESS_TYPE_IDS, BookingNeed, COPY_LENGTH_ORDER, CopyLength, HARNESS_SOURCE, LITTLE_MOVEMENT_AVOID, LITTLE_MOVEMENT_CAP, LintKind, MAX_ANALYSIS_MOODS, NEEDS, NEED_IDS, NeedSource, PresetFit, RANK_WEIGHTS, READ_ANIMATE, READ_WEIGHTS, RankReason, SPEC_ANALYSIS_SHAPE, SPEC_BUSINESS_TYPES, SPEC_HARNESS_EXAMPLES, SPEC_HARNESS_SOURCE, SPEC_NEEDS, SPEC_STRUCTURES, SPEC_USER_AVOIDS, STRUCTURES, StyleReadGround, StyleReadHeadings, StyleReadMotion, StyleReadSourceKind, USER_AVOID_IDS, VoiceFormality, VoicePerson, analysisPromptContext, businessTypeById, calmerChoice, inferNeeds, isLookRule, lintDesign, lookMatches, mapStyleRead, motionFor, oklchHue, presetHasSerifHeadings, presetLook, rankStylePresets, rankWithReferences, userAvoidRule, validateSiteAnalysis };
2479
- export type { AnalysisContext, AnalysisIssue, AnalysisValidation, AvoidRule, BusinessType, CheckRule, FactRead, GoodFit, LintFinding, LintSite, LookCondition, LookDesign, LookRule, MotionInput, MotionStart, NeedDef, NeedId, PromptContext, PromptContextOptions, PromptSection, RankInput, RankedPreset, SiteAnalysis, SiteNeed, StructureId, StyleHints, StyleRead, UserAvoidId };
2497
+ interface BusinessTypeMatch {
2498
+ id: string;
2499
+ /** The keywords found, in the type's order. */
2500
+ keywords: string[];
2501
+ /** The letters matched — longer, more specific keywords weigh more ("barber" over "bar"). */
2502
+ score: number;
2503
+ }
2504
+ /** The kind of business the words name, best first; empty when none of the keywords appears. Ties keep the board's order. */
2505
+ declare function matchBusinessTypes(text: string, types?: ReadonlyArray<BusinessType>): BusinessTypeMatch[];
2506
+ /** The single best kind of business the words name, or null. */
2507
+ declare function matchBusinessType(text: string, types?: ReadonlyArray<BusinessType>): string | null;
2508
+ /** An address as a chip shows it: no scheme, no `www.`, no trailing punctuation or slash, lower-case host. */
2509
+ declare function normalizeAddress(raw: string): string;
2510
+ /** Every website address typed or pasted in the words (an http(s) address or a bare domain), once each, in
2511
+ * order. An email address is not a website. */
2512
+ declare function addressesIn(text: string): string[];
2513
+ /** The words without one address (removing its chip): every spelling of it, and the space it leaves. */
2514
+ declare function withoutAddress(text: string, address: string): string;
2515
+
2516
+ export { ANIMATE_ADVICE_STEPS, AnalysisIssueCode, AnalysisLocale, AvoidCheck, BUSINESS_TYPES, BUSINESS_TYPE_IDS, BookingNeed, COPY_LENGTH_ORDER, CopyLength, HARNESS_SOURCE, LITTLE_MOVEMENT_AVOID, LITTLE_MOVEMENT_CAP, LintKind, MAX_ANALYSIS_MOODS, NEEDS, NEED_IDS, NeedSource, PresetFit, RANK_WEIGHTS, READ_ANIMATE, READ_WEIGHTS, RankReason, SPEC_ANALYSIS_SHAPE, SPEC_BUSINESS_TYPES, SPEC_HARNESS_EXAMPLES, SPEC_HARNESS_SOURCE, SPEC_NEEDS, SPEC_STRUCTURES, SPEC_USER_AVOIDS, STRUCTURES, StyleReadGround, StyleReadHeadings, StyleReadMotion, StyleReadSourceKind, USER_AVOID_IDS, VoiceFormality, VoicePerson, addressesIn, analysisPromptContext, businessTypeById, calmerChoice, inferNeeds, isLookRule, lintDesign, lookMatches, mapStyleRead, matchBusinessType, matchBusinessTypes, motionAdvice, motionFor, normalizeAddress, oklchHue, presetHasSerifHeadings, presetLook, rankStylePresets, rankWithReferences, userAvoidRule, validateSiteAnalysis, withoutAddress };
2517
+ export type { AnalysisContext, AnalysisIssue, AnalysisValidation, AvoidRule, BusinessType, BusinessTypeMatch, CheckRule, FactRead, GoodFit, LintFinding, LintSite, LookCondition, LookDesign, LookRule, MotionBasis, MotionInput, MotionStart, NeedDef, NeedId, PromptContext, PromptContextOptions, PromptSection, RankInput, RankedPreset, SiteAnalysis, SiteNeed, StructureId, StyleHints, StyleRead, UserAvoidId };
package/index.d.ts CHANGED
@@ -113,13 +113,15 @@ interface PageScrollStart {
113
113
  * Scrolling choice (`planScroll`); `scrollTo` actions and the dialog lock go through it whether or not
114
114
  * Lenis runs. Never on the server: there is no window to scroll.
115
115
  */
116
- declare class PageScrollProvider {
116
+ declare class PageScrollProvider implements OnDestroy {
117
117
  private zone;
118
118
  private lenis;
119
119
  private rafId?;
120
120
  private wrapper;
121
121
  private lockedOverflow;
122
122
  init(start: PageScrollStart): void;
123
+ /** Provided by a component (a page, the Design step's specimen): its Lenis and loop go with it. */
124
+ ngOnDestroy(): void;
123
125
  /** Stop Lenis and its loop; the page falls back to the browser's own scrolling. */
124
126
  destroy(): void;
125
127
  /** Hard-stop page scroll (dialog open, A1b): wheel/touch frozen until start(). */
@@ -3034,6 +3036,23 @@ declare function hasOwnPageTransition(anim: RouteAnimationLike | null | undefine
3034
3036
  */
3035
3037
  declare function pageTransitionOf(anim: RouteAnimationLike | null | undefined, ctx?: PageTransitionContext): PlannedPageTransition;
3036
3038
 
3039
+ /** The two layers a builder preview of a page change plays on, and the stage that holds them. */
3040
+ interface TransitionPreviewLayers {
3041
+ /** Holds both layers; it carries the perspective the 3D presets need. */
3042
+ stage: HTMLElement;
3043
+ /** The page that leaves. */
3044
+ out: HTMLElement | null;
3045
+ /** The page that arrives. */
3046
+ in: HTMLElement | null;
3047
+ }
3048
+ /**
3049
+ * Play a resolved page change (`resolvePageTransition`) on two stacked layers — the ONE dual-layer preview the
3050
+ * builder's Page panel and the Design step's specimen both play (rev 3.1 §3o). The live site plays the same
3051
+ * frames through its own transition service. Resolves when the arriving layer has finished (or at once when
3052
+ * there is no arriving layer). The layers are left at their last frame: the caller decides what to show after.
3053
+ */
3054
+ declare function playDualLayerTransition(resolved: ResolvedPageTransition, layers: TransitionPreviewLayers): Promise<void>;
3055
+
3037
3056
  /**
3038
3057
  * Gallery effect registry — the SHARED, effect-agnostic description of how each gallery slide looks
3039
3058
  * and animates in a given crossfade state. Modeled on `binding-pipes.ts` / `PAGE_TRANSITION_PRESETS`:
@@ -3156,5 +3175,5 @@ declare class GalleryLoop {
3156
3175
  private emit;
3157
3176
  }
3158
3177
 
3159
- export { A11yAttribute, A11yTrait, ACCESSIBLE_NAME_TYPES, AOSService, ARIA_CURRENT_PAGE, BACKDROP_TWIN_KEY, BACKDROP_WHOLE_KEY, BREAKPOINTS, Breakpoint, BreakpointQueryKind, BuilderButtonComponent, BuilderDividerComponent, BuilderHeadingComponent, BuilderIconComponent, BuilderImageComponent, BuilderLinkComponent, BuilderSpacerComponent, ButtonBlockComponent, COMPONENT_STATE_SELECTOR, CONTAINER_HTML_TAGS, CONTEXT_ACTION_TYPES, CURRENT_LINK_TYPES, CardComponent, ClientGalleryComponent, ClientListComponent, ClientListItemComponent, ClientRepeaterComponent, ClientSliderContainerComponent, ClientSliderSlideComponent, ComponentInstanceRegistryService, ComponentState, ContainerComponent, ContextStore, DECORATIVE_TYPES, DEFAULT_GALLERY_SCHEDULER, DEFAULT_PAGE_TRANSITION, DEFAULT_PAGE_TRANSITION_DURATION, DEFAULT_PAGE_TRANSITION_EASING, DEFAULT_PAGE_TRANSITION_PERSPECTIVE, DEFAULT_PHOSPHOR_WEIGHT, DEFAULT_WIDTH_BY_TYPE, DIALOG_CLOSE_ABOVE_ATTRIBUTE, DIALOG_NODE_TYPE, DeclarationContext, ENTRANCE_ACTION_TYPES, EXIT_ACTION_TYPES, FormRenderState, FormRuntimeService, GALLERY_EFFECTS, GALLERY_EFFECT_DESCRIPTORS, GalleryLoop, HOVER_DEFAULT_TRANSITION, HOVER_STATES, INTERACTION_HIDDEN_ATTRIBUTE, IOX_BUILDER_EVENTS, IOX_FORM_MESSAGES, IOX_FORM_NAVIGATOR, IOX_FORM_TRANSPORT, IS_PREVIEW, IoxAnimateContainer, IoxAosDirective, IoxAosModule, IoxBuilderComponentsModule, IoxComponentRegistryService, IoxDialogComponent, IoxFieldComponent, IoxFormComponent, IoxFormErrorComponent, IoxFormSuccessComponent, IoxPageComponent, IoxPageModule, IoxSubmitButtonComponent, IoxUiModule, IoxVideoComponent, LAST_EMITTED_STATES, LEGACY_DIRECTION_KEY, LEGACY_HOVER_TRANSITION, LEGACY_SWIPER_CONTROLS, LinkedContainerComponent, MANAGED_A11Y_ATTRIBUTES, MAX_LINES_KEY, NAMED_GROUP_ROLES, NO_PAGE_TRANSITION, ON_STATE_ROLES, PAGE_STYLE_TOKENS, PAGE_TOKEN_FALLBACKS, PAGE_TRANSITION_CATEGORIES, PAGE_TRANSITION_PRESETS, PART_CLASS_NAMES, PART_PAGINATION_MODIFIER, PHONE_TRANSITION_QUERY, PIPE_DESCRIPTORS, PIPE_REGISTRY, PageScrollProvider, PageStyleField, PageStyleSource, REDUCED_MOTION_QUERY, SCROLL_EASINGS, SLIDER_INPUTS_ATTRIBUTE, SLIDER_INPUT_NAMES, STATE_PSEUDO_CLASS, SUPPRESSED_STYLE_PROPS_BY_TYPE, SWIPER_A11Y_OPTIONS, SWIPER_RESPONSIVE_TRAITS, SWIPER_TRIGGER_EVENTS, ScrollEasing, SectionComponent, SwiperModuleName, TARGETED_STATE_SUFFIX, TEXT_ALIGN_KEY, TEXT_DIRECTION_KEY, TRANSITION_FEELS, TextAlign, TextBlockComponent, VALIDATION_STATES, VALIDATION_STATE_CLASS, VIRTUAL_TRAIT_GROUPS, VIRTUAL_TRAIT_KEYS, ViewPositionDirective, ViewPositionModule, VisibilityActionType, activeActions, applyBindingPipes, applyFeel, bootSlider, breakpointDef, breakpointForWidth, breakpointQueryFor, buildInteractionKeyframes, buildPartSelector, buildSwiperBreakpoints, buildTargetedStateSelector, buildTargetedSuffixSelector, buildTransitionTimeline, cascadeChain, clearInputOverride, clearOverride, closeAboveMediaQuery, collectContextSlotKeys, compileDeclarations, compilePageStyle, componentStatesFor, composeVirtualTraits, containerHtmlTag, contextActionEffect, contextSlotKeys, countOverrides, declaredInputNames, decomposeBackdropFilter, defaultHoverTransition, defaultMessageResolver, deriveSeoFromContent, describePageStyle, describeSeoFields, dialogCloseAboveMinWidth, domHasSliderParts, effectiveActionType, effectiveTransitionId, entrancePreStateStyle, feelFor, finalKeyframe, getSeoValue, hasHoverState, hasOwnPageTransition, hasOwnTransition, initialSlotValues, isActionEnabled, isContextAction, isCurrentLink, isExternalOrFragment, isGradientPaint, isInputOverriddenAt, isLastEmittedStateKey, isOverriddenAt, isValidSlotKey, isWholeBackdropSet, keyframePropertyName, keyframeStyleEntries, legacyEnterToTransition, mediaQueryFor, mergeSwiperBreakpoints, migrateLegacyDirection, migrateLegacyTextAlign, motionAllowed, nearestCommonAncestorId, needsAccessibleName, needsGroupName, nodeA11yAttributes, nodeCssId, nodeTraitValue, nodeUsesContext, normalizeAppPath, normalizeGalleryImage, normalizeIconClass, orderStatesForEmit, pageTransitionOf, parentBreakpoint, parseUrlList, partClassName, planResponsiveRules, planScroll, planSwiperOptions, prefersReducedMotion, renderDefaultDeclarations, resetPageStyleField, resetSeoField, resolveBoundValue, resolveContextValue, resolveGalleryEffect, resolveGalleryUrls, resolveInputChain, resolveInputs, resolvePageTransition, resolvePath, resolveSeo, resolveStateStyles, resolveStyleProps, resolveToggleVisibility, resolveValueChain, rewriteViewportUnits, safeSetInput, serializeA11yAttributes, setInputOverride, setOverride, setSeoAuto, setSeoBinding, setSeoOverride, setSeoValue, sliderControlElements, sliderInputsFromProps, sliderInputsOf, stateNamesIn, statePseudoClass, stateSelectorSuffix, stripSuppressedProps, swiperAutoplayAllowed, swiperLoopOptions, swiperModuleNames, syncNodeA11yAttributes, targetedStateSelector, viewportTransitionContext, withContextSlots, withDefaultHoverTransition, wrapInBreakpointQuery, wrapInMediaQuery };
3160
- export type { A11yNodeSource, BoundValueSpec, BreakpointDef, ContextActionEffect, ContextActionLike, ContextItemRef, ContextSlotDefault, ContextValueSpec, ContextWriteScope, CurrentLinkOptions, FormMessageResolver, GalleryBoundSource, GalleryDurations, GalleryEffect, GalleryEffectDescriptor, GalleryImage, GalleryLoopOptions, GalleryLoopScheduler, GalleryLoopState, GallerySlideState, IBuilderEventEmitter, InputMap, InteractionKeyframe, IoxContextSlot, IoxPipe, MatchMediaHost, NodeA11yContext, OverrideBreakpoint, PageScrollStart, PageStyleFieldState, PageStyleOptions, PageStyleSettings, PageTransitionCategory, PageTransitionCategoryMeta, PageTransitionContext, PageTransitionOptions, PageTransitionPreset, PipeArgSpec, PipeContext, PipeDescriptor, PlannedPageTransition, ResolvedPageTransition, ResponsiveLayer, ResponsiveOverrides, ResponsiveRuleSet, RouteAnimationLike, ScrollPlanContext, SeoDerivedContent, SeoFieldSource, SeoFieldState, SeoResolveContext, SetContextParams, SliderBootOptions, SliderControlElements, SliderInputs, StateStyleMap, StyleMap, StyleTreeNode, SwiperBreakpointMap, SwiperModuleFlags, SwiperPlan, SwiperPlanContext, ToggleableAction, TransitionFeel, TransitionPhase, TransitionTimelineInput, TransitionViewport, ValueSource, VirtualTraitGroup };
3178
+ export { A11yAttribute, A11yTrait, ACCESSIBLE_NAME_TYPES, AOSService, ARIA_CURRENT_PAGE, BACKDROP_TWIN_KEY, BACKDROP_WHOLE_KEY, BREAKPOINTS, Breakpoint, BreakpointQueryKind, BuilderButtonComponent, BuilderDividerComponent, BuilderHeadingComponent, BuilderIconComponent, BuilderImageComponent, BuilderLinkComponent, BuilderSpacerComponent, ButtonBlockComponent, COMPONENT_STATE_SELECTOR, CONTAINER_HTML_TAGS, CONTEXT_ACTION_TYPES, CURRENT_LINK_TYPES, CardComponent, ClientGalleryComponent, ClientListComponent, ClientListItemComponent, ClientRepeaterComponent, ClientSliderContainerComponent, ClientSliderSlideComponent, ComponentInstanceRegistryService, ComponentState, ContainerComponent, ContextStore, DECORATIVE_TYPES, DEFAULT_GALLERY_SCHEDULER, DEFAULT_PAGE_TRANSITION, DEFAULT_PAGE_TRANSITION_DURATION, DEFAULT_PAGE_TRANSITION_EASING, DEFAULT_PAGE_TRANSITION_PERSPECTIVE, DEFAULT_PHOSPHOR_WEIGHT, DEFAULT_WIDTH_BY_TYPE, DIALOG_CLOSE_ABOVE_ATTRIBUTE, DIALOG_NODE_TYPE, DeclarationContext, ENTRANCE_ACTION_TYPES, EXIT_ACTION_TYPES, FormRenderState, FormRuntimeService, GALLERY_EFFECTS, GALLERY_EFFECT_DESCRIPTORS, GalleryLoop, HOVER_DEFAULT_TRANSITION, HOVER_STATES, INTERACTION_HIDDEN_ATTRIBUTE, IOX_BUILDER_EVENTS, IOX_FORM_MESSAGES, IOX_FORM_NAVIGATOR, IOX_FORM_TRANSPORT, IS_PREVIEW, IoxAnimateContainer, IoxAosDirective, IoxAosModule, IoxBuilderComponentsModule, IoxComponentRegistryService, IoxDialogComponent, IoxFieldComponent, IoxFormComponent, IoxFormErrorComponent, IoxFormSuccessComponent, IoxPageComponent, IoxPageModule, IoxSubmitButtonComponent, IoxUiModule, IoxVideoComponent, LAST_EMITTED_STATES, LEGACY_DIRECTION_KEY, LEGACY_HOVER_TRANSITION, LEGACY_SWIPER_CONTROLS, LinkedContainerComponent, MANAGED_A11Y_ATTRIBUTES, MAX_LINES_KEY, NAMED_GROUP_ROLES, NO_PAGE_TRANSITION, ON_STATE_ROLES, PAGE_STYLE_TOKENS, PAGE_TOKEN_FALLBACKS, PAGE_TRANSITION_CATEGORIES, PAGE_TRANSITION_PRESETS, PART_CLASS_NAMES, PART_PAGINATION_MODIFIER, PHONE_TRANSITION_QUERY, PIPE_DESCRIPTORS, PIPE_REGISTRY, PageScrollProvider, PageStyleField, PageStyleSource, REDUCED_MOTION_QUERY, SCROLL_EASINGS, SLIDER_INPUTS_ATTRIBUTE, SLIDER_INPUT_NAMES, STATE_PSEUDO_CLASS, SUPPRESSED_STYLE_PROPS_BY_TYPE, SWIPER_A11Y_OPTIONS, SWIPER_RESPONSIVE_TRAITS, SWIPER_TRIGGER_EVENTS, ScrollEasing, SectionComponent, SwiperModuleName, TARGETED_STATE_SUFFIX, TEXT_ALIGN_KEY, TEXT_DIRECTION_KEY, TRANSITION_FEELS, TextAlign, TextBlockComponent, VALIDATION_STATES, VALIDATION_STATE_CLASS, VIRTUAL_TRAIT_GROUPS, VIRTUAL_TRAIT_KEYS, ViewPositionDirective, ViewPositionModule, VisibilityActionType, activeActions, applyBindingPipes, applyFeel, bootSlider, breakpointDef, breakpointForWidth, breakpointQueryFor, buildInteractionKeyframes, buildPartSelector, buildSwiperBreakpoints, buildTargetedStateSelector, buildTargetedSuffixSelector, buildTransitionTimeline, cascadeChain, clearInputOverride, clearOverride, closeAboveMediaQuery, collectContextSlotKeys, compileDeclarations, compilePageStyle, componentStatesFor, composeVirtualTraits, containerHtmlTag, contextActionEffect, contextSlotKeys, countOverrides, declaredInputNames, decomposeBackdropFilter, defaultHoverTransition, defaultMessageResolver, deriveSeoFromContent, describePageStyle, describeSeoFields, dialogCloseAboveMinWidth, domHasSliderParts, effectiveActionType, effectiveTransitionId, entrancePreStateStyle, feelFor, finalKeyframe, getSeoValue, hasHoverState, hasOwnPageTransition, hasOwnTransition, initialSlotValues, isActionEnabled, isContextAction, isCurrentLink, isExternalOrFragment, isGradientPaint, isInputOverriddenAt, isLastEmittedStateKey, isOverriddenAt, isValidSlotKey, isWholeBackdropSet, keyframePropertyName, keyframeStyleEntries, legacyEnterToTransition, mediaQueryFor, mergeSwiperBreakpoints, migrateLegacyDirection, migrateLegacyTextAlign, motionAllowed, nearestCommonAncestorId, needsAccessibleName, needsGroupName, nodeA11yAttributes, nodeCssId, nodeTraitValue, nodeUsesContext, normalizeAppPath, normalizeGalleryImage, normalizeIconClass, orderStatesForEmit, pageTransitionOf, parentBreakpoint, parseUrlList, partClassName, planResponsiveRules, planScroll, planSwiperOptions, playDualLayerTransition, prefersReducedMotion, renderDefaultDeclarations, resetPageStyleField, resetSeoField, resolveBoundValue, resolveContextValue, resolveGalleryEffect, resolveGalleryUrls, resolveInputChain, resolveInputs, resolvePageTransition, resolvePath, resolveSeo, resolveStateStyles, resolveStyleProps, resolveToggleVisibility, resolveValueChain, rewriteViewportUnits, safeSetInput, serializeA11yAttributes, setInputOverride, setOverride, setSeoAuto, setSeoBinding, setSeoOverride, setSeoValue, sliderControlElements, sliderInputsFromProps, sliderInputsOf, stateNamesIn, statePseudoClass, stateSelectorSuffix, stripSuppressedProps, swiperAutoplayAllowed, swiperLoopOptions, swiperModuleNames, syncNodeA11yAttributes, targetedStateSelector, viewportTransitionContext, withContextSlots, withDefaultHoverTransition, wrapInBreakpointQuery, wrapInMediaQuery };
3179
+ export type { A11yNodeSource, BoundValueSpec, BreakpointDef, ContextActionEffect, ContextActionLike, ContextItemRef, ContextSlotDefault, ContextValueSpec, ContextWriteScope, CurrentLinkOptions, FormMessageResolver, GalleryBoundSource, GalleryDurations, GalleryEffect, GalleryEffectDescriptor, GalleryImage, GalleryLoopOptions, GalleryLoopScheduler, GalleryLoopState, GallerySlideState, IBuilderEventEmitter, InputMap, InteractionKeyframe, IoxContextSlot, IoxPipe, MatchMediaHost, NodeA11yContext, OverrideBreakpoint, PageScrollStart, PageStyleFieldState, PageStyleOptions, PageStyleSettings, PageTransitionCategory, PageTransitionCategoryMeta, PageTransitionContext, PageTransitionOptions, PageTransitionPreset, PipeArgSpec, PipeContext, PipeDescriptor, PlannedPageTransition, ResolvedPageTransition, ResponsiveLayer, ResponsiveOverrides, ResponsiveRuleSet, RouteAnimationLike, ScrollPlanContext, SeoDerivedContent, SeoFieldSource, SeoFieldState, SeoResolveContext, SetContextParams, SliderBootOptions, SliderControlElements, SliderInputs, StateStyleMap, StyleMap, StyleTreeNode, SwiperBreakpointMap, SwiperModuleFlags, SwiperPlan, SwiperPlanContext, ToggleableAction, TransitionFeel, TransitionPhase, TransitionPreviewLayers, TransitionTimelineInput, TransitionViewport, ValueSource, VirtualTraitGroup };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vectoriox/iox-ui",
3
- "version": "5.47.0",
3
+ "version": "5.49.0",
4
4
  "peerDependencies": {
5
5
  "@angular/common": ">=20.0.0",
6
6
  "@angular/core": ">=20.0.0",
@@ -8,7 +8,7 @@ import {
8
8
  } from './flavor-seed';
9
9
  import { DesignChoices, DesignTokenRow, flavorSourceStamp, StoredDesignTokenRow } from './design-token.model';
10
10
  import { FONT_FAMILY_TOKENS, fontFamilyKey, FontFamilyToken, fontRoleReferences, parseFontFamilies, withoutScriptPartners } from './fonts';
11
- import { DesignConfigEntry, DesignConfigKey, designConfigEntries, flavorPackById, isRetiredFlavorId, packDesignChoices, StoredDesign } from './design-settings';
11
+ import { DesignConfigEntry, DesignConfigKey, designConfigEntries, flavorPackById, isRetiredFlavorId, packDesignChoices, sameDesignChoices, StoredDesign } from './design-settings';
12
12
 
13
13
  /**
14
14
  * Applying a look (design-system DS4): the COMPLETE `buildFlavorTokens` set for the chosen
@@ -147,8 +147,10 @@ export interface DesignRevertSnapshot extends FlavorRevertSnapshot {
147
147
  * What an Apply of `plan` stores as its revert snapshot — the ONE rule the builder's Apply and the
148
148
  * server-side site build (W2) share: the whole pre-apply token set, what the apply writes into each row,
149
149
  * and where a revert goes back to — never a null flavor (a studio with no design row wore its design's
150
- * defaults, so that is what it returns to). An apply that writes nothing keeps the previous snapshot:
151
- * clobbering a useful one with "nothing changed" would be a lie.
150
+ * defaults, so that is what it returns to). An apply that changes NOTHING keeps the previous snapshot:
151
+ * clobbering a useful one with "nothing changed" would be a lie. A choice that lives in the config only
152
+ * (Scrolling · How animated · Between pages) IS a change — pass `next` so a motion-only Apply gets its own
153
+ * snapshot and Revert goes back to the motion worn just before it (vectoriox/iox-ui#28).
152
154
  */
153
155
  export function designApplySnapshot(
154
156
  previous: StoredDesign,
@@ -156,8 +158,10 @@ export function designApplySnapshot(
156
158
  existing: ReadonlyArray<StoredDesignTokenRow>,
157
159
  plan: FlavorSeedPlan,
158
160
  appliedAt: number,
161
+ next?: DesignChoices,
159
162
  ): DesignRevertSnapshot | null {
160
- if (!plan.create.length && !plan.update.length) return previous.revert;
163
+ const wroteTokens = plan.create.length > 0 || plan.update.length > 0;
164
+ if (!wroteTokens && (!next || sameDesignChoices(previous.choices, next))) return previous.revert;
161
165
  const origin: Pick<DesignRevertSnapshot, 'from' | 'fromConfig'> = previous.storedFlavorId
162
166
  ? {
163
167
  from: { flavorId: previous.storedFlavorId, flavorVersion: previous.flavorVersion, brandSeed: previous.storedSeed },
@@ -0,0 +1,110 @@
1
+ import { AnimateId, SPEC_AXES } from '@vectoriox/iox-ui/design-core';
2
+
3
+ /**
4
+ * How animated (AI plan rev 3.1 §3p) — the ONE reading of the contract's `axes.animations` (what each level
5
+ * moves, how) and `axes.animationFamilies` (which parts a section family lets move). The pieces every stamper
6
+ * shares: the Design step's business specimen (H4-4) today, `planEntrances` in the composer (M1-6b) next. Pure.
7
+ *
8
+ * A level with `content` moves a section's content together, once; otherwise it names parts (heading, items,
9
+ * media…) and each part enters on its own, items one after another. Navigation, footers and globals are a family
10
+ * with no parts, so nothing there ever moves.
11
+ */
12
+
13
+ export enum EntrancePart {
14
+ Content = 'content',
15
+ Heading = 'heading',
16
+ Items = 'items',
17
+ Media = 'media',
18
+ RowMedia = 'rowMedia',
19
+ HeroText = 'heroText',
20
+ }
21
+
22
+ /** What an entrance stamp is marked with — a later restamp touches only what the look made (H4-5). */
23
+ export const ENTRANCE_ORIGIN_LOOK = 'look';
24
+ /** The trigger every entrance plays on. */
25
+ export const ENTRANCE_TRIGGER = 'viewportEnter';
26
+ /** The easing every entrance plays with (only `opacity` and `transform` move). */
27
+ export const ENTRANCE_EASING = 'ease-out';
28
+
29
+ /** One entrance as the contract states it. */
30
+ export interface EntranceSpec {
31
+ readonly type: string;
32
+ readonly duration: number;
33
+ readonly distance?: number;
34
+ /** Items: ms added per item, up to `cap` items. */
35
+ readonly stagger?: number;
36
+ readonly cap?: number;
37
+ /** The type a right-to-left page plays instead (a side-by-side row slides in from ITS side). */
38
+ readonly mirror?: string;
39
+ }
40
+
41
+ /** What one section family moves at one level. `content` set = the whole content together, and no parts. */
42
+ export interface SectionEntrances {
43
+ readonly content: EntranceSpec | null;
44
+ readonly parts: Readonly<Partial<Record<EntrancePart, EntranceSpec>>>;
45
+ }
46
+
47
+ /** The interaction action a stamp carries — the shape both interaction runtimes play. */
48
+ export interface EntranceAction {
49
+ type: string;
50
+ target: 'self';
51
+ duration: number;
52
+ delay: number;
53
+ easing: typeof ENTRANCE_EASING;
54
+ once: true;
55
+ params?: { distance: number };
56
+ }
57
+
58
+ export interface EntranceInteraction {
59
+ id: string;
60
+ trigger: typeof ENTRANCE_TRIGGER;
61
+ actions: [EntranceAction];
62
+ origin: typeof ENTRANCE_ORIGIN_LOOK;
63
+ }
64
+
65
+ type Level = Readonly<{ parts: Readonly<Partial<Record<EntrancePart, EntranceSpec>>> }>;
66
+ const LEVELS = SPEC_AXES.animations as unknown as Readonly<Record<string, Level>>;
67
+ const FAMILIES = SPEC_AXES.animationFamilies as unknown as Readonly<Record<string, ReadonlyArray<EntrancePart>>>;
68
+
69
+ const NOTHING: SectionEntrances = { content: null, parts: {} };
70
+
71
+ /** What a section of `family` moves at `level` — nothing for an unknown level or family. */
72
+ export function sectionEntrances(level: AnimateId | string | null | undefined, family: string): SectionEntrances {
73
+ const lv = level ? LEVELS[level] : undefined;
74
+ const allow = FAMILIES[family] ?? [];
75
+ if (!lv || !allow.length) return NOTHING;
76
+ if (lv.parts.content && allow.includes(EntrancePart.Content)) return { content: lv.parts.content, parts: {} };
77
+ const parts: Partial<Record<EntrancePart, EntranceSpec>> = {};
78
+ for (const part of allow) {
79
+ if (part === EntrancePart.Content) continue;
80
+ // A level without its own side-by-side entrance gives a row's picture the plain picture one.
81
+ const spec = lv.parts[part] ?? (part === EntrancePart.RowMedia ? lv.parts.media : undefined);
82
+ if (spec) parts[part] = spec;
83
+ }
84
+ return { content: null, parts };
85
+ }
86
+
87
+ /** The literal delay of the `index`-th item: `stagger` ms each, the last `cap` waiting no longer than the one before. */
88
+ export function staggerDelay(spec: EntranceSpec, index = 0): number {
89
+ if (!spec.stagger) return 0;
90
+ const cap = Math.max(1, spec.cap ?? 1);
91
+ return Math.min(Math.max(0, index), cap - 1) * spec.stagger;
92
+ }
93
+
94
+ /** The action a node plays: the spec's type (mirrored on a right-to-left page), its distance and its delay. */
95
+ export function entranceAction(spec: EntranceSpec, opts: { index?: number; rtl?: boolean } = {}): EntranceAction {
96
+ return {
97
+ type: opts.rtl && spec.mirror ? spec.mirror : spec.type,
98
+ target: 'self',
99
+ duration: spec.duration,
100
+ delay: staggerDelay(spec, opts.index),
101
+ easing: ENTRANCE_EASING,
102
+ once: true,
103
+ ...(spec.distance ? { params: { distance: spec.distance } } : {}),
104
+ };
105
+ }
106
+
107
+ /** The stamp on a node: `viewportEnter`, once, on itself (a duplicated node needs no remap), from the look. */
108
+ export function entranceInteraction(nodeId: string, action: EntranceAction): EntranceInteraction {
109
+ return { id: `${nodeId}-enter`, trigger: ENTRANCE_TRIGGER, actions: [action], origin: ENTRANCE_ORIGIN_LOOK };
110
+ }
@@ -0,0 +1,71 @@
1
+ import { BindScope, patternById, patternNodes, patternScopes } from './patterns';
2
+ import { CmsDataSourcePlan, DataSourcePlan, PagePlan, PlanDataSourceMode, PlanDataSourceType, SitePlan } from './site-plan.model';
3
+
4
+ /**
5
+ * What each page of a plan shows and where its data comes from (AI plan rev 3 §3d, `describePlanData`) — the
6
+ * Plan step's data lines, in the vocabulary the builder already uses. Pure and STRUCTURED: the screen words
7
+ * each kind in its own language ("Lists every Project", "One Project, found by its address /work/:slug").
8
+ */
9
+
10
+ export enum PageDataKind {
11
+ /** A section repeats every item of a content type. */
12
+ List = 'list',
13
+ /** A section repeats the newest `count` items. */
14
+ Latest = 'latest',
15
+ /** A template page: one item, found by its address — one page per item. */
16
+ One = 'one',
17
+ /** The next (or previous) item beside the page's own. */
18
+ Siblings = 'siblings',
19
+ /** A form: what it sends, to the studio's inbox (+ an email when Email is connected). */
20
+ Form = 'form',
21
+ /** Words and pictures, no data. */
22
+ Static = 'static',
23
+ }
24
+
25
+ export interface PageDataLine {
26
+ kind: PageDataKind;
27
+ /** The content type's label ("Projects"), for every kind that reads one. */
28
+ contentType?: string;
29
+ /** Latest: how many. */
30
+ count?: number;
31
+ /** One: the page's own address, with its parameter. */
32
+ route?: string;
33
+ /** Form: the form preset (`contact`). */
34
+ form?: string;
35
+ }
36
+
37
+ export interface PageData {
38
+ pageKey: string;
39
+ lines: PageDataLine[];
40
+ }
41
+
42
+ const sameLine = (a: PageDataLine, b: PageDataLine) => JSON.stringify(a) === JSON.stringify(b);
43
+
44
+ function sourceLine(source: DataSourcePlan | undefined, label: (name: string) => string): PageDataLine | null {
45
+ if (!source || source.type !== PlanDataSourceType.Cms) return null;
46
+ const cms = source as CmsDataSourcePlan;
47
+ if (cms.mode !== PlanDataSourceMode.Query) return null;
48
+ return cms.limit
49
+ ? { kind: PageDataKind.Latest, contentType: label(cms.contentType), count: cms.limit }
50
+ : { kind: PageDataKind.List, contentType: label(cms.contentType) };
51
+ }
52
+
53
+ function pageLines(page: PagePlan, plan: SitePlan, label: (name: string) => string): PageDataLine[] {
54
+ const lines: PageDataLine[] = [];
55
+ const add = (line: PageDataLine | null) => { if (line && !lines.some(l => sameLine(l, line))) lines.push(line); };
56
+ if (page.template) add({ kind: PageDataKind.One, contentType: label(page.template), route: page.route });
57
+ for (const section of page.sections) {
58
+ const pattern = patternById(section.pattern);
59
+ if (!pattern) continue;
60
+ if (section.binding) add(sourceLine(plan.dataSources.find(d => d.alias === section.binding), label));
61
+ if (page.template && patternScopes(pattern).has(BindScope.Next)) add({ kind: PageDataKind.Siblings, contentType: label(page.template) });
62
+ for (const node of patternNodes(pattern)) if (node.preset) add({ kind: PageDataKind.Form, form: node.preset });
63
+ }
64
+ return lines.length ? lines : [{ kind: PageDataKind.Static }];
65
+ }
66
+
67
+ /** One entry per page of the plan, in plan order. */
68
+ export function describePlanData(plan: SitePlan): PageData[] {
69
+ const label = (name: string) => plan.contentTypes.find(t => t.name === name)?.label ?? name;
70
+ return plan.pages.map(page => ({ pageKey: page.key, lines: pageLines(page, plan, label) }));
71
+ }
@@ -21,3 +21,5 @@ export * from './shape-check';
21
21
  export * from './validate-site-plan';
22
22
  export * from './compose-site';
23
23
  export * from './nav-shape';
24
+ export * from './entrances';
25
+ export * from './plan-data';
@@ -0,0 +1,85 @@
1
+ import { BUSINESS_TYPES, BusinessType } from './business-types';
2
+
3
+ /**
4
+ * What Start reads from the owner's words BEFORE any model runs (AI plan rev 3 §3d step 0, N-D3): the kind of
5
+ * business by the contract's own keywords (`BusinessType.match` — "the no-model fallback"), and the addresses
6
+ * typed into the box (each becomes a "style only" reference chip, R1). Pure; the template path uses it today and
7
+ * the reading stage (M1-1) checks the model against the same match.
8
+ */
9
+
10
+ /** A single Hebrew letter that prefixes a word (the, in, to, and, from, that, as) — "בסדנאות" holds "סדנ". */
11
+ const HEBREW_PREFIX = '[הבלומשכ]?';
12
+
13
+ const escapeRe = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
14
+
15
+ const KEYWORD_RES = new Map<string, RegExp>();
16
+
17
+ /** A keyword matches at the START of a word (so a stem — "סדנ", "consult" — matches its forms). */
18
+ function keywordRe(keyword: string): RegExp {
19
+ let re = KEYWORD_RES.get(keyword);
20
+ if (!re) {
21
+ re = new RegExp(`(^|[^\\p{L}\\p{N}])${HEBREW_PREFIX}${escapeRe(keyword.toLocaleLowerCase())}`, 'u');
22
+ KEYWORD_RES.set(keyword, re);
23
+ }
24
+ return re;
25
+ }
26
+
27
+ export interface BusinessTypeMatch {
28
+ id: string;
29
+ /** The keywords found, in the type's order. */
30
+ keywords: string[];
31
+ /** The letters matched — longer, more specific keywords weigh more ("barber" over "bar"). */
32
+ score: number;
33
+ }
34
+
35
+ /** The kind of business the words name, best first; empty when none of the keywords appears. Ties keep the board's order. */
36
+ export function matchBusinessTypes(text: string, types: ReadonlyArray<BusinessType> = BUSINESS_TYPES): BusinessTypeMatch[] {
37
+ const words = (text ?? '').toLocaleLowerCase();
38
+ if (!words.trim()) return [];
39
+ return types
40
+ .map((type, order) => {
41
+ const keywords = type.match.filter(k => keywordRe(k).test(words));
42
+ return { id: type.id, keywords, score: keywords.reduce((n, k) => n + k.length, 0), order };
43
+ })
44
+ .filter(m => m.score > 0)
45
+ .sort((a, b) => b.score - a.score || a.order - b.order)
46
+ .map(({ order, ...m }) => m);
47
+ }
48
+
49
+ /** The single best kind of business the words name, or null. */
50
+ export function matchBusinessType(text: string, types: ReadonlyArray<BusinessType> = BUSINESS_TYPES): string | null {
51
+ return matchBusinessTypes(text, types)[0]?.id ?? null;
52
+ }
53
+
54
+ const ADDRESS_RE = /(?:https?:\/\/)?(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,}(?:\/[^\s<>"']*)?/gi;
55
+ const TRAILING = /[.,;:!?)\]}'"״׳]+$/;
56
+
57
+ /** An address as a chip shows it: no scheme, no `www.`, no trailing punctuation or slash, lower-case host. */
58
+ export function normalizeAddress(raw: string): string {
59
+ const bare = raw.replace(TRAILING, '').replace(/^https?:\/\//i, '').replace(/^www\./i, '').replace(/\/+$/, '');
60
+ const slash = bare.indexOf('/');
61
+ return slash < 0 ? bare.toLowerCase() : bare.slice(0, slash).toLowerCase() + bare.slice(slash);
62
+ }
63
+
64
+ /** Every website address typed or pasted in the words (an http(s) address or a bare domain), once each, in
65
+ * order. An email address is not a website. */
66
+ export function addressesIn(text: string): string[] {
67
+ const found: string[] = [];
68
+ for (const m of (text ?? '').matchAll(ADDRESS_RE)) {
69
+ const before = m.index ? text[m.index - 1] : '';
70
+ if (before === '@' || /[\p{L}\p{N}_-]/u.test(before)) continue;
71
+ const address = normalizeAddress(m[0]);
72
+ if (address && !found.includes(address)) found.push(address);
73
+ }
74
+ return found;
75
+ }
76
+
77
+ /** The words without one address (removing its chip): every spelling of it, and the space it leaves. */
78
+ export function withoutAddress(text: string, address: string): string {
79
+ const out = (text ?? '').replace(ADDRESS_RE, (m, offset: number, whole: string) => {
80
+ const before = offset ? whole[offset - 1] : '';
81
+ if (before === '@' || /[\p{L}\p{N}_-]/u.test(before)) return m;
82
+ return normalizeAddress(m) === address ? '' : m;
83
+ });
84
+ return out.replace(/[ \t]{2,}/g, ' ').replace(/ +([.,;:!?])/g, '$1').trim();
85
+ }
@@ -1,4 +1,4 @@
1
- import { DesignChoices, DesignMotion, parseHexColor, rgbToOklab, SPEC_AXES } from '@vectoriox/iox-ui/design-core';
1
+ import { AnimateId, DesignChoices, DesignMotion, parseHexColor, rgbToOklab, SPEC_AXES } from '@vectoriox/iox-ui/design-core';
2
2
  import type { FactKey } from '@vectoriox/iox-ui/generation-core';
3
3
 
4
4
  import { AvoidCheck, BUSINESS_TYPES, BusinessType, COPY_LENGTH_ORDER, CopyLength, isLookRule } from './business-types';
@@ -85,10 +85,22 @@ export function lintDesign(site: LintSite, typeId: string, types: ReadonlyArray<
85
85
  }
86
86
  }
87
87
  }
88
- // Rev 3.1: two levels above what this kind of business usually wears (Subtle → Lively) is advice, never a block.
89
- if (site.design.animate && ANIMATE_ORDER.indexOf(site.design.animate) >= ANIMATE_ORDER.indexOf(type.moves.animate) + ANIMATE_ADVICE_STEPS) {
90
- const usual = (SPEC_AXES.animations as Readonly<Record<string, { label: string }>>)[type.moves.animate].label;
88
+ const advice = motionAdvice(type.id, site.design.animate, types);
89
+ if (advice) {
90
+ const usual = (SPEC_AXES.animations as Readonly<Record<string, { label: string }>>)[advice.usual].label;
91
91
  out.push({ kind: LintKind.Look, say: `${type.label} sites usually move less (${usual}); this much movement as people scroll can feel restless.` });
92
92
  }
93
93
  return out;
94
94
  }
95
+
96
+ /**
97
+ * Rev 3.1: two levels above what this kind of business usually wears (Subtle → Lively) is advice, never a block.
98
+ * Structured, so Design ▸ Motion says it in the studio's language: `usual` = the level the type wears.
99
+ */
100
+ export function motionAdvice(
101
+ typeId: string | null | undefined, animate: AnimateId | string | null | undefined, types: ReadonlyArray<BusinessType> = BUSINESS_TYPES,
102
+ ): { usual: AnimateId } | null {
103
+ const type = typeId ? types.find(t => t.id === typeId) : undefined;
104
+ if (!type || !animate || !ANIMATE_ORDER.includes(animate)) return null;
105
+ return ANIMATE_ORDER.indexOf(animate) >= ANIMATE_ORDER.indexOf(type.moves.animate) + ANIMATE_ADVICE_STEPS ? { usual: type.moves.animate } : null;
106
+ }