@thorprovider/storefront 0.4.0-mvp.0 → 0.4.0-mvp.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.
- package/README.md +173 -153
- package/VENDOR.md +69 -0
- package/assets/openspec/config.yaml +38 -0
- package/assets/templates/AGENTS.md +76 -0
- package/assets/templates/app/app/[[...slug]]/page.tsx +42 -1
- package/assets/templates/app/app/[[...slug]]/renderer.tsx +13 -2
- package/assets/templates/app/app/globals.css +5 -8
- package/assets/templates/app/app/layout.tsx +1 -5
- package/assets/templates/app/app/robots.ts +9 -3
- package/assets/templates/app/app/sitemap.ts +64 -5
- package/assets/templates/app/lib/__STOREFRONT__/actions.ts +70 -0
- package/assets/templates/app/lib/__STOREFRONT__/catalog.ts +15 -5
- package/assets/templates/app/lib/__STOREFRONT__/commerce.ts +71 -0
- package/assets/templates/app/lib/__STOREFRONT__/composition.ts +26 -0
- package/assets/templates/app/lib/__STOREFRONT__/handlers.core.ts +36 -4
- package/assets/templates/app/lib/__STOREFRONT__/registry.tsx +82 -50
- package/assets/templates/app/lib/__STOREFRONT__/runtime.ts +3 -0
- package/assets/templates/app/lib/__STOREFRONT__/schema.ts +29 -14
- package/assets/templates/app/lib/__STOREFRONT__/section-observation.ts +182 -0
- package/assets/templates/app/lib/__STOREFRONT__/section-sources.ts +130 -0
- package/assets/templates/app/lib/__STOREFRONT__/shell/content.ts +31 -7
- package/assets/templates/app/lib/__STOREFRONT__/shell/props.ts +4 -0
- package/assets/templates/app/lib/__STOREFRONT__/spec/index.ts +56 -26
- package/assets/templates/app/lib/__STOREFRONT__/spec/seo.ts +165 -0
- package/assets/templates/app/lib/__STOREFRONT__/store-context.ts +26 -7
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/index.ts +2 -1
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/products.client.tsx +100 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/products.tsx +19 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/schema.products.ts +39 -0
- package/assets/templates/features/checkout/handlers.ts +6 -0
- package/assets/templates/features/customer-auth/state.ts +10 -1
- package/assets/templates/views/account/index.tsx +1 -1
- package/assets/templates/views/account/registry.tsx +80 -19
- package/assets/templates/views/account/schema.ts +1 -0
- package/assets/templates/views/cart/content.ts +11 -0
- package/assets/templates/views/cart/data.ts +23 -0
- package/assets/templates/views/cart/index.tsx +53 -0
- package/assets/templates/views/cart/props.ts +12 -0
- package/assets/templates/views/cart/registry.tsx +81 -0
- package/assets/templates/views/cart/schema.ts +52 -0
- package/assets/templates/views/category/assembly/assembly.json +1 -0
- package/assets/templates/views/category/assembly/index.skeleton.tsx +64 -6
- package/assets/templates/views/category/assembly/registry.skeleton.tsx +2 -1
- package/assets/templates/views/category/assembly/slots/breadcrumb/Breadcrumb/binding.json +5 -0
- package/assets/templates/views/category/assembly/slots/breadcrumb/Breadcrumb/catalog.part +4 -0
- package/assets/templates/views/category/assembly/slots/breadcrumb/Breadcrumb/element.part +7 -0
- package/assets/templates/views/category/assembly/slots/breadcrumb/Breadcrumb/registry.part +18 -0
- package/assets/templates/views/category/index.tsx +76 -7
- package/assets/templates/views/category/props.ts +7 -0
- package/assets/templates/views/category/registry.tsx +19 -0
- package/assets/templates/views/category/schema.ts +13 -0
- package/assets/templates/views/home/assembly/assembly.json +9 -0
- package/assets/templates/views/home/assembly/index.skeleton.tsx +22 -4
- package/assets/templates/views/home/assembly/slots/hero/StorefrontHero1/element.part +1 -1
- package/assets/templates/views/home/assembly/slots/hero/StorefrontHero1/registry.part +2 -2
- package/assets/templates/views/home/data.ts +9 -0
- package/assets/templates/views/home/index.tsx +23 -5
- package/assets/templates/views/home/registry.tsx +2 -2
- package/assets/templates/views/product/data.ts +4 -2
- package/assets/templates/views/product/index.tsx +49 -5
- package/assets/templates/views/product/props.ts +7 -0
- package/assets/templates/views/product/registry.tsx +25 -0
- package/assets/templates/views/product/schema.ts +13 -0
- package/assets/templates/views/static/content.ts +4 -4
- package/assets/templates/views/static/index.tsx +1 -1
- package/bin/storefront.js +1231 -154
- package/examples/merchandising/category-led.json +37 -0
- package/examples/merchandising/collection-led.json +37 -0
- package/package.json +11 -7
- package/recipes/archetypes.json +9 -22
- package/recipes/catalog.json +40 -1
- package/recipes/features.json +25 -2
- package/recipes/recipe.schema.json +1 -1
- package/recipes/recipes.json +32 -30
- package/recipes/sections.json +10 -0
- package/recipes/validate.mjs +54 -0
- package/scripts/candidate.mjs +4 -4
- package/scripts/composers.mjs +25 -5
- package/scripts/env-file.mjs +86 -0
- package/scripts/file-classes.mjs +5 -0
- package/scripts/migrate-english-routes.mjs +276 -0
- package/scripts/pinning.mjs +74 -5
- package/scripts/plan-apply.mjs +149 -5
- package/scripts/reconcile.mjs +49 -11
- package/scripts/render-templates.mjs +97 -44
- package/scripts/section-config.mjs +253 -0
- package/scripts/starter.mjs +17 -0
- package/scripts/theme/color.mjs +185 -0
- package/scripts/theme/css-theme.mjs +233 -0
- package/scripts/theme/fonts.mjs +123 -0
- package/scripts/theme/import-export.mjs +125 -0
- package/scripts/theme/preset.mjs +93 -0
- package/scripts/theme/theme-plan.mjs +292 -0
- package/scripts/theme/tokens.mjs +137 -0
- package/scripts/view-assembly.mjs +47 -1
- package/skills/sf-feature/SKILL.md +32 -19
- package/skills/sf-setup/SKILL.md +79 -0
- package/skills/sf-theme-gen/SKILL.md +99 -36
- package/skills/sf-theme-gen/references/shadcn-theming.md +87 -0
- package/skills/{shadcn-theming/references/templates → sf-theme-gen/references}/theme-setup.md +15 -19
- package/skills/{shadcn-theming → sf-theme-gen}/references/theming-guide.md +6 -11
- package/skills/sf-view-gen/SKILL.md +63 -40
- package/skills/sf-view-gen/assets/recipes/archetypes.json +9 -22
- package/skills/sf-view-gen/assets/recipes/catalog.json +40 -1
- package/skills/sf-view-gen/assets/recipes/features.json +25 -2
- package/skills/sf-view-gen/assets/recipes/recipes.json +32 -30
- package/skills/sf-view-gen/assets/recipes/sections.json +10 -0
- package/skills/sf-view-gen/assets/templates/views/sections/products.client.tsx +100 -0
- package/skills/sf-view-gen/assets/templates/views/sections/products.tsx +19 -0
- package/skills/sf-view-gen/assets/templates/views/sections/schema.products.ts +39 -0
- package/skills/{json-render-core/SKILL.md → sf-view-gen/references/json-render-core.md} +4 -27
- package/skills/{json-render-next/SKILL.md → sf-view-gen/references/json-render-next.md} +5 -8
- package/skills/{json-render-react/SKILL.md → sf-view-gen/references/json-render-react.md} +3 -5
- package/skills/{json-render-shadcn/SKILL.md → sf-view-gen/references/json-render-shadcn.md} +3 -5
- package/skills/sf-view-gen/references/views.md +28 -37
- package/skills/sf-view-gen/scripts/validate.mjs +54 -0
- package/skills/storefront-workflow/SKILL.md +94 -31
- package/skills/{storefront-best-practices/reference/design.md → storefront-workflow/references/design-consistency.md} +13 -29
- package/CHANGELOG.md +0 -84
- package/assets/templates/views/user-panel/index.tsx +0 -106
- package/assets/templates/views/user-panel/registry.tsx +0 -54
- package/assets/templates/views/user-panel/schema.ts +0 -79
- package/commands/sf-add-view.md +0 -57
- package/commands/sf-feature.md +0 -28
- package/commands/sf-theme.md +0 -26
- package/commands/sf-view.md +0 -67
- package/skills/building-storefronts/SKILL.md +0 -178
- package/skills/building-storefronts/references/frontend-integration.md +0 -229
- package/skills/json-render-remotion/SKILL.md +0 -111
- package/skills/json-render-solid/SKILL.md +0 -204
- package/skills/nextjs-shadcn/SKILL.md +0 -367
- package/skills/nextjs-shadcn/references/architecture.md +0 -499
- package/skills/nextjs-shadcn/references/project-setup.md +0 -162
- package/skills/nextjs-shadcn/references/shadcn-platform.md +0 -261
- package/skills/nextjs-shadcn/references/sidebar.md +0 -274
- package/skills/nextjs-shadcn/references/styling.md +0 -555
- package/skills/shadcn-component-discovery/SKILL.md +0 -273
- package/skills/shadcn-component-discovery/references/registries.md +0 -226
- package/skills/shadcn-theming/SKILL.md +0 -104
- package/skills/storefront-best-practices/SKILL.md +0 -421
- package/skills/storefront-best-practices/reference/components/breadcrumbs.md +0 -123
- package/skills/storefront-best-practices/reference/components/cart-popup.md +0 -189
- package/skills/storefront-best-practices/reference/components/country-selector.md +0 -298
- package/skills/storefront-best-practices/reference/components/footer.md +0 -112
- package/skills/storefront-best-practices/reference/components/hero.md +0 -241
- package/skills/storefront-best-practices/reference/components/megamenu.md +0 -239
- package/skills/storefront-best-practices/reference/components/navbar.md +0 -397
- package/skills/storefront-best-practices/reference/components/popups.md +0 -221
- package/skills/storefront-best-practices/reference/components/product-card.md +0 -125
- package/skills/storefront-best-practices/reference/components/product-reviews.md +0 -217
- package/skills/storefront-best-practices/reference/components/product-slider.md +0 -174
- package/skills/storefront-best-practices/reference/components/search.md +0 -101
- package/skills/storefront-best-practices/reference/connecting-to-backend.md +0 -391
- package/skills/storefront-best-practices/reference/features/promotions.md +0 -307
- package/skills/storefront-best-practices/reference/features/wishlist.md +0 -230
- package/skills/storefront-best-practices/reference/layouts/account.md +0 -380
- package/skills/storefront-best-practices/reference/layouts/cart.md +0 -316
- package/skills/storefront-best-practices/reference/layouts/checkout.md +0 -486
- package/skills/storefront-best-practices/reference/layouts/home-page.md +0 -264
- package/skills/storefront-best-practices/reference/layouts/order-confirmation.md +0 -231
- package/skills/storefront-best-practices/reference/layouts/product-details.md +0 -527
- package/skills/storefront-best-practices/reference/layouts/product-listing.md +0 -520
- package/skills/storefront-best-practices/reference/layouts/static-pages.md +0 -356
- package/skills/storefront-best-practices/reference/medusa.md +0 -307
- /package/skills/{storefront-best-practices/reference → storefront-workflow/references}/mobile-responsiveness.md +0 -0
- /package/skills/{storefront-best-practices/reference → storefront-workflow/references}/seo.md +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: sf-view-gen
|
|
3
|
-
description: Add or remove a Thor Commerce storefront view, compose a view's
|
|
3
|
+
description: Add or remove a Thor Commerce storefront view, compose and reorder a view's executable sections, or customize a view's user-owned files. Use when the user asks to add or remove a view (home, category, product, cart, account, static), add/remove/reorder a section (faq, cta, products), change a variant, or change a view's content, props, or data. Delegates every structural write to the `storefront` CLI.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# sf-view-gen
|
|
@@ -10,51 +10,49 @@ Choose a view's composition **by meaning**, delegate the structural write to the
|
|
|
10
10
|
manifest (`storefront.config.json`) is the structural authority; the CLI is the
|
|
11
11
|
only writer of derived files.
|
|
12
12
|
|
|
13
|
-
## Read the
|
|
13
|
+
## Read the curated view set through the CLI (never from memory)
|
|
14
14
|
|
|
15
15
|
Run from the storefront project root. The CLI resolves the project from the
|
|
16
16
|
current directory; do not pass a directory.
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
|
-
npx @thorprovider/storefront views --json # views, routes,
|
|
19
|
+
npx @thorprovider/storefront views --json # views, routes, intents/variants, installed
|
|
20
20
|
npx @thorprovider/storefront recipes --json # whole curated registry (views + features)
|
|
21
21
|
npx @thorprovider/storefront features --json # feature bundles and what they own
|
|
22
22
|
```
|
|
23
23
|
|
|
24
24
|
The `--json` output is the same registry the CLI validates against, so it cannot
|
|
25
|
-
advertise a selection the reconciler will not honour.
|
|
26
|
-
`
|
|
27
|
-
|
|
28
|
-
`
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
`view add checkout` is rejected; run `feature add checkout` instead (see the
|
|
37
|
-
`sf-feature` skill).
|
|
38
|
-
- **Held post-MVP:** `user-panel` and recommendations are rejected before any
|
|
39
|
-
write. Do not try to install them. (Sections are composable — see below.)
|
|
25
|
+
advertise a selection the reconciler will not honour. There is **no fixed view
|
|
26
|
+
list in this skill**: `views --json` reports the authoritative curated set
|
|
27
|
+
(currently `home`, `product`, `cart`, `category`, `checkout`, `account`, and
|
|
28
|
+
`static`) annotated with `installed` and `supported`, so a view added to or
|
|
29
|
+
removed from the registry is reflected without editing this text.
|
|
30
|
+
|
|
31
|
+
`views --json` and `recipes --json` also report, per view, `registeredSlots` (the
|
|
32
|
+
non-removable core slots), `allowedSections` (the section types the view may
|
|
33
|
+
host), and `maxSections` (the editorial-instance bound), plus a top-level
|
|
34
|
+
`sectionTypes` list marking each type `executable` or `held` with a reason. Do not
|
|
35
|
+
read the JSON files directly; they are an implementation detail.
|
|
40
36
|
|
|
41
37
|
## Add a view
|
|
42
38
|
|
|
43
|
-
1. Read `views --json`. If the view is held
|
|
44
|
-
the correct path
|
|
39
|
+
1. Read `views --json`. If the view is held (`supported: false`) or is
|
|
40
|
+
feature-owned, stop and explain the correct path (a direct `view add` of a
|
|
41
|
+
feature-owned view is rejected — route it through the owning feature's
|
|
42
|
+
`feature add`; see the `sf-feature` skill).
|
|
45
43
|
2. Choose an **intent by meaning** for each slot from the view's
|
|
46
|
-
`supportedIntents` (
|
|
47
|
-
If the description is silent, use the default.
|
|
48
|
-
`--variant slot=Name` when the user names an exact supported variant.
|
|
44
|
+
`supportedIntents` (for example a "dense grid" description → the matching
|
|
45
|
+
intent). If the description is silent, use the default. Use
|
|
46
|
+
`--variant slot=Name` only when the user names an exact supported variant.
|
|
49
47
|
3. Delegate the write:
|
|
50
48
|
|
|
51
49
|
```bash
|
|
52
50
|
npx @thorprovider/storefront view add <name> [--intent slot=intent] [--variant slot=Variant]
|
|
53
51
|
```
|
|
54
52
|
|
|
55
|
-
The CLI validates, updates the manifest, and reconciles the derived files.
|
|
56
|
-
|
|
57
|
-
|
|
53
|
+
The CLI validates, updates the manifest, and reconciles the derived files. If
|
|
54
|
+
it exits non-zero, show its message and the supported options; do not hand-edit
|
|
55
|
+
derived files to work around it.
|
|
58
56
|
4. Report the created view and its route.
|
|
59
57
|
|
|
60
58
|
## Remove a view
|
|
@@ -75,11 +73,15 @@ natural language and never types a command.
|
|
|
75
73
|
|
|
76
74
|
1. Read `views --json`. For the target view use `registeredSlots` (the core
|
|
77
75
|
slots), `allowedSections` (the host allowlist), and `maxSections`; read the
|
|
78
|
-
top-level `sectionTypes` list for which types are
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
`
|
|
76
|
+
top-level `sectionTypes` list for which types are `executable`. Never treat the
|
|
77
|
+
`sections --json` installed-instance list as the set of available types.
|
|
78
|
+
2. Only `executable` types may be composed; a `held` type is refused with the
|
|
79
|
+
`reason` the registry reports — relay that reason, never invent one. A
|
|
80
|
+
`dataBound` `products` instance is configured separately by the developer in
|
|
81
|
+
`views/<view>/data.ts` → `sectionSources[id]` (a canonical query — not a sales
|
|
82
|
+
ranking); its variant only changes presentation, and until the selection is set
|
|
83
|
+
the instance renders a truthful "needs configuration" state that `doctor`
|
|
84
|
+
reports by file and instance id.
|
|
83
85
|
3. Compose through the CLI's section operations, carried in the operations
|
|
84
86
|
envelope `{ "formatVersion": 1, "operations": [ ... ] }`. Write the envelope
|
|
85
87
|
to a file, plan it, then apply the plan:
|
|
@@ -89,14 +91,14 @@ natural language and never types a command.
|
|
|
89
91
|
npx @thorprovider/storefront apply --plan plan.json
|
|
90
92
|
```
|
|
91
93
|
|
|
92
|
-
- `{ "op": "section.add", "view": "
|
|
94
|
+
- `{ "op": "section.add", "view": "<view>", "type": "<type>" }` — optional `id`,
|
|
93
95
|
`variant`, and `at` (0-based insertion index, default append). Omit `id` to
|
|
94
96
|
auto-mint one; a `variant` must be declared by the section type.
|
|
95
|
-
- `{ "op": "section.remove", "view": "
|
|
97
|
+
- `{ "op": "section.remove", "view": "<view>", "id": "<id>" }` — removes an
|
|
96
98
|
editorial instance. A registered slot (`slot:<slot>`) cannot be removed.
|
|
97
|
-
- `{ "op": "section.reorder", "view": "
|
|
98
|
-
|
|
99
|
-
|
|
99
|
+
- `{ "op": "section.reorder", "view": "<view>", "order": [ ... ] }` — `order`
|
|
100
|
+
must be a permutation of the view's current entry ids: every current id
|
|
101
|
+
exactly once, and every registered slot present.
|
|
100
102
|
|
|
101
103
|
4. `plan` is read-only and validates the whole intended state; `apply --plan` is
|
|
102
104
|
the only writer. On success the CLI updates the manifest and reconciles derived
|
|
@@ -107,7 +109,15 @@ natural language and never types a command.
|
|
|
107
109
|
`content.ts`; re-adding the same `id` rebinds it, while an auto-minted id never
|
|
108
110
|
rebinds preserved content.
|
|
109
111
|
|
|
110
|
-
##
|
|
112
|
+
## Variants
|
|
113
|
+
|
|
114
|
+
A slot's variant is a presentation choice carried by the manifest. Change it
|
|
115
|
+
through the CLI (`view add`/`remove` with `--variant`, or a `section.add` /
|
|
116
|
+
`section.reorder` operation for editorial sections); never hand-edit a derived
|
|
117
|
+
`index.tsx` or registry to swap an import identifier. Variant changes go through
|
|
118
|
+
the CLI plan/apply path so the manifest and the derived files stay in sync.
|
|
119
|
+
|
|
120
|
+
## Customize a view
|
|
111
121
|
|
|
112
122
|
Only **user-owned** files may be edited by hand. They are consumed by thin
|
|
113
123
|
derived wrappers, so `sync` regenerates the wrappers around your edits.
|
|
@@ -124,7 +134,18 @@ Derived (never edit; the CLI regenerates them): `index.tsx`, `registry.tsx`,
|
|
|
124
134
|
the event/prop contracts the derived wrapper imports.
|
|
125
135
|
3. Regenerate the wrappers: `npx @thorprovider/storefront sync`.
|
|
126
136
|
4. If the change needs a different block variant or a new view, use the CLI
|
|
127
|
-
(`view add`/`remove`); never edit
|
|
137
|
+
(`view add`/`remove` or the section operations); never edit a derived file to
|
|
138
|
+
swap a variant.
|
|
139
|
+
|
|
140
|
+
## References
|
|
141
|
+
|
|
142
|
+
Relative to this `SKILL.md`:
|
|
143
|
+
|
|
144
|
+
- `references/views.md` — view module shape, registering, and data contracts.
|
|
145
|
+
- `references/json-render-core.md` — catalog zod props and spec shape.
|
|
146
|
+
- `references/json-render-react.md` — registry mapping, providers, and actions.
|
|
147
|
+
- `references/json-render-next.md` — the Next rendering shape.
|
|
148
|
+
- `references/json-render-shadcn.md` — standard component definitions.
|
|
128
149
|
|
|
129
150
|
## Stack constraints
|
|
130
151
|
|
|
@@ -132,11 +153,13 @@ Derived (never edit; the CLI regenerates them): `index.tsx`, `registry.tsx`,
|
|
|
132
153
|
- Data only through `@thorprovider/adapters` + `@thorprovider/runtime/commerce`.
|
|
133
154
|
- Never `@thorprovider/medusa-extended`.
|
|
134
155
|
- No custom blocks without explicit user authorization.
|
|
156
|
+
- A missing capability is a gap to report, not a reason to install another UI
|
|
157
|
+
framework or hand-edit a derived file.
|
|
135
158
|
|
|
136
159
|
## Workflow model
|
|
137
160
|
|
|
138
|
-
Read `../storefront-workflow/SKILL.md` for the four phases
|
|
139
|
-
|
|
161
|
+
Read `../storefront-workflow/SKILL.md` for the four phases and the CLI/agent
|
|
162
|
+
authority.
|
|
140
163
|
|
|
141
164
|
## Completion criteria
|
|
142
165
|
|
|
@@ -88,6 +88,15 @@
|
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
90
|
},
|
|
91
|
+
"cart.view": {
|
|
92
|
+
"default": "review",
|
|
93
|
+
"intents": {
|
|
94
|
+
"review": {
|
|
95
|
+
"variant": "ShoppingCart4",
|
|
96
|
+
"when": "A standalone cart review page: line editing plus backend-authoritative totals before checkout."
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
},
|
|
91
100
|
"category.filter": {
|
|
92
101
|
"default": "sidebar",
|
|
93
102
|
"intents": {
|
|
@@ -148,28 +157,6 @@
|
|
|
148
157
|
"when": "A signed-in shopper's order history with per-order totals and detail on demand."
|
|
149
158
|
}
|
|
150
159
|
}
|
|
151
|
-
},
|
|
152
|
-
"user-panel.shell": {
|
|
153
|
-
"default": "standard",
|
|
154
|
-
"intents": {
|
|
155
|
-
"standard": {
|
|
156
|
-
"variant": "AppShell1",
|
|
157
|
-
"when": "A standard account shell with side navigation and content area."
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
},
|
|
161
|
-
"user-panel.orderHistory": {
|
|
162
|
-
"default": "compact",
|
|
163
|
-
"intents": {
|
|
164
|
-
"compact": {
|
|
165
|
-
"variant": "OrderHistory1",
|
|
166
|
-
"when": "A concise list of past orders with status and totals."
|
|
167
|
-
},
|
|
168
|
-
"detailed": {
|
|
169
|
-
"variant": "OrderHistory2",
|
|
170
|
-
"when": "Order history with line-item detail, tracking, and reorder actions."
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
160
|
}
|
|
174
161
|
}
|
|
175
162
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "./catalog.schema.json",
|
|
3
3
|
"catalogVersion": 1,
|
|
4
|
-
"description": "Initial semantic executable catalog (E1 tasks 1.2-1.3), classified by source-shape inspection of the @thorprovider/blocks prop contracts and the view templates. Scope: every slot the curated MVP views (home, product, category, checkout, account) render by default, covering both the selectable archetype slots declared by recipes/archetypes.json and the nested default slots that have no archetype (category.productCard, checkout.confirmation) so no legacy default support is dropped. The account view (change: storefront-customer-auth) declares the login/register slots: LoginPage1/SignUpPage1 are executable while LoginPage2 is held for its divergent callback contract. A variant is `executable` only when the slot's canonical data shape can feed its block props without a prop bridge and without inventing data; every other non-default variant is `held` with the specific reason. The one executable non-default swap is home.benefits Feature1 -> Feature2 (identical item contract, no bridge).
|
|
4
|
+
"description": "Initial semantic executable catalog (E1 tasks 1.2-1.3), classified by source-shape inspection of the @thorprovider/blocks prop contracts and the view templates. Scope: every slot the curated MVP views (home, product, category, checkout, account) render by default, covering both the selectable archetype slots declared by recipes/archetypes.json and the nested default slots that have no archetype (category.productCard, checkout.confirmation) so no legacy default support is dropped. The account view (change: storefront-customer-auth) declares the login/register slots: LoginPage1/SignUpPage1 are executable while LoginPage2 is held for its divergent callback contract. A variant is `executable` only when the slot's canonical data shape can feed its block props without a prop bridge and without inventing data; every other non-default variant is `held` with the specific reason. The one executable non-default swap is home.benefits Feature1 -> Feature2 (identical item contract, no bridge). E2 task 6.1 declares the base listing slot category.filter CategoryFilter2 as the single controlled listing (dataShape `CatalogQuery & CatalogResults`, state controlled) with the search/filter controls contributed by the logical mounts category.search / category.filters (no second block). Source-shape compatibility is not end-to-end backend behavior proof: the render is executable, but the category.search.apply / category.filter.apply actions remain held under the capability catalog (owner: storefront-catalog-search-filters, E2) until a real runtime proof. The `capabilities` map declares those operational capabilities with their requirements and verification state; E1 declares the held category.filter.apply action, E2 adds the held category.search.apply action and the held category.price.range capability that gates the category-filters showPrice option; no capability is claimed verified.",
|
|
5
5
|
"slots": {
|
|
6
6
|
"home.hero": {
|
|
7
7
|
"default": "StorefrontHero1",
|
|
@@ -157,6 +157,32 @@
|
|
|
157
157
|
}
|
|
158
158
|
}
|
|
159
159
|
},
|
|
160
|
+
"product.breadcrumb": {
|
|
161
|
+
"default": "Breadcrumb",
|
|
162
|
+
"variants": {
|
|
163
|
+
"Breadcrumb": {
|
|
164
|
+
"status": "executable",
|
|
165
|
+
"block": "Breadcrumb",
|
|
166
|
+
"dataShape": "BreadcrumbItemData[]",
|
|
167
|
+
"events": [],
|
|
168
|
+
"state": "uncontrolled",
|
|
169
|
+
"requirements": ["productBreadcrumb"]
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
"cart.view": {
|
|
174
|
+
"default": "ShoppingCart4",
|
|
175
|
+
"variants": {
|
|
176
|
+
"ShoppingCart4": {
|
|
177
|
+
"status": "executable",
|
|
178
|
+
"block": "ShoppingCart4",
|
|
179
|
+
"dataShape": "CartLineState[] & CartTotalsState",
|
|
180
|
+
"events": ["refreshCart", "updateQuantity", "removeFromCart", "checkout", "continueShopping"],
|
|
181
|
+
"state": "controlled",
|
|
182
|
+
"requirements": ["cart", "cartTotals"]
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
},
|
|
160
186
|
"category.filter": {
|
|
161
187
|
"default": "CategoryFilter2",
|
|
162
188
|
"variants": {
|
|
@@ -339,6 +365,19 @@
|
|
|
339
365
|
"requirements": ["accountCustomer", "accountAuthenticated"]
|
|
340
366
|
}
|
|
341
367
|
}
|
|
368
|
+
},
|
|
369
|
+
"category.breadcrumb": {
|
|
370
|
+
"default": "Breadcrumb",
|
|
371
|
+
"variants": {
|
|
372
|
+
"Breadcrumb": {
|
|
373
|
+
"status": "executable",
|
|
374
|
+
"block": "Breadcrumb",
|
|
375
|
+
"dataShape": "BreadcrumbItemData[]",
|
|
376
|
+
"events": [],
|
|
377
|
+
"state": "uncontrolled",
|
|
378
|
+
"requirements": ["categoryBreadcrumb"]
|
|
379
|
+
}
|
|
380
|
+
}
|
|
342
381
|
}
|
|
343
382
|
},
|
|
344
383
|
"capabilities": {
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"name": "customer-auth",
|
|
31
31
|
"mvp": true,
|
|
32
32
|
"description": "Customer authentication: registration, login, logout, and a minimal signed-in state. Owns the account view.",
|
|
33
|
-
"route": "/
|
|
33
|
+
"route": "/account",
|
|
34
34
|
"views": ["account"],
|
|
35
35
|
"state": [
|
|
36
36
|
"accountCustomer",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"name": "static-pages",
|
|
49
49
|
"mvp": true,
|
|
50
50
|
"description": "Content-driven static pages (contact, legal, about) served with per-page metadata. Owns the static view.",
|
|
51
|
-
"route": "/
|
|
51
|
+
"route": "/contact",
|
|
52
52
|
"views": ["static"],
|
|
53
53
|
"state": [],
|
|
54
54
|
"handlers": [],
|
|
@@ -122,6 +122,29 @@
|
|
|
122
122
|
"mounts": [{ "id": "category-filters", "host": "category", "slot": "filters" }],
|
|
123
123
|
"surfaces": []
|
|
124
124
|
},
|
|
125
|
+
{
|
|
126
|
+
"name": "navigability",
|
|
127
|
+
"mvp": true,
|
|
128
|
+
"description": "Cross-cutting navigability feature: entity-derived breadcrumbs with BreadcrumbList structured data on the product and category surfaces, plus skip-to-content, current-page indication, back-to-top, and a mobile bottom navigation in the shared shell.",
|
|
129
|
+
"views": [],
|
|
130
|
+
"state": [],
|
|
131
|
+
"handlers": [],
|
|
132
|
+
"adapter": { "needs": [] },
|
|
133
|
+
"config": {
|
|
134
|
+
"type": "object",
|
|
135
|
+
"additionalProperties": false,
|
|
136
|
+
"properties": {
|
|
137
|
+
"presentation": { "type": "string", "enum": ["default"], "default": "default" },
|
|
138
|
+
"showBackToTop": { "type": "boolean", "default": true }
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
"presentations": { "default": "Breadcrumb" },
|
|
142
|
+
"mounts": [
|
|
143
|
+
{ "id": "category-breadcrumb", "host": "category", "slot": "breadcrumb" },
|
|
144
|
+
{ "id": "product-breadcrumb", "host": "product", "slot": "breadcrumb" }
|
|
145
|
+
],
|
|
146
|
+
"surfaces": []
|
|
147
|
+
},
|
|
125
148
|
{
|
|
126
149
|
"name": "recommendations",
|
|
127
150
|
"mvp": false,
|
|
@@ -22,47 +22,69 @@
|
|
|
22
22
|
},
|
|
23
23
|
"optionalSections": ["testimonials", "faq", "statistics", "logoCloud", "pricing"],
|
|
24
24
|
"registeredSlots": ["hero", "benefits", "categories"],
|
|
25
|
-
"allowedSections": ["faq", "cta"],
|
|
25
|
+
"allowedSections": ["faq", "cta", "products"],
|
|
26
26
|
"maxSections": 4
|
|
27
27
|
},
|
|
28
28
|
{
|
|
29
29
|
"name": "product",
|
|
30
30
|
"mvp": true,
|
|
31
31
|
"description": "Single product detail page with overview, gallery, and reviews",
|
|
32
|
-
"route": "/
|
|
32
|
+
"route": "/products/[id]",
|
|
33
33
|
"loader": "product",
|
|
34
34
|
"requiredDataKeys": ["selectedProduct"],
|
|
35
35
|
"defaultVariants": {
|
|
36
36
|
"overview": "ProductOverview1",
|
|
37
|
-
"reviews": "ReviewRating1"
|
|
37
|
+
"reviews": "ReviewRating1",
|
|
38
|
+
"breadcrumb": "Breadcrumb"
|
|
38
39
|
},
|
|
39
40
|
"allowedVariants": {
|
|
40
41
|
"overview": ["ProductOverview1", "ProductOverview2", "ProductOverview5"],
|
|
41
42
|
"reviews": ["ReviewRating1", "ReviewRating2"],
|
|
43
|
+
"breadcrumb": ["Breadcrumb"],
|
|
42
44
|
"productCard": ["ProductCard1", "ProductCard2", "ProductCard3", "ProductCard5"]
|
|
43
45
|
},
|
|
44
46
|
"optionalSections": ["testimonials", "faq", "statistics"],
|
|
45
|
-
"registeredSlots": ["overview", "reviews"],
|
|
47
|
+
"registeredSlots": ["overview", "reviews", "breadcrumb"],
|
|
46
48
|
"allowedSections": [],
|
|
47
49
|
"maxSections": 4
|
|
48
50
|
},
|
|
51
|
+
{
|
|
52
|
+
"name": "cart",
|
|
53
|
+
"mvp": true,
|
|
54
|
+
"description": "Standalone cart review page with authoritative totals and quantity editing",
|
|
55
|
+
"route": "/cart",
|
|
56
|
+
"loader": "cart",
|
|
57
|
+
"requiredDataKeys": ["cart"],
|
|
58
|
+
"defaultVariants": {
|
|
59
|
+
"view": "ShoppingCart4"
|
|
60
|
+
},
|
|
61
|
+
"allowedVariants": {
|
|
62
|
+
"view": ["ShoppingCart4"]
|
|
63
|
+
},
|
|
64
|
+
"optionalSections": [],
|
|
65
|
+
"registeredSlots": ["view"],
|
|
66
|
+
"allowedSections": [],
|
|
67
|
+
"maxSections": 0
|
|
68
|
+
},
|
|
49
69
|
{
|
|
50
70
|
"name": "category",
|
|
51
71
|
"mvp": true,
|
|
52
72
|
"description": "Product listing filtered by category with filters and sorting",
|
|
53
|
-
"route": "/
|
|
73
|
+
"route": "/categories/[[...slug]]",
|
|
54
74
|
"loader": "category",
|
|
55
75
|
"requiredDataKeys": ["categoryProducts", "storeCategories"],
|
|
56
76
|
"defaultVariants": {
|
|
57
77
|
"filter": "CategoryFilter2",
|
|
58
|
-
"productCard": "ProductCard1"
|
|
78
|
+
"productCard": "ProductCard1",
|
|
79
|
+
"breadcrumb": "Breadcrumb"
|
|
59
80
|
},
|
|
60
81
|
"allowedVariants": {
|
|
61
82
|
"filter": ["CategoryFilter1", "CategoryFilter2", "CategoryFilter4"],
|
|
62
|
-
"productCard": ["ProductCard1", "ProductCard2", "ProductCard3", "ProductCard5"]
|
|
83
|
+
"productCard": ["ProductCard1", "ProductCard2", "ProductCard3", "ProductCard5"],
|
|
84
|
+
"breadcrumb": ["Breadcrumb"]
|
|
63
85
|
},
|
|
64
86
|
"optionalSections": ["logoCloud", "testimonials"],
|
|
65
|
-
"registeredSlots": ["filter", "productCard"],
|
|
87
|
+
"registeredSlots": ["breadcrumb", "filter", "productCard"],
|
|
66
88
|
"allowedSections": ["faq", "cta"],
|
|
67
89
|
"maxSections": 4
|
|
68
90
|
},
|
|
@@ -90,7 +112,7 @@
|
|
|
90
112
|
"name": "account",
|
|
91
113
|
"mvp": true,
|
|
92
114
|
"description": "Customer account area with sign-in, registration, and a minimal signed-in state",
|
|
93
|
-
"route": "/
|
|
115
|
+
"route": "/account",
|
|
94
116
|
"loader": "account",
|
|
95
117
|
"requiredDataKeys": ["accountCustomer"],
|
|
96
118
|
"defaultVariants": {
|
|
@@ -111,7 +133,7 @@
|
|
|
111
133
|
"name": "static",
|
|
112
134
|
"mvp": true,
|
|
113
135
|
"description": "Content-driven static pages (contact, legal, about) with per-page metadata",
|
|
114
|
-
"route": "/
|
|
136
|
+
"route": "/contact",
|
|
115
137
|
"loader": "static",
|
|
116
138
|
"requiredDataKeys": [],
|
|
117
139
|
"defaultVariants": {},
|
|
@@ -120,26 +142,6 @@
|
|
|
120
142
|
"registeredSlots": [],
|
|
121
143
|
"allowedSections": [],
|
|
122
144
|
"maxSections": 0
|
|
123
|
-
},
|
|
124
|
-
{
|
|
125
|
-
"name": "user-panel",
|
|
126
|
-
"mvp": false,
|
|
127
|
-
"description": "Customer account area with order history and account navigation",
|
|
128
|
-
"route": "/cuenta",
|
|
129
|
-
"loader": "userPanel",
|
|
130
|
-
"requiredDataKeys": ["lastOrder", "lastOrderLines", "orderPlaced"],
|
|
131
|
-
"defaultVariants": {
|
|
132
|
-
"shell": "AppShell1",
|
|
133
|
-
"orderHistory": "OrderHistory1"
|
|
134
|
-
},
|
|
135
|
-
"allowedVariants": {
|
|
136
|
-
"shell": ["AppShell1"],
|
|
137
|
-
"orderHistory": ["OrderHistory1", "OrderHistory2"]
|
|
138
|
-
},
|
|
139
|
-
"optionalSections": [],
|
|
140
|
-
"registeredSlots": ["shell", "orderHistory"],
|
|
141
|
-
"allowedSections": [],
|
|
142
|
-
"maxSections": 4
|
|
143
145
|
}
|
|
144
146
|
]
|
|
145
147
|
}
|
|
@@ -31,6 +31,16 @@
|
|
|
31
31
|
"propsHint": "badgeText, title, description, primaryCta/secondaryCta (label, href)",
|
|
32
32
|
"status": "executable"
|
|
33
33
|
},
|
|
34
|
+
{
|
|
35
|
+
"name": "products",
|
|
36
|
+
"description": "Data-bound product presentation (grid or combined grid-and-carousel) whose selection is configured independently per instance",
|
|
37
|
+
"elementType": "Products",
|
|
38
|
+
"variants": ["ProductCard1", "ProductShowcase1"],
|
|
39
|
+
"defaultVariant": "ProductCard1",
|
|
40
|
+
"dataBound": true,
|
|
41
|
+
"propsHint": "per-instance selection configured in views/<view>/data.ts sectionSources[id] (canonical category/collection/search/ordering query); heading, description, objective, ctaLabel/ctaHref are editorial content",
|
|
42
|
+
"status": "executable"
|
|
43
|
+
},
|
|
34
44
|
{
|
|
35
45
|
"name": "statistics",
|
|
36
46
|
"description": "Key metrics or trust statistics",
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
// Optional section fragment: products (mirrors recipes/sections.json) — client registry.
|
|
4
|
+
// The matching server-side catalog lives in products.tsx. A data-bound section
|
|
5
|
+
// renders one of two verified presentations (ProductCard1 grid, or the combined
|
|
6
|
+
// grid-and-carousel ProductShowcase1) chosen by the resolved `variant`. The
|
|
7
|
+
// selection and resolved products arrive through the view state; this boundary
|
|
8
|
+
// only validates them and wires navigation/purchase callbacks.
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
ProductCard1 as ProductCard1Block,
|
|
12
|
+
ProductShowcase1 as ProductShowcase1Block,
|
|
13
|
+
type ProductCard1Product,
|
|
14
|
+
} from '@thorprovider/blocks'
|
|
15
|
+
import { useStateStore } from '@json-render/react'
|
|
16
|
+
import { useSectionObservation } from '../../section-observation'
|
|
17
|
+
import { productsPropsSchema, type ProductsStatus } from './schema.products'
|
|
18
|
+
import type { ViewRegistry } from '../types'
|
|
19
|
+
|
|
20
|
+
// Truthful, actionable messages for the non-`ready` states. A failed or
|
|
21
|
+
// unconfigured section never fabricates products.
|
|
22
|
+
function sectionStatusMessage(status: ProductsStatus): string {
|
|
23
|
+
switch (status) {
|
|
24
|
+
case 'unconfigured':
|
|
25
|
+
return 'Esta sección de productos aún no tiene una selección configurada.'
|
|
26
|
+
case 'empty':
|
|
27
|
+
return 'Esta selección no tiene productos disponibles por ahora.'
|
|
28
|
+
case 'invalid-query':
|
|
29
|
+
return 'La selección de productos de esta sección no es válida.'
|
|
30
|
+
case 'unsupported-query':
|
|
31
|
+
return 'Esta selección usa criterios que el catálogo no admite.'
|
|
32
|
+
case 'unmounted-capability':
|
|
33
|
+
return 'Esta selección requiere una capacidad que este catálogo no tiene montada.'
|
|
34
|
+
case 'data-error':
|
|
35
|
+
return 'No se pudieron cargar los productos de esta sección.'
|
|
36
|
+
default:
|
|
37
|
+
return ''
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const productsRegistry: ViewRegistry = {
|
|
42
|
+
Products: ({ props, emit }) => {
|
|
43
|
+
const { set } = useStateStore()
|
|
44
|
+
const parsed = productsPropsSchema.parse(props)
|
|
45
|
+
const { containerRef, noteProductClick } = useSectionObservation({
|
|
46
|
+
sectionId: parsed.sectionId ?? '',
|
|
47
|
+
sectionType: parsed.sectionType ?? 'products',
|
|
48
|
+
...(parsed.variant ? { sectionVariant: parsed.variant } : {}),
|
|
49
|
+
...(parsed.objective ? { objective: parsed.objective } : {}),
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
if (parsed.status !== 'ready') {
|
|
53
|
+
return (
|
|
54
|
+
<section
|
|
55
|
+
aria-label={parsed.heading ?? 'Productos'}
|
|
56
|
+
className="mx-auto w-full max-w-7xl px-4 py-12"
|
|
57
|
+
>
|
|
58
|
+
{parsed.heading ? (
|
|
59
|
+
<h2 className="mb-2 text-2xl font-semibold tracking-tight">{parsed.heading}</h2>
|
|
60
|
+
) : null}
|
|
61
|
+
<p className="text-sm text-muted-foreground">{sectionStatusMessage(parsed.status)}</p>
|
|
62
|
+
</section>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// The detail affordance opens the product so a variant is chosen explicitly.
|
|
67
|
+
// It is also the section's observation origin (disabled by default).
|
|
68
|
+
const openProduct = (product: ProductCard1Product) => {
|
|
69
|
+
noteProductClick(product)
|
|
70
|
+
set('/lastClicked', product)
|
|
71
|
+
emit('openProduct')
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Product-card shortcut: the add affordance adds the product's first
|
|
75
|
+
// purchasable variant (resolved server-side from the product id), then opens
|
|
76
|
+
// the mini-cart. It is not the section's observation origin.
|
|
77
|
+
const addToCart = (product: ProductCard1Product) => {
|
|
78
|
+
set('/lastClicked', product)
|
|
79
|
+
emit('addClickedToCart')
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const shared = {
|
|
83
|
+
products: parsed.products,
|
|
84
|
+
heading: parsed.heading,
|
|
85
|
+
description: parsed.description,
|
|
86
|
+
onAddToCart: addToCart,
|
|
87
|
+
onProductClick: openProduct,
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<div ref={containerRef}>
|
|
92
|
+
{parsed.variant === 'ProductShowcase1' ? (
|
|
93
|
+
<ProductShowcase1Block {...shared} />
|
|
94
|
+
) : (
|
|
95
|
+
<ProductCard1Block {...shared} />
|
|
96
|
+
)}
|
|
97
|
+
</div>
|
|
98
|
+
)
|
|
99
|
+
},
|
|
100
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Optional section fragment: products (mirrors recipes/sections.json)
|
|
2
|
+
// Server-side part: element type + zod catalog entry. The component mappings
|
|
3
|
+
// live in products.client.tsx ('use client'). Unlike the stateless editorial
|
|
4
|
+
// fragments (faq/cta), the products section is data-bound: its per-instance
|
|
5
|
+
// selection is resolved by the host loader and delivered through the view state,
|
|
6
|
+
// so this catalog entry describes the *resolved* props shape.
|
|
7
|
+
|
|
8
|
+
import { productsPropsSchema } from './schema.products'
|
|
9
|
+
|
|
10
|
+
export const productsSection = {
|
|
11
|
+
elementType: 'Products',
|
|
12
|
+
catalog: {
|
|
13
|
+
Products: {
|
|
14
|
+
props: productsPropsSchema,
|
|
15
|
+
description: 'Data-bound product presentation (grid or combined grid-and-carousel).',
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
registryModule: './products.client',
|
|
19
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Optional section fragment: products (mirrors recipes/sections.json) — shared contract.
|
|
2
|
+
// Zod mirror of the resolved Products section props, shared by the server catalog
|
|
3
|
+
// (products.tsx) and the client registry (products.client.tsx). The product list
|
|
4
|
+
// reuses the canonical presentation contract owned by @thorprovider/blocks so the
|
|
5
|
+
// section renders exactly what the host mapped from the catalog result.
|
|
6
|
+
|
|
7
|
+
import { z } from 'zod'
|
|
8
|
+
import { productPresentationSchema } from '@thorprovider/blocks'
|
|
9
|
+
|
|
10
|
+
// The per-instance resolution status the host loader computes for a data-bound
|
|
11
|
+
// section. `ready`/`empty` carry resolved products; the remaining states carry no
|
|
12
|
+
// products and render an actionable, truthful message instead.
|
|
13
|
+
export const productsStatusSchema = z.enum([
|
|
14
|
+
'unconfigured',
|
|
15
|
+
'ready',
|
|
16
|
+
'empty',
|
|
17
|
+
'invalid-query',
|
|
18
|
+
'unsupported-query',
|
|
19
|
+
'unmounted-capability',
|
|
20
|
+
'data-error',
|
|
21
|
+
])
|
|
22
|
+
|
|
23
|
+
export const productsPropsSchema = z.object({
|
|
24
|
+
sectionId: z.string().min(1).optional(),
|
|
25
|
+
sectionType: z.string().min(1).optional(),
|
|
26
|
+
variant: z.string().optional(),
|
|
27
|
+
// Defensive defaults: a missing resolved state (e.g. the view loader could not
|
|
28
|
+
// run) degrades to a truthful error instead of crashing the render.
|
|
29
|
+
status: productsStatusSchema.default('data-error'),
|
|
30
|
+
products: z.array(productPresentationSchema).default([]),
|
|
31
|
+
heading: z.string().optional(),
|
|
32
|
+
description: z.string().optional(),
|
|
33
|
+
objective: z.string().optional(),
|
|
34
|
+
ctaLabel: z.string().optional(),
|
|
35
|
+
ctaHref: z.string().optional(),
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
export type ProductsProps = z.infer<typeof productsPropsSchema>
|
|
39
|
+
export type ProductsStatus = z.infer<typeof productsStatusSchema>
|