@vectoriox/iox-ui 5.39.0 → 5.40.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.
@@ -1805,6 +1805,41 @@ declare function parseSampleImageRef(value: unknown): number | null;
1805
1805
  * composer never repairs a plan. `context` carries what the composer may not read for itself (the year).
1806
1806
  */
1807
1807
  declare function composeSite(plan: SitePlan, context: ComposeContext): ComposedSite;
1808
+ /** The header pattern the Studio template stamps — and the builder palette's "Navigation bar". */
1809
+ declare const SITE_HEADER_PATTERN = "nav.inline-cta";
1810
+ /** A page of the studio as the builder lists it. */
1811
+ interface SitePageRef {
1812
+ readonly name: string;
1813
+ readonly route: string;
1814
+ readonly isHome?: boolean;
1815
+ }
1816
+ /** What the header is composed from when there is no Site Plan: the studio's name and its pages. */
1817
+ interface NavHeaderInput {
1818
+ readonly business: SiteBusiness;
1819
+ /** Every page of the studio, in the order the navigation should list them. */
1820
+ readonly pages: ReadonlyArray<SitePageRef>;
1821
+ }
1822
+ interface NavHeaderOptions {
1823
+ /** The header's id; its parts' ids derive from it (`<id>-0-1-…`), so a fresh id gives fresh parts. */
1824
+ readonly id: string;
1825
+ /** A phone-shape part's id (the builder passes its id generator). */
1826
+ readonly newId?: (suffix: string) => string;
1827
+ }
1828
+ interface ComposedNavHeader {
1829
+ /** The header — a global BEFORE element — carrying the pattern's phone shape (the drawer). */
1830
+ header: SerializedNode;
1831
+ issues: ComposeIssue[];
1832
+ }
1833
+ /** The pages a navigation links to: every page but Home (the brand links there) and template pages
1834
+ * (a `/work/:slug` route has no single address), in the studio's order. */
1835
+ declare function siteNavPages(pages: ReadonlyArray<SitePageRef>): SitePageRef[];
1836
+ /**
1837
+ * The Studio template's header, composed for a studio that has no Site Plan — the builder palette's
1838
+ * "Navigation bar". THE SAME pattern and the same composer as `composeSite`: the brand (the business name),
1839
+ * one plain Link per navigation page, the call to action (W1.1's one dangling-link rule when the studio has
1840
+ * no /contact page: an email to the business, else removed), and the pattern's phone shape. Pure.
1841
+ */
1842
+ declare function composeNavHeader(input: NavHeaderInput, options: NavHeaderOptions): ComposedNavHeader;
1808
1843
 
1809
1844
  /**
1810
1845
  * The phone shapes of a navigation bar (complex-components plan CC1, §7a + R1; boards 1–3). ONE pure tree
@@ -1864,6 +1899,8 @@ interface NavShapeOptions {
1864
1899
  barItems?: number;
1865
1900
  /** A new part's id (the builder passes its id generator). Default: `<nav id>-<suffix>`. */
1866
1901
  newId?: (suffix: string) => string;
1902
+ /** The studio's pages, for a header that links to none (a hand-built one): its menu lists these. */
1903
+ pages?: ReadonlyArray<NavPage>;
1867
1904
  }
