@vectoriox/iox-ui 5.50.0 → 5.52.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 { MOOD_LABELS, STYLE_PRESETS, flavorPackById, parseFontFamilies, SPEC_AXES, parseHexColor, rgbToOklab, MOOD_IDS, presetChoices } from '@vectoriox/iox-ui/design-core';
1
+ import { MOOD_IDS, MOOD_LABELS, STYLE_PRESETS, flavorPackById, parseFontFamilies, SPEC_AXES, parseHexColor, rgbToOklab, presetChoices } from '@vectoriox/iox-ui/design-core';
2
2
  import { shapeKeys, requiredShapeKeys, PathIssueCollector, isPlainObject, pathAt, FACT_KEYS, SPEC_SITE_LIBRARY, patternById, SITE_FACT_LABELS, SITE_TEMPLATES, STUDIO_TEMPLATE } from '@vectoriox/iox-ui/generation-core';
3
3
 
4
4
  // GENERATED from iox-ai-guidance flavor-spec.v1.json — do not edit; re-run scripts/gen-flavor-spec.mjs
@@ -170,19 +170,23 @@ const SPEC_BUSINESS_TYPES = [
170
170
  "good": [
171
171
  {
172
172
  "preset": "night-gallery",
173
- "reason": "Pictures sit on black, as in a darkened gallery."
173
+ "reason": "Pictures sit on black, as in a darkened gallery.",
174
+ "reasonHe": "התמונות יושבות על שחור, כמו בגלריה חשוכה."
174
175
  },
175
176
  {
176
177
  "preset": "gallery-minimal",
177
- "reason": "White space and a quiet serif keep every eye on the photos."
178
+ "reason": "White space and a quiet serif keep every eye on the photos.",
179
+ "reasonHe": "מרחב לבן וסריף שקט משאירים את כל תשומת הלב על התמונות."
178
180
  },
179
181
  {
180
182
  "preset": "frosted-portfolio",
181
- "reason": "Soft grey glass frames the pictures without competing."
183
+ "reason": "Soft grey glass frames the pictures without competing.",
184
+ "reasonHe": "זכוכית אפורה ורכה ממסגרת את התמונות בלי להתחרות בהן."
182
185
  },
183
186
  {
184
187
  "preset": "swiss-grid",
185
- "reason": "A strict grid lines your photos up like contact sheets."
188
+ "reason": "A strict grid lines your photos up like contact sheets.",
189
+ "reasonHe": "גריד קפדני מסדר את התמונות כמו גיליון קונטקט."
186
190
  }
187
191
  ],
188
192
  "colour": {
@@ -270,19 +274,23 @@ const SPEC_BUSINESS_TYPES = [
270
274
  "good": [
271
275
  {
272
276
  "preset": "gallery-minimal",
273
- "reason": "Calm white rooms let the materials show."
277
+ "reason": "Calm white rooms let the materials show.",
278
+ "reasonHe": "חללים לבנים ורגועים נותנים לחומרים לבלוט."
274
279
  },
275
280
  {
276
281
  "preset": "quiet-luxury",
277
- "reason": "Warm bronze says considered and high-end."
282
+ "reason": "Warm bronze says considered and high-end.",
283
+ "reasonHe": "ברונזה חמה אומרת: מוקפד ויוקרתי."
278
284
  },
279
285
  {
280
286
  "preset": "warm-editorial",
281
- "reason": "Reads like a design magazine feature about your rooms."
287
+ "reason": "Reads like a design magazine feature about your rooms.",
288
+ "reasonHe": "נקרא כמו כתבה במגזין עיצוב על החללים שלכם."
282
289
  },
283
290
  {
284
291
  "preset": "frosted-portfolio",
285
- "reason": "Light, airy panels around big project photos."
292
+ "reason": "Light, airy panels around big project photos.",
293
+ "reasonHe": "משטחים בהירים ואווריריים סביב תמונות פרויקט גדולות."
286
294
  }
287
295
  ],
288
296
  "colour": {
@@ -375,19 +383,23 @@ const SPEC_BUSINESS_TYPES = [
375
383
  "good": [
376
384
  {
377
385
  "preset": "swiss-grid",
378
- "reason": "A strict grid and clean type, like a drawing set."
386
+ "reason": "A strict grid and clean type, like a drawing set.",
387
+ "reasonHe": "גריד קפדני וטיפוגרפיה נקייה, כמו סט שרטוטים."
379
388
  },
380
389
  {
381
390
  "preset": "gallery-minimal",
382
- "reason": "Big images and generous margins, like a monograph."
391
+ "reason": "Big images and generous margins, like a monograph.",
392
+ "reasonHe": "תמונות גדולות ושוליים נדיבים, כמו מונוגרפיה."
383
393
  },
384
394
  {
385
395
  "preset": "night-gallery",
386
- "reason": "Buildings at dusk on a dark page."
396
+ "reason": "Buildings at dusk on a dark page.",
397
+ "reasonHe": "בניינים בשעת דמדומים על דף כהה."
387
398
  },
388
399
  {
389
400
  "preset": "frosted-portfolio",
390
- "reason": "Soft glass over large project photos."
401
+ "reason": "Soft glass over large project photos.",
402
+ "reasonHe": "זכוכית רכה מעל תמונות פרויקט גדולות."
391
403
  }
392
404
  ],
393
405
  "colour": {
@@ -479,19 +491,23 @@ const SPEC_BUSINESS_TYPES = [
479
491
  "good": [
480
492
  {
481
493
  "preset": "zine",
482
- "reason": "Shows you take risks with type and colour."
494
+ "reason": "Shows you take risks with type and colour.",
495
+ "reasonHe": "מראה שאתם לא מפחדים להעז בטיפוגרפיה ובצבע."
483
496
  },
484
497
  {
485
498
  "preset": "tech-studio",
486
- "reason": "Dark and exact, for digital work."
499
+ "reason": "Dark and exact, for digital work.",
500
+ "reasonHe": "כהה ומדויק, לעבודה דיגיטלית."
487
501
  },
488
502
  {
489
503
  "preset": "bold-maker",
490
- "reason": "Loud outlines that clients remember."
504
+ "reason": "Loud outlines that clients remember.",
505
+ "reasonHe": "קווי מתאר בולטים שלקוחות זוכרים."
491
506
  },
492
507
  {
493
508
  "preset": "swiss-grid",
494
- "reason": "Confident, classic design-studio grid."
509
+ "reason": "Confident, classic design-studio grid.",
510
+ "reasonHe": "גריד בטוח וקלאסי של סטודיו לעיצוב."
495
511
  }
496
512
  ],
497
513
  "colour": {
@@ -573,19 +589,23 @@ const SPEC_BUSINESS_TYPES = [
573
589
  "good": [
574
590
  {
575
591
  "preset": "zine",
576
- "reason": "Hand-made energy that suits an illustrator."
592
+ "reason": "Hand-made energy that suits an illustrator.",
593
+ "reasonHe": "אנרגיה של עבודת יד שמתאימה למאיירים."
577
594
  },
578
595
  {
579
596
  "preset": "gallery-minimal",
580
- "reason": "A white-cube gallery for your pieces."
597
+ "reason": "A white-cube gallery for your pieces.",
598
+ "reasonHe": "גלריה לבנה ונקייה ליצירות שלכם."
581
599
  },
582
600
  {
583
601
  "preset": "night-gallery",
584
- "reason": "Work glowing on a dark wall."
602
+ "reason": "Work glowing on a dark wall.",
603
+ "reasonHe": "היצירות זוהרות על קיר כהה."
585
604
  },
586
605
  {
587
606
  "preset": "warm-editorial",
588
- "reason": "Like a catalogue from a small gallery."
607
+ "reason": "Like a catalogue from a small gallery.",
608
+ "reasonHe": "כמו קטלוג של גלריה קטנה."
589
609
  }
590
610
  ],
591
611
  "colour": {
@@ -673,19 +693,23 @@ const SPEC_BUSINESS_TYPES = [
673
693
  "good": [
674
694
  {
675
695
  "preset": "warm-editorial",
676
- "reason": "Cream and terracotta echo clay; a serif with a hand-made feel."
696
+ "reason": "Cream and terracotta echo clay; a serif with a hand-made feel.",
697
+ "reasonHe": "שמנת וטרקוטה מהדהדים את החימר; סריף עם תחושה של עבודת יד."
677
698
  },
678
699
  {
679
700
  "preset": "bold-maker",
680
- "reason": "Proud, chunky outlines for a maker with a voice."
701
+ "reason": "Proud, chunky outlines for a maker with a voice.",
702
+ "reasonHe": "קווי מתאר עבים וגאים ליוצרים עם קול משלהם."
681
703
  },
682
704
  {
683
705
  "preset": "corner-cafe",
684
- "reason": "Friendly and round for workshops and classes."
706
+ "reason": "Friendly and round for workshops and classes.",
707
+ "reasonHe": "ידידותי ועגול, לסדנאות ולחוגים."
685
708
  },
686
709
  {
687
710
  "preset": "gallery-minimal",
688
- "reason": "Pieces shown like objects in a gallery."
711
+ "reason": "Pieces shown like objects in a gallery.",
712
+ "reasonHe": "העבודות מוצגות כמו חפצים בגלריה."
689
713
  }
690
714
  ],
691
715
  "colour": {
@@ -766,19 +790,23 @@ const SPEC_BUSINESS_TYPES = [
766
790
  "good": [
767
791
  {
768
792
  "preset": "quiet-luxury",
769
- "reason": "Bronze and serif: precious without shouting."
793
+ "reason": "Bronze and serif: precious without shouting.",
794
+ "reasonHe": "ברונזה וסריף: יקר ערך בלי לצעוק."
770
795
  },
771
796
  {
772
797
  "preset": "gallery-minimal",
773
- "reason": "Each piece alone on white, like a vitrine."
798
+ "reason": "Each piece alone on white, like a vitrine.",
799
+ "reasonHe": "כל תכשיט לבד על לבן, כמו בוויטרינה."
774
800
  },
775
801
  {
776
802
  "preset": "night-gallery",
777
- "reason": "Metal and stones shine on black."
803
+ "reason": "Metal and stones shine on black.",
804
+ "reasonHe": "מתכת ואבנים מנצנצות על שחור."
778
805
  },
779
806
  {
780
807
  "preset": "frosted-portfolio",
781
- "reason": "Soft glass, clean and premium."
808
+ "reason": "Soft glass, clean and premium.",
809
+ "reasonHe": "זכוכית רכה, נקי ויוקרתי."
782
810
  }
783
811
  ],
784
812
  "colour": {
@@ -863,19 +891,23 @@ const SPEC_BUSINESS_TYPES = [
863
891
  "good": [
864
892
  {
865
893
  "preset": "ink-studio",
866
- "reason": "Black, sharp and orange: a studio with attitude."
894
+ "reason": "Black, sharp and orange: a studio with attitude.",
895
+ "reasonHe": "שחור, חד וכתום: סטודיו עם אופי."
867
896
  },
868
897
  {
869
898
  "preset": "after-dark",
870
- "reason": "Dark and bold, open late."
899
+ "reason": "Dark and bold, open late.",
900
+ "reasonHe": "כהה ונועז, פתוח עד מאוחר."
871
901
  },
872
902
  {
873
903
  "preset": "zine",
874
- "reason": "Flash-sheet energy."
904
+ "reason": "Flash-sheet energy.",
905
+ "reasonHe": "אנרגיה של דף פלאש."
875
906
  },
876
907
  {
877
908
  "preset": "night-gallery",
878
- "reason": "Your work on black, like a portfolio book."
909
+ "reason": "Your work on black, like a portfolio book.",
910
+ "reasonHe": "העבודות שלכם על שחור, כמו תיק עבודות מודפס."
879
911
  }
880
912
  ],
881
913
  "colour": {
@@ -954,19 +986,23 @@ const SPEC_BUSINESS_TYPES = [
954
986
  "good": [
955
987
  {
956
988
  "preset": "soft-spa",
957
- "reason": "Rose cream, a fine serif and slow motion: calm before they arrive."
989
+ "reason": "Rose cream, a fine serif and slow motion: calm before they arrive.",
990
+ "reasonHe": "שמנת ורודה, סריף עדין ותנועה איטית: רוגע עוד לפני שמגיעים."
958
991
  },
959
992
  {
960
993
  "preset": "morning-glass",
961
- "reason": "Fresh, frosted and light."
994
+ "reason": "Fresh, frosted and light.",
995
+ "reasonHe": "רענן, חלבי ובהיר."
962
996
  },
963
997
  {
964
998
  "preset": "quiet-luxury",
965
- "reason": "For a high-end spa: bronze and space."
999
+ "reason": "For a high-end spa: bronze and space.",
1000
+ "reasonHe": "לספא יוקרתי: ברונזה והרבה מרחב."
966
1001
  },
967
1002
  {
968
1003
  "preset": "garden-party",
969
- "reason": "Soft and warm, for a friendly day spa."
1004
+ "reason": "Soft and warm, for a friendly day spa.",
1005
+ "reasonHe": "רך וחם, לספא יום ידידותי."
970
1006
  }
971
1007
  ],
972
1008
  "colour": {
@@ -1068,19 +1104,23 @@ const SPEC_BUSINESS_TYPES = [
1068
1104
  "good": [
1069
1105
  {
1070
1106
  "preset": "soft-spa",
1071
- "reason": "Elegant and soft for a beauty salon."
1107
+ "reason": "Elegant and soft for a beauty salon.",
1108
+ "reasonHe": "אלגנטי ורך לסלון יופי."
1072
1109
  },
1073
1110
  {
1074
1111
  "preset": "coral-studio",
1075
- "reason": "Bright and fun for a busy hair studio."
1112
+ "reason": "Bright and fun for a busy hair studio.",
1113
+ "reasonHe": "בהיר וכיפי למספרה עמוסה."
1076
1114
  },
1077
1115
  {
1078
1116
  "preset": "garden-party",
1079
- "reason": "Warm, friendly and a little romantic."
1117
+ "reason": "Warm, friendly and a little romantic.",
1118
+ "reasonHe": "חם, ידידותי וקצת רומנטי."
1080
1119
  },
1081
1120
  {
1082
1121
  "preset": "gallery-minimal",
1083
- "reason": "Minimal and chic, lets the cuts speak."
1122
+ "reason": "Minimal and chic, lets the cuts speak.",
1123
+ "reasonHe": "מינימלי ושיקי, נותן לתספורות לדבר."
1084
1124
  }
1085
1125
  ],
1086
1126
  "colour": {
@@ -1165,19 +1205,23 @@ const SPEC_BUSINESS_TYPES = [
1165
1205
  "good": [
1166
1206
  {
1167
1207
  "preset": "morning-glass",
1168
- "reason": "Fresh air and soft light, like a morning class."
1208
+ "reason": "Fresh air and soft light, like a morning class.",
1209
+ "reasonHe": "אוויר צח ואור רך, כמו שיעור בוקר."
1169
1210
  },
1170
1211
  {
1171
1212
  "preset": "soft-spa",
1172
- "reason": "Calm and warm."
1213
+ "reason": "Calm and warm.",
1214
+ "reasonHe": "רגוע וחם."
1173
1215
  },
1174
1216
  {
1175
1217
  "preset": "kind-community",
1176
- "reason": "Open and welcoming for a community studio."
1218
+ "reason": "Open and welcoming for a community studio.",
1219
+ "reasonHe": "פתוח ומזמין לסטודיו קהילתי."
1177
1220
  },
1178
1221
  {
1179
1222
  "preset": "fresh-practice",
1180
- "reason": "Light and gentle."
1223
+ "reason": "Light and gentle.",
1224
+ "reasonHe": "בהיר ועדין."
1181
1225
  }
1182
1226
  ],
1183
1227
  "colour": {
@@ -1265,23 +1309,28 @@ const SPEC_BUSINESS_TYPES = [
1265
1309
  "good": [
1266
1310
  {
1267
1311
  "preset": "coral-studio",
1268
- "reason": "Warm, energetic and bouncy."
1312
+ "reason": "Warm, energetic and bouncy.",
1313
+ "reasonHe": "חם, אנרגטי וקופצני."
1269
1314
  },
1270
1315
  {
1271
1316
  "preset": "fresh-market",
1272
- "reason": "Bright green energy."
1317
+ "reason": "Bright green energy.",
1318
+ "reasonHe": "אנרגיה ירוקה ובהירה."
1273
1319
  },
1274
1320
  {
1275
1321
  "preset": "after-dark",
1276
- "reason": "Dark and intense for a serious gym."
1322
+ "reason": "Dark and intense for a serious gym.",
1323
+ "reasonHe": "כהה ואינטנסיבי לחדר כושר רציני."
1277
1324
  },
1278
1325
  {
1279
1326
  "preset": "bold-maker",
1280
- "reason": "Loud and direct."
1327
+ "reason": "Loud and direct.",
1328
+ "reasonHe": "רועש וישיר."
1281
1329
  },
1282
1330
  {
1283
1331
  "preset": "festival",
1284
- "reason": "Poster energy for a dance or boxing studio."
1332
+ "reason": "Poster energy for a dance or boxing studio.",
1333
+ "reasonHe": "אנרגיה של פוסטר לסטודיו למחול או לאגרוף."
1285
1334
  }
1286
1335
  ],
1287
1336
  "colour": {
@@ -1366,19 +1415,23 @@ const SPEC_BUSINESS_TYPES = [
1366
1415
  "good": [
1367
1416
  {
1368
1417
  "preset": "fresh-practice",
1369
- "reason": "Soft and reassuring; nothing sharp."
1418
+ "reason": "Soft and reassuring; nothing sharp.",
1419
+ "reasonHe": "רך ומרגיע; שום דבר חד."
1370
1420
  },
1371
1421
  {
1372
1422
  "preset": "morning-glass",
1373
- "reason": "Calm, light and open."
1423
+ "reason": "Calm, light and open.",
1424
+ "reasonHe": "רגוע, בהיר ופתוח."
1374
1425
  },
1375
1426
  {
1376
1427
  "preset": "clear-care",
1377
- "reason": "Clear and professional."
1428
+ "reason": "Clear and professional.",
1429
+ "reasonHe": "ברור ומקצועי."
1378
1430
  },
1379
1431
  {
1380
1432
  "preset": "kind-community",
1381
- "reason": "Warm and human."
1433
+ "reason": "Warm and human.",
1434
+ "reasonHe": "חם ואנושי."
1382
1435
  }
1383
1436
  ],
1384
1437
  "colour": {
@@ -1471,19 +1524,23 @@ const SPEC_BUSINESS_TYPES = [
1471
1524
  "good": [
1472
1525
  {
1473
1526
  "preset": "clear-care",
1474
- "reason": "Clean and readable; trust first."
1527
+ "reason": "Clean and readable; trust first.",
1528
+ "reasonHe": "נקי וקריא; האמון קודם לכל."
1475
1529
  },
1476
1530
  {
1477
1531
  "preset": "fresh-practice",
1478
- "reason": "Friendly, light and calm."
1532
+ "reason": "Friendly, light and calm.",
1533
+ "reasonHe": "ידידותי, בהיר ורגוע."
1479
1534
  },
1480
1535
  {
1481
1536
  "preset": "trusted-advisor",
1482
- "reason": "Firm and professional."
1537
+ "reason": "Firm and professional.",
1538
+ "reasonHe": "יציב ומקצועי."
1483
1539
  },
1484
1540
  {
1485
1541
  "preset": "morning-glass",
1486
- "reason": "Modern and calm."
1542
+ "reason": "Modern and calm.",
1543
+ "reasonHe": "עכשווי ורגוע."
1487
1544
  }
1488
1545
  ],
1489
1546
  "colour": {
@@ -1576,15 +1633,18 @@ const SPEC_BUSINESS_TYPES = [
1576
1633
  "good": [
1577
1634
  {
1578
1635
  "preset": "fresh-practice",
1579
- "reason": "Fresh blue and soft shapes: a friendly smile."
1636
+ "reason": "Fresh blue and soft shapes: a friendly smile.",
1637
+ "reasonHe": "כחול רענן וצורות רכות: חיוך ידידותי."
1580
1638
  },
1581
1639
  {
1582
1640
  "preset": "clear-care",
1583
- "reason": "Clean and trustworthy."
1641
+ "reason": "Clean and trustworthy.",
1642
+ "reasonHe": "נקי ואמין."
1584
1643
  },
1585
1644
  {
1586
1645
  "preset": "morning-glass",
1587
- "reason": "Bright and modern."
1646
+ "reason": "Bright and modern.",
1647
+ "reasonHe": "בהיר ועכשווי."
1588
1648
  }
1589
1649
  ],
1590
1650
  "colour": {
@@ -1673,19 +1733,23 @@ const SPEC_BUSINESS_TYPES = [
1673
1733
  "good": [
1674
1734
  {
1675
1735
  "preset": "little-ones",
1676
- "reason": "Round, warm and playful."
1736
+ "reason": "Round, warm and playful.",
1737
+ "reasonHe": "עגול, חם ושובב."
1677
1738
  },
1678
1739
  {
1679
1740
  "preset": "kind-community",
1680
- "reason": "Friendly and caring."
1741
+ "reason": "Friendly and caring.",
1742
+ "reasonHe": "ידידותי ואכפתי."
1681
1743
  },
1682
1744
  {
1683
1745
  "preset": "fresh-practice",
1684
- "reason": "Clean and reassuring for a clinic."
1746
+ "reason": "Clean and reassuring for a clinic.",
1747
+ "reasonHe": "נקי ומרגיע למרפאה."
1685
1748
  },
1686
1749
  {
1687
1750
  "preset": "clear-care",
1688
- "reason": "Professional, for a medical practice."
1751
+ "reason": "Professional, for a medical practice.",
1752
+ "reasonHe": "מקצועי, כיאה למרפאה."
1689
1753
  }
1690
1754
  ],
1691
1755
  "colour": {
@@ -1766,19 +1830,23 @@ const SPEC_BUSINESS_TYPES = [
1766
1830
  "good": [
1767
1831
  {
1768
1832
  "preset": "warm-editorial",
1769
- "reason": "Warm paper and serif, like a printed menu."
1833
+ "reason": "Warm paper and serif, like a printed menu.",
1834
+ "reasonHe": "נייר חם וסריף, כמו תפריט מודפס."
1770
1835
  },
1771
1836
  {
1772
1837
  "preset": "corner-cafe",
1773
- "reason": "Friendly and casual."
1838
+ "reason": "Friendly and casual.",
1839
+ "reasonHe": "ידידותי וקליל."
1774
1840
  },
1775
1841
  {
1776
1842
  "preset": "after-dark",
1777
- "reason": "Dark and lively for an evening place."
1843
+ "reason": "Dark and lively for an evening place.",
1844
+ "reasonHe": "כהה ותוסס למקום של ערב."
1778
1845
  },
1779
1846
  {
1780
1847
  "preset": "quiet-luxury",
1781
- "reason": "For fine dining."
1848
+ "reason": "For fine dining.",
1849
+ "reasonHe": "למסעדת שף."
1782
1850
  }
1783
1851
  ],
1784
1852
  "colour": {
@@ -1870,19 +1938,23 @@ const SPEC_BUSINESS_TYPES = [
1870
1938
  "good": [
1871
1939
  {
1872
1940
  "preset": "corner-cafe",
1873
- "reason": "Warm, round and cheerful."
1941
+ "reason": "Warm, round and cheerful.",
1942
+ "reasonHe": "חם, עגול ועליז."
1874
1943
  },
1875
1944
  {
1876
1945
  "preset": "warm-editorial",
1877
- "reason": "Cream and brown, like a paper bag."
1946
+ "reason": "Cream and brown, like a paper bag.",
1947
+ "reasonHe": "שמנת וחום, כמו שקית נייר."
1878
1948
  },
1879
1949
  {
1880
1950
  "preset": "fresh-market",
1881
- "reason": "Bright and fresh for a juice or brunch spot."
1951
+ "reason": "Bright and fresh for a juice or brunch spot.",
1952
+ "reasonHe": "בהיר ורענן למקום של מיצים או בראנץ'."
1882
1953
  },
1883
1954
  {
1884
1955
  "preset": "garden-party",
1885
- "reason": "Soft and sweet for a patisserie."
1956
+ "reason": "Soft and sweet for a patisserie.",
1957
+ "reasonHe": "רך ומתוק לקונדיטוריה."
1886
1958
  }
1887
1959
  ],
1888
1960
  "colour": {
@@ -1968,19 +2040,23 @@ const SPEC_BUSINESS_TYPES = [
1968
2040
  "good": [
1969
2041
  {
1970
2042
  "preset": "quiet-luxury",
1971
- "reason": "Bronze and serif, like a good label."
2043
+ "reason": "Bronze and serif, like a good label.",
2044
+ "reasonHe": "ברונזה וסריף, כמו תווית טובה."
1972
2045
  },
1973
2046
  {
1974
2047
  "preset": "after-dark",
1975
- "reason": "Dark and lively, open late."
2048
+ "reason": "Dark and lively, open late.",
2049
+ "reasonHe": "כהה ותוסס, פתוח עד מאוחר."
1976
2050
  },
1977
2051
  {
1978
2052
  "preset": "warm-editorial",
1979
- "reason": "Heritage and story."
2053
+ "reason": "Heritage and story.",
2054
+ "reasonHe": "מורשת וסיפור."
1980
2055
  },
1981
2056
  {
1982
2057
  "preset": "ink-studio",
1983
- "reason": "Craft-beer attitude."
2058
+ "reason": "Craft-beer attitude.",
2059
+ "reasonHe": "האופי של מבשלת בוטיק."
1984
2060
  }
1985
2061
  ],
1986
2062
  "colour": {
@@ -2066,19 +2142,23 @@ const SPEC_BUSINESS_TYPES = [
2066
2142
  "good": [
2067
2143
  {
2068
2144
  "preset": "gallery-minimal",
2069
- "reason": "Clean and chic; the products lead."
2145
+ "reason": "Clean and chic; the products lead.",
2146
+ "reasonHe": "נקי ושיקי; המוצרים במרכז."
2070
2147
  },
2071
2148
  {
2072
2149
  "preset": "quiet-luxury",
2073
- "reason": "Premium and calm."
2150
+ "reason": "Premium and calm.",
2151
+ "reasonHe": "יוקרתי ורגוע."
2074
2152
  },
2075
2153
  {
2076
2154
  "preset": "bold-maker",
2077
- "reason": "A brand with a loud voice."
2155
+ "reason": "A brand with a loud voice.",
2156
+ "reasonHe": "מותג עם קול חזק."
2078
2157
  },
2079
2158
  {
2080
2159
  "preset": "zine",
2081
- "reason": "Indie and edgy."
2160
+ "reason": "Indie and edgy.",
2161
+ "reasonHe": "אינדי ונועז."
2082
2162
  }
2083
2163
  ],
2084
2164
  "colour": {
@@ -2156,19 +2236,23 @@ const SPEC_BUSINESS_TYPES = [
2156
2236
  "good": [
2157
2237
  {
2158
2238
  "preset": "garden-party",
2159
- "reason": "Soft, romantic and full of colour."
2239
+ "reason": "Soft, romantic and full of colour.",
2240
+ "reasonHe": "רך, רומנטי ומלא צבע."
2160
2241
  },
2161
2242
  {
2162
2243
  "preset": "kind-community",
2163
- "reason": "Leaf green and friendly."
2244
+ "reason": "Leaf green and friendly.",
2245
+ "reasonHe": "ירוק עלים וידידותי."
2164
2246
  },
2165
2247
  {
2166
2248
  "preset": "fresh-market",
2167
- "reason": "Bright, like a flower stall."
2249
+ "reason": "Bright, like a flower stall.",
2250
+ "reasonHe": "בהיר, כמו דוכן פרחים."
2168
2251
  },
2169
2252
  {
2170
2253
  "preset": "soft-spa",
2171
- "reason": "Elegant for a design florist."
2254
+ "reason": "Elegant for a design florist.",
2255
+ "reasonHe": "אלגנטי לחנות פרחים מעוצבת."
2172
2256
  }
2173
2257
  ],
2174
2258
  "colour": {
@@ -2247,19 +2331,23 @@ const SPEC_BUSINESS_TYPES = [
2247
2331
  "good": [
2248
2332
  {
2249
2333
  "preset": "warm-editorial",
2250
- "reason": "Warm and tactile, like a catalogue."
2334
+ "reason": "Warm and tactile, like a catalogue.",
2335
+ "reasonHe": "חם ומוחשי, כמו קטלוג."
2251
2336
  },
2252
2337
  {
2253
2338
  "preset": "gallery-minimal",
2254
- "reason": "Clean and calm; each piece stands alone."
2339
+ "reason": "Clean and calm; each piece stands alone.",
2340
+ "reasonHe": "נקי ורגוע; כל פריט עומד בפני עצמו."
2255
2341
  },
2256
2342
  {
2257
2343
  "preset": "quiet-luxury",
2258
- "reason": "Premium pieces deserve it."
2344
+ "reason": "Premium pieces deserve it.",
2345
+ "reasonHe": "פריטים יוקרתיים ראויים לזה."
2259
2346
  },
2260
2347
  {
2261
2348
  "preset": "frosted-portfolio",
2262
- "reason": "Soft glass and space."
2349
+ "reason": "Soft glass and space.",
2350
+ "reasonHe": "זכוכית רכה והרבה מרחב."
2263
2351
  }
2264
2352
  ],
2265
2353
  "colour": {
@@ -2339,19 +2427,23 @@ const SPEC_BUSINESS_TYPES = [
2339
2427
  "good": [
2340
2428
  {
2341
2429
  "preset": "trusted-advisor",
2342
- "reason": "Navy and plain type: serious and clear."
2430
+ "reason": "Navy and plain type: serious and clear.",
2431
+ "reasonHe": "כחול כהה ואותיות פשוטות: רציני וברור."
2343
2432
  },
2344
2433
  {
2345
2434
  "preset": "swiss-grid",
2346
- "reason": "Sharp and modern."
2435
+ "reason": "Sharp and modern.",
2436
+ "reasonHe": "חד ועכשווי."
2347
2437
  },
2348
2438
  {
2349
2439
  "preset": "tech-studio",
2350
- "reason": "For tech and data consultants."
2440
+ "reason": "For tech and data consultants.",
2441
+ "reasonHe": "ליועצי טכנולוגיה ודאטה."
2351
2442
  },
2352
2443
  {
2353
2444
  "preset": "clear-care",
2354
- "reason": "Light and approachable."
2445
+ "reason": "Light and approachable.",
2446
+ "reasonHe": "בהיר ונגיש."
2355
2447
  }
2356
2448
  ],
2357
2449
  "colour": {
@@ -2435,19 +2527,23 @@ const SPEC_BUSINESS_TYPES = [
2435
2527
  "good": [
2436
2528
  {
2437
2529
  "preset": "morning-glass",
2438
- "reason": "Fresh and hopeful."
2530
+ "reason": "Fresh and hopeful.",
2531
+ "reasonHe": "רענן ומלא תקווה."
2439
2532
  },
2440
2533
  {
2441
2534
  "preset": "trusted-advisor",
2442
- "reason": "Credible for business coaching."
2535
+ "reason": "Credible for business coaching.",
2536
+ "reasonHe": "אמין לאימון עסקי."
2443
2537
  },
2444
2538
  {
2445
2539
  "preset": "warm-editorial",
2446
- "reason": "Warm and personal, story-led."
2540
+ "reason": "Warm and personal, story-led.",
2541
+ "reasonHe": "חם ואישי, סביב הסיפור שלכם."
2447
2542
  },
2448
2543
  {
2449
2544
  "preset": "coral-studio",
2450
- "reason": "Energetic and upbeat."
2545
+ "reason": "Energetic and upbeat.",
2546
+ "reasonHe": "אנרגטי ואופטימי."
2451
2547
  }
2452
2548
  ],
2453
2549
  "colour": {
@@ -2526,19 +2622,23 @@ const SPEC_BUSINESS_TYPES = [
2526
2622
  "good": [
2527
2623
  {
2528
2624
  "preset": "trusted-advisor",
2529
- "reason": "Navy and gold: authority without fuss."
2625
+ "reason": "Navy and gold: authority without fuss.",
2626
+ "reasonHe": "כחול כהה וזהב: סמכותי בלי גינונים."
2530
2627
  },
2531
2628
  {
2532
2629
  "preset": "swiss-grid",
2533
- "reason": "Precise and modern."
2630
+ "reason": "Precise and modern.",
2631
+ "reasonHe": "מדויק ועכשווי."
2534
2632
  },
2535
2633
  {
2536
2634
  "preset": "clear-care",
2537
- "reason": "Clear and approachable."
2635
+ "reason": "Clear and approachable.",
2636
+ "reasonHe": "ברור ונגיש."
2538
2637
  },
2539
2638
  {
2540
2639
  "preset": "quiet-luxury",
2541
- "reason": "For a boutique firm."
2640
+ "reason": "For a boutique firm.",
2641
+ "reasonHe": "למשרד בוטיק."
2542
2642
  }
2543
2643
  ],
2544
2644
  "colour": {
@@ -2631,19 +2731,23 @@ const SPEC_BUSINESS_TYPES = [
2631
2731
  "good": [
2632
2732
  {
2633
2733
  "preset": "holiday-light",
2634
- "reason": "Bright glass and big photos."
2734
+ "reason": "Bright glass and big photos.",
2735
+ "reasonHe": "זכוכית בהירה ותמונות גדולות."
2635
2736
  },
2636
2737
  {
2637
2738
  "preset": "trusted-advisor",
2638
- "reason": "Professional and trustworthy."
2739
+ "reason": "Professional and trustworthy.",
2740
+ "reasonHe": "מקצועי ואמין."
2639
2741
  },
2640
2742
  {
2641
2743
  "preset": "quiet-luxury",
2642
- "reason": "For luxury property."
2744
+ "reason": "For luxury property.",
2745
+ "reasonHe": "לנדל\"ן יוקרה."
2643
2746
  },
2644
2747
  {
2645
2748
  "preset": "frosted-portfolio",
2646
- "reason": "Clean and modern."
2749
+ "reason": "Clean and modern.",
2750
+ "reasonHe": "נקי ועכשווי."
2647
2751
  }
2648
2752
  ],
2649
2753
  "colour": {
@@ -2731,19 +2835,23 @@ const SPEC_BUSINESS_TYPES = [
2731
2835
  "good": [
2732
2836
  {
2733
2837
  "preset": "kind-community",
2734
- "reason": "Warm, open and welcoming."
2838
+ "reason": "Warm, open and welcoming.",
2839
+ "reasonHe": "חם, פתוח ומזמין."
2735
2840
  },
2736
2841
  {
2737
2842
  "preset": "clear-care",
2738
- "reason": "Clear and credible."
2843
+ "reason": "Clear and credible.",
2844
+ "reasonHe": "ברור ואמין."
2739
2845
  },
2740
2846
  {
2741
2847
  "preset": "warm-editorial",
2742
- "reason": "For telling stories."
2848
+ "reason": "For telling stories.",
2849
+ "reasonHe": "לספר סיפורים."
2743
2850
  },
2744
2851
  {
2745
2852
  "preset": "little-ones",
2746
- "reason": "For children's causes."
2853
+ "reason": "For children's causes.",
2854
+ "reasonHe": "לעמותות למען ילדים."
2747
2855
  }
2748
2856
  ],
2749
2857
  "colour": {
@@ -2819,19 +2927,23 @@ const SPEC_BUSINESS_TYPES = [
2819
2927
  "good": [
2820
2928
  {
2821
2929
  "preset": "garden-party",
2822
- "reason": "Romantic and warm for weddings."
2930
+ "reason": "Romantic and warm for weddings.",
2931
+ "reasonHe": "רומנטי וחם לחתונות."
2823
2932
  },
2824
2933
  {
2825
2934
  "preset": "quiet-luxury",
2826
- "reason": "Elegant for a premium venue."
2935
+ "reason": "Elegant for a premium venue.",
2936
+ "reasonHe": "אלגנטי לאולם יוקרתי."
2827
2937
  },
2828
2938
  {
2829
2939
  "preset": "after-dark",
2830
- "reason": "Dark and lively for parties."
2940
+ "reason": "Dark and lively for parties.",
2941
+ "reasonHe": "כהה ותוסס למסיבות."
2831
2942
  },
2832
2943
  {
2833
2944
  "preset": "festival",
2834
- "reason": "Poster energy for festivals."
2945
+ "reason": "Poster energy for festivals.",
2946
+ "reasonHe": "אנרגיה של פוסטר לפסטיבלים."
2835
2947
  }
2836
2948
  ],
2837
2949
  "colour": {
@@ -2916,19 +3028,23 @@ const SPEC_BUSINESS_TYPES = [
2916
3028
  "good": [
2917
3029
  {
2918
3030
  "preset": "holiday-light",
2919
- "reason": "Sea-blue glass: makes people want to go."
3031
+ "reason": "Sea-blue glass: makes people want to go.",
3032
+ "reasonHe": "זכוכית בכחול של ים: מעורר חשק לבוא."
2920
3033
  },
2921
3034
  {
2922
3035
  "preset": "quiet-luxury",
2923
- "reason": "For a boutique hotel."
3036
+ "reason": "For a boutique hotel.",
3037
+ "reasonHe": "למלון בוטיק."
2924
3038
  },
2925
3039
  {
2926
3040
  "preset": "warm-editorial",
2927
- "reason": "Warm and personal for a B&B."
3041
+ "reason": "Warm and personal for a B&B.",
3042
+ "reasonHe": "חם ואישי לצימר."
2928
3043
  },
2929
3044
  {
2930
3045
  "preset": "morning-glass",
2931
- "reason": "Fresh and calm."
3046
+ "reason": "Fresh and calm.",
3047
+ "reasonHe": "רענן ורגוע."
2932
3048
  }
2933
3049
  ],
2934
3050
  "colour": {
@@ -3019,19 +3135,23 @@ const SPEC_BUSINESS_TYPES = [
3019
3135
  "good": [
3020
3136
  {
3021
3137
  "preset": "little-ones",
3022
- "reason": "Playful and safe for children."
3138
+ "reason": "Playful and safe for children.",
3139
+ "reasonHe": "שובב ובטוח לילדים."
3023
3140
  },
3024
3141
  {
3025
3142
  "preset": "kind-community",
3026
- "reason": "Warm and open."
3143
+ "reason": "Warm and open.",
3144
+ "reasonHe": "חם ופתוח."
3027
3145
  },
3028
3146
  {
3029
3147
  "preset": "coral-studio",
3030
- "reason": "Energetic for dance or music."
3148
+ "reason": "Energetic for dance or music.",
3149
+ "reasonHe": "אנרגטי למחול או למוזיקה."
3031
3150
  },
3032
3151
  {
3033
3152
  "preset": "fresh-market",
3034
- "reason": "Bright and lively."
3153
+ "reason": "Bright and lively.",
3154
+ "reasonHe": "בהיר ותוסס."
3035
3155
  }
3036
3156
  ],
3037
3157
  "colour": {
@@ -3598,6 +3718,194 @@ function presetLook(preset, mode = preset.modes[0]) {
3598
3718
  };
3599
3719
  }
3600
3720
 
3721
+ /** Who asked for a need: the owner named it, or the kind of business needs it ("we added"). */
3722
+ var NeedSource;
3723
+ (function (NeedSource) {
3724
+ NeedSource["You"] = "you";
3725
+ NeedSource["Type"] = "type";
3726
+ })(NeedSource || (NeedSource = {}));
3727
+ /** The needs that already let a visitor ask for a slot. */
3728
+ const BOOKING_NEEDS = ['booking-request', 'sign-up-form'];
3729
+ const BOOKING_REQUEST = 'booking-request';
3730
+ /**
3731
+ * What the site needs (AI plan rev 3 §3d, H3): what the owner named, then what this kind of business needs and
3732
+ * they did not name — its page set, in order, and a booking request for a business that must take bookings —
3733
+ * each marked `from: 'type'` (the Analysis step's "we added"). Pure; an unknown type adds nothing.
3734
+ */
3735
+ function inferNeeds(typeId, said = [], types = BUSINESS_TYPES) {
3736
+ const type = typeId ? types.find(t => t.id === typeId) : undefined;
3737
+ const out = said.map(n => ({ ...n, from: NeedSource.You }));
3738
+ const have = new Set(said.map(n => n.id));
3739
+ for (const id of type?.needs ?? []) {
3740
+ if (have.has(id))
3741
+ continue;
3742
+ have.add(id);
3743
+ out.push({ id, from: NeedSource.Type });
3744
+ }
3745
+ if (type?.booking === BookingNeed.Must && !BOOKING_NEEDS.some(id => have.has(id))) {
3746
+ out.push({ id: BOOKING_REQUEST, from: NeedSource.Type });
3747
+ }
3748
+ return out;
3749
+ }
3750
+
3751
+ /**
3752
+ * What the reading of a prompt returns (AI plan rev 3 §3d): one of the 30 kinds of business (and the two
3753
+ * nearest), who it is for, what the site needs, the facts the prompt STATES (each with the words it came from)
3754
+ * and those it does not, the owner's voice, moods, what they asked to avoid, a hero line, the language. A model
3755
+ * writes it; `validateSiteAnalysis` is the gate — it refuses with a path and never repairs (a model gets the
3756
+ * issues back and rewrites; the caller decides what to drop).
3757
+ */
3758
+ var AnalysisLocale;
3759
+ (function (AnalysisLocale) {
3760
+ AnalysisLocale["He"] = "he";
3761
+ AnalysisLocale["En"] = "en";
3762
+ })(AnalysisLocale || (AnalysisLocale = {}));
3763
+ var VoicePerson;
3764
+ (function (VoicePerson) {
3765
+ VoicePerson["We"] = "we";
3766
+ VoicePerson["I"] = "I";
3767
+ })(VoicePerson || (VoicePerson = {}));
3768
+ var VoiceFormality;
3769
+ (function (VoiceFormality) {
3770
+ VoiceFormality["Casual"] = "casual";
3771
+ VoiceFormality["Neutral"] = "neutral";
3772
+ VoiceFormality["Formal"] = "formal";
3773
+ })(VoiceFormality || (VoiceFormality = {}));
3774
+ var AnalysisIssueCode;
3775
+ (function (AnalysisIssueCode) {
3776
+ AnalysisIssueCode["Shape"] = "shape";
3777
+ AnalysisIssueCode["UnknownKey"] = "unknown-key";
3778
+ AnalysisIssueCode["Duplicate"] = "duplicate";
3779
+ AnalysisIssueCode["UnknownBusinessType"] = "unknown-business-type";
3780
+ AnalysisIssueCode["UnknownNeed"] = "unknown-need";
3781
+ AnalysisIssueCode["UnknownFact"] = "unknown-fact";
3782
+ /** A fact's quote is not words of the prompt: it was not stated, so it may not be filled. */
3783
+ AnalysisIssueCode["QuoteNotInPrompt"] = "quote-not-in-prompt";
3784
+ /** A fact reported both found and missing. */
3785
+ AnalysisIssueCode["FactFoundAndMissing"] = "fact-found-and-missing";
3786
+ AnalysisIssueCode["UnknownMood"] = "unknown-mood";
3787
+ AnalysisIssueCode["TooManyMoods"] = "too-many-moods";
3788
+ AnalysisIssueCode["UnknownAvoid"] = "unknown-avoid";
3789
+ AnalysisIssueCode["InvalidValue"] = "invalid-value";
3790
+ })(AnalysisIssueCode || (AnalysisIssueCode = {}));
3791
+ /** At most this many moods (the closed list a preset carries too). */
3792
+ const MAX_ANALYSIS_MOODS = 3;
3793
+ const ALTERNATIVES = 2;
3794
+ const SHAPE = SPEC_ANALYSIS_SHAPE;
3795
+ const KEYS = Object.fromEntries(Object.keys(SHAPE).map(k => [k, shapeKeys(SHAPE[k])]));
3796
+ const REQUIRED = Object.fromEntries(Object.keys(SHAPE).map(k => [k, requiredShapeKeys(SHAPE[k])]));
3797
+ class Collector extends PathIssueCollector {
3798
+ constructor() {
3799
+ super('analysis', { shape: AnalysisIssueCode.Shape, unknownKey: AnalysisIssueCode.UnknownKey, duplicate: AnalysisIssueCode.Duplicate });
3800
+ }
3801
+ /** An object of `shape` at `path`: no unknown key, every required key present. */
3802
+ object(value, shape, path) {
3803
+ if (!isPlainObject(value)) {
3804
+ this.add(AnalysisIssueCode.Shape, path, 'must be an object');
3805
+ return null;
3806
+ }
3807
+ this.keys(value, KEYS[shape], path);
3808
+ for (const key of REQUIRED[shape])
3809
+ if (value[key] === undefined)
3810
+ this.add(AnalysisIssueCode.Shape, pathAt(path, key), 'is required');
3811
+ return value;
3812
+ }
3813
+ oneOf(obj, key, allowed, path, code = AnalysisIssueCode.InvalidValue) {
3814
+ const v = obj[key];
3815
+ if (v !== undefined && (typeof v !== 'string' || !allowed.includes(v)))
3816
+ this.add(code, pathAt(path, key), `must be one of ${allowed.join(', ')}`);
3817
+ }
3818
+ strings(obj, key, path) {
3819
+ const list = this.array(obj, key, path);
3820
+ list.forEach((v, i) => { if (typeof v !== 'string')
3821
+ this.add(AnalysisIssueCode.Shape, `${pathAt(path, key)}[${i}]`, 'must be a string'); });
3822
+ return list.filter((v) => typeof v === 'string');
3823
+ }
3824
+ }
3825
+ /** Whitespace and case never decide whether words are the prompt's. */
3826
+ const normalise = (s) => s.toLowerCase().replace(/\s+/g, ' ').trim();
3827
+ function validateSiteAnalysis(value, ctx) {
3828
+ const c = new Collector();
3829
+ const a = c.object(value, 'analysis', '');
3830
+ if (!a)
3831
+ return { valid: false, issues: c.issues };
3832
+ c.oneOf(a, 'businessType', BUSINESS_TYPE_IDS, '', AnalysisIssueCode.UnknownBusinessType);
3833
+ const alternatives = c.strings(a, 'alternatives', '');
3834
+ if (Array.isArray(a['alternatives']) && a['alternatives'].length !== ALTERNATIVES)
3835
+ c.add(AnalysisIssueCode.Shape, 'alternatives', `must name ${ALTERNATIVES} business types`);
3836
+ alternatives.forEach((id, i) => {
3837
+ if (!BUSINESS_TYPE_IDS.includes(id))
3838
+ c.add(AnalysisIssueCode.UnknownBusinessType, `alternatives[${i}]`, `"${id}" is not a business type`);
3839
+ else if (id === a['businessType'])
3840
+ c.add(AnalysisIssueCode.Duplicate, `alternatives[${i}]`, 'is the business type itself');
3841
+ });
3842
+ const business = c.object(a['business'], 'business', 'business');
3843
+ if (business) {
3844
+ c.string(business, 'name', 'business');
3845
+ c.string(business, 'audience', 'business');
3846
+ }
3847
+ const needs = c.array(a, 'needs', '');
3848
+ c.unique(needs.map(n => (isPlainObject(n) && typeof n['id'] === 'string' ? n['id'] : null)), i => `needs[${i}].id`, 'need');
3849
+ needs.forEach((raw, i) => {
3850
+ const path = `needs[${i}]`;
3851
+ const need = c.object(raw, 'need', path);
3852
+ if (!need)
3853
+ return;
3854
+ c.oneOf(need, 'id', NEED_IDS, path, AnalysisIssueCode.UnknownNeed);
3855
+ c.oneOf(need, 'from', Object.values(NeedSource), path);
3856
+ c.string(need, 'name', path, false);
3857
+ });
3858
+ const promptWords = normalise(ctx.prompt ?? '');
3859
+ const found = new Set();
3860
+ if (!isPlainObject(a['facts']))
3861
+ c.add(AnalysisIssueCode.Shape, 'facts', 'must be an object');
3862
+ else
3863
+ for (const [key, raw] of Object.entries(a['facts'])) {
3864
+ const path = `facts.${key}`;
3865
+ if (!FACT_KEYS.includes(key)) {
3866
+ c.add(AnalysisIssueCode.UnknownFact, path, `"${key}" is not a fact (${FACT_KEYS.join(', ')})`);
3867
+ continue;
3868
+ }
3869
+ const fact = c.object(raw, 'factRead', path);
3870
+ if (!fact)
3871
+ continue;
3872
+ c.string(fact, 'value', path);
3873
+ const quote = c.string(fact, 'quote', path);
3874
+ if (quote && !promptWords.includes(normalise(quote)))
3875
+ c.add(AnalysisIssueCode.QuoteNotInPrompt, pathAt(path, 'quote'), 'is not words of the prompt — a fact the prompt does not state is missing, never filled');
3876
+ found.add(key);
3877
+ }
3878
+ c.strings(a, 'missingFacts', '').forEach((key, i) => {
3879
+ if (!FACT_KEYS.includes(key))
3880
+ c.add(AnalysisIssueCode.UnknownFact, `missingFacts[${i}]`, `"${key}" is not a fact`);
3881
+ else if (found.has(key))
3882
+ c.add(AnalysisIssueCode.FactFoundAndMissing, `missingFacts[${i}]`, `"${key}" is also in facts`);
3883
+ });
3884
+ const voice = c.object(a['voice'], 'voice', 'voice');
3885
+ if (voice) {
3886
+ c.string(voice, 'tone', 'voice');
3887
+ c.oneOf(voice, 'person', Object.values(VoicePerson), 'voice');
3888
+ c.oneOf(voice, 'formality', Object.values(VoiceFormality), 'voice');
3889
+ c.strings(voice, 'avoidWords', 'voice');
3890
+ }
3891
+ const moods = c.strings(a, 'moods', '');
3892
+ if (moods.length > MAX_ANALYSIS_MOODS)
3893
+ c.add(AnalysisIssueCode.TooManyMoods, 'moods', `at most ${MAX_ANALYSIS_MOODS}`);
3894
+ moods.forEach((m, i) => { if (!MOOD_IDS.includes(m))
3895
+ c.add(AnalysisIssueCode.UnknownMood, `moods[${i}]`, `"${m}" is not a mood`); });
3896
+ c.strings(a, 'avoid', '').forEach((id, i) => {
3897
+ if (!USER_AVOID_IDS.includes(id))
3898
+ c.add(AnalysisIssueCode.UnknownAvoid, `avoid[${i}]`, `"${id}" is not something an owner may ask to avoid`);
3899
+ });
3900
+ const hero = c.object(a['hero'], 'hero', 'hero');
3901
+ if (hero) {
3902
+ c.string(hero, 'headline', 'hero');
3903
+ c.string(hero, 'lead', 'hero');
3904
+ }
3905
+ c.oneOf(a, 'locale', Object.values(AnalysisLocale), '');
3906
+ return { valid: c.issues.length === 0, issues: c.issues };
3907
+ }
3908
+
3601
3909
  var PresetFit;
3602
3910
  (function (PresetFit) {
3603
3911
  PresetFit["Good"] = "good";
@@ -3672,9 +3980,23 @@ function rankStylePresets(input, presets = STYLE_PRESETS, types = BUSINESS_TYPES
3672
3980
  : goodAt >= 0 && type ? [RankReason.GoodFit, type.good[goodAt].reason]
3673
3981
  : sharedMoods.length ? [RankReason.SharedMoods, moodsLine(sharedMoods)]
3674
3982
  : [RankReason.None, ''];
3675
- return { id: preset.id, fit, score, reasonKind, reason, sharedMoods, order };
3983
+ const reasonHe = reasonKind === RankReason.GoodFit && type ? type.good[goodAt].reasonHe : undefined;
3984
+ return { id: preset.id, fit, score, reasonKind, reason, ...(reasonHe ? { reasonHe } : {}), sharedMoods, order };
3676
3985
  }).sort((a, b) => b.score - a.score || a.order - b.order);
3677
3986
  }
3987
+ /**
3988
+ * The line a style's card shows (its title): why it sits where it does, else what the style is — in the studio's
3989
+ * language where the contract has it. Decided 2026-10-25: good fits and descriptions have Hebrew (the name stays a
3990
+ * proper name); a type's or the owner's advice and the shared-moods line are English in the contract yet.
3991
+ */
3992
+ function presetReason(ranked, preset, locale) {
3993
+ const hebrew = locale === AnalysisLocale.He;
3994
+ if (hebrew && ranked.reasonKind === RankReason.GoodFit && ranked.reasonHe)
3995
+ return ranked.reasonHe;
3996
+ if (ranked.reason)
3997
+ return ranked.reason;
3998
+ return (hebrew ? preset?.blurbHe : preset?.blurb) ?? '';
3999
+ }
3678
4000
  // ── References: a reading of pictures or a site, as style (plan §3k, R4) ─────────────────────────────────
3679
4001
  var StyleReadSourceKind;
3680
4002
  (function (StyleReadSourceKind) {
@@ -3795,36 +4117,6 @@ function motionFor(input, presetId, read = null, types = BUSINESS_TYPES, presets
3795
4117
  return { ...out, why, basis };
3796
4118
  }
3797
4119
 
3798
- /** Who asked for a need: the owner named it, or the kind of business needs it ("we added"). */
3799
- var NeedSource;
3800
- (function (NeedSource) {
3801
- NeedSource["You"] = "you";
3802
- NeedSource["Type"] = "type";
3803
- })(NeedSource || (NeedSource = {}));
3804
- /** The needs that already let a visitor ask for a slot. */
3805
- const BOOKING_NEEDS = ['booking-request', 'sign-up-form'];
3806
- const BOOKING_REQUEST = 'booking-request';
3807
- /**
3808
- * What the site needs (AI plan rev 3 §3d, H3): what the owner named, then what this kind of business needs and
3809
- * they did not name — its page set, in order, and a booking request for a business that must take bookings —
3810
- * each marked `from: 'type'` (the Analysis step's "we added"). Pure; an unknown type adds nothing.
3811
- */
3812
- function inferNeeds(typeId, said = [], types = BUSINESS_TYPES) {
3813
- const type = typeId ? types.find(t => t.id === typeId) : undefined;
3814
- const out = said.map(n => ({ ...n, from: NeedSource.You }));
3815
- const have = new Set(said.map(n => n.id));
3816
- for (const id of type?.needs ?? []) {
3817
- if (have.has(id))
3818
- continue;
3819
- have.add(id);
3820
- out.push({ id, from: NeedSource.Type });
3821
- }
3822
- if (type?.booking === BookingNeed.Must && !BOOKING_NEEDS.some(id => have.has(id))) {
3823
- out.push({ id: BOOKING_REQUEST, from: NeedSource.Type });
3824
- }
3825
- return out;
3826
- }
3827
-
3828
4120
  /**
3829
4121
  * Design advice (AI plan rev 3 §3f, H5's core): the business type's avoid rules run against a built site's
3830
4122
  * look, home sections, facts and copy length. ADVICE only — Needs attention and the Publish dialog show it,
@@ -3910,164 +4202,6 @@ function motionAdvice(typeId, animate, types = BUSINESS_TYPES) {
3910
4202
  return ANIMATE_ORDER.indexOf(animate) >= ANIMATE_ORDER.indexOf(type.moves.animate) + ANIMATE_ADVICE_STEPS ? { usual: type.moves.animate } : null;
3911
4203
  }
3912
4204
 
3913
- /**
3914
- * What the reading of a prompt returns (AI plan rev 3 §3d): one of the 30 kinds of business (and the two
3915
- * nearest), who it is for, what the site needs, the facts the prompt STATES (each with the words it came from)
3916
- * and those it does not, the owner's voice, moods, what they asked to avoid, a hero line, the language. A model
3917
- * writes it; `validateSiteAnalysis` is the gate — it refuses with a path and never repairs (a model gets the
3918
- * issues back and rewrites; the caller decides what to drop).
3919
- */
3920
- var AnalysisLocale;
3921
- (function (AnalysisLocale) {
3922
- AnalysisLocale["He"] = "he";
3923
- AnalysisLocale["En"] = "en";
3924
- })(AnalysisLocale || (AnalysisLocale = {}));
3925
- var VoicePerson;
3926
- (function (VoicePerson) {
3927
- VoicePerson["We"] = "we";
3928
- VoicePerson["I"] = "I";
3929
- })(VoicePerson || (VoicePerson = {}));
3930
- var VoiceFormality;
3931
- (function (VoiceFormality) {
3932
- VoiceFormality["Casual"] = "casual";
3933
- VoiceFormality["Neutral"] = "neutral";
3934
- VoiceFormality["Formal"] = "formal";
3935
- })(VoiceFormality || (VoiceFormality = {}));
3936
- var AnalysisIssueCode;
3937
- (function (AnalysisIssueCode) {
3938
- AnalysisIssueCode["Shape"] = "shape";
3939
- AnalysisIssueCode["UnknownKey"] = "unknown-key";
3940
- AnalysisIssueCode["Duplicate"] = "duplicate";
3941
- AnalysisIssueCode["UnknownBusinessType"] = "unknown-business-type";
3942
- AnalysisIssueCode["UnknownNeed"] = "unknown-need";
3943
- AnalysisIssueCode["UnknownFact"] = "unknown-fact";
3944
- /** A fact's quote is not words of the prompt: it was not stated, so it may not be filled. */
3945
- AnalysisIssueCode["QuoteNotInPrompt"] = "quote-not-in-prompt";
3946
- /** A fact reported both found and missing. */
3947
- AnalysisIssueCode["FactFoundAndMissing"] = "fact-found-and-missing";
3948
- AnalysisIssueCode["UnknownMood"] = "unknown-mood";
3949
- AnalysisIssueCode["TooManyMoods"] = "too-many-moods";
3950
- AnalysisIssueCode["UnknownAvoid"] = "unknown-avoid";
3951
- AnalysisIssueCode["InvalidValue"] = "invalid-value";
3952
- })(AnalysisIssueCode || (AnalysisIssueCode = {}));
3953
- /** At most this many moods (the closed list a preset carries too). */
3954
- const MAX_ANALYSIS_MOODS = 3;
3955
- const ALTERNATIVES = 2;
3956
- const SHAPE = SPEC_ANALYSIS_SHAPE;
3957
- const KEYS = Object.fromEntries(Object.keys(SHAPE).map(k => [k, shapeKeys(SHAPE[k])]));
3958
- const REQUIRED = Object.fromEntries(Object.keys(SHAPE).map(k => [k, requiredShapeKeys(SHAPE[k])]));
3959
- class Collector extends PathIssueCollector {
3960
- constructor() {
3961
- super('analysis', { shape: AnalysisIssueCode.Shape, unknownKey: AnalysisIssueCode.UnknownKey, duplicate: AnalysisIssueCode.Duplicate });
3962
- }
3963
- /** An object of `shape` at `path`: no unknown key, every required key present. */
3964
- object(value, shape, path) {
3965
- if (!isPlainObject(value)) {
3966
- this.add(AnalysisIssueCode.Shape, path, 'must be an object');
3967
- return null;
3968
- }
3969
- this.keys(value, KEYS[shape], path);
3970
- for (const key of REQUIRED[shape])
3971
- if (value[key] === undefined)
3972
- this.add(AnalysisIssueCode.Shape, pathAt(path, key), 'is required');
3973
- return value;
3974
- }
3975
- oneOf(obj, key, allowed, path, code = AnalysisIssueCode.InvalidValue) {
3976
- const v = obj[key];
3977
- if (v !== undefined && (typeof v !== 'string' || !allowed.includes(v)))
3978
- this.add(code, pathAt(path, key), `must be one of ${allowed.join(', ')}`);
3979
- }
3980
- strings(obj, key, path) {
3981
- const list = this.array(obj, key, path);
3982
- list.forEach((v, i) => { if (typeof v !== 'string')
3983
- this.add(AnalysisIssueCode.Shape, `${pathAt(path, key)}[${i}]`, 'must be a string'); });
3984
- return list.filter((v) => typeof v === 'string');
3985
- }
3986
- }
3987
- /** Whitespace and case never decide whether words are the prompt's. */
3988
- const normalise = (s) => s.toLowerCase().replace(/\s+/g, ' ').trim();
3989
- function validateSiteAnalysis(value, ctx) {
3990
- const c = new Collector();
3991
- const a = c.object(value, 'analysis', '');
3992
- if (!a)
3993
- return { valid: false, issues: c.issues };
3994
- c.oneOf(a, 'businessType', BUSINESS_TYPE_IDS, '', AnalysisIssueCode.UnknownBusinessType);
3995
- const alternatives = c.strings(a, 'alternatives', '');
3996
- if (Array.isArray(a['alternatives']) && a['alternatives'].length !== ALTERNATIVES)
3997
- c.add(AnalysisIssueCode.Shape, 'alternatives', `must name ${ALTERNATIVES} business types`);
3998
- alternatives.forEach((id, i) => {
3999
- if (!BUSINESS_TYPE_IDS.includes(id))
4000
- c.add(AnalysisIssueCode.UnknownBusinessType, `alternatives[${i}]`, `"${id}" is not a business type`);
4001
- else if (id === a['businessType'])
4002
- c.add(AnalysisIssueCode.Duplicate, `alternatives[${i}]`, 'is the business type itself');
4003
- });
4004
- const business = c.object(a['business'], 'business', 'business');
4005
- if (business) {
4006
- c.string(business, 'name', 'business');
4007
- c.string(business, 'audience', 'business');
4008
- }
4009
- const needs = c.array(a, 'needs', '');
4010
- c.unique(needs.map(n => (isPlainObject(n) && typeof n['id'] === 'string' ? n['id'] : null)), i => `needs[${i}].id`, 'need');
4011
- needs.forEach((raw, i) => {
4012
- const path = `needs[${i}]`;
4013
- const need = c.object(raw, 'need', path);
4014
- if (!need)
4015
- return;
4016
- c.oneOf(need, 'id', NEED_IDS, path, AnalysisIssueCode.UnknownNeed);
4017
- c.oneOf(need, 'from', Object.values(NeedSource), path);
4018
- c.string(need, 'name', path, false);
4019
- });
4020
- const promptWords = normalise(ctx.prompt ?? '');
4021
- const found = new Set();
4022
- if (!isPlainObject(a['facts']))
4023
- c.add(AnalysisIssueCode.Shape, 'facts', 'must be an object');
4024
- else
4025
- for (const [key, raw] of Object.entries(a['facts'])) {
4026
- const path = `facts.${key}`;
4027
- if (!FACT_KEYS.includes(key)) {
4028
- c.add(AnalysisIssueCode.UnknownFact, path, `"${key}" is not a fact (${FACT_KEYS.join(', ')})`);
4029
- continue;
4030
- }
4031
- const fact = c.object(raw, 'factRead', path);
4032
- if (!fact)
4033
- continue;
4034
- c.string(fact, 'value', path);
4035
- const quote = c.string(fact, 'quote', path);
4036
- if (quote && !promptWords.includes(normalise(quote)))
4037
- c.add(AnalysisIssueCode.QuoteNotInPrompt, pathAt(path, 'quote'), 'is not words of the prompt — a fact the prompt does not state is missing, never filled');
4038
- found.add(key);
4039
- }
4040
- c.strings(a, 'missingFacts', '').forEach((key, i) => {
4041
- if (!FACT_KEYS.includes(key))
4042
- c.add(AnalysisIssueCode.UnknownFact, `missingFacts[${i}]`, `"${key}" is not a fact`);
4043
- else if (found.has(key))
4044
- c.add(AnalysisIssueCode.FactFoundAndMissing, `missingFacts[${i}]`, `"${key}" is also in facts`);
4045
- });
4046
- const voice = c.object(a['voice'], 'voice', 'voice');
4047
- if (voice) {
4048
- c.string(voice, 'tone', 'voice');
4049
- c.oneOf(voice, 'person', Object.values(VoicePerson), 'voice');
4050
- c.oneOf(voice, 'formality', Object.values(VoiceFormality), 'voice');
4051
- c.strings(voice, 'avoidWords', 'voice');
4052
- }
4053
- const moods = c.strings(a, 'moods', '');
4054
- if (moods.length > MAX_ANALYSIS_MOODS)
4055
- c.add(AnalysisIssueCode.TooManyMoods, 'moods', `at most ${MAX_ANALYSIS_MOODS}`);
4056
- moods.forEach((m, i) => { if (!MOOD_IDS.includes(m))
4057
- c.add(AnalysisIssueCode.UnknownMood, `moods[${i}]`, `"${m}" is not a mood`); });
4058
- c.strings(a, 'avoid', '').forEach((id, i) => {
4059
- if (!USER_AVOID_IDS.includes(id))
4060
- c.add(AnalysisIssueCode.UnknownAvoid, `avoid[${i}]`, `"${id}" is not something an owner may ask to avoid`);
4061
- });
4062
- const hero = c.object(a['hero'], 'hero', 'hero');
4063
- if (hero) {
4064
- c.string(hero, 'headline', 'hero');
4065
- c.string(hero, 'lead', 'hero');
4066
- }
4067
- c.oneOf(a, 'locale', Object.values(AnalysisLocale), '');
4068
- return { valid: c.issues.length === 0, issues: c.issues };
4069
- }
4070
-
4071
4205
  const FAMILY_OF = new Map(SPEC_SITE_LIBRARY.map(e => [e.id, e.family]));
4072
4206
  function analysisPromptContext(analysis, opts = {}) {
4073
4207
  const type = businessTypeById(analysis.businessType);
@@ -4197,5 +4331,5 @@ function structureFor(typeId) {
4197
4331
  * Generated bundle index. Do not edit.
4198
4332
  */
4199
4333
 
4200
- 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, structureFor, userAvoidRule, validateSiteAnalysis, withoutAddress };
4334
+ 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, presetReason, rankStylePresets, rankWithReferences, structureFor, userAvoidRule, validateSiteAnalysis, withoutAddress };
4201
4335
  //# sourceMappingURL=vectoriox-iox-ui-harness-core.mjs.map