@wix/agent-skills 1.19.0 → 1.19.1
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.
- package/package.json +1 -1
- package/skills/wix-app/SKILL.md +33 -31
- package/skills/wix-app/references/DASHBOARD_PAGE.md +6 -10
- package/skills/wix-app/references/EDITOR_REACT_COMPONENT.md +16 -7
- package/skills/wix-app/references/WIX_PATTERNS_DOCS.md +53 -131
- package/skills/wix-app/references/dashboard-page/UX_SUCCESS_MODEL.md +1 -2
- package/skills/wix-app/references/editor-react-component/ACCESSIBILITY.md +83 -73
- package/skills/wix-app/references/editor-react-component/COMPONENT-CONTRACT.md +3 -2
- package/skills/wix-app/references/editor-react-component/CSS-GUIDELINES.md +9 -0
- package/skills/wix-app/references/editor-react-component/PARTS.md +5 -2
- package/skills/wix-app/references/editor-react-component/REACT-GUIDELINES.md +1 -1
- package/skills/wix-app/scripts/pkg-root.cjs +73 -0
- package/skills/wix-app/scripts/scan-a11y-code.cjs +310 -40
- package/skills/wix-app/scripts/scan-a11y-eslint.cjs +118 -24
- package/skills/wix-app/scripts/scan-a11y-render.cjs +551 -0
- package/skills/wix-app/scripts/scan-a11y-review.cjs +373 -0
- package/skills/wix-base44-connector/SKILL.md +24 -22
- package/skills/wix-base44-connector/scripts/utils.cjs +39 -3
- package/skills/wix-docs/SKILL.md +47 -20
- package/skills/wix-headless/entry/bootstrap.mjs +106 -50
- package/skills/wix-headless/entry/skill.md +5 -3
- package/skills/wix-headless/references/managed/AUTHENTICATION.md +25 -5
- package/skills/wix-headless-fast/SKILL.md +25 -7
- package/skills/wix-headless-fast/entry/skill.md +23 -14
- package/skills/wix-headless-fast/install/fast-path.mjs +48 -5
- package/skills/wix-headless-fast/install/pin-agents-md.mjs +57 -0
- package/skills/wix-headless-fast/references/shared/CUSTOM_OPERATIONS.md +3 -3
- package/skills/wix-headless-fast/references/storefront/INSTRUCTIONS.md +204 -35
- package/skills/wix-headless-fast/references/storefront/app/hooks/storefront/useShop.ts +146 -36
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/cart-store.ts +8 -2
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/catalog.ts +95 -20
- package/skills/wix-headless-fast/references/storefront/seed/SEED.md +21 -2
- package/skills/wix-headless-fast/references/storefront/seed/seed-store.mjs +104 -18
- package/skills/wix-manage/SKILL.md +13 -7
- package/skills/wix-manage/references/google-ads/billing-and-payment.md +3 -3
- package/skills/wix-manage/references/google-ads/create-performance-max-campaign.md +8 -8
- package/skills/wix-manage/references/google-ads/get-campaign-suggestions.md +4 -4
- package/skills/wix-manage/references/google-ads/google-ads-dashboard-navigation.md +1 -1
- package/skills/wix-manage/references/google-ads/install-and-create-account.md +9 -9
- package/skills/wix-manage/references/google-ads/query-campaign-analytics.md +7 -7
- package/skills/wix-manage/references/seo/manage-content-plan.md +181 -135
- package/skills/wix-manage/references/seo/manage-google-search-console.md +272 -0
- package/skills/wix-manage/references/sites/upload-static-site.md +53 -0
- package/skills/wix-manage/references/stores/change-store-currency.md +1 -1
- package/skills/wix-vibe-headless/platforms/base44-ecom-light-seed-with-install.md +3 -1
- package/skills/wix-vibe-headless/platforms/base44-ecom-light-seed.md +3 -1
- package/skills/wix-vibe-headless/platforms/base44-ecom-light.md +1 -1
- package/skills/wix-vibe-headless/references/storefront/INSTRUCTIONS.md +1 -1
- package/skills/wix-app/references/dashboard-page/COLLECTION_TOOLKIT.md +0 -80
- package/skills/wix-app/references/dashboard-page/ENTITY_PAGE_TOOLKIT.md +0 -140
- package/skills/wix-app/references/dashboard-page/PATTERNS_BUNDLE_READING.md +0 -112
- package/skills/wix-headless-fast/references/storefront/app/components/storefront/ProductCard.tsx +0 -100
- package/skills/wix-headless-fast/references/storefront/app/components/storefront/ProductDetailView.tsx +0 -130
- package/skills/wix-headless-fast/references/storefront/app/components/storefront/ProductGrid.tsx +0 -48
- package/skills/wix-headless-fast/references/storefront/app/components/storefront/ShopView.tsx +0 -66
- package/skills/wix-headless-fast/references/storefront/app/components/storefront/VariantPicker.tsx +0 -103
- package/skills/wix-headless-fast/references/storefront/app-astro/pages/products/[slug].astro +0 -56
- package/skills/wix-headless-fast/references/storefront/app-astro/pages/shop.astro +0 -23
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
# Reading `@wix/patterns` Bundles and Docs
|
|
2
|
-
|
|
3
|
-
> **Precondition: an index entry.** Every path below is the `file` (or `bundle`) field of an entry
|
|
4
|
-
> in `<pkgRoot>/dist/dts-bundle/index.json` or `<pkgRoot>/dist/docs/index.json`. If you haven't
|
|
5
|
-
> read that index this session, you have no path to open — read it before anything else here.
|
|
6
|
-
> Getting to it — resolving the package root, the version floor, the docs lookup, and the rule
|
|
7
|
-
> against browsing `node_modules` by hand — is in
|
|
8
|
-
> [WIX_PATTERNS_DOCS.md](../WIX_PATTERNS_DOCS.md). Nothing here replaces those steps.
|
|
9
|
-
|
|
10
|
-
## Read the index once, then open its files in one call
|
|
11
|
-
|
|
12
|
-
One index read covers the whole page. So name every symbol you plan to write — components, hooks,
|
|
13
|
-
state types, prop types — look them all up in the index you now hold, and open what it named in a
|
|
14
|
-
single call with one `Read` per file:
|
|
15
|
-
|
|
16
|
-
```
|
|
17
|
-
call 1 Read <pkgRoot>/dist/docs/index.json
|
|
18
|
-
Read <pkgRoot>/dist/dts-bundle/index.json
|
|
19
|
-
|
|
20
|
-
call 2 Read <pkgRoot>/dist/docs/Table.md <- docs entry's `file`
|
|
21
|
-
Read <pkgRoot>/dist/docs/useTableCollection.md
|
|
22
|
-
Read <pkgRoot>/dist/dts-bundle/components/Table.d.ts <- its `bundle`
|
|
23
|
-
Read <pkgRoot>/dist/dts-bundle/hooks/useTableCollection.d.ts
|
|
24
|
-
Read <pkgRoot>/dist/dts-bundle/types/TableState.d.ts <- bundle entry's `file`
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
Two calls, not twenty-two — and nothing is lost by batching, because there is nothing to learn
|
|
28
|
-
between the files: every path came out of the same index and `bytes` already told you each size.
|
|
29
|
-
The same files opened one per call re-send the whole conversation once per file, which is where a
|
|
30
|
-
lookup session's token cost actually goes. Batch the follow-ups the same way: when a doc names an
|
|
31
|
-
example file, or a stub names another bundle (below), collect them and read them together.
|
|
32
|
-
|
|
33
|
-
## Where props live
|
|
34
|
-
|
|
35
|
-
From `@wix/patterns` **1.460.0** onward a doc does not repeat props that the bundle already
|
|
36
|
-
describes. Which one holds them is stated in the doc's own index entry:
|
|
37
|
-
|
|
38
|
-
| `dist/docs/index.json` entry | where the props are |
|
|
39
|
-
| --- | --- |
|
|
40
|
-
| has a `bundle` field | that bundle — the doc's `### Props` is a pointer to it, on purpose |
|
|
41
|
-
| no `bundle` field | the doc's own `### Props` table, as before |
|
|
42
|
-
|
|
43
|
-
Roughly 74 of 167 docs point at a bundle and 62 still carry a table, so expect both. When a doc
|
|
44
|
-
says `Read \`dist/dts-bundle/...\``, that **is** the props answer — read the named file rather
|
|
45
|
-
than treating the doc as incomplete. The bundle is the better source anyway: it keeps the
|
|
46
|
-
`extends` clause with its exclusions, so `Omit<PopoverMenuItemProps, 'text' | 'prefixIcon' | 'onClick'>` tells
|
|
47
|
-
you what you do *not* inherit, which the old doc link did not.
|
|
48
|
-
|
|
49
|
-
## Types the docs don't cover
|
|
50
|
-
|
|
51
|
-
The same bundles also hold the types those props use — `Filter<T>`, `RangeItem<T>`, `CursorQuery`, a `...Props` interface. `Read <pkgRoot>/dist/dts-bundle/index.json`, look up the type name, then `Read <pkgRoot>/dist/dts-bundle/<entry.file>` using exactly the `file` path the index gives — **never reconstruct the path from the name**; bundles are nested one directory per kind (`components/Table.d.ts`, `hooks/useForm.d.ts`, `types/RangeItem.d.ts`, …), so guessing `<Name>.d.ts` at the top level is wrong by construction.
|
|
52
|
-
|
|
53
|
-
Several of these types (`RangeItem`, `Filter`, `CursorQuery`, the filter factory functions) actually live in `@wix/bex-core` — the bundle already resolves and inlines the real declaration, so you get the full shape with no deep, undeclared `@wix/bex-core/dist/types/...` path to chase. Still always import it from `@wix/patterns`, per the index's `importPath` field, never from wherever the bundle says it's really declared.
|
|
54
|
-
|
|
55
|
-
That inlining applies to the data shapes you write. State objects you *receive* are a separate case — see the next section.
|
|
56
|
-
|
|
57
|
-
## A bundle stops where you stop writing code
|
|
58
|
-
|
|
59
|
-
A bundle carries its own name's declaration plus every shape **you** would write by hand. It deliberately does *not* expand what the library hands you, so two kinds of one-line stub are normal and are answers, not truncation:
|
|
60
|
-
|
|
61
|
-
```ts
|
|
62
|
-
/**
|
|
63
|
-
* `TableState` — you receive this, you don't construct it.
|
|
64
|
-
* Produced by `useTableCollection()` (also `useAmbassadorTable()`, `useTableContext()`).
|
|
65
|
-
* Look `TableState` up in the bundle index for its own API.
|
|
66
|
-
*/
|
|
67
|
-
declare class TableState<T, F> {
|
|
68
|
-
}
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
An empty body with a **"Produced by"** note means: call that hook or factory to get one. That is usually the whole answer — `<Table state={...}>` needs `useTableCollection()`, not `TableState`'s internals. If you do need its members (`state.toolbar`, `state.visibleColumns`), it has its own index entry — look the name up and read that bundle, where its body is complete.
|
|
72
|
-
|
|
73
|
-
A stub saying **"cut here because it has its own bundle"** means the same thing for a shape you *do* write: it wasn't copied in twice, so look the name up and read its own file.
|
|
74
|
-
|
|
75
|
-
Every bundle fits in a single read; the index's `bytes` field says how big before you open it. So a bundle is never partially shown — if something looks missing, it was cut on purpose. Both stubs above name where to find it.
|
|
76
|
-
|
|
77
|
-
### The one that doesn't: a bare `import`
|
|
78
|
-
|
|
79
|
-
A plain `import { X } from '<module>'` at the top of a bundle, with no note attached, is the third shape you will meet — and unlike the two above it does **not** tell you where to look. What to do depends on the path, not the name:
|
|
80
|
-
|
|
81
|
-
**A normal entry point** — `react`, `react-hook-form`, `history`, `@wix/design-system`. Resolve it the ordinary way. For `@wix/design-system` names use the `wix-design-system` skill; don't read WDS files directly.
|
|
82
|
-
|
|
83
|
-
**A deep internal path** — `@wix/design-system/dist/types/DropdownLayout`, `@wix/bex-utils/@wix/bi-logger-os-data/v2/types`. This is an upstream defect: the bundle recorded where the type is *declared* instead of where it is exported. Two things follow, and the second is the one that saves you time:
|
|
84
|
-
|
|
85
|
-
- Never import that path in your own code, and don't go read the file it points at. It is not a public entry point, and the name is often not re-exported from the package root either — `DropdownLayoutOption` is not exported from `@wix/design-system`, so there is no shorter import to substitute.
|
|
86
|
-
- You usually don't need the declaration at all. Where the type is a **prop you fill in** — WDS option shapes like `SingleSelectFilter`'s and `AutoCompleteFilter`'s `items` — look the *component* up in the `wix-design-system` skill and use the shape its docs show. Where it is a **pass-through you never construct** — the BI logger params behind optional fields like `biAdditionalInfo` — pass what the patterns doc shows, or omit it; it is optional.
|
|
87
|
-
|
|
88
|
-
If a name you genuinely need stays unresolved after that, stop and say so, and name the bundle and the exact import path that dead-ended. Do not guess a shape and do not go spelunking in `node_modules` — a wrong guess compiles here and breaks at runtime, which is worse than the missing type.
|
|
89
|
-
|
|
90
|
-
A handful of names carry `"status": "unreachable"` — plus `"unexported": true` from **1.465.0** — with a message saying not to import them (the `...BaseProps` interfaces a component's props `extends`). Read those for the props they contribute; don't write an import for them.
|
|
91
|
-
|
|
92
|
-
`status` also carries `"deprecated"`, and there the `statusMessage` names the replacement — `PrimaryPageButton` says *"Use `PrimaryActions` component instead."* Check it before you commit to a name — nothing else in the lookup path will stop you, since a deprecated component still compiles and still renders. If the index calls a name deprecated, use what its message names instead.
|
|
93
|
-
|
|
94
|
-
## Subpath entry points
|
|
95
|
-
|
|
96
|
-
`@wix/patterns` is 31 entry points, not one namespace, and `/form` re-exports `@wix/bex-core/form`. To see what's importable from a specific one: `Read <pkgRoot>/dist/dts-bundle/exports/<subpath>.d.ts` directly (`.` is `exports/index.d.ts`; a nested one like `./testkit/backend` is `exports/testkit/backend.d.ts`). This only lists the curated names covered above — a file with nothing in it (a one-line comment) means no curated name lives on that subpath yet, not that the subpath doesn't exist.
|
|
97
|
-
|
|
98
|
-
## Following cross-references
|
|
99
|
-
|
|
100
|
-
Docs link to related names as Storybook URLs (`[TableState](./?path=/story/...--tablestate)`) — resolve the link text back to a filename via `dist/docs/index.json`, the same way you found the first doc, and `Read` it if you actually need it. There's no automatic multi-level expansion here; follow only the links you need.
|
|
101
|
-
|
|
102
|
-
Links to `https://www.docs.wixdesignsystem.com/` are external (Wix Design System) — not part of `@wix/patterns` docs. Likewise, a bundled `.d.ts` that references a deep `@wix/design-system/dist/...` path is pointing at that library's own internals — look the name up via the `wix-design-system` skill's own tool, not by opening the path.
|
|
103
|
-
|
|
104
|
-
## Tips
|
|
105
|
-
|
|
106
|
-
- **Compound components** have separate docs per sub-part: `CollectionPage.md`, `CollectionPage.Header.md`, `CollectionPage.Content.md`.
|
|
107
|
-
- **Hook docs** list configuration options as props — in the API table when the doc has no
|
|
108
|
-
`bundle` field, otherwise in the bundle it points at.
|
|
109
|
-
- **Heavy examples live beside the doc.** A variation whose code was long is written out
|
|
110
|
-
separately, and the doc keeps the heading, the description and an exact path:
|
|
111
|
-
`Example code: read \`dist/docs/ToolbarFilters/apply-changes.tsx\``. Read that path only if you
|
|
112
|
-
need that particular variation — the heading and description are usually enough to choose.
|
package/skills/wix-headless-fast/references/storefront/app/components/storefront/ProductCard.tsx
DELETED
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
// Grid tile — a REFERENCE implementation over the ProductSummary DTO. Complete and correct
|
|
2
|
-
// (badges, sale price, hover image, quick-add); styled entirely from the @theme tokens via
|
|
3
|
-
// Tailwind utilities. Designing your own card on the same DTO is encouraged.
|
|
4
|
-
import type { ComponentType, ReactNode } from "react";
|
|
5
|
-
import type { ProductSummary } from "../../wix/storefront/types";
|
|
6
|
-
import { useCart } from "../../hooks/storefront/useCart";
|
|
7
|
-
|
|
8
|
-
export interface LinkLikeProps {
|
|
9
|
-
href: string;
|
|
10
|
-
className?: string;
|
|
11
|
-
children?: ReactNode;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export interface ProductCardProps {
|
|
15
|
-
product: ProductSummary;
|
|
16
|
-
/** Route pattern for detail pages; default matches the shipped /products/[slug] page. */
|
|
17
|
-
productHref?: (slug: string) => string;
|
|
18
|
-
/** Router-specific link (react-router Link, next/link); defaults to a plain <a>. */
|
|
19
|
-
LinkComponent?: ComponentType<LinkLikeProps>;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const PlainLink = ({ href, className, children }: LinkLikeProps) => (
|
|
23
|
-
<a href={href} className={className}>
|
|
24
|
-
{children}
|
|
25
|
-
</a>
|
|
26
|
-
);
|
|
27
|
-
|
|
28
|
-
export default function ProductCard({
|
|
29
|
-
product,
|
|
30
|
-
productHref = (slug) => `/products/${slug}`,
|
|
31
|
-
LinkComponent = PlainLink,
|
|
32
|
-
}: ProductCardProps) {
|
|
33
|
-
const { addToCart, busy } = useCart();
|
|
34
|
-
const soldOut = product.availability === "OUT_OF_STOCK" && !product.preorder;
|
|
35
|
-
const priceLabel =
|
|
36
|
-
product.maxPrice && product.maxPrice !== product.price
|
|
37
|
-
? `${product.price} – ${product.maxPrice}`
|
|
38
|
-
: product.price;
|
|
39
|
-
|
|
40
|
-
return (
|
|
41
|
-
<div className="group">
|
|
42
|
-
<LinkComponent href={productHref(product.slug)} className="block no-underline">
|
|
43
|
-
<div className="relative aspect-square overflow-hidden rounded-lg bg-secondary">
|
|
44
|
-
{product.imageUrl && (
|
|
45
|
-
<img
|
|
46
|
-
src={product.imageUrl}
|
|
47
|
-
alt={product.name}
|
|
48
|
-
loading="lazy"
|
|
49
|
-
className="h-full w-full object-cover transition-transform duration-300 group-hover:scale-[1.03]"
|
|
50
|
-
/>
|
|
51
|
-
)}
|
|
52
|
-
{product.hoverImageUrl && (
|
|
53
|
-
<img
|
|
54
|
-
src={product.hoverImageUrl}
|
|
55
|
-
alt=""
|
|
56
|
-
aria-hidden="true"
|
|
57
|
-
loading="lazy"
|
|
58
|
-
className="absolute inset-0 h-full w-full object-cover opacity-0 transition-opacity duration-300 group-hover:opacity-100"
|
|
59
|
-
/>
|
|
60
|
-
)}
|
|
61
|
-
{product.ribbon && (
|
|
62
|
-
<span className="absolute left-3 top-3 rounded-full bg-background/90 px-3 py-1 text-xs font-semibold text-foreground backdrop-blur">
|
|
63
|
-
{product.ribbon}
|
|
64
|
-
</span>
|
|
65
|
-
)}
|
|
66
|
-
{soldOut && (
|
|
67
|
-
<span className="absolute right-3 top-3 rounded-full bg-background/90 px-3 py-1 text-xs font-semibold text-muted-foreground backdrop-blur">
|
|
68
|
-
Sold out
|
|
69
|
-
</span>
|
|
70
|
-
)}
|
|
71
|
-
{product.preorder && (
|
|
72
|
-
<span className="absolute right-3 top-3 rounded-full bg-background/90 px-3 py-1 text-xs font-semibold text-foreground backdrop-blur">
|
|
73
|
-
Pre-order
|
|
74
|
-
</span>
|
|
75
|
-
)}
|
|
76
|
-
</div>
|
|
77
|
-
<p className="mt-3 text-sm font-medium text-foreground">{product.name}</p>
|
|
78
|
-
<p className="mt-0.5 flex items-baseline gap-2 text-sm">
|
|
79
|
-
<span className="text-foreground">{priceLabel}</span>
|
|
80
|
-
{product.compareAtPrice && (
|
|
81
|
-
<span className="text-muted-foreground line-through">{product.compareAtPrice}</span>
|
|
82
|
-
)}
|
|
83
|
-
</p>
|
|
84
|
-
{product.optionsSummary && (
|
|
85
|
-
<p className="mt-0.5 text-xs text-muted-foreground">{product.optionsSummary}</p>
|
|
86
|
-
)}
|
|
87
|
-
</LinkComponent>
|
|
88
|
-
{product.quickAddable && (
|
|
89
|
-
<button
|
|
90
|
-
type="button"
|
|
91
|
-
disabled={busy}
|
|
92
|
-
onClick={() => addToCart(product.id).catch(() => {})}
|
|
93
|
-
className="mt-2 rounded-full border border-border px-4 py-1.5 text-xs font-medium text-foreground transition-colors hover:bg-secondary disabled:opacity-50"
|
|
94
|
-
>
|
|
95
|
-
Add to cart
|
|
96
|
-
</button>
|
|
97
|
-
)}
|
|
98
|
-
</div>
|
|
99
|
-
);
|
|
100
|
-
}
|
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
// The full product detail surface: gallery, price, option/modifier picker, quantity,
|
|
2
|
-
// add-to-cart — styled from the @theme tokens. Mount as-is (Astro: an island with the
|
|
3
|
-
// server-fetched `initial` product; SPA: pass the `slug`). All selection→variant logic lives
|
|
4
|
-
// in useProductDetail — never bypass it to add a product with options.
|
|
5
|
-
import { useState } from "react";
|
|
6
|
-
import { useProductDetail } from "../../hooks/storefront/useProductDetail";
|
|
7
|
-
import VariantPicker from "./VariantPicker";
|
|
8
|
-
|
|
9
|
-
export interface ProductDetailViewProps {
|
|
10
|
-
/** Server-fetched product (Astro/Next SSR). */
|
|
11
|
-
initial?: import("../../wix/storefront/types").ProductDetail | null;
|
|
12
|
-
/** SPA alternative: fetch by slug on mount. */
|
|
13
|
-
slug?: string;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export default function ProductDetailView({ initial, slug }: ProductDetailViewProps) {
|
|
17
|
-
const {
|
|
18
|
-
product,
|
|
19
|
-
notFound,
|
|
20
|
-
optionGroups,
|
|
21
|
-
selectOption,
|
|
22
|
-
modifierValues,
|
|
23
|
-
setModifier,
|
|
24
|
-
price,
|
|
25
|
-
compareAtPrice,
|
|
26
|
-
canAdd,
|
|
27
|
-
quantity,
|
|
28
|
-
setQuantity,
|
|
29
|
-
add,
|
|
30
|
-
adding,
|
|
31
|
-
error,
|
|
32
|
-
} = useProductDetail({ initial, slug });
|
|
33
|
-
const [imageIndex, setImageIndex] = useState(0);
|
|
34
|
-
|
|
35
|
-
if (notFound)
|
|
36
|
-
return <p className="py-16 text-center text-muted-foreground">This product doesn't exist (anymore).</p>;
|
|
37
|
-
if (!product) return <div className="aspect-square max-w-md animate-pulse rounded-xl bg-secondary" />;
|
|
38
|
-
|
|
39
|
-
const soldOut = product.availability === "OUT_OF_STOCK" && !product.preorder;
|
|
40
|
-
const mainImage = product.gallery[imageIndex] ?? product.imageUrl;
|
|
41
|
-
|
|
42
|
-
return (
|
|
43
|
-
<div className="grid gap-10 md:grid-cols-2">
|
|
44
|
-
<div>
|
|
45
|
-
<div className="aspect-square overflow-hidden rounded-xl bg-secondary">
|
|
46
|
-
{mainImage && <img src={mainImage} alt={product.name} className="h-full w-full object-cover" />}
|
|
47
|
-
</div>
|
|
48
|
-
{product.gallery.length > 1 && (
|
|
49
|
-
<div className="mt-3 flex flex-wrap gap-2">
|
|
50
|
-
{product.gallery.map((url, i) => (
|
|
51
|
-
<button
|
|
52
|
-
key={url}
|
|
53
|
-
type="button"
|
|
54
|
-
aria-label={`Image ${i + 1}`}
|
|
55
|
-
onClick={() => setImageIndex(i)}
|
|
56
|
-
className={`h-16 w-16 overflow-hidden rounded-md border-2 transition-colors ${
|
|
57
|
-
i === imageIndex ? "border-primary" : "border-transparent hover:border-border"
|
|
58
|
-
}`}
|
|
59
|
-
>
|
|
60
|
-
<img src={url} alt="" className="h-full w-full object-cover" />
|
|
61
|
-
</button>
|
|
62
|
-
))}
|
|
63
|
-
</div>
|
|
64
|
-
)}
|
|
65
|
-
</div>
|
|
66
|
-
|
|
67
|
-
<div>
|
|
68
|
-
<h1 className="text-2xl font-semibold tracking-tight">{product.name}</h1>
|
|
69
|
-
<p className="mt-2 flex items-baseline gap-3">
|
|
70
|
-
<span className="text-xl font-medium">{price}</span>
|
|
71
|
-
{compareAtPrice && <span className="text-base text-muted-foreground line-through">{compareAtPrice}</span>}
|
|
72
|
-
</p>
|
|
73
|
-
{soldOut && <p className="mt-2 text-sm font-medium text-red-600">Out of stock</p>}
|
|
74
|
-
{product.preorder && <p className="mt-2 text-sm text-muted-foreground">Available for pre-order</p>}
|
|
75
|
-
|
|
76
|
-
{product.descriptionHtml && (
|
|
77
|
-
// plainDescription is HTML (despite the name) — render it, don't print it.
|
|
78
|
-
<div
|
|
79
|
-
className="mt-4 space-y-2 text-sm leading-relaxed text-muted-foreground [&_strong]:font-semibold [&_strong]:text-foreground"
|
|
80
|
-
dangerouslySetInnerHTML={{ __html: product.descriptionHtml }}
|
|
81
|
-
/>
|
|
82
|
-
)}
|
|
83
|
-
|
|
84
|
-
<div className="mt-6">
|
|
85
|
-
<VariantPicker
|
|
86
|
-
optionGroups={optionGroups}
|
|
87
|
-
selectOption={selectOption}
|
|
88
|
-
modifiers={product.modifiers}
|
|
89
|
-
modifierValues={modifierValues}
|
|
90
|
-
setModifier={setModifier}
|
|
91
|
-
/>
|
|
92
|
-
</div>
|
|
93
|
-
|
|
94
|
-
{!soldOut && (
|
|
95
|
-
<div className="mt-2 flex items-center gap-3">
|
|
96
|
-
<div className="inline-flex items-center gap-3 rounded-full border border-border px-3 py-2">
|
|
97
|
-
<button
|
|
98
|
-
type="button"
|
|
99
|
-
aria-label="Decrease quantity"
|
|
100
|
-
disabled={quantity <= 1}
|
|
101
|
-
onClick={() => setQuantity(quantity - 1)}
|
|
102
|
-
className="px-1 text-base disabled:opacity-40"
|
|
103
|
-
>
|
|
104
|
-
−
|
|
105
|
-
</button>
|
|
106
|
-
<span className="text-sm tabular-nums">{quantity}</span>
|
|
107
|
-
<button
|
|
108
|
-
type="button"
|
|
109
|
-
aria-label="Increase quantity"
|
|
110
|
-
onClick={() => setQuantity(quantity + 1)}
|
|
111
|
-
className="px-1 text-base"
|
|
112
|
-
>
|
|
113
|
-
+
|
|
114
|
-
</button>
|
|
115
|
-
</div>
|
|
116
|
-
<button
|
|
117
|
-
type="button"
|
|
118
|
-
disabled={!canAdd || adding}
|
|
119
|
-
onClick={() => add().catch(() => {})}
|
|
120
|
-
className="rounded-full bg-primary px-8 py-3 text-sm font-semibold text-primary-foreground transition-opacity hover:opacity-90 disabled:opacity-50"
|
|
121
|
-
>
|
|
122
|
-
{adding ? "Adding…" : canAdd ? "Add to cart" : "Select options"}
|
|
123
|
-
</button>
|
|
124
|
-
</div>
|
|
125
|
-
)}
|
|
126
|
-
{error && <p className="mt-3 text-sm text-red-600">{error}</p>}
|
|
127
|
-
</div>
|
|
128
|
-
</div>
|
|
129
|
-
);
|
|
130
|
-
}
|
package/skills/wix-headless-fast/references/storefront/app/components/storefront/ProductGrid.tsx
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
// Listing grid with loading skeletons and an honest empty state — a REFERENCE layout on the
|
|
2
|
-
// @theme tokens. Keep the states (they're correct); choosing a grid arrangement that fits the
|
|
3
|
-
// brand is encouraged. Never mock products: empty catalog → the empty state.
|
|
4
|
-
import type { ComponentType } from "react";
|
|
5
|
-
import type { ProductSummary } from "../../wix/storefront/types";
|
|
6
|
-
import ProductCard, { type ProductCardProps } from "./ProductCard";
|
|
7
|
-
|
|
8
|
-
export interface ProductGridProps {
|
|
9
|
-
/** null → loading skeletons; [] → the empty state. */
|
|
10
|
-
products: ProductSummary[] | null;
|
|
11
|
-
emptyMessage?: string;
|
|
12
|
-
productHref?: ProductCardProps["productHref"];
|
|
13
|
-
LinkComponent?: ProductCardProps["LinkComponent"];
|
|
14
|
-
/** Swap in your own tile (built on ProductSummary) without re-doing the states. */
|
|
15
|
-
CardComponent?: ComponentType<ProductCardProps>;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export default function ProductGrid({
|
|
19
|
-
products,
|
|
20
|
-
emptyMessage = "No products yet — check back soon.",
|
|
21
|
-
productHref,
|
|
22
|
-
LinkComponent,
|
|
23
|
-
CardComponent = ProductCard,
|
|
24
|
-
}: ProductGridProps) {
|
|
25
|
-
if (products === null) {
|
|
26
|
-
return (
|
|
27
|
-
<div className="grid grid-cols-2 gap-x-5 gap-y-8 sm:grid-cols-3 lg:grid-cols-4" aria-busy="true">
|
|
28
|
-
{Array.from({ length: 8 }, (_, i) => (
|
|
29
|
-
<div key={i}>
|
|
30
|
-
<div className="aspect-square animate-pulse rounded-lg bg-secondary" />
|
|
31
|
-
<div className="mt-3 h-3.5 w-3/4 animate-pulse rounded bg-secondary" />
|
|
32
|
-
<div className="mt-2 h-3.5 w-1/3 animate-pulse rounded bg-secondary" />
|
|
33
|
-
</div>
|
|
34
|
-
))}
|
|
35
|
-
</div>
|
|
36
|
-
);
|
|
37
|
-
}
|
|
38
|
-
if (products.length === 0) {
|
|
39
|
-
return <p className="py-16 text-center text-muted-foreground">{emptyMessage}</p>;
|
|
40
|
-
}
|
|
41
|
-
return (
|
|
42
|
-
<div className="grid grid-cols-2 gap-x-5 gap-y-8 sm:grid-cols-3 lg:grid-cols-4">
|
|
43
|
-
{products.map((p) => (
|
|
44
|
-
<CardComponent key={p.id} product={p} productHref={productHref} LinkComponent={LinkComponent} />
|
|
45
|
-
))}
|
|
46
|
-
</div>
|
|
47
|
-
);
|
|
48
|
-
}
|
package/skills/wix-headless-fast/references/storefront/app/components/storefront/ShopView.tsx
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
// The full shop surface: live category filter bar + product grid, styled from the @theme
|
|
2
|
-
// tokens. Mount as-is (Astro: one island with server-fetched initial data; SPA: no props).
|
|
3
|
-
import { useShop } from "../../hooks/storefront/useShop";
|
|
4
|
-
import type { Category, ProductSummary } from "../../wix/storefront/types";
|
|
5
|
-
import ProductGrid, { type ProductGridProps } from "./ProductGrid";
|
|
6
|
-
|
|
7
|
-
export interface ShopViewProps {
|
|
8
|
-
/** Server-fetched data (Astro/Next SSR) — omit in a SPA and it fetches client-side. */
|
|
9
|
-
initialProducts?: ProductSummary[];
|
|
10
|
-
initialCategories?: Category[];
|
|
11
|
-
emptyMessage?: string;
|
|
12
|
-
productHref?: ProductGridProps["productHref"];
|
|
13
|
-
LinkComponent?: ProductGridProps["LinkComponent"];
|
|
14
|
-
CardComponent?: ProductGridProps["CardComponent"];
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const pill = (active: boolean) =>
|
|
18
|
-
`rounded-full border px-4 py-1.5 text-sm font-medium transition-colors ${
|
|
19
|
-
active
|
|
20
|
-
? "border-primary bg-primary text-primary-foreground"
|
|
21
|
-
: "border-border text-foreground hover:bg-secondary"
|
|
22
|
-
}`;
|
|
23
|
-
|
|
24
|
-
export default function ShopView({
|
|
25
|
-
initialProducts,
|
|
26
|
-
initialCategories,
|
|
27
|
-
emptyMessage,
|
|
28
|
-
productHref,
|
|
29
|
-
LinkComponent,
|
|
30
|
-
CardComponent,
|
|
31
|
-
}: ShopViewProps) {
|
|
32
|
-
const { products, categories, activeCategoryId, setActiveCategoryId, loading, error } = useShop({
|
|
33
|
-
initialProducts,
|
|
34
|
-
initialCategories,
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
return (
|
|
38
|
-
<div>
|
|
39
|
-
{categories.length > 1 && (
|
|
40
|
-
<div className="mb-8 flex flex-wrap gap-2" role="group" aria-label="Categories">
|
|
41
|
-
<button type="button" className={pill(activeCategoryId === null)} onClick={() => setActiveCategoryId(null)}>
|
|
42
|
-
All
|
|
43
|
-
</button>
|
|
44
|
-
{categories.map((c) => (
|
|
45
|
-
<button
|
|
46
|
-
key={c.id}
|
|
47
|
-
type="button"
|
|
48
|
-
className={pill(activeCategoryId === c.id)}
|
|
49
|
-
onClick={() => setActiveCategoryId(c.id)}
|
|
50
|
-
>
|
|
51
|
-
{c.name}
|
|
52
|
-
</button>
|
|
53
|
-
))}
|
|
54
|
-
</div>
|
|
55
|
-
)}
|
|
56
|
-
{error && <p className="mb-4 text-sm text-red-600">{error}</p>}
|
|
57
|
-
<ProductGrid
|
|
58
|
-
products={loading ? null : products}
|
|
59
|
-
emptyMessage={emptyMessage}
|
|
60
|
-
productHref={productHref}
|
|
61
|
-
LinkComponent={LinkComponent}
|
|
62
|
-
CardComponent={CardComponent}
|
|
63
|
-
/>
|
|
64
|
-
</div>
|
|
65
|
-
);
|
|
66
|
-
}
|
package/skills/wix-headless-fast/references/storefront/app/components/storefront/VariantPicker.tsx
DELETED
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
// Option/modifier selector — wire as-is on the PDP (color options render as swatches, text
|
|
2
|
-
// options as pills, modifiers as pills or a text input). Styled from the @theme tokens.
|
|
3
|
-
import type { ProductModifier } from "../../wix/storefront/types";
|
|
4
|
-
import type { OptionGroupView } from "../../hooks/storefront/useProductDetail";
|
|
5
|
-
|
|
6
|
-
export interface VariantPickerProps {
|
|
7
|
-
optionGroups: OptionGroupView[];
|
|
8
|
-
selectOption: (optionName: string, choiceName: string) => void;
|
|
9
|
-
modifiers?: ProductModifier[];
|
|
10
|
-
modifierValues?: Record<string, string>;
|
|
11
|
-
setModifier?: (key: string, value: string) => void;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const chip = (selected: boolean) =>
|
|
15
|
-
`rounded-full border px-4 py-1.5 text-sm transition-colors disabled:cursor-not-allowed disabled:opacity-40 disabled:line-through ${
|
|
16
|
-
selected
|
|
17
|
-
? "border-primary bg-primary text-primary-foreground"
|
|
18
|
-
: "border-border text-foreground hover:bg-secondary"
|
|
19
|
-
}`;
|
|
20
|
-
|
|
21
|
-
export default function VariantPicker({
|
|
22
|
-
optionGroups,
|
|
23
|
-
selectOption,
|
|
24
|
-
modifiers = [],
|
|
25
|
-
modifierValues = {},
|
|
26
|
-
setModifier,
|
|
27
|
-
}: VariantPickerProps) {
|
|
28
|
-
return (
|
|
29
|
-
<div>
|
|
30
|
-
{optionGroups.map((group) => (
|
|
31
|
-
<div className="mb-5" key={group.id || group.name}>
|
|
32
|
-
<p className="mb-2 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
|
33
|
-
{group.name}
|
|
34
|
-
</p>
|
|
35
|
-
<div className="flex flex-wrap items-center gap-2">
|
|
36
|
-
{group.choices.map((choice) =>
|
|
37
|
-
group.isColor && choice.colorCode ? (
|
|
38
|
-
<button
|
|
39
|
-
key={choice.choiceId || choice.name}
|
|
40
|
-
type="button"
|
|
41
|
-
style={{ background: choice.colorCode }}
|
|
42
|
-
title={choice.name}
|
|
43
|
-
aria-label={`${group.name}: ${choice.name}`}
|
|
44
|
-
aria-pressed={choice.selected}
|
|
45
|
-
disabled={!choice.inStock}
|
|
46
|
-
onClick={() => selectOption(group.name, choice.name)}
|
|
47
|
-
className={`h-8 w-8 rounded-full border transition-shadow disabled:cursor-not-allowed disabled:opacity-35 ${
|
|
48
|
-
choice.selected
|
|
49
|
-
? "border-primary ring-2 ring-primary ring-offset-2 ring-offset-background"
|
|
50
|
-
: "border-border"
|
|
51
|
-
}`}
|
|
52
|
-
/>
|
|
53
|
-
) : (
|
|
54
|
-
<button
|
|
55
|
-
key={choice.choiceId || choice.name}
|
|
56
|
-
type="button"
|
|
57
|
-
aria-pressed={choice.selected}
|
|
58
|
-
disabled={!choice.inStock}
|
|
59
|
-
onClick={() => selectOption(group.name, choice.name)}
|
|
60
|
-
className={chip(choice.selected)}
|
|
61
|
-
>
|
|
62
|
-
{choice.name}
|
|
63
|
-
</button>
|
|
64
|
-
),
|
|
65
|
-
)}
|
|
66
|
-
</div>
|
|
67
|
-
</div>
|
|
68
|
-
))}
|
|
69
|
-
|
|
70
|
-
{modifiers.map((m) => (
|
|
71
|
-
<div className="mb-5" key={m.key}>
|
|
72
|
-
<p className="mb-2 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
|
73
|
-
{m.name}
|
|
74
|
-
{m.mandatory ? " *" : ""}
|
|
75
|
-
</p>
|
|
76
|
-
{m.type === "text" ? (
|
|
77
|
-
<input
|
|
78
|
-
type="text"
|
|
79
|
-
value={modifierValues[m.key] ?? ""}
|
|
80
|
-
onChange={(e) => setModifier?.(m.key, e.target.value)}
|
|
81
|
-
aria-label={m.name}
|
|
82
|
-
className="w-full rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground outline-none transition-shadow focus:ring-2 focus:ring-primary"
|
|
83
|
-
/>
|
|
84
|
-
) : (
|
|
85
|
-
<div className="flex flex-wrap gap-2">
|
|
86
|
-
{m.choices.map((c) => (
|
|
87
|
-
<button
|
|
88
|
-
key={c.key}
|
|
89
|
-
type="button"
|
|
90
|
-
aria-pressed={modifierValues[m.key] === c.key}
|
|
91
|
-
onClick={() => setModifier?.(m.key, c.key)}
|
|
92
|
-
className={chip(modifierValues[m.key] === c.key)}
|
|
93
|
-
>
|
|
94
|
-
{c.name}
|
|
95
|
-
</button>
|
|
96
|
-
))}
|
|
97
|
-
</div>
|
|
98
|
-
)}
|
|
99
|
-
</div>
|
|
100
|
-
))}
|
|
101
|
-
</div>
|
|
102
|
-
);
|
|
103
|
-
}
|
package/skills/wix-headless-fast/references/storefront/app-astro/pages/products/[slug].astro
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
// Product detail page — wire as-is; theme via SiteLayout's tokens.
|
|
3
|
-
//
|
|
4
|
-
// This is a Wix ITEM PAGE: the wixMetadata export + <SEO.Tags> below are what let the site
|
|
5
|
-
// owner edit this page's title/description/OG in the dashboard, and register the route in
|
|
6
|
-
// the sitemap. Keep all three SEO pieces when editing this file.
|
|
7
|
-
import SiteLayout from "../../layouts/SiteLayout.astro";
|
|
8
|
-
import ProductDetailView from "../../components/storefront/ProductDetailView";
|
|
9
|
-
import { fetchProductBySlug } from "../../wix/storefront/catalog";
|
|
10
|
-
import { WIX_APPS } from "@wix/essentials";
|
|
11
|
-
import { SEO } from "@wix/seo/components";
|
|
12
|
-
import { loadSEOTagsServiceConfig } from "@wix/seo/services";
|
|
13
|
-
import { seoTags } from "@wix/seo";
|
|
14
|
-
|
|
15
|
-
export const wixMetadata = {
|
|
16
|
-
appDefId: WIX_APPS.checkoutAndOrders.id,
|
|
17
|
-
pageIdentifier: WIX_APPS.checkoutAndOrders.productPageMetadata.pageIdentifier,
|
|
18
|
-
identifiers: {
|
|
19
|
-
slug: WIX_APPS.checkoutAndOrders.productPageMetadata.identifiers.handle,
|
|
20
|
-
},
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
const slug = Astro.params.slug!;
|
|
24
|
-
|
|
25
|
-
// Behind Wix's proxy the request URL is the internal one — the real public page URL
|
|
26
|
-
// arrives on x-wix-forwarded-url. The SEO service needs the public URL.
|
|
27
|
-
const forwardedUrl = Astro.request.headers.get("x-wix-forwarded-url");
|
|
28
|
-
const pageUrl =
|
|
29
|
-
forwardedUrl && URL.canParse(forwardedUrl) && /^https?:$/.test(new URL(forwardedUrl).protocol)
|
|
30
|
-
? forwardedUrl
|
|
31
|
-
: Astro.url.href;
|
|
32
|
-
|
|
33
|
-
let product = null;
|
|
34
|
-
let seoTagsServiceConfig = null;
|
|
35
|
-
try {
|
|
36
|
-
[product, seoTagsServiceConfig] = await Promise.all([
|
|
37
|
-
fetchProductBySlug(slug),
|
|
38
|
-
loadSEOTagsServiceConfig({
|
|
39
|
-
pageUrl,
|
|
40
|
-
itemType: seoTags.ItemType.STORES_PRODUCT,
|
|
41
|
-
itemData: { slug },
|
|
42
|
-
}),
|
|
43
|
-
]);
|
|
44
|
-
} catch {
|
|
45
|
-
// Guarded: an unhandled SSR throw truncates the response mid-stream.
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
if (!product) {
|
|
49
|
-
return new Response(null, { status: 404 });
|
|
50
|
-
}
|
|
51
|
-
---
|
|
52
|
-
|
|
53
|
-
<SiteLayout title={product.name}>
|
|
54
|
-
<SEO.Tags seoTagsServiceConfig={seoTagsServiceConfig} slot="seo-tags" />
|
|
55
|
-
<ProductDetailView client:load initial={product} />
|
|
56
|
-
</SiteLayout>
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
// Shop listing — products and categories are fetched SERVER-SIDE (SEO) and handed to the
|
|
3
|
-
// island as serialized DTO props; category switches then filter live on the client.
|
|
4
|
-
// Wire as-is; theme via SiteLayout's tokens.
|
|
5
|
-
import SiteLayout from "../layouts/SiteLayout.astro";
|
|
6
|
-
import ShopView from "../components/storefront/ShopView";
|
|
7
|
-
import { fetchProducts, fetchCategories } from "../wix/storefront/catalog";
|
|
8
|
-
import type { Category, ProductSummary } from "../wix/storefront/types";
|
|
9
|
-
|
|
10
|
-
let products: ProductSummary[] = [];
|
|
11
|
-
let categories: Category[] = [];
|
|
12
|
-
try {
|
|
13
|
-
[products, categories] = await Promise.all([fetchProducts(), fetchCategories()]);
|
|
14
|
-
} catch {
|
|
15
|
-
// An unguarded SSR throw truncates the response mid-stream; the island renders the
|
|
16
|
-
// shipped empty state instead.
|
|
17
|
-
}
|
|
18
|
-
---
|
|
19
|
-
|
|
20
|
-
<SiteLayout title="Shop">
|
|
21
|
-
<h1 class="mb-8 text-2xl font-semibold tracking-tight">Shop</h1>
|
|
22
|
-
<ShopView client:load initialProducts={products} initialCategories={categories} />
|
|
23
|
-
</SiteLayout>
|