@wix/agent-skills 1.16.0 → 1.16.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/package.json +1 -1
  2. package/skills/wix-app/SKILL.md +72 -8
  3. package/skills/wix-app/references/CODE_QUALITY.md +0 -1
  4. package/skills/wix-app/references/DASHBOARD_MODAL.md +21 -14
  5. package/skills/wix-app/references/DASHBOARD_PAGE.md +30 -184
  6. package/skills/wix-app/references/EDITOR_REACT_COMPONENT.md +19 -3
  7. package/skills/wix-app/references/WIX_PATTERNS_DOCS.md +172 -0
  8. package/skills/wix-app/references/dashboard-page/WDS_LAYOUT.md +187 -0
  9. package/skills/wix-app/references/editor-react-component/A11Y-REVIEW.md +12 -7
  10. package/skills/wix-app/references/editor-react-component/ANIMATED-COMPONENTS.md +31 -2
  11. package/skills/wix-app/references/editor-react-component/COMPONENT-API.md +6 -1
  12. package/skills/wix-app/references/editor-react-component/COMPONENT-CONFIGURATION.md +60 -0
  13. package/skills/wix-app/references/editor-react-component/DIRECTIONALITY.md +6 -6
  14. package/skills/wix-app/references/editor-react-component/FUNCTION-HANDLERS.md +167 -0
  15. package/skills/wix-app/references/editor-react-component/REACT-GUIDELINES.md +2 -2
  16. package/skills/wix-app/references/editor-react-component/SITE-CONTEXT-HOOKS.md +187 -0
  17. package/skills/wix-app/scripts/scan-a11y-code.cjs +777 -0
  18. package/skills/wix-app/scripts/scan-a11y-eslint.cjs +201 -0
  19. package/skills/wix-base44-connector/SKILL.md +185 -0
  20. package/skills/wix-base44-connector/scripts/utils.js +212 -0
  21. package/skills/wix-design-system/SKILL.md +19 -15
  22. package/skills/wix-docs/SKILL.md +75 -1
  23. package/skills/wix-headless/references/CAPABILITIES.md +16 -3
  24. package/skills/wix-headless/references/SDK_HANDOFF.md +1 -1
  25. package/skills/wix-headless/references/SEED.md +1 -1
  26. package/skills/wix-headless/references/inline-recipes/how-to-code-forms.md +560 -102
  27. package/skills/wix-headless/references/inline-recipes/setup-forms.md +210 -143
  28. package/skills/wix-manage/SKILL.md +20 -17
  29. package/skills/wix-manage/references/accessibility/scan-site-accessibility.md +98 -0
  30. package/skills/wix-manage/references/analytics/analytics-dashboard-navigation.md +1 -2
  31. package/skills/wix-manage/references/domains/domain-search-purchase-and-connect.md +2603 -0
  32. package/skills/wix-manage/references/events/create-wix-event.md +112 -90
  33. package/skills/wix-manage/references/events/manage-wix-events.md +33 -94
  34. package/skills/wix-manage/references/sites/create-headless-site.md +1 -0
  35. package/skills/wix-manage/references/sites/manage-oauth-apps.md +154 -0
  36. package/skills/wix-manage/references/sites/read-site-context.md +125 -0
  37. package/skills/wix-manage/references/sites/site-import.md +67 -15
  38. package/skills/wix-manage/references/stores/create-product-catalog-v3.md +273 -0
  39. package/skills/wix-replatform/resources/rp-target-wix/lib/wix-writers.js +1 -1
  40. package/skills/wix-vibe-headless/SKILL.md +2 -1
  41. package/skills/wix-vibe-headless/install/deploy.cjs +63 -3
  42. package/skills/wix-vibe-headless/platforms/base44-ecom-light-seed.md +104 -0
  43. package/skills/wix-vibe-headless/platforms/base44-ecom-light.md +74 -0
  44. package/skills/wix-vibe-headless/platforms/base44.md +15 -40
  45. package/skills/wix-vibe-headless/references/blog/INSTRUCTIONS.md +7 -11
  46. package/skills/wix-vibe-headless/references/blog/seed/SEED.md +1 -1
  47. package/skills/wix-vibe-headless/references/bookings/INSTRUCTIONS.md +7 -11
  48. package/skills/wix-vibe-headless/references/bookings/seed/SEED.md +1 -1
  49. package/skills/wix-vibe-headless/references/cms/INSTRUCTIONS.md +1 -2
  50. package/skills/wix-vibe-headless/references/events/INSTRUCTIONS.md +7 -12
  51. package/skills/wix-vibe-headless/references/events/seed/SEED.md +1 -1
  52. package/skills/wix-vibe-headless/references/members/INSTRUCTIONS.md +7 -11
  53. package/skills/wix-vibe-headless/references/portfolio/INSTRUCTIONS.md +7 -11
  54. package/skills/wix-vibe-headless/references/portfolio/seed/SEED.md +1 -1
  55. package/skills/wix-vibe-headless/references/pricing-plans/INSTRUCTIONS.md +7 -11
  56. package/skills/wix-vibe-headless/references/pricing-plans/seed/SEED.md +1 -1
  57. package/skills/wix-vibe-headless/references/restaurants/INSTRUCTIONS.md +7 -11
  58. package/skills/wix-vibe-headless/references/restaurants/seed/SEED.md +1 -1
  59. package/skills/wix-vibe-headless/references/shared/app/components/WixManageBanner.jsx +1 -1
  60. package/skills/wix-vibe-headless/references/shared/app/rest/wix-config.js +5 -3
  61. package/skills/wix-vibe-headless/references/storefront/INSTRUCTIONS.md +75 -13
  62. package/skills/wix-vibe-headless/references/storefront/app/components/ProductCard.jsx +29 -86
  63. package/skills/wix-vibe-headless/references/storefront/app/components/ProductGrid.jsx +3 -8
  64. package/skills/wix-vibe-headless/references/storefront/app/components/VariantPicker.jsx +78 -105
  65. package/skills/wix-vibe-headless/references/storefront/app/hooks/useProductCard.js +92 -0
  66. package/skills/wix-vibe-headless/references/storefront/app/hooks/useVariantOptions.js +76 -0
  67. package/skills/wix-vibe-headless/references/storefront/seed/SEED.md +1 -1
  68. package/skills/wix-vibe-headless/references/storefront/seed/seed-store.js +44 -5
  69. package/skills/wix-app/references/AUTO_PATTERNS_DASHBOARD.md +0 -326
  70. package/skills/wix-app/references/auto-patterns-dashboard/action-cell.md +0 -115
  71. package/skills/wix-app/references/auto-patterns-dashboard/app-config-structure.md +0 -264
  72. package/skills/wix-app/references/auto-patterns-dashboard/app-context.md +0 -37
  73. package/skills/wix-app/references/auto-patterns-dashboard/bulk-actions.md +0 -86
  74. package/skills/wix-app/references/auto-patterns-dashboard/collection-page-actions.md +0 -119
  75. package/skills/wix-app/references/auto-patterns-dashboard/collection-page.md +0 -103
  76. package/skills/wix-app/references/auto-patterns-dashboard/custom-actions-override.md +0 -76
  77. package/skills/wix-app/references/auto-patterns-dashboard/custom-columns-override.md +0 -82
  78. package/skills/wix-app/references/auto-patterns-dashboard/custom-components-override.md +0 -89
  79. package/skills/wix-app/references/auto-patterns-dashboard/custom-header-override.md +0 -124
  80. package/skills/wix-app/references/auto-patterns-dashboard/custom-sections-override.md +0 -91
  81. package/skills/wix-app/references/auto-patterns-dashboard/custom-slots-override.md +0 -71
  82. package/skills/wix-app/references/auto-patterns-dashboard/entity-page-actions.md +0 -70
  83. package/skills/wix-app/references/auto-patterns-dashboard/entity-page-view-actions.md +0 -73
  84. package/skills/wix-app/references/auto-patterns-dashboard/entity-page.md +0 -98
  85. package/skills/wix-app/references/auto-patterns-dashboard/example-patterns.json +0 -278
  86. package/skills/wix-app/references/auto-patterns-dashboard/pages-configuration.md +0 -94
  87. package/skills/wix-app/references/auto-patterns-dashboard/resolved-action.md +0 -43
  88. package/skills/wix-app/references/auto-patterns-dashboard/sdk-utilities.md +0 -66
  89. package/skills/wix-app/references/auto-patterns-dashboard/views.md +0 -76
  90. package/skills/wix-app/scripts/generate-auto-patterns.js +0 -510
  91. package/skills/wix-manage/references/domains/domain-search-and-purchase.md +0 -437
  92. package/skills/wix-manage/references/stores/create-product-from-image.md +0 -1031
  93. package/skills/wix-manage/references/stores/create-product-with-options-catalog-v3.md +0 -623
  94. package/skills/wix-manage/references/stores/setup-online-store-catalog-v3.md +0 -106
