@vectoriox/iox-ui 5.66.0 → 5.67.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.
@@ -11598,7 +11598,7 @@ function navShapeBreakpoint(globals) {
11598
11598
  }
11599
11599
  const prop = (n, name) => n.props?.[name];
11600
11600
  const isPageLink = (n) => prop(n, 'linkType') === LinkType.Page && typeof prop(n, 'pageRoute') === 'string';
11601
- function textOf(n) {
11601
+ function textOf$1(n) {
11602
11602
  if (n.type === LINK)
11603
11603
  return String(prop(n, 'label') ?? '');
11604
11604
  let words = '';
@@ -11610,7 +11610,7 @@ function textOf(n) {
11610
11610
  function navPages(root) {
11611
11611
  const links = findAll([root], NavRole.Link);
11612
11612
  const source = links.length ? links : collectHeaderLinks(root);
11613
- return source.filter(isPageLink).map(n => ({ label: textOf(n), route: String(prop(n, 'pageRoute')) }));
11613
+ return source.filter(isPageLink).map(n => ({ label: textOf$1(n), route: String(prop(n, 'pageRoute')) }));
11614
11614
  }
11615
11615
  /** Where a phone menu's pages come from: the header's own page Links, else the studio's pages. */
11616
11616
  var NavPagesSource;
@@ -11744,7 +11744,7 @@ function drawer(kit, root, pages, at) {
11744
11744
  const dr = part(kit, 'drawer', DIALOG$1, {
11745
11745
  placement: 'end', closeOnBackdrop: true, showCloseButton: false, ariaLabel: NAV_SHAPE_COPY.menu, closeAbove: at,
11746
11746
  }, 'dialog', NavRole.Drawer, { width: '86vw', maxWidth: '360px', display: 'flex', flexDirection: 'column', gap: '20px' });
11747
- const brandWords = textOf(find([root], NavRole.Brand) ?? root) || NAV_PART_LABELS[NavRole.Root];
11747
+ const brandWords = textOf$1(find([root], NavRole.Brand) ?? root) || NAV_PART_LABELS[NavRole.Root];
11748
11748
  const head = part(kit, 'drawer-head', CONTAINER, {}, 'box', NavRole.DrawerHead, { display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: '12px' });
11749
11749
  const close = part(kit, 'drawer-close', BUTTON, { tag: 'button', ariaLabel: NAV_SHAPE_COPY.closeMenu }, 'button-ghost', NavRole.DrawerClose, { ...MENU_BUTTON_LAY });
11750
11750
  close.children = [iconNode(kit, 'drawer-close-icon', CLOSE_ICON)];
@@ -11974,7 +11974,7 @@ function nameParts(root) {
11974
11974
  }
11975
11975
  const cta = find([root], NavRole.Cta);
11976
11976
  if (cta && !cta.label) {
11977
- const words = textOf(cta);
11977
+ const words = textOf$1(cta);
11978
11978
  if (words)
11979
11979
  cta.label = words;
11980
11980
  }
@@ -13352,6 +13352,9 @@ var BuildNoticeCode;
13352
13352
  BuildNoticeCode["ContentTypeNotPublic"] = "content-type-not-public";
13353
13353
  /** The stock library could not be read: the picture slots keep the empty box, and the run says so. */
13354
13354
  BuildNoticeCode["StockUnavailable"] = "stock-unavailable";
13355
+ /** The stock library was read but holds no photo for this kind of business (never built, or reviewed down to
13356
+ * none): the picture slots keep the empty box, and the run says so (I2.1 — never silently). */
13357
+ BuildNoticeCode["StockMissing"] = "stock-missing";
13355
13358
  })(BuildNoticeCode || (BuildNoticeCode = {}));
13356
13359
  var PreflightRefusalCode;
13357
13360
  (function (PreflightRefusalCode) {
@@ -15309,10 +15312,217 @@ function publishVerdict(audit, acknowledged = []) {
15309
15312
  }
15310
15313
 
15311
15314
  /**
15312
- * Stock photos in a built site (AI plan §3g / §3m, I2): a FREE picture from the platform's curated library, copied
15315
+ * How a stock photo is judged against a picture slot (AI plan §3g, I2.1) — pure and deterministic, so the same plan and
15316
+ * the same library always place the same photos. A photo is described by what the library's manifest carries (the
15317
+ * provider's tags, its orientation, the reviewer's subject and notes — each optional, derived when absent); a slot by
15318
+ * its INTENT (what kind of picture its section wants), its aspect ratio and the plan's words around it.
15319
+ */
15320
+ var StockOrientation;
15321
+ (function (StockOrientation) {
15322
+ StockOrientation["Horizontal"] = "horizontal";
15323
+ StockOrientation["Vertical"] = "vertical";
15324
+ StockOrientation["Square"] = "square";
15325
+ })(StockOrientation || (StockOrientation = {}));
15326
+ /** What a photo mainly shows — the reviewer may set it; otherwise it is read from the provider's tags. */
15327
+ var StockSubject;
15328
+ (function (StockSubject) {
15329
+ /** A room, a shop front, a garden — somewhere. */
15330
+ StockSubject["Place"] = "place";
15331
+ /** A close-up: a texture, a tool, an ingredient. */
15332
+ StockSubject["Detail"] = "detail";
15333
+ /** A thing a business sells or serves: a cake, a bottle, a bouquet. */
15334
+ StockSubject["Product"] = "product";
15335
+ })(StockSubject || (StockSubject = {}));
15336
+ /** What a slot wants to show. */
15337
+ var StockIntent;
15338
+ (function (StockIntent) {
15339
+ /** The hero / a page header / a call to action: wide and atmospheric. */
15340
+ StockIntent["Atmosphere"] = "atmosphere";
15341
+ /** A story or a visit section: the place. */
15342
+ StockIntent["Place"] = "place";
15343
+ /** A feature row: a detail of the work or the service. */
15344
+ StockIntent["Detail"] = "detail";
15345
+ /** A list item's picture (a product, a service, a dish). */
15346
+ StockIntent["Product"] = "product";
15347
+ })(StockIntent || (StockIntent = {}));
15348
+ var StockReviewVerdict;
15349
+ (function (StockReviewVerdict) {
15350
+ StockReviewVerdict["Approved"] = "approved";
15351
+ StockReviewVerdict["Rejected"] = "rejected";
15352
+ })(StockReviewVerdict || (StockReviewVerdict = {}));
15353
+ /** What the review found in a photo that keeps it out of every site (the licence: no person, no logo or brand). */
15354
+ var StockReviewFlag;
15355
+ (function (StockReviewFlag) {
15356
+ StockReviewFlag["Person"] = "person";
15357
+ StockReviewFlag["Logo"] = "logo";
15358
+ })(StockReviewFlag || (StockReviewFlag = {}));
15359
+ /** The section families a stock photo may stand in for a picture in, and what each wants. */
15360
+ const STOCK_FAMILY_INTENTS = {
15361
+ hero: StockIntent.Atmosphere,
15362
+ header: StockIntent.Atmosphere,
15363
+ cta: StockIntent.Atmosphere,
15364
+ story: StockIntent.Place,
15365
+ contact: StockIntent.Place,
15366
+ feature: StockIntent.Detail,
15367
+ content: StockIntent.Detail,
15368
+ };
15369
+ /** Per intent, the subjects it takes, best first. */
15370
+ const STOCK_INTENT_SUBJECTS = {
15371
+ [StockIntent.Atmosphere]: [StockSubject.Place, StockSubject.Detail, StockSubject.Product],
15372
+ [StockIntent.Place]: [StockSubject.Place, StockSubject.Detail, StockSubject.Product],
15373
+ [StockIntent.Detail]: [StockSubject.Detail, StockSubject.Product, StockSubject.Place],
15374
+ [StockIntent.Product]: [StockSubject.Product, StockSubject.Detail, StockSubject.Place],
15375
+ };
15376
+ /** The order slots choose in: the hero picks first, a list item last. */
15377
+ const STOCK_INTENT_ORDER = [StockIntent.Atmosphere, StockIntent.Place, StockIntent.Detail, StockIntent.Product];
15378
+ /** The words that tell a photo's subject when the reviewer did not name it (singular, lower case). */
15379
+ const STOCK_SUBJECT_WORDS = {
15380
+ [StockSubject.Place]: [
15381
+ 'room', 'interior', 'studio', 'salon', 'shop', 'store', 'boutique', 'restaurant', 'cafe', 'bar', 'bakery', 'cellar', 'kitchen',
15382
+ 'gym', 'clinic', 'office', 'lobby', 'hall', 'building', 'architecture', 'facade', 'exterior', 'street', 'garden', 'terrace',
15383
+ 'patio', 'pool', 'sauna', 'window', 'landscape', 'view', 'beach', 'forest', 'mountain', 'city', 'house', 'workshop', 'counter',
15384
+ 'shelf', 'reception', 'spa', 'hotel', 'bedroom', 'bathroom', 'living', 'courtyard', 'vineyard', 'market',
15385
+ ],
15386
+ [StockSubject.Detail]: [
15387
+ 'texture', 'detail', 'closeup', 'macro', 'pattern', 'fabric', 'wood', 'wooden', 'stone', 'marble', 'leaf', 'towel', 'tool',
15388
+ 'equipment', 'ingredient', 'spice', 'grain', 'bean', 'oil', 'water', 'steam', 'shadow', 'paper', 'thread', 'brush', 'ceramic',
15389
+ ],
15390
+ [StockSubject.Product]: [
15391
+ 'product', 'bottle', 'wine', 'beer', 'coffee', 'cup', 'mug', 'tea', 'cake', 'bread', 'pastry', 'croissant', 'cookie', 'dessert',
15392
+ 'chocolate', 'food', 'dish', 'plate', 'meal', 'pizza', 'salad', 'soup', 'burger', 'fruit', 'flower', 'bouquet', 'jewelry',
15393
+ 'jewellery', 'ring', 'necklace', 'cosmetic', 'cream', 'soap', 'perfume', 'candle', 'shoe', 'bag', 'dress', 'clothing', 'book', 'toy',
15394
+ ],
15395
+ };
15396
+ /** Words that say nothing about what a picture shows (the plan's sample and placeholder words among them). */
15397
+ const STOP_WORDS = new Set([
15398
+ 'the', 'and', 'with', 'for', 'your', 'our', 'you', 'from', 'into', 'this', 'that', 'here', 'there', 'about', 'page', 'section',
15399
+ 'sample', 'replace', 'image', 'photo', 'picture', 'sentence', 'words', 'own', 'how', 'what', 'who', 'are', 'all', 'one', 'two',
15400
+ 'three', 'more', 'less', 'very', 'tell', 'story', 'item', 'service', 'project',
15401
+ ]);
15402
+ const MIN_WORD = 3;
15403
+ const HTML_TAG = /<[^>]*>/g;
15404
+ const WORD_SPLIT = /[^\p{L}\p{N}]+/u;
15405
+ /** Nominal ratios (width / height) of an orientation when the manifest gives no size. */
15406
+ const ORIENTATION_RATIO = {
15407
+ [StockOrientation.Horizontal]: 3 / 2,
15408
+ [StockOrientation.Vertical]: 2 / 3,
15409
+ [StockOrientation.Square]: 1,
15410
+ };
15411
+ /** The share of a photo a slot may crop away and still be a fit: a portrait never fills a 21 / 9 band. */
15412
+ const STOCK_MIN_KEPT = 0.5;
15413
+ /** What an unknown shape keeps, for scoring only (neither rewarded nor refused). */
15414
+ const UNKNOWN_KEPT = 0.75;
15415
+ /** Score weights — subject, then shape, then the plan's own words, then the business type's imagery words. */
15416
+ const SUBJECT_POINTS = [4, 2, 1];
15417
+ const UNKNOWN_SUBJECT_POINTS = 1.5;
15418
+ const SHAPE_POINTS = 4;
15419
+ const WORD_POINTS = 2;
15420
+ const MAX_WORD_MATCHES = 3;
15421
+ const IMAGERY_POINTS = 1;
15422
+ const MAX_IMAGERY_MATCHES = 2;
15423
+ /**
15424
+ * A word as compared — a canonical form, never shown: lower case, a plain plural made singular, and `-ie` / `-y` one
15425
+ * ending (English cannot tell `cookies` from `pastries` by rule, so `cookie`, `cookies`, `pastry`, `pastries` all
15426
+ * fold to the same shape as their singular).
15427
+ */
15428
+ function stockWord(raw) {
15429
+ let w = raw.toLowerCase();
15430
+ if (w.length > 3 && w.endsWith('s') && !w.endsWith('ss') && !STOP_WORDS.has(w))
15431
+ w = w.slice(0, -1);
15432
+ return w.length > 3 && w.endsWith('ie') ? `${w.slice(0, -2)}y` : w;
15433
+ }
15434
+ /** The meaningful words of a text, in order, each once. */
15435
+ function stockWords(...texts) {
15436
+ const out = new Set();
15437
+ for (const text of texts) {
15438
+ if (typeof text !== 'string')
15439
+ continue;
15440
+ for (const raw of text.replace(HTML_TAG, ' ').split(WORD_SPLIT)) {
15441
+ const w = stockWord(raw);
15442
+ if (w.length >= MIN_WORD && !STOP_WORDS.has(w) && !/^\d+$/.test(w))
15443
+ out.add(w);
15444
+ }
15445
+ }
15446
+ return [...out];
15447
+ }
15448
+ /** A CSS aspect ratio (`4 / 3`, `1.5`) as width / height, or undefined. */
15449
+ function aspectRatioOf(aspect) {
15450
+ if (typeof aspect === 'number')
15451
+ return aspect > 0 ? aspect : undefined;
15452
+ if (typeof aspect !== 'string')
15453
+ return undefined;
15454
+ const [w, h] = aspect.split('/').map(s => Number(s.trim()));
15455
+ const ratio = h === undefined ? w : w / h;
15456
+ return Number.isFinite(ratio) && ratio > 0 ? ratio : undefined;
15457
+ }
15458
+ /** A photo may be placed: not rejected and nothing flagged by the review. */
15459
+ function stockPhotoUsable(photo) {
15460
+ return photo.review?.verdict !== StockReviewVerdict.Rejected && !(photo.review?.flags?.length);
15461
+ }
15462
+ /** The words a photo is placed by: its tags (else its alt — the provider's own words) and the reviewer's notes. */
15463
+ function stockPhotoWords(photo) {
15464
+ const tags = photo.tags?.length ? photo.tags.join(', ') : photo.alt;
15465
+ return stockWords(tags, photo.review?.notes);
15466
+ }
15467
+ /** Its width / height: the size when known, else its orientation's nominal shape, else undefined. */
15468
+ function stockPhotoRatio(photo) {
15469
+ if (photo.width && photo.height && photo.width > 0 && photo.height > 0)
15470
+ return photo.width / photo.height;
15471
+ return photo.orientation ? ORIENTATION_RATIO[photo.orientation] : undefined;
15472
+ }
15473
+ /** The orientation of a width / height ratio. */
15474
+ function orientationOfRatio(ratio) {
15475
+ if (ratio > 1.05)
15476
+ return StockOrientation.Horizontal;
15477
+ return ratio < 0.95 ? StockOrientation.Vertical : StockOrientation.Square;
15478
+ }
15479
+ /** The reviewer's subject, else the one its words name most (ties: the enum's order), else undefined. */
15480
+ function stockPhotoSubject(photo) {
15481
+ if (photo.subject && Object.values(StockSubject).includes(photo.subject))
15482
+ return photo.subject;
15483
+ const words = new Set(stockPhotoWords(photo));
15484
+ let best;
15485
+ let most = 0;
15486
+ for (const subject of Object.values(StockSubject)) {
15487
+ const n = STOCK_SUBJECT_WORDS[subject].filter(w => words.has(stockWord(w))).length;
15488
+ if (n > most) {
15489
+ most = n;
15490
+ best = subject;
15491
+ }
15492
+ }
15493
+ return best;
15494
+ }
15495
+ /** The share of the photo a slot shows (1 = the same shape), or undefined when either shape is unknown. */
15496
+ function stockKept(photo, aspect) {
15497
+ const ratio = stockPhotoRatio(photo);
15498
+ if (!ratio || !aspect)
15499
+ return undefined;
15500
+ return Math.min(ratio, aspect) / Math.max(ratio, aspect);
15501
+ }
15502
+ /** A photo fits a slot when the slot keeps at least half of it (an unknown shape fits). */
15503
+ function stockFits(photo, need) {
15504
+ const kept = stockKept(photo, need.aspect);
15505
+ return kept === undefined || kept >= STOCK_MIN_KEPT;
15506
+ }
15507
+ /** How well a photo answers a slot: its subject for the intent, its shape, the plan's words and the type's imagery. */
15508
+ function scoreStockPhoto(photo, need, imagery = []) {
15509
+ const subject = stockPhotoSubject(photo);
15510
+ const rank = subject ? STOCK_INTENT_SUBJECTS[need.intent].indexOf(subject) : -1;
15511
+ const subjectPoints = subject ? SUBJECT_POINTS[rank] ?? 0 : UNKNOWN_SUBJECT_POINTS;
15512
+ const shapePoints = SHAPE_POINTS * (stockKept(photo, need.aspect) ?? UNKNOWN_KEPT);
15513
+ const words = new Set(stockPhotoWords(photo));
15514
+ const matched = need.words.filter(w => words.has(w)).length;
15515
+ const imageryMatched = imagery.filter(w => words.has(w)).length;
15516
+ return subjectPoints + shapePoints + WORD_POINTS * Math.min(MAX_WORD_MATCHES, matched) + IMAGERY_POINTS * Math.min(MAX_IMAGERY_MATCHES, imageryMatched);
15517
+ }
15518
+
15519
+ /**
15520
+ * Stock photos in a built site (AI plan §3g / §3m, I2 + I2.1): a FREE picture from the platform's curated library, copied
15313
15521
  * into the studio's own storage (never hot-linked), placed only in an EMPTY picture slot of a section that may show
15314
15522
  * one — never the owner's work (a collection, a detail page, a gallery: what the nine portfolio types sell is theirs),
15315
- * never a person (testimonials, the team), never anywhere when the business type says `stock: false`. Its alt text is
15523
+ * never a person (testimonials, the team), never anywhere when the business type says `stock: false` — and, since I2.1
15524
+ * (Doron, 2026-10-28), in the picture of a MARKED sample item (a product, a service, a dish) of a type that may show
15525
+ * stock. Which photo goes where is chosen by what the slot is about (`stock-match.ts`). Its alt text is
15316
15526
  * the provider's description of what it shows (never a claim), and the credit line its licence requires — when it
15317
15527
  * requires one — is placed in the footer. Pure: the build step does the copying.
15318
15528
  */
@@ -15341,30 +15551,124 @@ const STOCK_PROVIDER_TERMS = {
15341
15551
  },
15342
15552
  };
15343
15553
  /** The section families a stock photo may stand in for a picture in. */
15344
- const STOCK_FAMILIES = new Set(['hero', 'feature', 'content', 'story', 'cta', 'contact', 'header']);
15345
- /** The empty picture slots of a composed site a stock photo may fill, in page and section order. */
15554
+ const STOCK_FAMILIES = new Set(Object.keys(STOCK_FAMILY_INTENTS));
15555
+ /** The aspect a list item's picture is shown at (Card v2's Image top, `composite-core` card layouts). */
15556
+ const STOCK_ITEM_ASPECT = 4 / 3;
15557
+ var StockSlotKind;
15558
+ (function (StockSlotKind) {
15559
+ /** An empty Image of a composed section. */
15560
+ StockSlotKind["Section"] = "section";
15561
+ /** The image field of a marked sample item. */
15562
+ StockSlotKind["Item"] = "item";
15563
+ })(StockSlotKind || (StockSlotKind = {}));
15564
+ /** Where a photo may not appear twice: one page; one content type's samples (a list shows them together). */
15565
+ function stockGroupOf(target) {
15566
+ return target.kind === StockSlotKind.Section ? `page:${target.owner}` : `items:${target.contentType}`;
15567
+ }
15568
+ const TEXT_PROPS = ['content', 'label'];
15569
+ /** The words of a subtree's text (Headings, Text, Links), the given node left out. */
15570
+ function textOf(nodes, skip) {
15571
+ const out = [];
15572
+ walkSerialized(nodes, n => {
15573
+ if (n.id === skip)
15574
+ return;
15575
+ for (const prop of TEXT_PROPS)
15576
+ if (typeof n.props[prop] === 'string')
15577
+ out.push(n.props[prop]);
15578
+ });
15579
+ return out;
15580
+ }
15581
+ /**
15582
+ * The empty picture slots of a composed site a stock photo may fill, in page and section order — each with its intent
15583
+ * (the section's family), its aspect, and the words around it: the text beside it (its row), the section's slots and
15584
+ * the page's name and search words.
15585
+ */
15346
15586
  function stockSlots(plan, composed) {
15347
15587
  const empty = new Set(composed.placeholders.filter(p => p.kind === PlaceholderKind.Image).map(p => p.nodeId));
15348
15588
  const out = [];
15349
15589
  composed.pages.forEach((page, p) => {
15590
+ const pagePlan = plan.pages[p];
15350
15591
  page.layout.forEach((root, s) => {
15351
- const pattern = plan.pages[p]?.sections[s]?.pattern;
15592
+ const section = pagePlan?.sections[s];
15593
+ const pattern = section?.pattern;
15352
15594
  const family = patternById(pattern)?.family;
15353
- if (!pattern || !family || !STOCK_FAMILIES.has(family))
15595
+ const intent = family ? STOCK_FAMILY_INTENTS[family] : undefined;
15596
+ if (!pattern || !intent)
15354
15597
  return;
15355
- walkSerialized([root], n => {
15356
- if (n.type === 'Image' && empty.has(n.id) && !n.bindings.length)
15357
- out.push({ nodeId: n.id, owner: page.key, pattern });
15598
+ const sectionWords = [...Object.values(section?.slots ?? {}), pagePlan?.name, pagePlan?.seo?.title, pagePlan?.seo?.description];
15599
+ walkSerialized([root], (n, ancestors) => {
15600
+ if (n.type !== 'Image' || !empty.has(n.id) || n.bindings.length)
15601
+ return;
15602
+ const row = ancestors[ancestors.length - 1];
15603
+ out.push({
15604
+ kind: StockSlotKind.Section, nodeId: n.id, owner: page.key, pattern, intent,
15605
+ aspect: aspectRatioOf(n.styleProps['aspectRatio']),
15606
+ words: stockWords(...(row ? textOf([row], n.id) : []), ...sectionWords),
15607
+ });
15358
15608
  });
15359
15609
  });
15360
15610
  });
15361
15611
  return out;
15362
15612
  }
15363
- /** Which photo goes where: the library's photos in order, one per slot, never twice; none when the type says no stock. */
15364
- function planStockPhotos(slots, photos, stockAllowed) {
15613
+ /**
15614
+ * The sample items' pictures a stock photo may fill: per content type with an `image` role, each MARKED sample whose
15615
+ * image is still a bundled sample (`{sampleImage:k}`), with the type's and the item's own words.
15616
+ */
15617
+ function stockItemSlots(plan, composed) {
15618
+ const out = [];
15619
+ for (const type of plan.contentTypes) {
15620
+ const field = roleField(type, ContentRole.Image);
15621
+ if (!field)
15622
+ continue;
15623
+ const words = (item) => [type.label, type.name, ...[ContentRole.Title, ContentRole.Summary].map(r => item[roleField(type, r) ?? ''])];
15624
+ (composed.items[type.name] ?? []).forEach((item, index) => {
15625
+ if (!item.isSample || parseSampleImageRef(item[field]) === null)
15626
+ return;
15627
+ out.push({ kind: StockSlotKind.Item, contentType: type.name, index, field, intent: StockIntent.Product, aspect: STOCK_ITEM_ASPECT, words: stockWords(...words(item)) });
15628
+ });
15629
+ }
15630
+ return out;
15631
+ }
15632
+ const intentRank = (intent) => STOCK_INTENT_ORDER.indexOf(intent);
15633
+ /**
15634
+ * Which photo goes where (I2.1). Slots choose in intent order (the hero first, list items last; page order within),
15635
+ * each taking the best-scoring photo that FITS its shape (`stockFits`) and is not already on its page (or among its
15636
+ * type's samples) — a photo no slot has taken yet always before one another page shows. Ties go to the library's
15637
+ * order. A slot nothing fits keeps its empty box (a sample its abstract picture). None at all when the type says no
15638
+ * stock, and never a photo the review rejected or flagged. Returned in the slots' order.
15639
+ */
15640
+ function planStockPhotos(slots, photos, stockAllowed, opts = {}) {
15365
15641
  if (!stockAllowed)
15366
15642
  return [];
15367
- return slots.slice(0, photos.length).map((slot, i) => ({ ...slot, photo: photos[i] }));
15643
+ const usable = photos.filter(stockPhotoUsable);
15644
+ const imagery = stockWords(opts.imagery);
15645
+ const order = slots.map((slot, i) => ({ slot, i })).sort((a, b) => intentRank(a.slot.intent) - intentRank(b.slot.intent) || a.i - b.i);
15646
+ const used = new Set();
15647
+ const shownIn = new Map();
15648
+ const placed = new Map();
15649
+ for (const { slot, i } of order) {
15650
+ const group = stockGroupOf(slot);
15651
+ const here = shownIn.get(group) ?? new Set();
15652
+ const candidates = usable.filter(p => !here.has(p.key) && stockFits(p, slot));
15653
+ const fresh = candidates.filter(p => !used.has(p.key));
15654
+ const pool = fresh.length ? fresh : candidates;
15655
+ let best;
15656
+ let bestScore = -Infinity;
15657
+ for (const photo of pool) {
15658
+ const score = scoreStockPhoto(photo, slot, imagery);
15659
+ if (score > bestScore) {
15660
+ best = photo;
15661
+ bestScore = score;
15662
+ }
15663
+ }
15664
+ if (!best)
15665
+ continue;
15666
+ used.add(best.key);
15667
+ here.add(best.key);
15668
+ shownIn.set(group, here);
15669
+ placed.set(i, { ...slot, photo: best });
15670
+ }
15671
+ return slots.map((_, i) => placed.get(i)).filter((p) => !!p);
15368
15672
  }
15369
15673
  /** A placed photo's copy in the studio's storage: the key the build writes it under. */
15370
15674
  function stockCopyKey(photo) {
@@ -15384,6 +15688,18 @@ function applyStockPhotos(trees, placed) {
15384
15688
  });
15385
15689
  return changed;
15386
15690
  }
15691
+ /** Put the studio's copy of each photo into its sample item's image field. Returns the items changed. */
15692
+ function applyStockItemPhotos(items, placed) {
15693
+ let changed = 0;
15694
+ for (const p of placed) {
15695
+ const item = items[p.contentType]?.[p.index];
15696
+ if (!item)
15697
+ continue;
15698
+ item[p.field] = p.url;
15699
+ changed++;
15700
+ }
15701
+ return changed;
15702
+ }
15387
15703
  /** The line a picture's licence requires on the site, or null when it requires none (`STOCK_PROVIDER_TERMS`). */
15388
15704
  function stockCreditLine(credit) {
15389
15705
  return STOCK_PROVIDER_TERMS[credit.provider]?.credit ?? null;
@@ -15437,5 +15753,5 @@ function addStockCredits(footer, credits) {
15437
15753
  * Generated bundle index. Do not edit.
15438
15754
  */
15439
15755
 
15440
- export { ACTIVE_BUILD_STATUSES, AI_REFUSAL_NEXT, AI_REFUSAL_STATUS, AI_STEPS, AI_STEP_LABELS, AI_STEP_NEEDS, AI_STEP_PART_TOKENS, AI_STEP_TYPICAL_TOKENS, AiCallStatus, AiCapability, AiFeature, AiGenerationStage, AiKeyMode, AiLimit, AiNextKind, AiProviderId, AiRefusalCode, AiStep, BAR_ITEMS_MAX, BAR_ITEMS_MIN, BASELINE_ROLES, BUILD_STEPS, BaselineRole, BindScope, BuildNoticeCode, BuildStep, CONTENT_ROLE_MEANINGS, CREDITS_PER_USD, CardBindTarget, CheckSeverity, ComposeError, ComposeIssueCode, ContentRole, ContentTypeAction, CreatedKind, DANGLING_LINK_CODES, DEFAULT_PHONE_MENU_BREAKPOINT, DEFAULT_SITE_LOCALE, ENTRANCE_EASING, ENTRANCE_ORIGIN_LOOK, ENTRANCE_TRIGGER, EntrancePart, EntranceSide, FACT_KEYS, FINAL_GENERATION_STAGES, FINDING_SEVERITY, GRAMMAR_TOO_LARGE_MESSAGE, GlobalSlot, HEBREW_PREFIXES, JSON_SCHEMA_BANNED_KEYWORDS, JsonSchemaShapeError, KeptReason, NAV_PART_LABELS, NAV_SHAPE_COPY, NEUTRAL_STYLE_PROPS, NODE_BASELINES, NO_TOKENS, NavPagesSource, NavRole, NavShape, NavShapeError, PAGE_ARCHETYPES, PLAN_EDITS_ANSWER_SHAPES, PLAN_EDITS_PART_SCHEMA_NAMES, PLAN_EDIT_OP_PART, PLAN_EDIT_SHAPE, PLAN_EDIT_WIRE_KEYS, PLAN_PARTS, PLAN_PART_KEYS, PLAN_WIRE_DERIVED, PLAN_WIRE_PART_SCHEMA_NAMES, PLAN_WIRE_SHAPE, PRICING_PATTERN, PUBLISH_BLOCKING_PLACEHOLDERS, PageDataKind, PathIssueCollector, PatternDirective, PatternGlobalPosition, PatternOpens, PatternPlaceholder, PatternRecipe, PhoneMenuBreakpoint, PlaceholderKind, PlanChangeAction, PlanChangeKind, PlanDataSourceMode, PlanDataSourceType, PlanEditIssueCode, PlanEditOp, PlanEditsOutcome, PlanPart, PlanWireEntry, PlanWireShapeError, PlanWordIssueCode, PreflightRefusalCode, PublishRefusalCode, REQUIRED_CONTENT_ROLES, RelativeOp, SAMPLE_IMAGE_REF, SITE_CHECKS, SITE_FACT_LABELS, SITE_HEADER_PATTERN, SITE_LIBRARY, SITE_LOCALES, SITE_NEED_IDS, SITE_PATTERNS, SITE_PLAN_VERSION, SITE_TEMPLATES, SPEC_SITE_ARCHETYPES, SPEC_SITE_COPY_LENGTHS, SPEC_SITE_DENSITY_COPY, SPEC_SITE_FACTS, SPEC_SITE_LIBRARY, SPEC_SITE_LOCALES, SPEC_SITE_NEED_IDS, SPEC_SITE_PATTERNS, SPEC_SITE_PLAN_SHAPE, SPEC_SITE_REQUIRED_ROLES, SPEC_SITE_ROLES, SPEC_SITE_TEMPLATES, STOCK_FAMILIES, STOCK_PROVIDER_TERMS, STRICT_SCHEMA_BUDGET, STRICT_SCHEMA_LIMITS, STUDIO_TEMPLATE, SiteBuildKind, SiteBuildStatus, SiteCheckId, SiteFindingCode, SiteLocale, SitePlanError, SitePlanIssueCode, SitePlanSourceKind, SiteTemplateError, SiteTemplateId, StepState, StockProviderId, addBarItem, addStockCredits, aiStepParts, applyPlanEdits, applyStockPhotos, archetypeIssues, assertStyleSlots, assertValidSitePlan, auditSite, barIconForRoute, blocksPublish, buildKeys, buildStepFacts, builtHomePageId, composeNavHeader, composeSection, composeSite, contentTypePreviewFields, contentTypeToWire, copyLengthOf, costUsd, createSerializedNode, creditsOfUsd, dataSourceToWire, derivedDataSources, describePlanData, describeShapeKey, entranceAction, entranceInteraction, estimateStepCredits, expandPlanWire, expandWireContentType, expandWireDataSource, expandWireEdit, expandWireField, expandWirePage, expandWireSection, failedBuildStep, fieldToWire, findNavRoot, isActiveBuild, isNonEmptyString, isPlainObject, isRtlLocale, jsonAnyOf, jsonBoolean, jsonClosedObject, jsonInteger, jsonListOf, jsonOneOf, jsonSchemaKeywords, jsonSchemaObjects, jsonShapedObject, jsonText, modelCanDo, moveBarItem, navPages, navShapeBreakpoint, navShapeOf, nextSourceOf, nodeBindPaths, pageToWire, parsePatternPath, parseSampleImageRef, pathAt, patternById, patternHasForm, patternLinkSlots, patternListGap, patternManifest, patternNeedsBinding, patternNodes, patternOptionalParts, patternRoleNeeds, patternScopes, patternSlotBudgets, patternSlots, patternTakesBinding, phoneMenuPages, planEditOps, planEditToWire, planEditWireGaps, planEditWireKeys, planEditWireOps, planEditWireShape, planEditsFromWire, planEditsJsonSchema, planEditsToWire, planEntrances, planFromTemplate, planNavShape, planPartOfPath, planPartToWire, planStockPhotos, planToWire, planWireGaps, planWirePartJsonSchema, planWirePartShape, planWordIssues, publishRefusal, publishVerdict, recipeListBinds, releasedLayoutDefaults, removeBarItem, requiredAcknowledgements, requiredShapeKeys, roleField, routeMatches, routeParams, sampleImageRef, sectionEntrances, sectionPatternIds, sectionToWire, setStyleProps, shapeKeys, showcaseGlobals, showcasePages, showcasePlans, siteNavPages, slotBudget, stockCopyKey, stockCreditLine, stockCreditLines, stockCreditNode, stockCreditNodeId, stockSlots, strictSchemaComplexity, strictSchemaRefusal, typicalStepTokens, validatePlanEdits, validateSitePlan, walkSerialized, wireContentTypeJsonSchema, wireDataSourceJsonSchema, wireFieldJsonSchema, wirePageJsonSchema, wireSectionJsonSchema, withDerivedSources, wordPattern, writtenTexts };
15756
+ export { ACTIVE_BUILD_STATUSES, AI_REFUSAL_NEXT, AI_REFUSAL_STATUS, AI_STEPS, AI_STEP_LABELS, AI_STEP_NEEDS, AI_STEP_PART_TOKENS, AI_STEP_TYPICAL_TOKENS, AiCallStatus, AiCapability, AiFeature, AiGenerationStage, AiKeyMode, AiLimit, AiNextKind, AiProviderId, AiRefusalCode, AiStep, BAR_ITEMS_MAX, BAR_ITEMS_MIN, BASELINE_ROLES, BUILD_STEPS, BaselineRole, BindScope, BuildNoticeCode, BuildStep, CONTENT_ROLE_MEANINGS, CREDITS_PER_USD, CardBindTarget, CheckSeverity, ComposeError, ComposeIssueCode, ContentRole, ContentTypeAction, CreatedKind, DANGLING_LINK_CODES, DEFAULT_PHONE_MENU_BREAKPOINT, DEFAULT_SITE_LOCALE, ENTRANCE_EASING, ENTRANCE_ORIGIN_LOOK, ENTRANCE_TRIGGER, EntrancePart, EntranceSide, FACT_KEYS, FINAL_GENERATION_STAGES, FINDING_SEVERITY, GRAMMAR_TOO_LARGE_MESSAGE, GlobalSlot, HEBREW_PREFIXES, JSON_SCHEMA_BANNED_KEYWORDS, JsonSchemaShapeError, KeptReason, NAV_PART_LABELS, NAV_SHAPE_COPY, NEUTRAL_STYLE_PROPS, NODE_BASELINES, NO_TOKENS, NavPagesSource, NavRole, NavShape, NavShapeError, PAGE_ARCHETYPES, PLAN_EDITS_ANSWER_SHAPES, PLAN_EDITS_PART_SCHEMA_NAMES, PLAN_EDIT_OP_PART, PLAN_EDIT_SHAPE, PLAN_EDIT_WIRE_KEYS, PLAN_PARTS, PLAN_PART_KEYS, PLAN_WIRE_DERIVED, PLAN_WIRE_PART_SCHEMA_NAMES, PLAN_WIRE_SHAPE, PRICING_PATTERN, PUBLISH_BLOCKING_PLACEHOLDERS, PageDataKind, PathIssueCollector, PatternDirective, PatternGlobalPosition, PatternOpens, PatternPlaceholder, PatternRecipe, PhoneMenuBreakpoint, PlaceholderKind, PlanChangeAction, PlanChangeKind, PlanDataSourceMode, PlanDataSourceType, PlanEditIssueCode, PlanEditOp, PlanEditsOutcome, PlanPart, PlanWireEntry, PlanWireShapeError, PlanWordIssueCode, PreflightRefusalCode, PublishRefusalCode, REQUIRED_CONTENT_ROLES, RelativeOp, SAMPLE_IMAGE_REF, SITE_CHECKS, SITE_FACT_LABELS, SITE_HEADER_PATTERN, SITE_LIBRARY, SITE_LOCALES, SITE_NEED_IDS, SITE_PATTERNS, SITE_PLAN_VERSION, SITE_TEMPLATES, SPEC_SITE_ARCHETYPES, SPEC_SITE_COPY_LENGTHS, SPEC_SITE_DENSITY_COPY, SPEC_SITE_FACTS, SPEC_SITE_LIBRARY, SPEC_SITE_LOCALES, SPEC_SITE_NEED_IDS, SPEC_SITE_PATTERNS, SPEC_SITE_PLAN_SHAPE, SPEC_SITE_REQUIRED_ROLES, SPEC_SITE_ROLES, SPEC_SITE_TEMPLATES, STOCK_FAMILIES, STOCK_FAMILY_INTENTS, STOCK_INTENT_ORDER, STOCK_INTENT_SUBJECTS, STOCK_ITEM_ASPECT, STOCK_MIN_KEPT, STOCK_PROVIDER_TERMS, STOCK_SUBJECT_WORDS, STRICT_SCHEMA_BUDGET, STRICT_SCHEMA_LIMITS, STUDIO_TEMPLATE, SiteBuildKind, SiteBuildStatus, SiteCheckId, SiteFindingCode, SiteLocale, SitePlanError, SitePlanIssueCode, SitePlanSourceKind, SiteTemplateError, SiteTemplateId, StepState, StockIntent, StockOrientation, StockProviderId, StockReviewFlag, StockReviewVerdict, StockSlotKind, StockSubject, addBarItem, addStockCredits, aiStepParts, applyPlanEdits, applyStockItemPhotos, applyStockPhotos, archetypeIssues, aspectRatioOf, assertStyleSlots, assertValidSitePlan, auditSite, barIconForRoute, blocksPublish, buildKeys, buildStepFacts, builtHomePageId, composeNavHeader, composeSection, composeSite, contentTypePreviewFields, contentTypeToWire, copyLengthOf, costUsd, createSerializedNode, creditsOfUsd, dataSourceToWire, derivedDataSources, describePlanData, describeShapeKey, entranceAction, entranceInteraction, estimateStepCredits, expandPlanWire, expandWireContentType, expandWireDataSource, expandWireEdit, expandWireField, expandWirePage, expandWireSection, failedBuildStep, fieldToWire, findNavRoot, isActiveBuild, isNonEmptyString, isPlainObject, isRtlLocale, jsonAnyOf, jsonBoolean, jsonClosedObject, jsonInteger, jsonListOf, jsonOneOf, jsonSchemaKeywords, jsonSchemaObjects, jsonShapedObject, jsonText, modelCanDo, moveBarItem, navPages, navShapeBreakpoint, navShapeOf, nextSourceOf, nodeBindPaths, orientationOfRatio, pageToWire, parsePatternPath, parseSampleImageRef, pathAt, patternById, patternHasForm, patternLinkSlots, patternListGap, patternManifest, patternNeedsBinding, patternNodes, patternOptionalParts, patternRoleNeeds, patternScopes, patternSlotBudgets, patternSlots, patternTakesBinding, phoneMenuPages, planEditOps, planEditToWire, planEditWireGaps, planEditWireKeys, planEditWireOps, planEditWireShape, planEditsFromWire, planEditsJsonSchema, planEditsToWire, planEntrances, planFromTemplate, planNavShape, planPartOfPath, planPartToWire, planStockPhotos, planToWire, planWireGaps, planWirePartJsonSchema, planWirePartShape, planWordIssues, publishRefusal, publishVerdict, recipeListBinds, releasedLayoutDefaults, removeBarItem, requiredAcknowledgements, requiredShapeKeys, roleField, routeMatches, routeParams, sampleImageRef, scoreStockPhoto, sectionEntrances, sectionPatternIds, sectionToWire, setStyleProps, shapeKeys, showcaseGlobals, showcasePages, showcasePlans, siteNavPages, slotBudget, stockCopyKey, stockCreditLine, stockCreditLines, stockCreditNode, stockCreditNodeId, stockFits, stockGroupOf, stockItemSlots, stockKept, stockPhotoRatio, stockPhotoSubject, stockPhotoUsable, stockPhotoWords, stockSlots, stockWord, stockWords, strictSchemaComplexity, strictSchemaRefusal, typicalStepTokens, validatePlanEdits, validateSitePlan, walkSerialized, wireContentTypeJsonSchema, wireDataSourceJsonSchema, wireFieldJsonSchema, wirePageJsonSchema, wireSectionJsonSchema, withDerivedSources, wordPattern, writtenTexts };
15441
15757
  //# sourceMappingURL=vectoriox-iox-ui-generation-core.mjs.map