@thorprovider/storefront 0.3.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/CHANGELOG.md +61 -0
- package/README.md +333 -0
- package/assets/templates/app/app/[[...slug]]/page.tsx +17 -0
- package/assets/templates/app/app/[[...slug]]/renderer.tsx +10 -0
- package/assets/templates/app/app/globals.css +101 -0
- package/assets/templates/app/app/layout.tsx +35 -0
- package/assets/templates/app/lib/__STOREFRONT__/catalog.ts +88 -0
- package/assets/templates/app/lib/__STOREFRONT__/handlers.ts +42 -0
- package/assets/templates/app/lib/__STOREFRONT__/mappers.ts +47 -0
- package/assets/templates/app/lib/__STOREFRONT__/registry.tsx +98 -0
- package/assets/templates/app/lib/__STOREFRONT__/runtime.ts +17 -0
- package/assets/templates/app/lib/__STOREFRONT__/spec/index.ts +48 -0
- package/assets/templates/app/lib/__STOREFRONT__/spec/page-root.ts +26 -0
- package/assets/templates/app/lib/__STOREFRONT__/spec/types.ts +14 -0
- package/assets/templates/app/lib/__STOREFRONT__/state.ts +35 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/index.ts +7 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/faq.tsx +29 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/logoCloud.tsx +29 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/pricing.tsx +33 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/statistics.tsx +29 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/sections/testimonials.tsx +28 -0
- package/assets/templates/app/lib/__STOREFRONT__/views/types.ts +46 -0
- package/assets/templates/views/home/index.tsx +142 -0
- package/assets/templates/views/home/registry.tsx +65 -0
- package/bin/storefront.js +531 -0
- package/commands/sf-add-view.md +26 -0
- package/commands/sf-theme.md +20 -0
- package/commands/sf-view.md +25 -0
- package/package.json +48 -0
- package/recipes/archetype.schema.json +39 -0
- package/recipes/archetypes.json +148 -0
- package/recipes/recipe.schema.json +59 -0
- package/recipes/recipes.json +90 -0
- package/recipes/sections.json +46 -0
- package/recipes/validate.mjs +190 -0
- package/scripts/render-templates.mjs +255 -0
- package/scripts/sync-assets.mjs +87 -0
- package/skills/building-storefronts/SKILL.md +178 -0
- package/skills/building-storefronts/references/frontend-integration.md +229 -0
- package/skills/json-render-core/SKILL.md +291 -0
- package/skills/json-render-next/SKILL.md +194 -0
- package/skills/json-render-react/SKILL.md +298 -0
- package/skills/json-render-remotion/SKILL.md +111 -0
- package/skills/json-render-shadcn/SKILL.md +159 -0
- package/skills/json-render-solid/SKILL.md +204 -0
- package/skills/nextjs-shadcn/SKILL.md +367 -0
- package/skills/nextjs-shadcn/references/architecture.md +499 -0
- package/skills/nextjs-shadcn/references/project-setup.md +162 -0
- package/skills/nextjs-shadcn/references/shadcn-platform.md +261 -0
- package/skills/nextjs-shadcn/references/sidebar.md +274 -0
- package/skills/nextjs-shadcn/references/styling.md +555 -0
- package/skills/sf-theme-gen/SKILL.md +61 -0
- package/skills/sf-view-gen/SKILL.md +116 -0
- package/skills/sf-view-gen/assets/recipes/archetypes.json +148 -0
- package/skills/sf-view-gen/assets/recipes/recipes.json +90 -0
- package/skills/sf-view-gen/assets/recipes/sections.json +46 -0
- package/skills/sf-view-gen/assets/templates/views/category/index.tsx +86 -0
- package/skills/sf-view-gen/assets/templates/views/category/registry.tsx +48 -0
- package/skills/sf-view-gen/assets/templates/views/checkout/index.tsx +192 -0
- package/skills/sf-view-gen/assets/templates/views/checkout/registry.tsx +45 -0
- package/skills/sf-view-gen/assets/templates/views/product/index.tsx +122 -0
- package/skills/sf-view-gen/assets/templates/views/product/registry.tsx +49 -0
- package/skills/sf-view-gen/assets/templates/views/sections/faq.client.tsx +20 -0
- package/skills/sf-view-gen/assets/templates/views/sections/faq.tsx +21 -0
- package/skills/sf-view-gen/assets/templates/views/sections/logoCloud.client.tsx +18 -0
- package/skills/sf-view-gen/assets/templates/views/sections/logoCloud.tsx +21 -0
- package/skills/sf-view-gen/assets/templates/views/sections/pricing.client.tsx +20 -0
- package/skills/sf-view-gen/assets/templates/views/sections/pricing.tsx +23 -0
- package/skills/sf-view-gen/assets/templates/views/sections/statistics.client.tsx +18 -0
- package/skills/sf-view-gen/assets/templates/views/sections/statistics.tsx +21 -0
- package/skills/sf-view-gen/assets/templates/views/sections/testimonials.client.tsx +19 -0
- package/skills/sf-view-gen/assets/templates/views/sections/testimonials.tsx +19 -0
- package/skills/sf-view-gen/assets/templates/views/user-panel/index.tsx +168 -0
- package/skills/sf-view-gen/assets/templates/views/user-panel/registry.tsx +49 -0
- package/skills/sf-view-gen/references/views.md +69 -0
- package/skills/sf-view-gen/scripts/validate.mjs +190 -0
- package/skills/shadcn-component-discovery/SKILL.md +273 -0
- package/skills/shadcn-component-discovery/references/registries.md +226 -0
- package/skills/shadcn-theming/SKILL.md +104 -0
- package/skills/shadcn-theming/references/templates/theme-setup.md +109 -0
- package/skills/shadcn-theming/references/theming-guide.md +90 -0
- package/skills/storefront-best-practices/SKILL.md +421 -0
- package/skills/storefront-best-practices/reference/components/breadcrumbs.md +123 -0
- package/skills/storefront-best-practices/reference/components/cart-popup.md +189 -0
- package/skills/storefront-best-practices/reference/components/country-selector.md +298 -0
- package/skills/storefront-best-practices/reference/components/footer.md +112 -0
- package/skills/storefront-best-practices/reference/components/hero.md +241 -0
- package/skills/storefront-best-practices/reference/components/megamenu.md +239 -0
- package/skills/storefront-best-practices/reference/components/navbar.md +397 -0
- package/skills/storefront-best-practices/reference/components/popups.md +221 -0
- package/skills/storefront-best-practices/reference/components/product-card.md +125 -0
- package/skills/storefront-best-practices/reference/components/product-reviews.md +217 -0
- package/skills/storefront-best-practices/reference/components/product-slider.md +174 -0
- package/skills/storefront-best-practices/reference/components/search.md +101 -0
- package/skills/storefront-best-practices/reference/connecting-to-backend.md +391 -0
- package/skills/storefront-best-practices/reference/design.md +388 -0
- package/skills/storefront-best-practices/reference/features/promotions.md +307 -0
- package/skills/storefront-best-practices/reference/features/wishlist.md +230 -0
- package/skills/storefront-best-practices/reference/layouts/account.md +380 -0
- package/skills/storefront-best-practices/reference/layouts/cart.md +316 -0
- package/skills/storefront-best-practices/reference/layouts/checkout.md +486 -0
- package/skills/storefront-best-practices/reference/layouts/home-page.md +264 -0
- package/skills/storefront-best-practices/reference/layouts/order-confirmation.md +231 -0
- package/skills/storefront-best-practices/reference/layouts/product-details.md +527 -0
- package/skills/storefront-best-practices/reference/layouts/product-listing.md +520 -0
- package/skills/storefront-best-practices/reference/layouts/static-pages.md +356 -0
- package/skills/storefront-best-practices/reference/medusa.md +307 -0
- package/skills/storefront-best-practices/reference/mobile-responsiveness.md +183 -0
- package/skills/storefront-best-practices/reference/seo.md +195 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sf-view-gen
|
|
3
|
+
description: Add a new view or modify an existing view in a Thor Commerce storefront, selecting block variants and optional sections from this package's recipes
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# sf-view-gen
|
|
7
|
+
|
|
8
|
+
Generate or modify a single view, driven by the **recipe registry inside this skill's own directory**, so storefronts are not identical.
|
|
9
|
+
|
|
10
|
+
## Source of truth
|
|
11
|
+
|
|
12
|
+
The workflow uses only the assets in this skill directory. There is no external project to consult and no package to locate: the recipes and templates it reads live under `assets/`, and the validator under `scripts/`. When a required choice is not determined by those files, ask the user.
|
|
13
|
+
|
|
14
|
+
## Inputs (the only target-project facts this skill reads)
|
|
15
|
+
|
|
16
|
+
- the existence of the target project's `package.json`
|
|
17
|
+
- the existence of `lib/<name>/`
|
|
18
|
+
- the existence of the project's `.opencode/`
|
|
19
|
+
- the values returned by the wizard (view name, description of what the view should contain or emphasize)
|
|
20
|
+
|
|
21
|
+
## Outputs (what this skill produces)
|
|
22
|
+
|
|
23
|
+
- generated files: the view module under `lib/<name>/views/<view>/` and one barrel entry in `lib/<name>/views/index.ts`
|
|
24
|
+
- declared setup commands: the typecheck command it reports
|
|
25
|
+
- a summary listing every file created and every command run
|
|
26
|
+
|
|
27
|
+
## Assets (inside this skill)
|
|
28
|
+
|
|
29
|
+
- `assets/recipes/recipes.json` — per view: `route`, `loader`, `requiredDataKeys`, `defaultVariants`, `allowedVariants`, `optionalSections`
|
|
30
|
+
- `assets/recipes/sections.json` — optional sections → `elementType`, `variants`, `defaultVariant`, `propsHint`
|
|
31
|
+
- `assets/recipes/archetypes.json` — the **archetype (intent) layer**: per slot (`<view>.<slot>`) a set of named **intents**, each with a `variant` and a `when` usage hint, plus the slot `default` intent
|
|
32
|
+
- `assets/templates/views/<view>/` — the view modules it copies
|
|
33
|
+
- `assets/templates/views/sections/` — the optional-section fragments
|
|
34
|
+
- `scripts/validate.mjs` — the validator
|
|
35
|
+
- `references/views.md` — the view composition contract
|
|
36
|
+
- `../json-render-core/` — vendored catalog/schema/spec reference
|
|
37
|
+
- `../json-render-react/` — vendored registry/action reference
|
|
38
|
+
|
|
39
|
+
Paths are relative to this `SKILL.md`. Run the validator from here:
|
|
40
|
+
```bash
|
|
41
|
+
node scripts/validate.mjs list
|
|
42
|
+
node scripts/validate.mjs show <view>
|
|
43
|
+
node scripts/validate.mjs archetypes
|
|
44
|
+
node scripts/validate.mjs resolve <view> <slot> <intent>
|
|
45
|
+
node scripts/validate.mjs validate '<composition-json>'
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Allowed views
|
|
49
|
+
`home`, `product`, `category`, `checkout`, `user-panel`
|
|
50
|
+
|
|
51
|
+
Reject any other name with the full allowed list.
|
|
52
|
+
|
|
53
|
+
## json-render references (consult when authoring element types)
|
|
54
|
+
|
|
55
|
+
When adding a view you register new element `type`s in the view module: the zod
|
|
56
|
+
catalog entry in `views/<view>/index.tsx` and the component mapping in
|
|
57
|
+
`views/<view>/registry.tsx` ('use client'). Consult the vendored references
|
|
58
|
+
instead of working from memory:
|
|
59
|
+
|
|
60
|
+
- `../json-render-core/` — catalog/schema/spec authoring (the shape of a catalog entry and a spec element)
|
|
61
|
+
- `../json-render-react/` — registry mapping and action wiring (`defineRegistry`, `Renderer`, `useStateStore`)
|
|
62
|
+
|
|
63
|
+
**Selection rule:** choose an **intent by meaning** from the user's description (never a raw variant ordinal), then resolve the intent to its variant via `assets/recipes/archetypes.json`. The resolved variant must be allowed by the recipe for that slot. Choose optional sections the same way. Never invent an intent, variant, or component. If the description is silent, use the archetype slot `default`.
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
node scripts/validate.mjs resolve home hero showcase # -> StorefrontHero2
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Stack constraints
|
|
70
|
+
- UI: only `@thorprovider/blocks`
|
|
71
|
+
- Data: only `@thorprovider/adapters` + `@thorprovider/runtime/commerce`
|
|
72
|
+
- Never `@thorprovider/medusa-extended`
|
|
73
|
+
- No custom blocks without explicit user authorization
|
|
74
|
+
- **Two-file view split (RSC):** `index.tsx` is a pure Server Component module (name, elements, zod catalog, route, loader) and imports `registry` from the sibling `registry.tsx` as data. `registry.tsx` is the **`'use client'`** module and is the only place that imports values from `@json-render/react` or `@thorprovider/blocks`. Never add those imports to `index.tsx` — `@json-render/react` calls `createContext` at module scope, which breaks the RSC server graph.
|
|
75
|
+
- Every element `type` used in a spec MUST be registered in the view's zod catalog (`index.tsx`) AND its client registry (`registry.tsx`); the shared `catalog.ts` / `registry.tsx` compose both through the barrel and are never hand-edited
|
|
76
|
+
|
|
77
|
+
## Add-view workflow
|
|
78
|
+
|
|
79
|
+
1. Parse `$ARGUMENTS` → view name (+ optional description). Validate name. Confirm `lib/<name>/` exists (else run `npx @thorprovider/storefront create` to scaffold it).
|
|
80
|
+
2. `node scripts/validate.mjs show <view>` — read slots, allowed variants + sections. `node scripts/validate.mjs archetypes` — read slots with their named intents.
|
|
81
|
+
3. Choose an intent per slot by meaning from the description (defaults if silent), resolve each with `node scripts/validate.mjs resolve <view> <slot> <intent>`, choose optional sections the same way, then validate the full composition with `node scripts/validate.mjs validate '<json>'` **before writing anything**. On failure → stop, show the allowed options.
|
|
82
|
+
4. Copy the view template: `assets/templates/views/<view>/` → `lib/<name>/views/<view>/`. Each template is the two-file split: `index.tsx` (server spec) + `registry.tsx` ('use client' component mappings).
|
|
83
|
+
5. Register it in the barrel `lib/<name>/views/index.ts` — ONE import and ONE array entry:
|
|
84
|
+
```ts
|
|
85
|
+
import { <view> } from './<view>'
|
|
86
|
+
export const views: ViewModule[] = [home, <view>]
|
|
87
|
+
```
|
|
88
|
+
6. Substitute variants: each slot's block is imported under a stable alias (e.g. `ProductOverview1 as ProductOverviewBlock`), shipping the recipe default. Replace **only the imported identifier** with the resolved variant — never the alias, never the props:
|
|
89
|
+
```ts
|
|
90
|
+
import { ProductOverview5 as ProductOverviewBlock } from '@thorprovider/blocks'
|
|
91
|
+
```
|
|
92
|
+
7. Include any chosen optional section — fragments live at `assets/templates/views/sections/` as a two-file split:
|
|
93
|
+
- `<section>.tsx` (server): exports the section object with `elementType`, the zod `catalog` entry, and `registryModule: './<section>.client'`.
|
|
94
|
+
- `<section>.client.tsx` ('use client'): exports `registry: ViewRegistry` with the component mapping.
|
|
95
|
+
Import the section object from `<section>` into `index.tsx` and merge its elements/catalog into the view; put the component mapping from `<section>.client` into the view's `registry.tsx` (re-exporting the fragment's registry).
|
|
96
|
+
8. Verify: `yarn typecheck`. Do NOT hand-edit `catalog.ts`, `registry.tsx`, `runtime.ts`, or `spec/index.ts` — they compose whatever the barrel exports.
|
|
97
|
+
|
|
98
|
+
## Modify workflow (`/sf-view`)
|
|
99
|
+
|
|
100
|
+
1. Parse view name + change. Validate name.
|
|
101
|
+
2. Read `lib/<name>/views/<view>/index.tsx` (server spec) and `registry.tsx` (client mappings) fully.
|
|
102
|
+
3. Apply ONLY the change. Preserve ids, `type`, untouched `props`, `$state`, `on` wiring, route/loader, and the barrel entry.
|
|
103
|
+
4. If new blocks are needed, choose intents from `archetypes.json`, resolve to allowed variants, and validate before writing.
|
|
104
|
+
5. Re-run `yarn typecheck`.
|
|
105
|
+
|
|
106
|
+
## Generation headers
|
|
107
|
+
Every generated/edited file gets:
|
|
108
|
+
```
|
|
109
|
+
// Generated by @thorprovider/storefront (sf-view-gen) from recipes/<view>
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Error handling
|
|
113
|
+
- Unknown view → error + allowed list, stop.
|
|
114
|
+
- Missing `lib/<name>/` → run `npx @thorprovider/storefront create`, stop.
|
|
115
|
+
- Undeclared variant/section → reject via `recipes/validate.mjs`, show allowed options.
|
|
116
|
+
- Type errors → fix before finishing; never leave broken specs.
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "./archetype.schema.json",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Archetype (intent) layer. Maps a slot to named intents so selection is by meaning, not by variant ordinal. Each intent resolves to a variant that MUST be allowed by the recipe for that slot.",
|
|
5
|
+
"slots": {
|
|
6
|
+
"home.hero": {
|
|
7
|
+
"default": "showcase",
|
|
8
|
+
"intents": {
|
|
9
|
+
"compact": {
|
|
10
|
+
"variant": "StorefrontHero1",
|
|
11
|
+
"when": "A simple, text-forward hero with a single call to action and no product showcase."
|
|
12
|
+
},
|
|
13
|
+
"showcase": {
|
|
14
|
+
"variant": "StorefrontHero2",
|
|
15
|
+
"when": "A product-led hero that displays featured products alongside the headline."
|
|
16
|
+
},
|
|
17
|
+
"editorial": {
|
|
18
|
+
"variant": "StorefrontHero5",
|
|
19
|
+
"when": "A storytelling or brand-editorial hero with strong imagery and a narrative tone."
|
|
20
|
+
},
|
|
21
|
+
"promotional": {
|
|
22
|
+
"variant": "StorefrontHero6",
|
|
23
|
+
"when": "A campaign hero built around an offer, discount, or seasonal promotion."
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"home.benefits": {
|
|
28
|
+
"default": "compact",
|
|
29
|
+
"intents": {
|
|
30
|
+
"compact": {
|
|
31
|
+
"variant": "Feature1",
|
|
32
|
+
"when": "A short list of benefits with icons and one-line descriptions."
|
|
33
|
+
},
|
|
34
|
+
"detailed": {
|
|
35
|
+
"variant": "Feature2",
|
|
36
|
+
"when": "Benefits that need longer explanations, richer layout, or supporting media."
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"home.categories": {
|
|
41
|
+
"default": "standard",
|
|
42
|
+
"intents": {
|
|
43
|
+
"standard": {
|
|
44
|
+
"variant": "ProductCategory2",
|
|
45
|
+
"when": "A balanced grid of category cards with image and label."
|
|
46
|
+
},
|
|
47
|
+
"iconGrid": {
|
|
48
|
+
"variant": "ProductCategory1",
|
|
49
|
+
"when": "A compact icon-based category grid for many small categories."
|
|
50
|
+
},
|
|
51
|
+
"featured": {
|
|
52
|
+
"variant": "ProductCategory4",
|
|
53
|
+
"when": "Few, large featured category tiles that act as primary navigation."
|
|
54
|
+
},
|
|
55
|
+
"editorial": {
|
|
56
|
+
"variant": "ProductCategory5",
|
|
57
|
+
"when": "Category tiles presented as editorial imagery blocks."
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"product.overview": {
|
|
62
|
+
"default": "standard",
|
|
63
|
+
"intents": {
|
|
64
|
+
"standard": {
|
|
65
|
+
"variant": "ProductOverview1",
|
|
66
|
+
"when": "A straightforward product detail layout: gallery, title, price, add to cart."
|
|
67
|
+
},
|
|
68
|
+
"gallery": {
|
|
69
|
+
"variant": "ProductOverview2",
|
|
70
|
+
"when": "The product needs emphasis on a large image gallery with thumbnails."
|
|
71
|
+
},
|
|
72
|
+
"rich": {
|
|
73
|
+
"variant": "ProductOverview5",
|
|
74
|
+
"when": "A detailed product page with tabs, specs, and extensive structured information."
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
"product.reviews": {
|
|
79
|
+
"default": "list",
|
|
80
|
+
"intents": {
|
|
81
|
+
"list": {
|
|
82
|
+
"variant": "ReviewRating1",
|
|
83
|
+
"when": "A simple list of customer reviews with star ratings."
|
|
84
|
+
},
|
|
85
|
+
"summary": {
|
|
86
|
+
"variant": "ReviewRating2",
|
|
87
|
+
"when": "Reviews led by an aggregate rating summary and distribution."
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
"category.filter": {
|
|
92
|
+
"default": "sidebar",
|
|
93
|
+
"intents": {
|
|
94
|
+
"inline": {
|
|
95
|
+
"variant": "CategoryFilter1",
|
|
96
|
+
"when": "A compact inline filter bar above the product grid."
|
|
97
|
+
},
|
|
98
|
+
"sidebar": {
|
|
99
|
+
"variant": "CategoryFilter2",
|
|
100
|
+
"when": "A left sidebar with facets, price range, and sort controls."
|
|
101
|
+
},
|
|
102
|
+
"rich": {
|
|
103
|
+
"variant": "CategoryFilter4",
|
|
104
|
+
"when": "Advanced faceted filtering with multiple facet groups and active-filter chips."
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
"checkout.checkout": {
|
|
109
|
+
"default": "singlePage",
|
|
110
|
+
"intents": {
|
|
111
|
+
"singlePage": {
|
|
112
|
+
"variant": "CheckoutForm1",
|
|
113
|
+
"when": "A single-page checkout combining contact, shipping, and payment."
|
|
114
|
+
},
|
|
115
|
+
"multiStep": {
|
|
116
|
+
"variant": "CheckoutForm2",
|
|
117
|
+
"when": "A stepper checkout split into address, delivery, and payment steps."
|
|
118
|
+
},
|
|
119
|
+
"compact": {
|
|
120
|
+
"variant": "CheckoutForm3",
|
|
121
|
+
"when": "A minimal one-column checkout for fast, low-friction purchase."
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
"user-panel.shell": {
|
|
126
|
+
"default": "standard",
|
|
127
|
+
"intents": {
|
|
128
|
+
"standard": {
|
|
129
|
+
"variant": "AppShell1",
|
|
130
|
+
"when": "A standard account shell with side navigation and content area."
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
"user-panel.orderHistory": {
|
|
135
|
+
"default": "compact",
|
|
136
|
+
"intents": {
|
|
137
|
+
"compact": {
|
|
138
|
+
"variant": "OrderHistory1",
|
|
139
|
+
"when": "A concise list of past orders with status and totals."
|
|
140
|
+
},
|
|
141
|
+
"detailed": {
|
|
142
|
+
"variant": "OrderHistory2",
|
|
143
|
+
"when": "Order history with line-item detail, tracking, and reorder actions."
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "./recipe.schema.json",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"views": [
|
|
5
|
+
{
|
|
6
|
+
"name": "home",
|
|
7
|
+
"description": "Storefront landing page with hero, benefits, and category highlights",
|
|
8
|
+
"route": "/",
|
|
9
|
+
"loader": "home",
|
|
10
|
+
"requiredDataKeys": ["heroProducts", "categories"],
|
|
11
|
+
"defaultVariants": {
|
|
12
|
+
"hero": "StorefrontHero1",
|
|
13
|
+
"benefits": "Feature1",
|
|
14
|
+
"categories": "ProductCategory2"
|
|
15
|
+
},
|
|
16
|
+
"allowedVariants": {
|
|
17
|
+
"hero": ["StorefrontHero1", "StorefrontHero2", "StorefrontHero5", "StorefrontHero6"],
|
|
18
|
+
"benefits": ["Feature1", "Feature2"],
|
|
19
|
+
"categories": ["ProductCategory1", "ProductCategory2", "ProductCategory4", "ProductCategory5"],
|
|
20
|
+
"productCard": ["ProductCard1", "ProductCard2", "ProductCard3", "ProductCard5"]
|
|
21
|
+
},
|
|
22
|
+
"optionalSections": ["testimonials", "faq", "statistics", "logoCloud", "pricing"]
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"name": "product",
|
|
26
|
+
"description": "Single product detail page with overview, gallery, and reviews",
|
|
27
|
+
"route": "/productos/[id]",
|
|
28
|
+
"loader": "product",
|
|
29
|
+
"requiredDataKeys": ["selectedProduct"],
|
|
30
|
+
"defaultVariants": {
|
|
31
|
+
"overview": "ProductOverview1",
|
|
32
|
+
"reviews": "ReviewRating1"
|
|
33
|
+
},
|
|
34
|
+
"allowedVariants": {
|
|
35
|
+
"overview": ["ProductOverview1", "ProductOverview2", "ProductOverview5"],
|
|
36
|
+
"reviews": ["ReviewRating1", "ReviewRating2"],
|
|
37
|
+
"productCard": ["ProductCard1", "ProductCard2", "ProductCard3", "ProductCard5"]
|
|
38
|
+
},
|
|
39
|
+
"optionalSections": ["testimonials", "faq", "statistics"]
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"name": "category",
|
|
43
|
+
"description": "Product listing filtered by category with filters and sorting",
|
|
44
|
+
"route": "/categorias/[[...slug]]",
|
|
45
|
+
"loader": "category",
|
|
46
|
+
"requiredDataKeys": ["categoryProducts", "storeCategories"],
|
|
47
|
+
"defaultVariants": {
|
|
48
|
+
"filter": "CategoryFilter2",
|
|
49
|
+
"productCard": "ProductCard1"
|
|
50
|
+
},
|
|
51
|
+
"allowedVariants": {
|
|
52
|
+
"filter": ["CategoryFilter1", "CategoryFilter2", "CategoryFilter4"],
|
|
53
|
+
"productCard": ["ProductCard1", "ProductCard2", "ProductCard3", "ProductCard5"]
|
|
54
|
+
},
|
|
55
|
+
"optionalSections": ["logoCloud", "testimonials"]
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"name": "checkout",
|
|
59
|
+
"description": "Multi-step checkout with cart summary, shipping, and payment",
|
|
60
|
+
"route": "/checkout",
|
|
61
|
+
"loader": "checkout",
|
|
62
|
+
"requiredDataKeys": ["checkoutCountries", "checkoutPaymentMethods"],
|
|
63
|
+
"defaultVariants": {
|
|
64
|
+
"checkout": "CheckoutForm1",
|
|
65
|
+
"confirmation": "CheckoutConfirmation"
|
|
66
|
+
},
|
|
67
|
+
"allowedVariants": {
|
|
68
|
+
"checkout": ["CheckoutForm1", "CheckoutForm2", "CheckoutForm3"],
|
|
69
|
+
"confirmation": ["CheckoutConfirmation"]
|
|
70
|
+
},
|
|
71
|
+
"optionalSections": []
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"name": "user-panel",
|
|
75
|
+
"description": "Customer account area with order history and account navigation",
|
|
76
|
+
"route": "/cuenta",
|
|
77
|
+
"loader": "userPanel",
|
|
78
|
+
"requiredDataKeys": ["lastOrder", "lastOrderLines", "orderPlaced"],
|
|
79
|
+
"defaultVariants": {
|
|
80
|
+
"shell": "AppShell1",
|
|
81
|
+
"orderHistory": "OrderHistory1"
|
|
82
|
+
},
|
|
83
|
+
"allowedVariants": {
|
|
84
|
+
"shell": ["AppShell1"],
|
|
85
|
+
"orderHistory": ["OrderHistory1", "OrderHistory2"]
|
|
86
|
+
},
|
|
87
|
+
"optionalSections": []
|
|
88
|
+
}
|
|
89
|
+
]
|
|
90
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "./section.schema.json",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"sections": [
|
|
5
|
+
{
|
|
6
|
+
"name": "testimonials",
|
|
7
|
+
"description": "Customer testimonials or social proof",
|
|
8
|
+
"elementType": "Testimonials",
|
|
9
|
+
"variants": ["Testimonial1", "Testimonial2"],
|
|
10
|
+
"defaultVariant": "Testimonial1",
|
|
11
|
+
"propsHint": "title, badge, items[] (quote, author, role, avatar)"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"name": "faq",
|
|
15
|
+
"description": "Frequently asked questions accordion",
|
|
16
|
+
"elementType": "Faq",
|
|
17
|
+
"variants": ["FaqBlock1"],
|
|
18
|
+
"defaultVariant": "FaqBlock1",
|
|
19
|
+
"propsHint": "title, description, items[] (question, answer)"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"name": "statistics",
|
|
23
|
+
"description": "Key metrics or trust statistics",
|
|
24
|
+
"elementType": "Statistics",
|
|
25
|
+
"variants": ["Statistic1"],
|
|
26
|
+
"defaultVariant": "Statistic1",
|
|
27
|
+
"propsHint": "title, items[] (value, label)"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"name": "logoCloud",
|
|
31
|
+
"description": "Brand or partner logo strip",
|
|
32
|
+
"elementType": "LogoCloud",
|
|
33
|
+
"variants": ["LogoCloud1"],
|
|
34
|
+
"defaultVariant": "LogoCloud1",
|
|
35
|
+
"propsHint": "title, platforms[]/logos[] (name, src)"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"name": "pricing",
|
|
39
|
+
"description": "Pricing tiers or plans",
|
|
40
|
+
"elementType": "Pricing",
|
|
41
|
+
"variants": ["PricingSection1"],
|
|
42
|
+
"defaultVariant": "PricingSection1",
|
|
43
|
+
"propsHint": "title, description, tiers[] (name, price, features, cta)"
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// Thor Commerce view module: category
|
|
2
|
+
// Server Component module: elements + catalog entries + route + loader only.
|
|
3
|
+
// The client-side component mappings live in ./registry.tsx ('use client'); this
|
|
4
|
+
// module imports them as data so the RSC graph never evaluates @json-render/react
|
|
5
|
+
// on the server. Registered by importing it in ../index.ts. Recipe: recipes.json -> "category".
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
getCatalog,
|
|
9
|
+
getCategoryProductsByHandleWithSource,
|
|
10
|
+
} from '@thorprovider/runtime/commerce'
|
|
11
|
+
import { z } from 'zod'
|
|
12
|
+
import { pageRoot } from '../../spec/page-root'
|
|
13
|
+
import type { StorefrontElement } from '../../spec/types'
|
|
14
|
+
import type { ViewModule } from '../types'
|
|
15
|
+
import { registry } from './registry'
|
|
16
|
+
|
|
17
|
+
const elements: Record<string, StorefrontElement> = {
|
|
18
|
+
'category.filter': {
|
|
19
|
+
type: 'CategoryFilter',
|
|
20
|
+
props: {
|
|
21
|
+
products: { $state: '/categoryProducts' },
|
|
22
|
+
sortOptions: [
|
|
23
|
+
{ value: 'newest', label: 'Más recientes' },
|
|
24
|
+
{ value: 'price-asc', label: 'Precio: de menor a mayor' },
|
|
25
|
+
{ value: 'price-desc', label: 'Precio: de mayor a menor' },
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
on: {
|
|
29
|
+
openProduct: { action: 'openProduct' },
|
|
30
|
+
applyFilters: { action: 'applyFilters' },
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
'category.grid': {
|
|
34
|
+
type: 'ProductCard',
|
|
35
|
+
props: {
|
|
36
|
+
heading: 'Productos',
|
|
37
|
+
products: { $state: '/categoryProducts' },
|
|
38
|
+
},
|
|
39
|
+
on: {
|
|
40
|
+
addToCart: { action: 'addToCart' },
|
|
41
|
+
openProduct: { action: 'openProduct' },
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const catalog = {
|
|
47
|
+
CategoryFilter: {
|
|
48
|
+
props: z.object({
|
|
49
|
+
products: z.array(z.record(z.string(), z.unknown())),
|
|
50
|
+
sortOptions: z
|
|
51
|
+
.array(z.object({ value: z.string(), label: z.string() }))
|
|
52
|
+
.optional(),
|
|
53
|
+
categories: z.array(z.record(z.string(), z.unknown())).optional(),
|
|
54
|
+
pagination: z.array(z.union([z.number(), z.string()])).optional(),
|
|
55
|
+
}),
|
|
56
|
+
description: 'Product listing filters, sorting, and results.',
|
|
57
|
+
},
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const category: ViewModule = {
|
|
61
|
+
name: 'category',
|
|
62
|
+
elements,
|
|
63
|
+
catalog,
|
|
64
|
+
registry,
|
|
65
|
+
route: {
|
|
66
|
+
path: '/categorias/[[...slug]]',
|
|
67
|
+
loaderName: 'category',
|
|
68
|
+
layout: 'default',
|
|
69
|
+
metadata: {
|
|
70
|
+
title: 'Categoría',
|
|
71
|
+
description: 'Explora los productos de esta categoría en __SITE_NAME__.',
|
|
72
|
+
},
|
|
73
|
+
entry: pageRoot('category', ['category.filter', 'category.grid'], elements),
|
|
74
|
+
},
|
|
75
|
+
loader: {
|
|
76
|
+
category: async (params) => {
|
|
77
|
+
const slug = Array.isArray(params.slug) ? params.slug[0] : params.slug
|
|
78
|
+
const { products, categories } = await getCatalog()
|
|
79
|
+
const categoryResult = slug
|
|
80
|
+
? await getCategoryProductsByHandleWithSource(slug)
|
|
81
|
+
: null
|
|
82
|
+
const categoryProducts = categoryResult ? categoryResult.products : products
|
|
83
|
+
return { categoryProducts, storeCategories: categories }
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
// Thor Commerce view registry fragment: category
|
|
4
|
+
// Client boundary for the category view. Imports value bindings from
|
|
5
|
+
// @json-render/react and components from @thorprovider/blocks — both must stay
|
|
6
|
+
// on the client side of the RSC graph. Kept separate from index.tsx so the view
|
|
7
|
+
// spec (elements/catalog/route/loader) stays a pure Server Component module.
|
|
8
|
+
|
|
9
|
+
import { CategoryFilter2 as CategoryFilterBlock } from '@thorprovider/blocks'
|
|
10
|
+
import type { CardProduct, CategoryFilter2Filters, FilterProduct } from '@thorprovider/blocks'
|
|
11
|
+
import { useStateStore } from '@json-render/react'
|
|
12
|
+
import type { ViewRegistry } from '../types'
|
|
13
|
+
|
|
14
|
+
export const registry: ViewRegistry = {
|
|
15
|
+
CategoryFilter: ({ props, emit }) => {
|
|
16
|
+
const { set } = useStateStore()
|
|
17
|
+
const products: FilterProduct[] = ((props.products ?? []) as CardProduct[]).map(
|
|
18
|
+
(p) => ({
|
|
19
|
+
id: String(p.id),
|
|
20
|
+
name: p.title,
|
|
21
|
+
price: `$${p.price.toFixed(2)}`,
|
|
22
|
+
image: p.image,
|
|
23
|
+
}),
|
|
24
|
+
)
|
|
25
|
+
return (
|
|
26
|
+
<CategoryFilterBlock
|
|
27
|
+
products={products}
|
|
28
|
+
sortOptions={props.sortOptions as never}
|
|
29
|
+
categories={props.categories as never}
|
|
30
|
+
pagination={props.pagination as never}
|
|
31
|
+
onProductClick={(product) => {
|
|
32
|
+
set('/lastClicked', product)
|
|
33
|
+
emit('openProduct')
|
|
34
|
+
}}
|
|
35
|
+
onPageChange={(page) => {
|
|
36
|
+
set('/categoryPage', page)
|
|
37
|
+
}}
|
|
38
|
+
onSortChange={(sort) => {
|
|
39
|
+
set('/categorySort', sort)
|
|
40
|
+
}}
|
|
41
|
+
onFilterChange={(filters: CategoryFilter2Filters) => {
|
|
42
|
+
set('/filters', filters)
|
|
43
|
+
emit('applyFilters')
|
|
44
|
+
}}
|
|
45
|
+
/>
|
|
46
|
+
)
|
|
47
|
+
},
|
|
48
|
+
}
|