@tanqory/dev-mcp 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +94 -2
- package/bin/tanqory-dev-mcp.mjs +16 -0
- package/bundled/examples/blank/config/settings.json +3 -0
- package/bundled/examples/blank/locales/en.json +22 -0
- package/bundled/examples/blank/locales/th.json +22 -0
- package/bundled/examples/blank/regions/footer.json +35 -0
- package/bundled/examples/blank/regions/header.json +13 -0
- package/bundled/examples/blank/src/bundle.ts +15 -0
- package/bundled/examples/blank/src/components/parts.tsx +32 -0
- package/bundled/examples/blank/src/entry.client.tsx +6 -0
- package/bundled/examples/blank/src/entry.server.tsx +13 -0
- package/bundled/examples/blank/src/sections/chrome.tsx +132 -0
- package/bundled/examples/blank/src/sections/commerce.tsx +242 -0
- package/bundled/examples/blank/src/sections/content.tsx +111 -0
- package/bundled/examples/blank/src/sections/editorial.tsx +107 -0
- package/bundled/examples/blank/src/settings.ts +15 -0
- package/bundled/examples/blank/src/styles/theme.css +195 -0
- package/bundled/examples/blank/src/templates.ts +14 -0
- package/bundled/examples/blank/src/theme.tsx +76 -0
- package/bundled/examples/blank/template.json +24 -0
- package/bundled/examples/blank/templates/404.json +14 -0
- package/bundled/examples/blank/templates/account.json +14 -0
- package/bundled/examples/blank/templates/article.json +14 -0
- package/bundled/examples/blank/templates/blog.json +14 -0
- package/bundled/examples/blank/templates/cart.json +14 -0
- package/bundled/examples/blank/templates/collection.json +14 -0
- package/bundled/examples/blank/templates/index.json +36 -0
- package/bundled/examples/blank/templates/page.contact.json +18 -0
- package/bundled/examples/blank/templates/page.json +14 -0
- package/bundled/examples/blank/templates/product.json +18 -0
- package/bundled/examples/blank/templates/search.json +14 -0
- package/bundled/examples/blank/tests/theme.test.ts +49 -0
- package/bundled/theme-abi/contracts/render-context.v2.d.ts +849 -0
- package/bundled/theme-abi/contracts/theme-abi-v2.md +1833 -0
- package/bundled/theme-abi/generated/capabilities.json +80 -0
- package/bundled/theme-abi/generated/field-types.json +869 -0
- package/bundled/theme-abi/generated/limits.json +173 -0
- package/bundled/theme-abi/schemas/conformance-report.v2.schema.json +482 -0
- package/bundled/theme-abi/schemas/theme-artifact.v1.schema.json +230 -0
- package/bundled/theme-abi/schemas/theme-manifest.v2.schema.json +254 -0
- package/bundled/theme-conformance/docs/storefront-tier.md +250 -0
- package/bundled/theme-conformance/schema/conformance-report.v2.schema.json +482 -0
- package/bundled/theme-conformance/schema/storefront-report.v1.schema.json +110 -0
- package/bundled/theme-conformance/schema/x-tanqory-ai.v0.schema.json +70 -0
- package/bundled/theme-kit/docs/headless-api.md +220 -0
- package/bundled/theme-kit/generated/kit-strings.json +180 -0
- package/bundled/theme-kit/types/headless/index.d.ts +36 -0
- package/bundled/theme-kit/types/headless/theme.d.ts +99 -0
- package/bundled/theme-kit/types/sdk/schema.d.ts +197 -0
- package/bundled/versions.json +8 -0
- package/package.json +53 -3
- package/src/bm25.mjs +69 -0
- package/src/browser.mjs +123 -0
- package/src/cli.mjs +127 -0
- package/src/devserver.mjs +82 -0
- package/src/docs.mjs +329 -0
- package/src/index.mjs +33 -0
- package/src/kit.mjs +150 -0
- package/src/manifest.mjs +82 -0
- package/src/prompts.mjs +97 -0
- package/src/scaffold.mjs +471 -0
- package/src/schemas.mjs +127 -0
- package/src/server.mjs +595 -0
- package/src/theme.mjs +91 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/** Commerce sections: product, recommendations, collection, search, cart and the account page. */
|
|
2
|
+
import {
|
|
3
|
+
AccountSlot,
|
|
4
|
+
AddToCart,
|
|
5
|
+
CartLine,
|
|
6
|
+
CartLineDecrement,
|
|
7
|
+
CartLineIncrement,
|
|
8
|
+
CartLineQuantity,
|
|
9
|
+
CartLineRemove,
|
|
10
|
+
CheckoutButton,
|
|
11
|
+
CollectionProvider,
|
|
12
|
+
FacetFilters,
|
|
13
|
+
Image,
|
|
14
|
+
Pagination,
|
|
15
|
+
ProductForm,
|
|
16
|
+
ProductPrice,
|
|
17
|
+
ProductQuantity,
|
|
18
|
+
SearchForm,
|
|
19
|
+
SortSelect,
|
|
20
|
+
VariantPicker,
|
|
21
|
+
defineSection,
|
|
22
|
+
field,
|
|
23
|
+
formatMoney,
|
|
24
|
+
useCart,
|
|
25
|
+
useRecommendations,
|
|
26
|
+
useSearch,
|
|
27
|
+
useTranslate,
|
|
28
|
+
type ProductFormState,
|
|
29
|
+
} from '@tanqory/theme-kit/headless'
|
|
30
|
+
import { Grid } from '../components/parts'
|
|
31
|
+
|
|
32
|
+
function stockLine(p: ProductFormState, t: ReturnType<typeof useTranslate>): string {
|
|
33
|
+
if (!p.variant) return ''
|
|
34
|
+
if (!p.available) return t('product.soldOut')
|
|
35
|
+
if (p.quantityAvailable !== null && p.quantityAvailable <= 5) return t('product.lowStock', { count: p.quantityAvailable })
|
|
36
|
+
return t('product.inStock')
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function ProductDetail({ settings }: { settings: { showVendor: boolean } }): JSX.Element {
|
|
40
|
+
const t = useTranslate()
|
|
41
|
+
return (
|
|
42
|
+
<ProductForm className="t-pdp">
|
|
43
|
+
{(p) =>
|
|
44
|
+
p.product ? (
|
|
45
|
+
<>
|
|
46
|
+
<div className="t-pdp__media">
|
|
47
|
+
<Image image={p.image} className="t-pdp__img" loading="eager" sizes="(max-width: 720px) 100vw, 50vw" fallback={<span className="t-pdp__img t-pdp__img--empty" />} />
|
|
48
|
+
</div>
|
|
49
|
+
<div className="t-pdp__info">
|
|
50
|
+
{settings.showVendor && p.product.vendor ? <p className="t-pdp__vendor">{p.product.vendor}</p> : null}
|
|
51
|
+
<h1 className="t-pdp__title">{p.product.title}</h1>
|
|
52
|
+
<ProductPrice className="t-pdp__price" compareClassName="t-pdp__was" />
|
|
53
|
+
<VariantPicker className="t-pdp__options" optionClassName="t-opt" legendClassName="t-opt__legend" valueClassName="t-chip" />
|
|
54
|
+
<p className="t-pdp__stock" aria-live="polite">
|
|
55
|
+
{stockLine(p, t)}
|
|
56
|
+
</p>
|
|
57
|
+
<div className="t-pdp__buy">
|
|
58
|
+
<ProductQuantity className="t-input t-input--qty" />
|
|
59
|
+
<AddToCart className="t-btn t-btn--solid t-pdp__add" />
|
|
60
|
+
</div>
|
|
61
|
+
{p.product.descriptionHtml ? <div className="t-pdp__desc" dangerouslySetInnerHTML={{ __html: p.product.descriptionHtml }} /> : null}
|
|
62
|
+
</div>
|
|
63
|
+
</>
|
|
64
|
+
) : (
|
|
65
|
+
<p className="t-muted">{t('theme.product.missing')}</p>
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
</ProductForm>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export const product = defineSection({
|
|
73
|
+
type: 'product',
|
|
74
|
+
title: 'Product',
|
|
75
|
+
description: 'The product: image, title, price, options and add to cart.',
|
|
76
|
+
category: 'products',
|
|
77
|
+
requires: ['product'],
|
|
78
|
+
settings: { showVendor: field.boolean(true, { label: 'Show the brand' }) },
|
|
79
|
+
component: ProductDetail,
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
export const recommendations = defineSection({
|
|
83
|
+
// The data plan's convention: this type loads the product's recommendations in the server render.
|
|
84
|
+
type: 'product-recommendations',
|
|
85
|
+
title: 'Recommendations',
|
|
86
|
+
description: 'Products shoppers buy with this one.',
|
|
87
|
+
category: 'products',
|
|
88
|
+
requires: ['product'],
|
|
89
|
+
settings: { heading: field.text('Worn with', { label: 'Heading' }) },
|
|
90
|
+
presets: [{ name: 'Recommendations' }],
|
|
91
|
+
component: function Recommended({ settings }) {
|
|
92
|
+
const { products } = useRecommendations()
|
|
93
|
+
if (!products.length) return <section className="t-shelf t-shelf--empty" aria-hidden="true" />
|
|
94
|
+
return (
|
|
95
|
+
<section className="t-shelf">
|
|
96
|
+
<div className="t-shelf__head">
|
|
97
|
+
<h2 className="t-shelf__title">{settings.heading}</h2>
|
|
98
|
+
</div>
|
|
99
|
+
<Grid products={products.slice(0, 4)} />
|
|
100
|
+
</section>
|
|
101
|
+
)
|
|
102
|
+
},
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
export const collection = defineSection({
|
|
106
|
+
type: 'collection',
|
|
107
|
+
title: 'Collection',
|
|
108
|
+
description: 'The collection’s products with sort, filters and paging.',
|
|
109
|
+
category: 'products',
|
|
110
|
+
requires: ['collection'],
|
|
111
|
+
settings: { showDescription: field.boolean(true, { label: 'Show the description' }) },
|
|
112
|
+
component: function CollectionPage({ settings }) {
|
|
113
|
+
const t = useTranslate()
|
|
114
|
+
return (
|
|
115
|
+
<CollectionProvider>
|
|
116
|
+
{(c) => (
|
|
117
|
+
<section className="t-plp">
|
|
118
|
+
<header className="t-plp__head">
|
|
119
|
+
<p className="t-plp__kicker">{t('theme.collection.kicker')}</p>
|
|
120
|
+
<h1 className="t-plp__title">{c.collection?.title}</h1>
|
|
121
|
+
{settings.showDescription && c.collection?.description ? <p className="t-plp__desc">{c.collection.description}</p> : null}
|
|
122
|
+
{c.total !== null ? <p className="t-plp__count">{t('collection.productCount', { count: c.total })}</p> : null}
|
|
123
|
+
</header>
|
|
124
|
+
<div className="t-plp__body">
|
|
125
|
+
<aside className="t-plp__filters" aria-label={t('collection.filter')}>
|
|
126
|
+
<SortSelect className="t-sort" labelClassName="t-label" />
|
|
127
|
+
<FacetFilters className="t-facets" facetClassName="t-facet" legendClassName="t-label" valueClassName="t-check" clearClassName="t-link" />
|
|
128
|
+
</aside>
|
|
129
|
+
<div className="t-plp__main">
|
|
130
|
+
{c.products.length ? <Grid products={c.products} busy={c.status === 'loading'} eager={4} /> : <p className="t-muted">{c.filtered ? t('collection.empty') : t('theme.collection.none')}</p>}
|
|
131
|
+
{c.status === 'error' ? <p className="t-muted" role="alert">{t('collection.loadError')}</p> : null}
|
|
132
|
+
<Pagination className="t-pages" nextClassName="t-btn" previousClassName="t-btn" />
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
</section>
|
|
136
|
+
)}
|
|
137
|
+
</CollectionProvider>
|
|
138
|
+
)
|
|
139
|
+
},
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
export const search = defineSection({
|
|
143
|
+
type: 'search',
|
|
144
|
+
title: 'Search results',
|
|
145
|
+
category: 'products',
|
|
146
|
+
requires: ['search'],
|
|
147
|
+
settings: {},
|
|
148
|
+
component: function SearchPage() {
|
|
149
|
+
const s = useSearch()
|
|
150
|
+
const t = useTranslate()
|
|
151
|
+
return (
|
|
152
|
+
<section className="t-results">
|
|
153
|
+
<h1 className="t-results__title">{t('search.search')}</h1>
|
|
154
|
+
<SearchForm className="t-find t-find--page" inputClassName="t-find__input" buttonClassName="t-btn" resultsClassName="t-find__results" />
|
|
155
|
+
{s.query ? <p className="t-results__count" role="status">{s.products.length ? t('search.results', { count: s.total ?? s.products.length, query: s.query }) : t('search.none', { query: s.query })}</p> : null}
|
|
156
|
+
{s.products.length ? <Grid products={s.products} eager={4} /> : null}
|
|
157
|
+
</section>
|
|
158
|
+
)
|
|
159
|
+
},
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
export const cart = defineSection({
|
|
163
|
+
type: 'cart',
|
|
164
|
+
title: 'Cart',
|
|
165
|
+
category: 'cart',
|
|
166
|
+
requires: ['cart'],
|
|
167
|
+
settings: {},
|
|
168
|
+
component: function CartPage() {
|
|
169
|
+
const c = useCart()
|
|
170
|
+
const t = useTranslate()
|
|
171
|
+
return (
|
|
172
|
+
<section className="t-cart">
|
|
173
|
+
<h1 className="t-cart__title">{t('cart.title')}</h1>
|
|
174
|
+
{c.loadError && !c.ready ? <p className="t-muted" role="status">{t('cart.loadError')}</p> : null}
|
|
175
|
+
{c.lines.length === 0 ? (
|
|
176
|
+
<div className="t-cart__empty">
|
|
177
|
+
<p>{t('cart.empty')}</p>
|
|
178
|
+
<a className="t-btn" href="/collections/all">
|
|
179
|
+
{t('theme.cart.continue')}
|
|
180
|
+
</a>
|
|
181
|
+
</div>
|
|
182
|
+
) : (
|
|
183
|
+
<>
|
|
184
|
+
<ul className="t-lines">
|
|
185
|
+
{c.lines.map((line) => (
|
|
186
|
+
<CartLine key={line.id} line={line} className="t-line">
|
|
187
|
+
{(l) => (
|
|
188
|
+
<>
|
|
189
|
+
<Image image={line.variant.image} alt="" className="t-line__img" fallback={<span className="t-line__img t-line__img--empty" />} />
|
|
190
|
+
<div className="t-line__info">
|
|
191
|
+
<a className="t-line__title" href={line.product.url}>
|
|
192
|
+
{line.product.title}
|
|
193
|
+
</a>
|
|
194
|
+
{line.variant.title && line.variant.title !== 'Default Title' ? <p className="t-muted">{line.variant.title}</p> : null}
|
|
195
|
+
<p className="t-muted">{formatMoney(line.variant.price)}</p>
|
|
196
|
+
{l.refusal ? <p role="alert">{t('cart.limitReached', { max: l.refusal.maxQuantity ?? line.quantity })}</p> : l.failed ? <p role="alert">{t('cart.lineFailed')}</p> : null}
|
|
197
|
+
</div>
|
|
198
|
+
<div className="t-line__qty">
|
|
199
|
+
<CartLineDecrement className="t-step" />
|
|
200
|
+
<CartLineQuantity className="t-input t-input--qty" />
|
|
201
|
+
<CartLineIncrement className="t-step" />
|
|
202
|
+
</div>
|
|
203
|
+
<p className="t-line__total">{formatMoney(line.cost.total)}</p>
|
|
204
|
+
<CartLineRemove className="t-link" />
|
|
205
|
+
</>
|
|
206
|
+
)}
|
|
207
|
+
</CartLine>
|
|
208
|
+
))}
|
|
209
|
+
</ul>
|
|
210
|
+
<div className="t-cart__summary">
|
|
211
|
+
<p className="t-cart__row">
|
|
212
|
+
<span>{t('cart.subtotal')}</span> <span>{formatMoney(c.subtotal)}</span>
|
|
213
|
+
</p>
|
|
214
|
+
<p className="t-cart__row t-cart__row--total">
|
|
215
|
+
<span>{t('cart.total')}</span> <span>{formatMoney(c.total)}</span>
|
|
216
|
+
</p>
|
|
217
|
+
<p className="t-muted">{t('theme.cart.taxes')}</p>
|
|
218
|
+
<CheckoutButton className="t-btn t-btn--solid t-cart__checkout" />
|
|
219
|
+
</div>
|
|
220
|
+
</>
|
|
221
|
+
)}
|
|
222
|
+
</section>
|
|
223
|
+
)
|
|
224
|
+
},
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
export const account = defineSection({
|
|
228
|
+
type: 'account',
|
|
229
|
+
title: 'Account',
|
|
230
|
+
category: 'account',
|
|
231
|
+
requires: ['customer'],
|
|
232
|
+
settings: {},
|
|
233
|
+
component: function AccountPage() {
|
|
234
|
+
const t = useTranslate()
|
|
235
|
+
return (
|
|
236
|
+
<section className="t-account">
|
|
237
|
+
<h1 className="t-account__title">{t('account.title')}</h1>
|
|
238
|
+
<AccountSlot className="t-account__frame" />
|
|
239
|
+
</section>
|
|
240
|
+
)
|
|
241
|
+
},
|
|
242
|
+
})
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/** Content sections: a page, the contact form, the journal (blog), an article and the 404 page. */
|
|
2
|
+
import { ContactForm, defineSection, field, useArticle, useBlog, usePageContent, useTranslate } from '@tanqory/theme-kit/headless'
|
|
3
|
+
|
|
4
|
+
export const page = defineSection({
|
|
5
|
+
type: 'page',
|
|
6
|
+
title: 'Page',
|
|
7
|
+
category: 'content',
|
|
8
|
+
requires: ['page'],
|
|
9
|
+
settings: {},
|
|
10
|
+
component: function PageBody() {
|
|
11
|
+
const p = usePageContent()
|
|
12
|
+
if (!p) return <section className="t-page" />
|
|
13
|
+
return (
|
|
14
|
+
<article className="t-page">
|
|
15
|
+
<h1 className="t-page__title">{p.title}</h1>
|
|
16
|
+
<div className="t-prose" dangerouslySetInnerHTML={{ __html: p.html }} />
|
|
17
|
+
</article>
|
|
18
|
+
)
|
|
19
|
+
},
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
export const contact = defineSection({
|
|
23
|
+
type: 'contact-form',
|
|
24
|
+
title: 'Contact form',
|
|
25
|
+
description: 'A message form that reaches the store.',
|
|
26
|
+
category: 'forms',
|
|
27
|
+
settings: { heading: field.text('Write to us', { label: 'Heading' }) },
|
|
28
|
+
presets: [{ name: 'Contact form' }],
|
|
29
|
+
component: ({ settings }) => (
|
|
30
|
+
<section className="t-contact">
|
|
31
|
+
<h2 className="t-contact__title">{settings.heading}</h2>
|
|
32
|
+
<ContactForm className="t-form" inputClassName="t-input" labelClassName="t-label" buttonClassName="t-btn t-btn--solid" messageClassName="t-form__msg" />
|
|
33
|
+
</section>
|
|
34
|
+
),
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
export const blog = defineSection({
|
|
38
|
+
type: 'blog',
|
|
39
|
+
title: 'Journal',
|
|
40
|
+
category: 'content',
|
|
41
|
+
requires: ['blog'],
|
|
42
|
+
settings: {},
|
|
43
|
+
component: function Journal() {
|
|
44
|
+
const b = useBlog()
|
|
45
|
+
const t = useTranslate()
|
|
46
|
+
if (!b) return <section className="t-journal" />
|
|
47
|
+
return (
|
|
48
|
+
<section className="t-journal">
|
|
49
|
+
<h1 className="t-journal__title">{b.title}</h1>
|
|
50
|
+
<ul className="t-journal__list">
|
|
51
|
+
{b.articles.map((a) => (
|
|
52
|
+
<li key={a.id} className="t-journal__item">
|
|
53
|
+
<a className="t-journal__link" href={a.url}>
|
|
54
|
+
<span className="t-journal__name">{a.title}</span>
|
|
55
|
+
{a.excerpt ? <span className="t-journal__excerpt">{a.excerpt}</span> : null}
|
|
56
|
+
<span className="t-journal__more">{t('theme.journal.read')}</span>
|
|
57
|
+
</a>
|
|
58
|
+
</li>
|
|
59
|
+
))}
|
|
60
|
+
</ul>
|
|
61
|
+
</section>
|
|
62
|
+
)
|
|
63
|
+
},
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
export const article = defineSection({
|
|
67
|
+
type: 'article',
|
|
68
|
+
title: 'Article',
|
|
69
|
+
category: 'content',
|
|
70
|
+
requires: ['article'],
|
|
71
|
+
settings: {},
|
|
72
|
+
component: function Story() {
|
|
73
|
+
const a = useArticle()
|
|
74
|
+
const b = useBlog()
|
|
75
|
+
const t = useTranslate()
|
|
76
|
+
if (!a) return <article className="t-story" />
|
|
77
|
+
return (
|
|
78
|
+
<article className="t-story">
|
|
79
|
+
{b ? (
|
|
80
|
+
<a className="t-story__back" href={b.url}>
|
|
81
|
+
{t('theme.journal.back', { blog: b.title })}
|
|
82
|
+
</a>
|
|
83
|
+
) : null}
|
|
84
|
+
<h1 className="t-story__title">{a.title}</h1>
|
|
85
|
+
{a.author ? <p className="t-muted">{t('theme.journal.by', { author: a.author })}</p> : null}
|
|
86
|
+
<div className="t-prose" dangerouslySetInnerHTML={{ __html: a.contentHtml ?? '' }} />
|
|
87
|
+
</article>
|
|
88
|
+
)
|
|
89
|
+
},
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
export const notFound = defineSection({
|
|
93
|
+
// The runtime renders this type as the body of every 404 page (an unknown product, collection, page, blog).
|
|
94
|
+
type: 'not-found',
|
|
95
|
+
title: 'Not found',
|
|
96
|
+
category: 'content',
|
|
97
|
+
settings: {},
|
|
98
|
+
component: function Lost() {
|
|
99
|
+
const t = useTranslate()
|
|
100
|
+
return (
|
|
101
|
+
<section className="t-lost">
|
|
102
|
+
<p className="t-hero__kicker">404</p>
|
|
103
|
+
<h1 className="t-lost__title">{t('theme.notFound.title')}</h1>
|
|
104
|
+
<p className="t-muted">{t('theme.notFound.body')}</p>
|
|
105
|
+
<a className="t-btn" href="/">
|
|
106
|
+
{t('theme.notFound.home')}
|
|
107
|
+
</a>
|
|
108
|
+
</section>
|
|
109
|
+
)
|
|
110
|
+
},
|
|
111
|
+
})
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/** Editorial sections: a hero, a featured collection, a rich-text note and the newsletter sign-up. */
|
|
2
|
+
import { Image, NewsletterForm, ai, defineSection, field, useCollection, useTranslate } from '@tanqory/theme-kit/headless'
|
|
3
|
+
import { Grid, cx } from '../components/parts'
|
|
4
|
+
|
|
5
|
+
export const hero = defineSection({
|
|
6
|
+
type: 'hero',
|
|
7
|
+
title: 'Hero',
|
|
8
|
+
description: 'A headline with a kicker, a short text, an optional image and one link.',
|
|
9
|
+
category: 'editorial',
|
|
10
|
+
settings: {
|
|
11
|
+
kicker: field.text('Welcome', { label: 'Kicker' }),
|
|
12
|
+
heading: field.text('Your store, your theme', { label: 'Heading' }),
|
|
13
|
+
body: field.richtext('', { label: 'Text' }),
|
|
14
|
+
image: field.resource('image', '', { label: 'Image' }),
|
|
15
|
+
buttonLabel: field.text('Shop all', { label: 'Button label' }),
|
|
16
|
+
buttonLink: field.url('/collections/all', { label: 'Button link' }),
|
|
17
|
+
},
|
|
18
|
+
presets: [{ name: 'Hero', settings: { kicker: 'Welcome', heading: 'Your store, your theme' } }],
|
|
19
|
+
ai: ai.hero({ heading: 'heading', subheading: 'kicker', image: 'image', button: 'buttonLabel', link: 'buttonLink' }),
|
|
20
|
+
component: ({ settings }) => (
|
|
21
|
+
<section className={cx('t-hero', settings.image && 't-hero--image')}>
|
|
22
|
+
{settings.image ? <Image image={settings.image} alt="" className="t-hero__img" loading="eager" /> : null}
|
|
23
|
+
<div className="t-hero__copy">
|
|
24
|
+
{settings.kicker ? <p className="t-hero__kicker">{settings.kicker}</p> : null}
|
|
25
|
+
<h1 className="t-hero__title">{settings.heading}</h1>
|
|
26
|
+
{settings.body ? <div className="t-hero__body" dangerouslySetInnerHTML={{ __html: settings.body }} /> : null}
|
|
27
|
+
{settings.buttonLabel && settings.buttonLink ? (
|
|
28
|
+
<a className="t-btn" href={settings.buttonLink}>
|
|
29
|
+
{settings.buttonLabel}
|
|
30
|
+
</a>
|
|
31
|
+
) : null}
|
|
32
|
+
</div>
|
|
33
|
+
</section>
|
|
34
|
+
),
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
function Shelf({ settings }: { settings: { heading: string; collection: string; count: number } }): JSX.Element {
|
|
38
|
+
const c = useCollection(settings.collection)
|
|
39
|
+
const t = useTranslate()
|
|
40
|
+
const products = c.products.slice(0, settings.count)
|
|
41
|
+
return (
|
|
42
|
+
<section className="t-shelf">
|
|
43
|
+
<div className="t-shelf__head">
|
|
44
|
+
<h2 className="t-shelf__title">{settings.heading || c.collection?.title}</h2>
|
|
45
|
+
{c.collection ? (
|
|
46
|
+
<a className="t-shelf__all" href={c.collection.url}>
|
|
47
|
+
{t('theme.shelf.viewAll')}
|
|
48
|
+
</a>
|
|
49
|
+
) : null}
|
|
50
|
+
</div>
|
|
51
|
+
{products.length ? <Grid products={products} eager={4} /> : <p className="t-muted">{t('theme.shelf.empty')}</p>}
|
|
52
|
+
</section>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const featuredCollection = defineSection({
|
|
57
|
+
type: 'featured-collection',
|
|
58
|
+
title: 'Featured collection',
|
|
59
|
+
description: 'A row of products from one collection.',
|
|
60
|
+
category: 'products',
|
|
61
|
+
settings: {
|
|
62
|
+
heading: field.text('Featured', { label: 'Heading' }),
|
|
63
|
+
collection: field.resource('collection', 'all', { label: 'Collection' }),
|
|
64
|
+
count: field.range(8, 4, 16, 4, { label: 'Products to show' }),
|
|
65
|
+
},
|
|
66
|
+
presets: [{ name: 'Featured collection', settings: { heading: 'Featured', collection: 'all' } }],
|
|
67
|
+
ai: ai.productGrid('collection', { heading: 'heading' }),
|
|
68
|
+
component: Shelf,
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
export const note = defineSection({
|
|
72
|
+
type: 'rich-text',
|
|
73
|
+
title: 'Rich text',
|
|
74
|
+
description: 'A heading and a paragraph.',
|
|
75
|
+
category: 'editorial',
|
|
76
|
+
settings: {
|
|
77
|
+
heading: field.text('About us', { label: 'Heading' }),
|
|
78
|
+
body: field.richtext('<p>Tell customers what your store is about.</p>', { label: 'Text' }),
|
|
79
|
+
},
|
|
80
|
+
presets: [{ name: 'Rich text' }],
|
|
81
|
+
ai: ai.richText({ heading: 'heading', body: 'body' }),
|
|
82
|
+
component: ({ settings }) => (
|
|
83
|
+
<section className="t-note">
|
|
84
|
+
{settings.heading ? <h2 className="t-note__title">{settings.heading}</h2> : null}
|
|
85
|
+
{settings.body ? <div className="t-note__body" dangerouslySetInnerHTML={{ __html: settings.body }} /> : null}
|
|
86
|
+
</section>
|
|
87
|
+
),
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
export const letters = defineSection({
|
|
91
|
+
type: 'newsletter',
|
|
92
|
+
title: 'Newsletter',
|
|
93
|
+
description: 'An email sign-up for the store’s newsletter.',
|
|
94
|
+
category: 'forms',
|
|
95
|
+
settings: {
|
|
96
|
+
heading: field.text('Subscribe to our emails', { label: 'Heading' }),
|
|
97
|
+
body: field.textarea('Be the first to hear about new products.', { label: 'Text' }),
|
|
98
|
+
},
|
|
99
|
+
presets: [{ name: 'Newsletter' }],
|
|
100
|
+
component: ({ settings }) => (
|
|
101
|
+
<section className="t-letters">
|
|
102
|
+
<h2 className="t-letters__title">{settings.heading}</h2>
|
|
103
|
+
{settings.body ? <p className="t-letters__body">{settings.body}</p> : null}
|
|
104
|
+
<NewsletterForm className="t-letters__form" inputClassName="t-input" buttonClassName="t-btn t-btn--solid" messageClassName="t-letters__msg" />
|
|
105
|
+
</section>
|
|
106
|
+
),
|
|
107
|
+
})
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** The theme settings (the editor's Theme panel). Add your own; every key needs a default. */
|
|
2
|
+
import { defineSettings } from '@tanqory/theme-kit/headless'
|
|
3
|
+
|
|
4
|
+
export const settings = defineSettings({
|
|
5
|
+
width: {
|
|
6
|
+
type: 'select',
|
|
7
|
+
label: 'Page width',
|
|
8
|
+
group: 'Layout',
|
|
9
|
+
default: 'normal',
|
|
10
|
+
options: [
|
|
11
|
+
{ value: 'normal', label: 'Normal' },
|
|
12
|
+
{ value: 'wide', label: 'Wide' },
|
|
13
|
+
],
|
|
14
|
+
},
|
|
15
|
+
})
|