1868
1905
  interface NavShapeResult {
1869
1906
  /** The globals after the edit (the Navigation bar edited in place, the drawer/bar added or removed). */
@@ -1896,6 +1933,18 @@ declare function navShapeOf(globals: ReadonlyArray<SerializedNode>): NavShape |
1896
1933
  declare function navShapeBreakpoint(globals: ReadonlyArray<SerializedNode>): PhoneMenuBreakpoint;
1897
1934
  /** The pages the header links to, in order: its Links (W1.1 — one plain Link per page in the navigation). */
1898
1935
  declare function navPages(root: SerializedNode): NavPage[];
1936
+ /** Where a phone menu's pages come from: the header's own page Links, else the studio's pages. */
1937
+ declare enum NavPagesSource {
1938
+ Header = "header",
1939
+ Site = "site",
1940
+ None = "none"
1941
+ }
1942
+ /** The pages a phone menu lists: the header's (`navPages`), else `sitePages` — a hand-built header with no
1943
+ * page links still gets a working menu. */
1944
+ declare function phoneMenuPages(root: SerializedNode, sitePages?: ReadonlyArray<NavPage>): {
1945
+ pages: NavPage[];
1946
+ source: NavPagesSource;
1947
+ };
1899
1948
  /**
1900
1949
  * Put a site's header into `shape`. The Navigation bar is `navId` or the global carrying the root role
1901
1950
  * (else the first Container wearing `nav`, which this then marks with roles). Returns new globals;
@@ -1911,7 +1960,19 @@ interface AddBarItemResult {
1911
1960
  * The bar's Items list "Add item" (board 2): one more item, before More — the next navigation page the
1912
1961
  * bar does not link to yet (Home when every page is there). Refused at five (Q2). Input not mutated.
1913
1962
  */
1914
- declare function addBarItem(globals: ReadonlyArray<SerializedNode>, options?: Pick<NavShapeOptions, 'newId'>): AddBarItemResult;
1963
+ declare function addBarItem(globals: ReadonlyArray<SerializedNode>, options?: Pick<NavShapeOptions, 'newId' | 'pages'>): AddBarItemResult;
1964
+ interface BarItemsEditResult {
1965
+ globals: SerializedNode[];
1966
+ /** False when nothing changed: no bar, an unknown item, or the bar would drop below three items (Q2). */
1967
+ changed: boolean;
1968
+ }
1969
+ /**
1970
+ * The bar's Items list "Remove" (board 2): the item goes, More included — refused below three items (Q2:
1971
+ * 3–5), so the bar never shrinks into something a thumb cannot use. Input not mutated.
1972
+ */
1973
+ declare function removeBarItem(globals: ReadonlyArray<SerializedNode>, itemId: string): BarItemsEditResult;
1974
+ /** The bar's Items list drag-to-reorder (board 2): item `from` moves to `to`. Input not mutated. */
1975
+ declare function moveBarItem(globals: ReadonlyArray<SerializedNode>, from: number, to: number): BarItemsEditResult;
1915
1976
 
1916
- 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, NavRole, NavShape, NavShapeError, PatternDirective, PatternGlobalPosition, PatternOpens, PatternPlaceholder, PatternRecipe, PhoneMenuBreakpoint, PlaceholderKind, PlanDataSourceMode, PlanDataSourceType, RelativeOp, SAMPLE_IMAGE_REF, SITE_FACT_LABELS, SITE_PATTERNS, SITE_PLAN_VERSION, SITE_TEMPLATES, SPEC_SITE_FACTS, SPEC_SITE_PATTERNS, SPEC_SITE_PLAN_SHAPE, SPEC_SITE_TEMPLATES, STUDIO_TEMPLATE, SitePlanError, SitePlanIssueCode, SitePlanSourceKind, SiteTemplateError, SiteTemplateId, addBarItem, altSourceScopes, assertStyleSlots, assertValidSitePlan, barIconForRoute, composeSite, createSerializedNode, findNavRoot, navPages, navShapeBreakpoint, navShapeOf, nextSourceOf, nodeBindPaths, parseSampleImageRef, patternById, patternNeedsBinding, patternNodes, patternOptionalParts, patternScopes, patternSlots, patternTakesBinding, planFromTemplate, planNavShape, releasedLayoutDefaults, requiredShapeKeys, routeParams, sampleImageRef, setStyleProps, shapeKeys, validateSitePlan, walkSerialized };
1917
- export type { AddBarItemResult, CmsDataSourcePlan, ComposeContext, ComposeIssue, ComposedDataSource, ComposedItem, ComposedPage, ComposedPlaceholder, ComposedRoute, ComposedSite, ContentFieldPlan, ContentSeedPlan, ContentTypePlan, CreateNodeOptions, DataSourcePlan, FactKey, NavPage, NavShapeOptions, NavShapeResult, NodeBaseline, PagePlan, PageSectionPlan, PatternNode, RelativeDataSourcePlan, SerializedBinding, SerializedNode, SerializedResponsive, SiteBusiness, SitePattern, SitePlan, SitePlanIssue, SitePlanSource, SitePlanValidation, SiteTemplate, TemplatePlanInput };
1977
+ 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, 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_PATTERNS, SPEC_SITE_PLAN_SHAPE, SPEC_SITE_TEMPLATES, STUDIO_TEMPLATE, SitePlanError, SitePlanIssueCode, SitePlanSourceKind, SiteTemplateError, SiteTemplateId, addBarItem, altSourceScopes, assertStyleSlots, assertValidSitePlan, barIconForRoute, composeNavHeader, composeSite, createSerializedNode, findNavRoot, moveBarItem, navPages, navShapeBreakpoint, navShapeOf, nextSourceOf, nodeBindPaths, parseSampleImageRef, patternById, patternNeedsBinding, patternNodes, patternOptionalParts, patternScopes, patternSlots, patternTakesBinding, phoneMenuPages, planFromTemplate, planNavShape, releasedLayoutDefaults, removeBarItem, requiredShapeKeys, routeParams, sampleImageRef, setStyleProps, shapeKeys, siteNavPages, validateSitePlan, walkSerialized };
1978
+ export type { AddBarItemResult, BarItemsEditResult, CmsDataSourcePlan, ComposeContext, ComposeIssue, ComposedDataSource, ComposedItem, ComposedNavHeader, ComposedPage, ComposedPlaceholder, ComposedRoute, ComposedSite, ContentFieldPlan, ContentSeedPlan, ContentTypePlan, CreateNodeOptions, DataSourcePlan, FactKey, NavHeaderInput, NavHeaderOptions, NavPage, NavShapeOptions, NavShapeResult, NodeBaseline, PagePlan, PageSectionPlan, PatternNode, RelativeDataSourcePlan, SerializedBinding, SerializedNode, SerializedResponsive, SiteBusiness, SitePageRef, SitePattern, SitePlan, SitePlanIssue, SitePlanSource, SitePlanValidation, SiteTemplate, TemplatePlanInput };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vectoriox/iox-ui",
3
- "version": "5.39.0",
3
+ "version": "5.40.0",
4
4
  "peerDependencies": {
5
5
  "@angular/common": ">=20.0.0",
6
6
  "@angular/core": ">=20.0.0",
@@ -886,3 +886,82 @@ export function composeSite(plan: SitePlan, context: ComposeContext): ComposedSi
886
886
  issues,
887
887
  };