@@ -1,118 +1,91 @@
1
- // PDP choice controls: one OptionSelector per product.options[] (variant choices) and one
2
- // ModifierSelector per product.modifiers[] (TEXT_CHOICES → buttons, FREE_TEXT → input). Driven by
3
- // useProductDetail — pass its selection state/handlers in. Styled with base44 design tokens (shadcn Tailwind classes).
1
+ // PDP choice controls built on useVariantOptions — swap this component for your own if you want
2
+ // a different look; the data contract (optionGroups / modifierGroups) stays the same.
3
+ // Styled with base44 design tokens (shadcn Tailwind classes).
4
+ import { useVariantOptions } from "@/hooks/useVariantOptions";
4
5
 
5
6
  const chipBase = "py-1.5 px-3 cursor-pointer text-sm font-body border rounded-sm";
6
7
  const chipIdle = "border-border bg-card text-foreground";
7
8
  const chipActive = "border-primary bg-primary text-primary-foreground";
8
9
  const label = "block mb-1.5 text-[13px] font-semibold text-muted-foreground";
9
10
 
10
- // A colour/swatch option renders as swatches, a text option as pills — the render type is on the
11
- // option (`optionRenderType`) and mirrored per choice (`choiceType`: ONE_COLOR vs CHOICE_TEXT).
12
- // Colour choices carry `colorCode` (hex) and usually `linkedMedia`, so a swatch is real colour, and
13
- // picking one moves the gallery to that colour's photo.
14
- const isColorChoice = (option, choice) =>
15
- option?.optionRenderType === "COLOR_CHOICES" ||
16
- option?.optionRenderType === "SWATCH_CHOICES" ||
17
- choice?.choiceType === "ONE_COLOR";
18
-
19
- function OptionSelector({ option, selected, onSelect }) {
20
- // `visible: false` is a choice the merchant retired — it stays in the payload, so filter it out or
21
- // the buyer can pick something the catalogue no longer offers.
22
- const choices = (option.choicesSettings?.choices || []).filter((c) => c.visible !== false);
23
- if (!choices.length) return null;
24
- const swatches = choices.some((c) => isColorChoice(option, c) && c.colorCode);
25
-
26
- return (
27
- <div className="mb-4">
28
- <label className={label}>
29
- {option.name}
30
- {/* With swatches the chosen colour's name isn't otherwise on screen — echo it. */}
31
- {swatches && selected && (
32
- <span className="ml-1.5 font-normal text-foreground">
33
- {choices.find((c) => c.choiceId === selected)?.name}
34
- </span>
35
- )}
36
- </label>
37
- <div className="flex flex-wrap gap-2">
38
- {choices.map((c) => {
39
- const active = selected === c.choiceId;
40
- const out = c.inStock === false;
41
- if (swatches && c.colorCode) {
42
- return (
43
- <button
44
- key={c.choiceId}
45
- type="button"
46
- disabled={out}
47
- onClick={() => onSelect(option.id, c.choiceId)}
48
- aria-pressed={active}
49
- aria-label={`${c.name}${out ? " (out of stock)" : ""}`}
50
- title={c.name}
51
- className={`relative w-9 h-9 rounded-full cursor-pointer transition-shadow ring-1 ring-inset ring-black/15 ${
52
- active ? "ring-2 ring-offset-2 ring-offset-background ring-primary" : ""
53
- } ${out ? "opacity-40 cursor-not-allowed" : "hover:ring-2 hover:ring-offset-1 hover:ring-offset-background hover:ring-border"}`}
54
- style={{ backgroundColor: c.colorCode }}
55
- >
56
- {/* A struck line reads as "unavailable" without hiding which colour it was. */}
57
- {out && (
58
- <span aria-hidden="true" className="absolute inset-0 grid place-items-center">
59
- <span className="w-full h-px bg-destructive rotate-45" />
60
- </span>
61
- )}
62
- </button>
63
- );
64
- }
65
- return (
66
- <button
67
- key={c.choiceId}
68
- type="button"
69
- disabled={out}
70
- aria-pressed={active}
71
- onClick={() => onSelect(option.id, c.choiceId)}
72
- className={`${chipBase} ${active ? chipActive : chipIdle} ${out ? "opacity-40 line-through" : ""}`}
73
- >
74
- {c.name}
75
- </button>
76
- );
77
- })}
78
- </div>
79
- </div>
80
- );
81
- }
82
-
83
- function ModifierSelector({ modifier, value, onChange }) {
84
- const key = modifier.modifierRenderType === "FREE_TEXT" ? modifier.freeTextSettings?.key : modifier.key;
85
- if (modifier.modifierRenderType === "FREE_TEXT") {
86
- return (
87
- <div className="mb-4">
88
- <label className={label}>{modifier.name}{modifier.mandatory && " *"}</label>
89
- <input value={value || ""} onChange={(e) => onChange(key, e.target.value)}
90
- className="w-full py-2 px-3 font-body border border-input rounded-sm bg-background text-foreground" />
91
- </div>
92
- );
93
- }
94
- return (
95
- <div className="mb-4">
96
- <label className={label}>{modifier.name}{modifier.mandatory && " *"}</label>
97
- <div className="flex flex-wrap gap-2">
98
- {modifier.choicesSettings?.choices?.map((c) => (
99
- <button key={c.key} aria-pressed={value === c.key} onClick={() => onChange(key, c.key)}
100
- className={`${chipBase} ${value === c.key ? chipActive : chipIdle}`}>{c.name}</button>
101
- ))}
102
- </div>
103
- </div>
104
- );
105
- }
106
-
107
11
  export default function VariantPicker({ options, modifiers, selectedOptions, selectOption, modifierValues, setModifier }) {
12
+ const { optionGroups, modifierGroups } = useVariantOptions(options, modifiers, selectedOptions, modifierValues);
13
+
108
14
  return (
109
15
  <>
110
- {options.map((o) => (
111
- <OptionSelector key={o.id} option={o} selected={selectedOptions[o.id]} onSelect={selectOption} />
16
+ {optionGroups.map((group) => (
17
+ <div key={group.id} className="mb-4">
18
+ <label className={label}>
19
+ {group.name}
20
+ {group.isColor && (
21
+ <span className="ml-1.5 font-normal text-foreground">
22
+ {group.choices.find((c) => c.selected)?.name}
23
+ </span>
24
+ )}
25
+ </label>
26
+ <div className="flex flex-wrap gap-2">
27
+ {group.choices.map((c) =>
28
+ c.isColorSwatch ? (
29
+ <button
30
+ key={c.choiceId}
31
+ type="button"
32
+ disabled={!c.inStock}
33
+ onClick={() => selectOption(group.id, c.choiceId)}
34
+ aria-pressed={c.selected}
35
+ aria-label={`${c.name}${!c.inStock ? " (out of stock)" : ""}`}
36
+ title={c.name}
37
+ className={`relative w-9 h-9 rounded-full cursor-pointer transition-shadow ring-1 ring-inset ring-black/15 ${
38
+ c.selected ? "ring-2 ring-offset-2 ring-offset-background ring-primary" : ""
39
+ } ${!c.inStock ? "opacity-40 cursor-not-allowed" : "hover:ring-2 hover:ring-offset-1 hover:ring-offset-background hover:ring-border"}`}
40
+ style={{ backgroundColor: c.colorCode }}
41
+ >
42
+ {!c.inStock && (
43
+ <span aria-hidden="true" className="absolute inset-0 grid place-items-center">
44
+ <span className="w-full h-px bg-destructive rotate-45" />
45
+ </span>
46
+ )}
47
+ </button>
48
+ ) : (
49
+ <button
50
+ key={c.choiceId}
51
+ type="button"
52
+ disabled={!c.inStock}
53
+ aria-pressed={c.selected}
54
+ onClick={() => selectOption(group.id, c.choiceId)}
55
+ className={`${chipBase} ${c.selected ? chipActive : chipIdle} ${!c.inStock ? "opacity-40 line-through" : ""}`}
56
+ >
57
+ {c.name}
58
+ </button>
59
+ )
60
+ )}
61
+ </div>
62
+ </div>
112
63
  ))}
113
- {modifiers.map((m) => (
114
- <ModifierSelector key={m.key || m.freeTextSettings?.key} modifier={m}
115
- value={modifierValues[m.key || m.freeTextSettings?.key]} onChange={setModifier} />
64
+
65
+ {modifierGroups.map((m) => (
66
+ <div key={m.key} className="mb-4">
67
+ <label className={label}>{m.name}{m.mandatory && " *"}</label>
68
+ {m.type === "text" ? (
69
+ <input
70
+ value={m.value}
71
+ onChange={(e) => setModifier(m.key, e.target.value)}
72
+ className="w-full py-2 px-3 font-body border border-input rounded-sm bg-background text-foreground"
73
+ />
74
+ ) : (
75
+ <div className="flex flex-wrap gap-2">
76
+ {m.choices.map((c) => (
77
+ <button
78
+ key={c.key}
79
+ aria-pressed={c.selected}
80
+ onClick={() => setModifier(m.key, c.key)}
81
+ className={`${chipBase} ${c.selected ? chipActive : chipIdle}`}
82
+ >
83
+ {c.name}
84
+ </button>
85
+ ))}
86
+ </div>
87
+ )}
88
+ </div>
116
89
  ))}
117
90
  </>
118
91
  );
@@ -0,0 +1,92 @@
1
+ // Headless data layer for a product grid tile.
2
+ // Normalises raw Wix product fields into render-agnostic structures so you can build
3
+ // whatever card UI you want (image layout, badge style, price display, quick-add trigger)
4
+ // without touching the badge-priority logic, price-range maths, or colour-dot extraction.
5
+ //
6
+ // Usage:
7
+ // const { isSoldOut, leftBadges, promoBadge, priceDisplay, compareAtDisplay,
8
+ // colors, optionLabel, isQuickAddable, image, hoverImage } = useProductCard(product);
9
+ // // then render however you want, call addToCart(product.id) from useCart() for quick-add.
10
+
11
+ import { useMemo } from "react";
12
+ import { productImage, productGallery } from "@/lib/storeImage";
13
+
14
+ function discountPercent(product) {
15
+ const now = Number(product?.actualPriceRange?.minValue?.amount);
16
+ const was = Number(product?.compareAtPriceRange?.minValue?.amount);
17
+ if (!now || !was || was <= now) return null;
18
+ return Math.round(((was - now) / was) * 100);
19
+ }
20
+
21
+ export function useProductCard(product) {
22
+ return useMemo(() => {
23
+ const status = product?.inventory?.availabilityStatus;
24
+ const isSoldOut = status === "OUT_OF_STOCK";
25
+ // preorderStatus is only meaningful when the product is out of stock
26
+ const isPreorder = isSoldOut && product?.inventory?.preorderStatus === "ENABLED";
27
+ const isPartiallyOutOfStock = status === "PARTIALLY_OUT_OF_STOCK";
28
+
29
+ // Left-side badges (stacked, top-left of the image): stock / pre-order state.
30
+ const leftBadges = [];
31
+ if (isPreorder) leftBadges.push({ type: "pre-order", label: "Pre-order" });
32
+ else if (isSoldOut) leftBadges.push({ type: "sold-out", label: "Sold out" });
33
+ if (isPartiallyOutOfStock) leftBadges.push({ type: "limited-stock", label: "Limited stock" });
34
+
35
+ // Right-side promo badge: discount % beats a merchant ribbon (a "Sale" ribbon is redundant
36
+ // when the % is already showing). Only one at a time.
37
+ const discount = discountPercent(product);
38
+ const ribbon = product?.ribbon?.name;
39
+ const promoBadge = discount
40
+ ? { type: "discount", label: `-${discount}%` }
41
+ : ribbon
42
+ ? { type: "ribbon", label: ribbon }
43
+ : null;
44
+
45
+ // Price: show a min–max range when variants span different prices so the PDP doesn't
46
+ // appear to raise the price once a variant is selected.
47
+ const priceMin = product?.actualPriceRange?.minValue?.formattedAmount;
48
+ const priceMax = product?.actualPriceRange?.maxValue?.formattedAmount;
49
+ const compareAt = product?.compareAtPriceRange?.minValue?.formattedAmount;
50
+ const priceDisplay = priceMax && priceMax !== priceMin ? `${priceMin} – ${priceMax}` : priceMin;
51
+ const compareAtDisplay = compareAt && compareAt !== priceMin ? compareAt : null;
52
+
53
+ // Options preview for the tile summary row.
54
+ // Colour options → real hex dots (more informative than "3 colours").
55
+ // Non-colour options → "3 sizes · 2 materials" (pluralised from the merchant's own name).
56
+ const labels = [], colors = [];
57
+ for (const o of product?.options || []) {
58
+ const choices = (o.choicesSettings?.choices || []).filter((c) => c.visible !== false);
59
+ if (!choices.length) continue;
60
+ const swatches = choices.map((c) => c.colorCode).filter(Boolean);
61
+ if (swatches.length) { colors.push(...swatches); continue; }
62
+ const n = o.name.toLowerCase();
63
+ labels.push(`${choices.length} ${choices.length === 1 || n.endsWith("s") ? n : `${n}s`}`);
64
+ }
65
+ const optionLabel = labels.join(" · ");
66
+ const hasOptions = (product?.options?.length || 0) > 0;
67
+
68
+ // Quick-add is only safe for single-variant products (no option choices to resolve).
69
+ // Sold-out with pre-order still shows a CTA, but it links to the PDP, not quick-add.
70
+ const isQuickAddable = !hasOptions && !isSoldOut;
71
+
72
+ // Images: normalised through lib/storeImage so URLs are consistent across the tile,
73
+ // the PDP gallery, and the cart. Hover image is the second gallery shot (if one exists).
74
+ const image = productImage(product);
75
+ const hoverImage = productGallery(product)[1]?.url ?? null;
76
+
77
+ return {
78
+ isSoldOut,
79
+ isPreorder,
80
+ isPartiallyOutOfStock,
81
+ leftBadges, // [{ type: 'pre-order'|'sold-out'|'limited-stock', label }]
82
+ promoBadge, // { type: 'discount'|'ribbon', label } | null
83
+ priceDisplay, // formatted price (range or single value)
84
+ compareAtDisplay, // formatted compare-at price | null
85
+ colors, // hex strings — render as dots; the tile shows up to however many you want
86
+ optionLabel, // "3 sizes · 2 materials" or empty string
87
+ isQuickAddable,
88
+ image, // primary image URL | null
89
+ hoverImage, // second image URL for hover effect | null
90
+ };
91
+ }, [product]);
92
+ }
@@ -0,0 +1,76 @@
1
+ // Headless data layer for product option/variant/modifier picking.
2
+ // Normalises raw Wix product options + modifiers into flat, render-agnostic structures so you
3
+ // can build whatever UI you want (pills, radios, dropdowns, swatches) without touching the
4
+ // data-edge-case logic (visible filtering, color detection, modifier key resolution).
5
+ //
6
+ // Usage:
7
+ // const { optionGroups, modifierGroups } = useVariantOptions(
8
+ // options, modifiers, selectedOptions, modifierValues
9
+ // );
10
+ // // then map over optionGroups/modifierGroups to render your own controls and call
11
+ // // selectOption(optionId, choiceId) / setModifier(key, value) from useProductDetail.
12
+
13
+ import { useMemo } from "react";
14
+
15
+ export function useVariantOptions(options, modifiers, selectedOptions, modifierValues) {
16
+ const optionGroups = useMemo(() =>
17
+ (options || []).map((option) => {
18
+ const rawChoices = option.choicesSettings?.choices || [];
19
+ // `visible: false` = a choice the merchant retired — filter it so buyers can't select it.
20
+ const choices = rawChoices
21
+ .filter((c) => c.visible !== false)
22
+ .map((c) => ({
23
+ choiceId: c.choiceId,
24
+ name: c.name,
25
+ colorCode: c.colorCode || null,
26
+ // true when this choice should render as a colour swatch (circle) rather than a pill.
27
+ isColorSwatch: !!(
28
+ (option.optionRenderType === "COLOR_CHOICES" ||
29
+ option.optionRenderType === "SWATCH_CHOICES" ||
30
+ c.choiceType === "ONE_COLOR") &&
31
+ c.colorCode
32
+ ),
33
+ inStock: c.inStock !== false,
34
+ selected: selectedOptions[option.id] === c.choiceId,
35
+ }));
36
+ return {
37
+ id: option.id,
38
+ name: option.name,
39
+ // true if ANY choice in this group is a colour swatch — lets you branch on group type.
40
+ isColor: choices.some((c) => c.isColorSwatch),
41
+ choices,
42
+ };
43
+ }),
44
+ [options, selectedOptions]
45
+ );
46
+
47
+ const modifierGroups = useMemo(() =>
48
+ (modifiers || []).map((m) => {
49
+ const isText = m.modifierRenderType === "FREE_TEXT";
50
+ const key = isText ? m.freeTextSettings?.key : m.key;
51
+ if (isText) {
52
+ return {
53
+ key,
54
+ name: m.name,
55
+ mandatory: !!m.mandatory,
56
+ type: "text",
57
+ value: modifierValues[key] || "",
58
+ };
59
+ }
60
+ return {
61
+ key,
62
+ name: m.name,
63
+ mandatory: !!m.mandatory,
64
+ type: "choices",
65
+ choices: (m.choicesSettings?.choices || []).map((c) => ({
66
+ key: c.key,
67
+ name: c.name,
68
+ selected: modifierValues[key] === c.key,
69
+ })),
70
+ };
71
+ }),
72
+ [modifiers, modifierValues]
73
+ );
74
+
75
+ return { optionGroups, modifierGroups };
76
+ }
@@ -6,7 +6,7 @@ seed operation. Load it and call **`setupStore` — the one-call path** — with
6
6
 
7
7
  ```js
8
8
  // build-time exec_tool
9
- const { accessToken } = await base44.asServiceRole.connectors.getConnection("wix"); // Base44 (generic: use $TOKEN)
9
+ const { accessToken } = await base44.asServiceRole.connectors.getConnection("wix");
10
10
  const fs = require("fs");
11
11
  // exec_tool's require can return EMPTY exports for these build-time modules — load the file itself:
12
12
  const seed = (() => { const m = { exports: {} };
@@ -82,14 +82,51 @@ async function waitForCatalogV3(ctx, { attempts = 40, delayMs = 2000 } = {}) {
82
82
  }
83
83
  }
84
84
 
85
- // plain string -> Wix rich-text description node tree (plainDescription is HTML/nodes, not text)
85
+ // description string -> Wix rich-text node tree.
86
+ //
87
+ // Descriptions arrive as HTML as often as not: asked to describe a product, a model writes
88
+ // <p>…</p><strong>Care:</strong><br/>. The writable field is `description` (Ricos nodes) — the HTML
89
+ // `plainDescription` the storefront renders is read-only, derived from them. So markup dropped into
90
+ // a single TEXT node is stored as literal text, comes back escaped, and the PDP shows the tags to
91
+ // the buyer. Convert the tags a model actually emits; a string with no tags stays one paragraph.
92
+ const HTML_ENTITIES = { "&amp;": "&", "&lt;": "<", "&gt;": ">", "&quot;": '"', "&#39;": "'", "&nbsp;": " " };
93
+
94
+ function decodeEntities(s) {
95
+ return s.replace(/&(?:amp|lt|gt|quot|#39|nbsp);/g, (m) => HTML_ENTITIES[m] ?? m);
96
+ }
97
+
98
+ // One paragraph's inner HTML -> TEXT nodes, carrying bold/italic as Ricos decorations.
99
+ function mkTextNodes(html) {
100
+ const nodes = [];
101
+ let bold = 0, italic = 0, last = 0, m;
102
+ const tag = /<(\/?)(strong|b|em|i)\s*\/?>/gi;
103
+ const push = (raw) => {
104
+ const text = decodeEntities(raw.replace(/<[^>]*>/g, ""));
105
+ if (!text) return;
106
+ const decorations = [];
107
+ if (bold > 0) decorations.push({ type: "BOLD" });
108
+ if (italic > 0) decorations.push({ type: "ITALIC" });
109
+ nodes.push({ type: "TEXT", textData: { text, decorations } });
110
+ };
111
+ while ((m = tag.exec(html)) !== null) {
112
+ push(html.slice(last, m.index));
113
+ const step = m[1] ? -1 : 1;
114
+ if (/^(strong|b)$/i.test(m[2])) bold = Math.max(0, bold + step);
115
+ else italic = Math.max(0, italic + step);
116
+ last = tag.lastIndex;
117
+ }
118
+ push(html.slice(last));
119
+ return nodes.length ? nodes : [{ type: "TEXT", textData: { text: "", decorations: [] } }];
120
+ }
121
+
86
122
  function mkDesc(text, i) {
123
+ const blocks = String(text ?? "").split(/<\/p\s*>|<br\s*\/?>/i).map((b) => b.trim()).filter(Boolean);
87
124
  return {
88
- nodes: [{
89
- type: "PARAGRAPH", id: `desc-${i}`,
90
- nodes: [{ type: "TEXT", textData: { text: text || "" } }],
125
+ nodes: (blocks.length ? blocks : [""]).map((block, n) => ({
126
+ type: "PARAGRAPH", id: `desc-${i}-${n}`,
127
+ nodes: mkTextNodes(block),
91
128
  paragraphData: { textStyle: { textAlignment: "AUTO" } },
92
- }],
129
+ })),
93
130
  metadata: { version: 1, id: `desc-meta-${i}` },
94
131
  };
95
132
  }
@@ -157,6 +194,8 @@ async function listProducts(ctx) {
157
194
  * Bulk-create products.
158
195
  * @param products [{ name, description, price, compareAtPrice?, quantity,
159
196
  * options?: [{ name, type?:"text"|"color", choices:["8","9"] | [{name,colorCode}] }] }]
197
+ * description: plain text, or simple HTML (`<p>`, `<br/>`, `<strong>`, `<em>`) — converted to
198
+ * Wix rich text here, so the storefront renders paragraphs and bold rather than tag text.
160
199
  * options = ONLY things the buyer selects-and-buys (Size, Color) -> become variants.
161
200
  * Display-only attributes go in name/category/description, NOT options. Default: no options.
162
201
  * visible/physicalProperties/variant-expansion handled here. `quantity` is the stock created.