888
888
  }
889
+
890
+ // ── The site header on its own (CC1.1) ──────────────────────────────────────────────────────────
891
+
892
+ /** The header pattern the Studio template stamps — and the builder palette's "Navigation bar". */
893
+ export const SITE_HEADER_PATTERN = 'nav.inline-cta';
894
+
895
+ /** A page of the studio as the builder lists it. */
896
+ export interface SitePageRef {
897
+ readonly name: string;
898
+ readonly route: string;
899
+ readonly isHome?: boolean;
900
+ }
901
+
902
+ /** What the header is composed from when there is no Site Plan: the studio's name and its pages. */
903
+ export interface NavHeaderInput {
904
+ readonly business: SiteBusiness;
905
+ /** Every page of the studio, in the order the navigation should list them. */
906
+ readonly pages: ReadonlyArray<SitePageRef>;
907
+ }
908
+
909
+ export interface NavHeaderOptions {
910
+ /** The header's id; its parts' ids derive from it (`<id>-0-1-…`), so a fresh id gives fresh parts. */
911
+ readonly id: string;
912
+ /** A phone-shape part's id (the builder passes its id generator). */
913
+ readonly newId?: (suffix: string) => string;
914
+ }
915
+
916
+ export interface ComposedNavHeader {
917
+ /** The header — a global BEFORE element — carrying the pattern's phone shape (the drawer). */
918
+ header: SerializedNode;
919
+ issues: ComposeIssue[];
920
+ }
921
+
922
+ const ROUTE_PARAM = ':';
923
+ const HOME_PATH = '/';
924
+ const KEY_UNSAFE = /[^a-z0-9]+/gi;
925
+
926
+ /** The pages a navigation links to: every page but Home (the brand links there) and template pages
927
+ * (a `/work/:slug` route has no single address), in the studio's order. */
928
+ export function siteNavPages(pages: ReadonlyArray<SitePageRef>): SitePageRef[] {
929
+ return pages.filter(p => !!p.route && !p.isHome && p.route !== HOME_PATH && !p.route.includes(ROUTE_PARAM));
930
+ }
931
+
932
+ function navPagePlans(pages: ReadonlyArray<SitePageRef>): PagePlan[] {
933
+ const used = new Set<string>();
934
+ return pages.map((p, i) => {
935
+ let key = p.route.replace(KEY_UNSAFE, '-').replace(/^-+|-+$/g, '').toLowerCase() || `page-${i}`;
936
+ for (let n = 2; used.has(key); n++) key = `${key}-${n}`;
937
+ used.add(key);
938
+ return { key, name: p.name, route: p.route, inNav: true, isHome: p.isHome === true, sections: [] };
939
+ });
940
+ }
941
+
942
+ /**
943
+ * The Studio template's header, composed for a studio that has no Site Plan — the builder palette's
944
+ * "Navigation bar". THE SAME pattern and the same composer as `composeSite`: the brand (the business name),
945
+ * one plain Link per navigation page, the call to action (W1.1's one dangling-link rule when the studio has
946
+ * no /contact page: an email to the business, else removed), and the pattern's phone shape. Pure.
947
+ */
948
+ export function composeNavHeader(input: NavHeaderInput, options: NavHeaderOptions): ComposedNavHeader {
949
+ if (!nonEmpty(options?.id)) throw new ComposeError('composeNavHeader needs the header id');
950
+ const pattern = patternById(SITE_HEADER_PATTERN) as SitePattern;
951
+ const all = navPagePlans(input.pages);
952
+ const navRoutes = new Set(siteNavPages(input.pages).map(p => p.route));
953
+ const pages = all.map(p => ({ ...p, inNav: navRoutes.has(p.route) }));
954
+ const plan = { business: input.business, pages, dataSources: [], contentTypes: [] } as unknown as SitePlan;
955
+ const email = nonEmpty(input.business.facts?.email) ? input.business.facts!.email!.trim() : null;
956
+ const issues: ComposeIssue[] = [];
957
+ const ctx: Ctx = {
958
+ owner: GlobalSlot.Header, page: null, section: null, plan, year: 0, email, navPage: null, nextAlias: null,
959
+ placeholders: [], issues, routes: new Set(pages.map(p => p.route)),
960
+ };
961
+ const root = composePattern(pattern, options.id, ctx);
962
+ root.isGlobal = true;
963
+ root.globalPosition = pattern.global;
964
+ const shape = pattern.phoneMenu as NavShape | undefined;
965
+ const header = shape ? planNavShape([root], shape, { newId: options.newId }, root.id).globals[0] : root;
966
+ return { header, issues };
967
+ }
@@ -67,6 +67,8 @@ export interface NavShapeOptions {
67
67
  barItems?: number;
68
68
  /** A new part's id (the builder passes its id generator). Default: `<nav id>-<suffix>`. */
69
69
  newId?: (suffix: string) => string;
70
+ /** The studio's pages, for a header that links to none (a hand-built one): its menu lists these. */
71
+ pages?: ReadonlyArray<NavPage>;
70
72
  }
71
73
 
72
74
  export interface NavShapeResult {
@@ -199,6 +201,21 @@ export function navPages(root: SerializedNode): NavPage[] {
199
201
  return source.filter(isPageLink).map(n => ({ label: textOf(n), route: String(prop(n, 'pageRoute')) }));
200
202
  }
201
203
 
204
+ /** Where a phone menu's pages come from: the header's own page Links, else the studio's pages. */
205
+ export enum NavPagesSource {
206
+ Header = 'header',
207
+ Site = 'site',
208
+ None = 'none',
209
+ }
210
+
211
+ /** The pages a phone menu lists: the header's (`navPages`), else `sitePages` — a hand-built header with no
212
+ * page links still gets a working menu. */
213
+ export function phoneMenuPages(root: SerializedNode, sitePages: ReadonlyArray<NavPage> = []): { pages: NavPage[]; source: NavPagesSource } {
214
+ const own = navPages(root);
215
+ if (own.length) return { pages: own, source: NavPagesSource.Header };
216
+ return sitePages.length ? { pages: [...sitePages], source: NavPagesSource.Site } : { pages: [], source: NavPagesSource.None };
217
+ }
218
+
202
219
  /** A hand-built header: every page Link outside a drawer. */
203
220
  function collectHeaderLinks(root: SerializedNode): SerializedNode[] {
204
221
  const out: SerializedNode[] = [];
@@ -439,7 +456,7 @@ export function planNavShape(globals: ReadonlyArray<SerializedNode>, shape: NavS
439
456
  const kit = makeKit(out, root.id, options.newId);
440
457
 
441
458
  adoptRoles(root);
442
- const pages = navPages(root);
459
+ const { pages } = phoneMenuPages(root, options.pages);
443
460
  const wantDrawer = shape !== NavShape.Bar;
444
461
  const wantBar = shape !== NavShape.Drawer;
445
462
  const wantMenuButton = shape === NavShape.Drawer;
@@ -538,7 +555,7 @@ export interface AddBarItemResult {
538
555
  * The bar's Items list "Add item" (board 2): one more item, before More — the next navigation page the
539
556
  * bar does not link to yet (Home when every page is there). Refused at five (Q2). Input not mutated.
540
557
  */
541
- export function addBarItem(globals: ReadonlyArray<SerializedNode>, options: Pick<NavShapeOptions, 'newId'> = {}): AddBarItemResult {
558
+ export function addBarItem(globals: ReadonlyArray<SerializedNode>, options: Pick<NavShapeOptions, 'newId' | 'pages'> = {}): AddBarItemResult {
542
559
  const out = clone(globals) as SerializedNode[];
543
560
  const root = findNavRoot(out);
544
561
  const b = find(out, NavRole.Bar);
@@ -546,10 +563,43 @@ export function addBarItem(globals: ReadonlyArray<SerializedNode>, options: Pick
546
563
  const kids = b.children ?? (b.children = []);
547
564
  if (kids.length >= BAR_ITEMS_MAX) return { globals: out, added: false };
548
565
  const linked = new Set(kids.filter(c => c.role === NavRole.BarItem).map(c => prop(c, 'pageRoute')));
549
- const page = barPages(root, navPages(root), BAR_ITEMS_MAX + 1).find(p => !linked.has(p.route))
566
+ const page = barPages(root, phoneMenuPages(root, options.pages).pages, BAR_ITEMS_MAX + 1).find(p => !linked.has(p.route))
550
567
  ?? { label: NAV_SHAPE_COPY.home, route: HOME_ROUTE };
551
568
  const item = barItem(makeKit(out, root.id, options.newId), kids.length, page);
552
569
  const more = kids.findIndex(c => c.role === NavRole.More);
553
570
  if (more >= 0) kids.splice(more, 0, item); else kids.push(item);
554
571
  return { globals: out, added: true };
555
572
  }
573
+
574
+ export interface BarItemsEditResult {
575
+ globals: SerializedNode[];
576
+ /** False when nothing changed: no bar, an unknown item, or the bar would drop below three items (Q2). */
577
+ changed: boolean;
578
+ }
579
+
580
+ function barOf(globals: ReadonlyArray<SerializedNode>): { out: SerializedNode[]; kids: SerializedNode[] | null } {
581
+ const out = clone(globals) as SerializedNode[];
582
+ const b = find(out, NavRole.Bar);
583
+ return { out, kids: b ? (b.children ?? (b.children = [])) : null };
584
+ }
585
+
586
+ /**
587
+ * The bar's Items list "Remove" (board 2): the item goes, More included — refused below three items (Q2:
588
+ * 3–5), so the bar never shrinks into something a thumb cannot use. Input not mutated.
589
+ */
590
+ export function removeBarItem(globals: ReadonlyArray<SerializedNode>, itemId: string): BarItemsEditResult {
591
+ const { out, kids } = barOf(globals);
592
+ const i = kids ? kids.findIndex(c => c.id === itemId) : -1;
593
+ if (!kids || i < 0 || kids.length <= BAR_ITEMS_MIN) return { globals: out, changed: false };
594
+ kids.splice(i, 1);
595
+ return { globals: out, changed: true };
596
+ }
597
+
598
+ /** The bar's Items list drag-to-reorder (board 2): item `from` moves to `to`. Input not mutated. */
599
+ export function moveBarItem(globals: ReadonlyArray<SerializedNode>, from: number, to: number): BarItemsEditResult {
600
+ const { out, kids } = barOf(globals);
601
+ if (!kids || from === to || from < 0 || to < 0 || from >= kids.length || to >= kids.length) return { globals: out, changed: false };
602
+ const [item] = kids.splice(from, 1);
603
+ kids.splice(to, 0, item);
604
+ return { globals: out, changed: true };
605
+ }