@tanqory/dev-mcp 0.0.0-stage → 0.1.0-alpha.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.
Files changed (65) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +94 -2
  3. package/bin/tanqory-dev-mcp.mjs +16 -0
  4. package/bundled/examples/blank/config/settings.json +3 -0
  5. package/bundled/examples/blank/locales/en.json +22 -0
  6. package/bundled/examples/blank/locales/th.json +22 -0
  7. package/bundled/examples/blank/regions/footer.json +35 -0
  8. package/bundled/examples/blank/regions/header.json +13 -0
  9. package/bundled/examples/blank/src/bundle.ts +15 -0
  10. package/bundled/examples/blank/src/components/parts.tsx +32 -0
  11. package/bundled/examples/blank/src/entry.client.tsx +6 -0
  12. package/bundled/examples/blank/src/entry.server.tsx +13 -0
  13. package/bundled/examples/blank/src/sections/chrome.tsx +132 -0
  14. package/bundled/examples/blank/src/sections/commerce.tsx +242 -0
  15. package/bundled/examples/blank/src/sections/content.tsx +111 -0
  16. package/bundled/examples/blank/src/sections/editorial.tsx +107 -0
  17. package/bundled/examples/blank/src/settings.ts +15 -0
  18. package/bundled/examples/blank/src/styles/theme.css +195 -0
  19. package/bundled/examples/blank/src/templates.ts +14 -0
  20. package/bundled/examples/blank/src/theme.tsx +76 -0
  21. package/bundled/examples/blank/template.json +24 -0
  22. package/bundled/examples/blank/templates/404.json +14 -0
  23. package/bundled/examples/blank/templates/account.json +14 -0
  24. package/bundled/examples/blank/templates/article.json +14 -0
  25. package/bundled/examples/blank/templates/blog.json +14 -0
  26. package/bundled/examples/blank/templates/cart.json +14 -0
  27. package/bundled/examples/blank/templates/collection.json +14 -0
  28. package/bundled/examples/blank/templates/index.json +36 -0
  29. package/bundled/examples/blank/templates/page.contact.json +18 -0
  30. package/bundled/examples/blank/templates/page.json +14 -0
  31. package/bundled/examples/blank/templates/product.json +18 -0
  32. package/bundled/examples/blank/templates/search.json +14 -0
  33. package/bundled/examples/blank/tests/theme.test.ts +49 -0
  34. package/bundled/theme-abi/contracts/render-context.v2.d.ts +849 -0
  35. package/bundled/theme-abi/contracts/theme-abi-v2.md +1833 -0
  36. package/bundled/theme-abi/generated/capabilities.json +80 -0
  37. package/bundled/theme-abi/generated/field-types.json +869 -0
  38. package/bundled/theme-abi/generated/limits.json +173 -0
  39. package/bundled/theme-abi/schemas/conformance-report.v2.schema.json +482 -0
  40. package/bundled/theme-abi/schemas/theme-artifact.v1.schema.json +230 -0
  41. package/bundled/theme-abi/schemas/theme-manifest.v2.schema.json +254 -0
  42. package/bundled/theme-conformance/docs/storefront-tier.md +221 -0
  43. package/bundled/theme-conformance/schema/conformance-report.v2.schema.json +482 -0
  44. package/bundled/theme-conformance/schema/storefront-report.v1.schema.json +110 -0
  45. package/bundled/theme-conformance/schema/x-tanqory-ai.v0.schema.json +70 -0
  46. package/bundled/theme-kit/docs/headless-api.md +220 -0
  47. package/bundled/theme-kit/generated/kit-strings.json +180 -0
  48. package/bundled/theme-kit/types/headless/index.d.ts +36 -0
  49. package/bundled/theme-kit/types/headless/theme.d.ts +99 -0
  50. package/bundled/theme-kit/types/sdk/schema.d.ts +197 -0
  51. package/bundled/versions.json +8 -0
  52. package/package.json +53 -3
  53. package/src/bm25.mjs +69 -0
  54. package/src/browser.mjs +123 -0
  55. package/src/cli.mjs +127 -0
  56. package/src/devserver.mjs +82 -0
  57. package/src/docs.mjs +329 -0
  58. package/src/index.mjs +33 -0
  59. package/src/kit.mjs +150 -0
  60. package/src/manifest.mjs +82 -0
  61. package/src/prompts.mjs +97 -0
  62. package/src/scaffold.mjs +471 -0
  63. package/src/schemas.mjs +127 -0
  64. package/src/server.mjs +595 -0
  65. package/src/theme.mjs +91 -0
@@ -0,0 +1,220 @@
1
+ # Headless API (`@tanqory/theme-kit/headless`)
2
+
3
+ Every storefront function, none of the styling. Your theme owns all markup and CSS; the kit supplies data,
4
+ behaviour and accessible primitives that render semantic HTML carrying the platform's `tq-storefront-hooks/1`
5
+ attributes. Each primitive accepts `className` and most accept render-prop `children` for full control.
6
+
7
+ The kit ships no CSS. The only class it adds is `visuallyHidden()` (an `sx()` rule). There is never a `style=""`
8
+ attribute: the platform CSP has `style-src-attr 'none'`. Use classes or `sx()` for dynamic values.
9
+
10
+ ```
11
+ @tanqory/theme-kit/headless hooks, primitives, defineSection / defineBlock / defineTheme, t(), formatMoney, <Seo>, <Image>, ai.*
12
+ @tanqory/theme-kit/server createRender({ manifest, bundle, storefront: {} }) the ABI render(request, ctx)
13
+ @tanqory/theme-kit/client hydrate({ bundle }) the browser entry; live edit on the editor canvas
14
+ @tanqory/theme-kit/build buildManifest(...), checkContent, withKitStrings the manifest from your declarations (Node, pure)
15
+ ```
16
+
17
+ ## 1. A theme in four files
18
+
19
+ ```tsx
20
+ // theme.tsx
21
+ import { defineTheme, defineSection, defineBlock, field, ai, useMenu, CartToggle, CartDrawer, ConsentBanner } from '@tanqory/theme-kit/headless'
22
+
23
+ const header = defineSection({
24
+ type: 'header', title: 'Header', zone: 'header',
25
+ settings: { menu: field.resource('menu', 'main-menu', { label: 'Menu' }) },
26
+ ai: ai.headerMenu('menu'),
27
+ component: ({ settings }) => {
28
+ const menu = useMenu(settings.menu)
29
+ return <header className="mast"><nav aria-label="Main">…{menu?.items…}</nav><CartToggle className="mast__cart" /></header>
30
+ },
31
+ })
32
+
33
+ export default defineTheme({
34
+ name: 'My theme',
35
+ sections: [header, /* hero, product, collection, cart, search, footer, not-found … */],
36
+ Layout: ({ children }) => <>{children}<CartDrawer className="drawer">{(cart) => …}</CartDrawer><ConsentBanner className="consent" /></>,
37
+ })
38
+ ```
39
+
40
+ ```tsx
41
+ // entry.server.tsx — built into ONE file with no imports
42
+ import { createRender } from '@tanqory/theme-kit/server'
43
+ import { bundle } from './bundle'
44
+ import manifestJson from './generated/manifest.json'
45
+ export const manifest = manifestJson
46
+ export const render = createRender({ manifest, bundle, storefront: { plan: { localization: true } } })
47
+ ```
48
+
49
+ ```tsx
50
+ // entry.client.tsx
51
+ import { hydrate } from '@tanqory/theme-kit/client'
52
+ import { bundle } from './bundle'
53
+ hydrate({ bundle })
54
+ ```
55
+
56
+ ```ts
57
+ // bundle.ts
58
+ import type { ThemeBundle } from '@tanqory/theme-kit/headless'
59
+ import theme from './theme'
60
+ export const bundle: ThemeBundle = {
61
+ theme, templates: {}, groups: {}, settings: {}, settingsSchema: {}, customSections: {}, manifest: null,
62
+ locales: import.meta.glob('./locales/*.json', { eager: true }),
63
+ }
64
+ ```
65
+
66
+ The manifest comes from your declarations: `buildManifest({ name, version, theme, settings, templates, forms })`
67
+ (see §7). `examples/aurora` in this repository is a complete theme built this way.
68
+
69
+ ## 2. `createRender({ storefront })`
70
+
71
+ `storefront` turns on what the headless API relies on. `storefront: {}` is enough; every member defaults to on.
72
+ Without it, the render is the 2.0.0-alpha.0 one, plus the hreflang alternates every theme gets from 2.0.0-alpha.2 (`createRender({ hreflang: false })` leaves them out).
73
+
74
+ | Member | Default | Effect |
75
+ |---|---|---|
76
+ | `structuredData` | `true` | BreadcrumbList JSON-LD (product, collection, page, article) and CollectionPage + ItemList on a collection. The product page's single Product JSON-LD is printed in every mode |
77
+ | `merchantStrings` | `true` | Reads the merchant's dashboard wording for your strings (`ctx.data.localization.strings`, one call beside the first round; the data plan keeps the call limit with it) and lets `t()` prefer it |
78
+ | `plan.localization` | `false` | Reads the shopper's country and lists the store's countries (the page then varies on `x-tq-country`). Needed by `<LocalizationForm>`'s country select |
79
+ | `plan.account` | `false` | Reads `request.isSignedIn` (the page then varies on it) for `useCustomer().signedIn` |
80
+ | `plan.recommendations` | `false` | Loads the product's recommendations in the server render (otherwise the browser loads them) |
81
+ | `plan.collections` | `false` | Loads the store's collection list |
82
+
83
+ The data plan also keeps its section conventions: a `header` or `footer` section with a `showLocale: true`
84
+ setting reads the country, a `header` section reads sign-in state unless `showAccount` is `false`, a
85
+ `product-recommendations` section loads recommendations, and a `collection-list` section loads every
86
+ collection. A section of type `not-found` renders the body of a 404 page.
87
+
88
+ The render also provides `usePage()` (route kind, handle, params, canonical, languages, assets base, merchant
89
+ strings) and copies it into the hydration state, so the browser renders from the same bytes.
90
+
91
+ ## 3. Declaring sections and blocks
92
+
93
+ ```ts
94
+ defineSection({
95
+ type: 'hero', // persisted: never rename once stores saved it
96
+ title: 'Hero', description?, category?, zone?: 'header' | 'main' | 'footer',
97
+ settings: { heading: field.text('After dark', { label: 'Heading' }), image: field.resource('image') },
98
+ blocks?: [defineBlock({ type: 'slide', title: 'Slide', settings: {…} })],
99
+ presets?: [{ name: 'Hero', settings?, blocks? }], // presets[0] is the editor's "Add section" preview
100
+ requires?: ['product'], // offered only on templates of these route kinds
101
+ ai?: ai.hero({ heading: 'heading', image: 'image' }),
102
+ component: ({ settings, blocks }) => …,
103
+ })
104
+ ```
105
+
106
+ The component receives `settings` resolved against the schema (defaults filled, coerced; a resource setting is
107
+ its handle string) and `blocks` in the merchant's order (`{ type, settings }`). The section renderer wraps every
108
+ section in its `data-tq-node="<id>"` element (`class="tq-node"`; style it `display: contents` to keep it out of
109
+ your layout), and
110
+ on the editor canvas a setting edit re-renders the section in place (`tq:canvas`, announced with
111
+ `tq:canvas-live`). You write nothing for either.
112
+
113
+ `field.*`: `text`, `textarea`, `richtext`, `url`, `color`, `boolean`, `select(options, default, { optionLabels })`,
114
+ `range(default, min, max, step, { unit })`, `number(…)`, `resource('image' | 'video' | 'menu' | 'collection' |
115
+ 'product' | 'page' | 'blog' | 'article', default?)`. Every field takes `{ label, info, placeholder, group }`.
116
+
117
+ ## 4. Data hooks
118
+
119
+ | Hook | Returns |
120
+ |---|---|
121
+ | `useSettings()` | the theme's global settings: the manifest defaults with the store's saved values over them |
122
+ | `useShop()` | `Shop`: name, logo, contact, social, payment methods, policies, consent banner config |
123
+ | `useMenu(handle)` | `Menu \| null` (items carry `active` for the current route) |
124
+ | `useProduct(handleOrProduct?)` | `{ product, variant, options[{ name, values[{ value, selected, available, variantId }] }], selectOption, selectVariant, price, compareAtPrice, onSale, available, quantityAvailable, image, quantity, setQuantity, unavailable }`. Without an argument: the enclosing `<ProductForm>`'s product, else the route's, opened on `?variant=` in the server render. A variant choice keeps `?variant=` in the URL |
125
+ | `useCollection(handle?)` | `{ collection, products, total, status, facets, selected, price, filtered, toggleFilter, setPrice, clearFilters, sort, sortOptions, setSort, hasMore, loadMore, page, pageHref }`. Inside `<CollectionProvider>` every caller shares one state. The route collection's sort and filters live in the URL (`?sort=`, `?filter.<facet>=`); the server renders `?sort=` and `?page=N`, a change re-asks the data plane |
126
+ | `useSearch()` | `{ query, products, articles, pages, total, sort, sortOptions, href(q, sort?) }` for `/search?q=` |
127
+ | `usePredictiveSearch(query, { limit, debounceMs })` | `{ results: { products[{ handle, title, url, price, image }], collections, queries } \| null, loading }` (browser, `data:search`) |
128
+ | `useCart()` | `{ cart, lines, itemCount, subtotal, total, checkoutUrl, ready, loading, loadError, isOpen, justAdded, open, close, add(variantId, qty), setQuantity, remove, setNote, isBusy(id), hasFailed(id), refusal(id) }` |
129
+ | `useCustomer()` | `{ signedIn: boolean \| null, accountUrl, accountHref }`. Customer data never reaches theme code |
130
+ | `useLocalization()` | `{ country, language, countries[{ code, name, currencyCode, currencySymbol, current }], languages[{ code, name, current }], changeCountry, changeLanguage, href({ country, language }) }` |
131
+ | `useRecommendations()` | `{ products, loading }` for the route's product |
132
+ | `useConsent()` | `{ required, accept, reject, decide, reopen, privacyUrl, text }` (`required` is never true on a preview render) |
133
+ | `usePageContent()` / `useBlog(handle?)` / `useArticle()` / `usePolicy()` | the route's content page `{ title, html }`, blog (with its articles), article (with `contentHtml`), published policy |
134
+ | `usePage()` | `{ kind, handle, params, canonical, language, ui, languages, preview, assetsBase, strings }` |
135
+
136
+ ## 5. Primitives
137
+
138
+ | Primitive | Renders | Behaviour |
139
+ |---|---|---|
140
+ | `<ProductForm product?>` | `<form data-tq-product-form>` | shares one product state; submit adds the selected variant and quantity (`cart.create` / `cart.addLines`), which opens the cart; a sold-out variant is never sent |
141
+ | `<VariantPicker>` | `<div data-tq-variant-picker>` → `<fieldset><legend>` + a labelled `<input type="radio">` per value (`data-selected`, `data-available` on the label) | selecting updates price, stock, image and `?variant=`; `renderValue` for swatches |
142
+ | `<ProductPrice>` | `<span data-tq-price>`, regular price in `<s>` on a sale | formatted in the market currency |
143
+ | `<ProductQuantity>` | labelled `<input type="number" min="1">` | |
144
+ | `<AddToCart>` | `<button type="submit" data-tq-action="cart:add">` | disabled and "Sold out" when the variant cannot be bought; "Adding…" in flight |
145
+ | `<CartToggle drawer?>` | `<a href="/cart" data-tq-action="cart:open" aria-haspopup="dialog" aria-expanded>` | opens the drawer (plain link without JavaScript) |
146
+ | `<CartDrawer>` | `role="dialog" aria-modal="true"` (+ optional backdrop) while open | focus moves in, Tab is trapped, Escape closes, focus returns to the opener |
147
+ | `<CartLine line>` | `<li data-tq-cart-line>` | gives its controls the line |
148
+ | `<CartLineQuantity>` | labelled `<input type="number" data-tq-quantity>` | saved on Enter or blur; 0 removes; invalid input is put back |
149
+ | `<CartLineIncrement>` / `<CartLineDecrement>` / `<CartLineRemove>` | buttons `data-tq-action="cart:increment"` / `cart:decrement` / `cart:remove` | disabled while the line's request is open |
150
+ | `<CheckoutButton>` | `<a data-tq-action="cart:checkout" href={Cart.checkoutUrl}>` | checkout is the platform's |
151
+ | `<CollectionProvider handle?>` | nothing | one `useCollection()` state for the subtree |
152
+ | `<SortSelect>` | `<form method="get">` + labelled `<select name="sort" data-tq-sort>` | re-sorts in place; without JavaScript the form submits `?sort=` |
153
+ | `<FacetFilters>` | `<div data-tq-filter>` → `<fieldset>` + checkbox per value (`data-active`), price from/to, "Clear filters" | |
154
+ | `<Pagination>` | `<nav data-tq-pagination>` → "Load more" `<a rel="next" href="?page=N">` | appends in place; a real page link without JavaScript |
155
+ | `<SearchForm>` | `<form role="search" action="/search">` + `<input type="search" name="q" data-tq-search>` + polite live region | predictive suggestions as links |
156
+ | `<LocalizationForm>` | `<form method="get" data-tq-localization>` + `<select name="country">` ("Japan (JPY ¥)") + `<select name="locale">` | a choice loads `?country=` / `?locale=` |
157
+ | `<NewsletterForm>` / `<ContactForm>` | `<form method="post" data-tq-form="newsletter\|contact">` + honeypot | native platform post (declare `forms` in the manifest); the gateway's CSRF fields are kept on hydration; status messages after the post |
158
+ | `<ConsentBanner>` | `<section data-tq-consent-banner aria-label>` | only when the platform asks; the merchant's wording wins; never in preview |
159
+ | `<AccountSlot>` | `<iframe data-tq-slot="account" sandbox="allow-scripts allow-forms allow-same-origin">` | the platform's account area on the customer route |
160
+
161
+ ## 6. Platform helpers
162
+
163
+ - **`t()`**: `const t = useTranslate(); t('product.addToCart')`, `t('cart.cartCount', { count })`. Lookup order:
164
+ the merchant's dashboard wording → your `locales/<lang>.json` → the kit's words for that language
165
+ (`KIT_STRINGS`, English and Thai) → English → the key. `withKitStrings(locales)` (build) lays your files over
166
+ the kit's, so every key the primitives use exists in every locale you ship.
167
+ - **`formatMoney(money, { display, trimZeros })`**: `$37.50`, `¥5,625`, `฿1,312.50`, `1.250 KWD`. The currency's
168
+ own minor units (`currencyDecimals`), identical on the server and in the browser.
169
+ - **`<Image image alt>`**: `image` is an image from the data or an image setting's URL; `https:` URLs, or `asset:client/<file>` under the artifact's asset base; anything
170
+ else renders `fallback`. Intrinsic width/height, `loading="lazy"`, `decoding="async"` by default.
171
+ - **`<Seo title description image noindex jsonLd>`**: overrides for this page's head, collected during the
172
+ server render. Title, description, canonical, Open Graph and the Product/Collection/Breadcrumb
173
+ JSON-LD are already printed from the data, and so are the hreflang alternates (every theme, `createRender({ hreflang })`, default on); a Product object in `jsonLd` is ignored (exactly one per page).
174
+ - **`<SkipLink>`**: to `#main`, the `<main>` the section renderer wraps the page body in.
175
+
176
+ ## 7. The manifest (`@tanqory/theme-kit/build`)
177
+
178
+ ```js
179
+ import { buildManifest, checkContent, withKitStrings } from '@tanqory/theme-kit/build'
180
+ import { validateManifest, validateFieldValue } from '@tanqory/theme-abi'
181
+
182
+ const { manifest, problems } = buildManifest({
183
+ name: '@acme/theme', version: '1.0.0', theme,
184
+ settings, // defineSettings({...}) → settingsSchema, grouped by `group`
185
+ templates: { index: { route: 'home' }, product: { route: 'product' }, '404': { route: 'not_found' }, … },
186
+ forms: { newsletter: true, contact: true },
187
+ capabilities: { server: ['data:inventory'], browser: ['data:inventory'] },
188
+ checkValue: validateFieldValue,
189
+ })
190
+ ```
191
+
192
+ It derives `components` (sections and blocks with their attributes, regions, children, parents, requires,
193
+ presets), `settingsSchema`, `routes`, `templates`, `regions` (header and footer shared, `main` per template),
194
+ `forms` and `x-tanqory-ai`. Validate the result with `validateManifest` before writing it.
195
+
196
+ **Capabilities.** The defaults are what the kit's runtime calls (`SERVER_CAPABILITIES`, `BROWSER_CAPABILITIES`).
197
+ `data:shop` is always declared: the platform gives shop data to the browser snapshot only to a theme that
198
+ declares it (decision D6). Add `data:inventory` to show stock counts.
199
+
200
+ ## 8. AI compatibility
201
+
202
+ Declare which of your sections play the roles Tanqory AI fills; the build prints the profile as
203
+ `manifest["x-tanqory-ai"]` (draft v0) and `checkAiProfile` verifies every component, block and setting it names.
204
+
205
+ | Helper | Role | Keys (setting types) |
206
+ |---|---|---|
207
+ | `ai.hero({ heading, subheading?, image?, button?, link?, block? })` | `hero` | text / image / text / url |
208
+ | `ai.headerMenu(setting)` | `headerMenu` | a `menu` setting |
209
+ | `ai.footerMenu(setting, { block })` | `footerMenus` | a `menu` setting (usually on a block) |
210
+ | `ai.footerBrand({ block? })` | `footerBrand` | |
211
+ | `ai.productGrid(setting, { heading? })` | `productGrid` | a `collection` or `product` setting |
212
+ | `ai.richText({ heading?, body? })` | `richText` | text settings |
213
+
214
+ ## 9. Storefront hooks emitted (`tq-storefront-hooks/1`)
215
+
216
+ `data-tq-action="cart:add|cart:open|cart:checkout|cart:remove|cart:increment"`, `data-tq-quantity`,
217
+ `data-tq-price`, `data-tq-variant-picker`, `data-tq-sort`, `data-tq-filter`, `data-tq-pagination`,
218
+ `data-tq-localization`, `data-tq-search`, `data-tq-consent-banner`, plus `data-tq-form`, `data-tq-slot="account"`
219
+ and `data-tq-node` from the platform contract. A menu button for a mobile menu is the theme's own markup: give it
220
+ `data-tq-action="menu:open"`.
@@ -0,0 +1,180 @@
1
+ {
2
+ "en": {
3
+ "a11y.close": "Close",
4
+ "a11y.skipToContent": "Skip to content",
5
+ "account.title": "Account",
6
+ "breadcrumbs.home": "Home",
7
+ "cart.cartCount": "Cart ({count})",
8
+ "cart.checkout": "Check out",
9
+ "cart.close": "Close cart",
10
+ "cart.decrease": "Decrease quantity",
11
+ "cart.empty": "Your cart is empty.",
12
+ "cart.increase": "Increase quantity",
13
+ "cart.limitReached": "You can buy up to {max} of this item.",
14
+ "cart.lineFailed": "This change could not be saved.",
15
+ "cart.loadError": "We couldn't load your cart. Retrying…",
16
+ "cart.loading": "Loading your cart…",
17
+ "cart.openCart": "Cart",
18
+ "cart.quantity": "Quantity",
19
+ "cart.remove": "Remove",
20
+ "cart.subtotal": "Subtotal",
21
+ "cart.title": "Your cart",
22
+ "cart.total": "Total",
23
+ "collection.clearFilters": "Clear filters",
24
+ "collection.empty": "No products match these filters.",
25
+ "collection.filter": "Filter",
26
+ "collection.loadError": "We couldn't load more products.",
27
+ "collection.loadMore": "Load more",
28
+ "collection.loading": "Loading…",
29
+ "collection.pagination": "Pagination",
30
+ "collection.previous": "Previous page",
31
+ "collection.priceFrom": "From",
32
+ "collection.priceTo": "To",
33
+ "collection.productCount": "{count} products",
34
+ "collection.sort": "Sort",
35
+ "consent.accept": "Accept",
36
+ "consent.body": "We use cookies to run this store and, with your permission, to measure and improve it.",
37
+ "consent.privacy": "Privacy policy",
38
+ "consent.reject": "Decline",
39
+ "consent.title": "Cookies",
40
+ "contact.done": "Thanks — we'll get back to you soon.",
41
+ "contact.message": "Message",
42
+ "contact.name": "Name",
43
+ "contact.phone": "Phone",
44
+ "contact.send": "Send message",
45
+ "filter.available": "Availability",
46
+ "filter.available.false": "Out of stock",
47
+ "filter.available.true": "In stock",
48
+ "filter.price": "Price",
49
+ "filter.productType": "Product type",
50
+ "filter.vendor": "Brand",
51
+ "form.email": "Email",
52
+ "form.expired": "This page expired. Please submit the form again.",
53
+ "form.invalidEmail": "Enter a valid email address.",
54
+ "form.rateLimited": "Too many attempts. Please try again in a minute.",
55
+ "form.unavailable": "This form isn't available right now. Please try again later.",
56
+ "form.website": "Website",
57
+ "localization.country": "Country/region",
58
+ "localization.language": "Language",
59
+ "localization.update": "Update",
60
+ "menu.closeMenu": "Close menu",
61
+ "menu.menu": "Menu",
62
+ "newsletter.done": "Thanks — you're on the list.",
63
+ "newsletter.subscribe": "Subscribe",
64
+ "product.addFailed": "This item could not be added to your cart.",
65
+ "product.addToCart": "Add to cart",
66
+ "product.adding": "Adding…",
67
+ "product.inStock": "In stock",
68
+ "product.lowStock": "Only {count} left",
69
+ "product.quantity": "Quantity",
70
+ "product.regularPrice": "Regular price",
71
+ "product.salePrice": "Sale price",
72
+ "product.soldOut": "Sold out",
73
+ "product.unavailable": "Unavailable",
74
+ "search.none": "No results for “{query}”.",
75
+ "search.placeholder": "Search the store",
76
+ "search.products": "Products",
77
+ "search.results": "{count} results for “{query}”",
78
+ "search.search": "Search",
79
+ "search.submit": "Search",
80
+ "search.suggestions": "Suggestions",
81
+ "sort.best-selling": "Best selling",
82
+ "sort.created-asc": "Oldest",
83
+ "sort.created-desc": "Newest",
84
+ "sort.featured": "Featured",
85
+ "sort.price-asc": "Price, low to high",
86
+ "sort.price-desc": "Price, high to low",
87
+ "sort.relevance": "Relevance",
88
+ "sort.title-asc": "Name, A–Z",
89
+ "sort.title-desc": "Name, Z–A"
90
+ },
91
+ "th": {
92
+ "a11y.close": "ปิด",
93
+ "a11y.skipToContent": "ข้ามไปยังเนื้อหา",
94
+ "account.title": "บัญชี",
95
+ "breadcrumbs.home": "หน้าแรก",
96
+ "cart.cartCount": "ตะกร้า ({count})",
97
+ "cart.checkout": "ชำระเงิน",
98
+ "cart.close": "ปิดตะกร้า",
99
+ "cart.decrease": "ลดจำนวน",
100
+ "cart.empty": "ตะกร้าของคุณว่างอยู่",
101
+ "cart.increase": "เพิ่มจำนวน",
102
+ "cart.limitReached": "ซื้อสินค้านี้ได้สูงสุด {max} ชิ้น",
103
+ "cart.lineFailed": "บันทึกการเปลี่ยนแปลงนี้ไม่สำเร็จ",
104
+ "cart.loadError": "โหลดตะกร้าไม่สำเร็จ กำลังลองใหม่…",
105
+ "cart.loading": "กำลังโหลดตะกร้า…",
106
+ "cart.openCart": "ตะกร้า",
107
+ "cart.quantity": "จำนวน",
108
+ "cart.remove": "นำออก",
109
+ "cart.subtotal": "ยอดรวมย่อย",
110
+ "cart.title": "ตะกร้าของคุณ",
111
+ "cart.total": "ยอดรวม",
112
+ "collection.clearFilters": "ล้างตัวกรอง",
113
+ "collection.empty": "ไม่มีสินค้าที่ตรงกับตัวกรองนี้",
114
+ "collection.filter": "ตัวกรอง",
115
+ "collection.loadError": "โหลดสินค้าเพิ่มไม่สำเร็จ",
116
+ "collection.loadMore": "โหลดเพิ่ม",
117
+ "collection.loading": "กำลังโหลด…",
118
+ "collection.pagination": "การแบ่งหน้า",
119
+ "collection.previous": "หน้าก่อนหน้า",
120
+ "collection.priceFrom": "ตั้งแต่",
121
+ "collection.priceTo": "ถึง",
122
+ "collection.productCount": "สินค้า {count} รายการ",
123
+ "collection.sort": "เรียงตาม",
124
+ "consent.accept": "ยอมรับ",
125
+ "consent.body": "เราใช้คุกกี้เพื่อให้ร้านค้าทำงาน และเพื่อวัดผลและปรับปรุงร้านเมื่อคุณอนุญาต",
126
+ "consent.privacy": "นโยบายความเป็นส่วนตัว",
127
+ "consent.reject": "ปฏิเสธ",
128
+ "consent.title": "คุกกี้",
129
+ "contact.done": "ขอบคุณ เราจะติดต่อกลับโดยเร็ว",
130
+ "contact.message": "ข้อความ",
131
+ "contact.name": "ชื่อ",
132
+ "contact.phone": "โทรศัพท์",
133
+ "contact.send": "ส่งข้อความ",
134
+ "filter.available": "สถานะสินค้า",
135
+ "filter.available.false": "สินค้าหมด",
136
+ "filter.available.true": "มีสินค้า",
137
+ "filter.price": "ราคา",
138
+ "filter.productType": "ประเภทสินค้า",
139
+ "filter.vendor": "แบรนด์",
140
+ "form.email": "อีเมล",
141
+ "form.expired": "หน้านี้หมดอายุแล้ว กรุณาส่งแบบฟอร์มอีกครั้ง",
142
+ "form.invalidEmail": "กรุณากรอกอีเมลให้ถูกต้อง",
143
+ "form.rateLimited": "ลองหลายครั้งเกินไป กรุณาลองใหม่ในอีกสักครู่",
144
+ "form.unavailable": "แบบฟอร์มนี้ใช้งานไม่ได้ในขณะนี้ กรุณาลองใหม่ภายหลัง",
145
+ "form.website": "เว็บไซต์",
146
+ "localization.country": "ประเทศ/ภูมิภาค",
147
+ "localization.language": "ภาษา",
148
+ "localization.update": "อัปเดต",
149
+ "menu.closeMenu": "ปิดเมนู",
150
+ "menu.menu": "เมนู",
151
+ "newsletter.done": "ขอบคุณ คุณได้สมัครรับข่าวสารแล้ว",
152
+ "newsletter.subscribe": "สมัครรับข่าวสาร",
153
+ "product.addFailed": "ไม่สามารถเพิ่มสินค้านี้ลงตะกร้าได้",
154
+ "product.addToCart": "เพิ่มลงตะกร้า",
155
+ "product.adding": "กำลังเพิ่ม…",
156
+ "product.inStock": "มีสินค้า",
157
+ "product.lowStock": "เหลือเพียง {count} ชิ้น",
158
+ "product.quantity": "จำนวน",
159
+ "product.regularPrice": "ราคาปกติ",
160
+ "product.salePrice": "ราคาลด",
161
+ "product.soldOut": "สินค้าหมด",
162
+ "product.unavailable": "ไม่มีจำหน่าย",
163
+ "search.none": "ไม่พบผลลัพธ์สำหรับ “{query}”",
164
+ "search.placeholder": "ค้นหาในร้าน",
165
+ "search.products": "สินค้า",
166
+ "search.results": "พบ {count} รายการสำหรับ “{query}”",
167
+ "search.search": "ค้นหา",
168
+ "search.submit": "ค้นหา",
169
+ "search.suggestions": "คำแนะนำ",
170
+ "sort.best-selling": "ขายดี",
171
+ "sort.created-asc": "เก่าที่สุด",
172
+ "sort.created-desc": "ใหม่ล่าสุด",
173
+ "sort.featured": "แนะนำ",
174
+ "sort.price-asc": "ราคา ต่ำไปสูง",
175
+ "sort.price-desc": "ราคา สูงไปต่ำ",
176
+ "sort.relevance": "ความเกี่ยวข้อง",
177
+ "sort.title-asc": "ชื่อ ก–ฮ",
178
+ "sort.title-desc": "ชื่อ ฮ–ก"
179
+ }
180
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * `@tanqory/theme-kit/headless` — every storefront function, no styling. A theme supplies all markup and CSS;
3
+ * the kit supplies the data, the behaviour and accessible, semantic primitives carrying the platform's
4
+ * `tq-storefront-hooks/1` attributes (`data-tq-action`, `data-tq-price`, `data-tq-variant-picker`, …).
5
+ *
6
+ * declare defineTheme · defineSection · defineBlock · field · ai.*
7
+ * data useShop · useMenu · useProduct · useCollection · useSearch · usePredictiveSearch · useCart ·
8
+ * useCustomer · useLocalization · useRecommendations · useConsent · usePage
9
+ * commerce ProductForm · VariantPicker · ProductPrice · ProductQuantity · AddToCart · CartToggle · CartDrawer ·
10
+ * CartLine (+ Quantity / Increment / Decrement / Remove) · CheckoutButton · CollectionProvider ·
11
+ * FacetFilters · SortSelect · Pagination · SearchForm · LocalizationForm · NewsletterForm ·
12
+ * ContactForm · ConsentBanner · AccountSlot
13
+ * platform Seo · Image · SkipLink · useTranslate (t) · formatMoney · currencyDecimals · sx
14
+ *
15
+ * The server entry is `createRender({ manifest, bundle, storefront: {} })` (`@tanqory/theme-kit/server`); the
16
+ * browser entry is `hydrate({ bundle })` (`@tanqory/theme-kit/client`), which also applies live edits on the
17
+ * editor canvas. The manifest comes from `buildManifest` (`@tanqory/theme-kit/build`).
18
+ */
19
+ export { defineTheme, defineSection, defineBlock, type HeadlessThemeDefinition, type SectionDefinition, type SectionComponentProps, type SectionPresetDef, type BlockSchema, type BlockInstance, type HeadlessSection, type SettingsFor } from './theme.js';
20
+ export { ai, aiProfileOf, checkAiProfile, type AiRole, type AiProfile } from './ai.js';
21
+ export { field, defineSchema, resolveSettings, type Field, type Fields, type Schema } from '../sdk/schema.js';
22
+ export { defineSettings, type SettingSpec } from '../sdk/theme.js';
23
+ export { useSettings, useShop, useMenu, useProduct, useProductState, useCollection, useCollectionState, useSearch, usePredictiveSearch, useCart, useCustomer, useLocalization, useRecommendations, useConsent, usePageContent, useBlog, useArticle, usePolicy, initialVariant, optionStates, ProductContext, CollectionContext, COLLECTION_SORTS, SEARCH_SORT_IDS, type ProductState, type OptionValueState, type CollectionState, type CollectionSort, type SearchState, type PredictiveResults, type CartStateHeadless as CartState, type CustomerState, type LocalizationState, type CountryChoice, type LanguageChoice, type ConsentState, } from './hooks.js';
24
+ export { usePage, DETACHED_PAGE, type PageInfo } from './page.js';
25
+ export { ProductForm, VariantPicker, ProductPrice, ProductQuantity, AddToCart, useProductForm, type ProductFormState, type ProductFormProps, type VariantPickerProps, type ProductPriceProps, type ProductQuantityProps, type AddToCartProps } from './primitives/product.js';
26
+ export { CartToggle, CartDrawer, CartLine, CartLineQuantity, CartLineIncrement, CartLineDecrement, CartLineRemove, CheckoutButton, useCartLine, type CartLineState, type CartToggleProps, type CartDrawerProps, type CartLineProps } from './primitives/cart.js';
27
+ export { CollectionProvider, SortSelect, FacetFilters, Pagination, type CollectionProviderProps, type SortSelectProps, type FacetFiltersProps, type PaginationProps } from './primitives/collection.js';
28
+ export { SearchForm, LocalizationForm, NewsletterForm, ContactForm, ConsentBanner, AccountSlot, type SearchFormProps, type LocalizationFormProps, type PlatformFormProps, type PlatformFormState, type FormStatus, type ConsentBannerProps } from './primitives/storefront.js';
29
+ export { Seo, Image, SkipLink, resolveImageUrl, type SeoProps, type ImageProps } from './platform.js';
30
+ export { useTranslate, translate, interpolate, KIT_STRINGS, kitStrings, type Translate, type TranslateVars, type Strings } from './i18n.js';
31
+ export { formatMoney, currencyDecimals, currencySymbol, type MoneyLike, type FormatMoneyOptions } from './money.js';
32
+ export { productJsonLd, collectionJsonLd, breadcrumbJsonLd, type JsonLd, type Crumb } from './structured-data.js';
33
+ export { sx, cx as sxJoin } from '../sdk/sx.js';
34
+ export { visuallyHidden } from './primitives/util.js';
35
+ export type { Shop, Menu, MenuItem, Link, Image as ImageData, Video, Money, Product, ProductVariant, ProductOption, Collection, Facet, FacetValue, SortOption, Cart, CartLine as CartLineData, Article, Blog, Policy, Localization, Market, BreadcrumbItem, SearchResults } from '../sdk/types.js';
36
+ export type { ThemeBundle } from '../runtime/slots.js';
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Declaring a headless theme: its sections and blocks with their settings schema, and the theme itself.
3
+ *
4
+ * const hero = defineSection({
5
+ * type: 'hero', title: 'Hero', settings: { heading: field.text('After dark', { label: 'Heading' }) },
6
+ * presets: [{ name: 'Hero' }],
7
+ * ai: ai.hero({ heading: 'heading' }),
8
+ * component: ({ settings }) => <section><h1>{settings.heading}</h1></section>,
9
+ * })
10
+ * export default defineTheme({ name: 'Aurora', sections: [header, hero, footer], Layout })
11
+ *
12
+ * A section component receives `{ settings, blocks }`: every setting resolved against its schema (defaults filled,
13
+ * legacy values mapped, types coerced; a resource setting is its handle, for `useMenu` / `useCollection` /
14
+ * `useProduct` / `<Image>`), and its blocks in order, each with its own resolved settings. The section renderer
15
+ * wraps each section in its `data-tq-node` element, and the canvas applies live edits to these props.
16
+ */
17
+ import type { ComponentType, ReactNode } from 'react';
18
+ import { type Fields, type HandlesOf, type Schema, type SettingsOf } from '../sdk/schema.js';
19
+ import type { SectionZone, ThemeDefinition, ThemeSection } from '../sdk/theme.js';
20
+ import type { AiRole } from './ai.js';
21
+ /** A section's or block's settings as its component receives them. */
22
+ export type SettingsFor<F extends Fields> = SettingsOf<Schema<F>> & HandlesOf<Schema<F>>;
23
+ /** One block of a section, in the merchant's order. */
24
+ export interface BlockInstance {
25
+ type: string;
26
+ settings: Record<string, unknown>;
27
+ }
28
+ export interface SectionComponentProps<F extends Fields = Fields> {
29
+ settings: SettingsFor<F>;
30
+ blocks: BlockInstance[];
31
+ }
32
+ /** A block definition (`defineBlock`): a settings schema that nests in sections. */
33
+ export type BlockSchema<F extends Fields = Fields> = Schema<F> & {
34
+ readonly ai?: AiRole | readonly AiRole[];
35
+ };
36
+ /** Declare a block: its persisted type, its settings, and the sections it may be placed in. */
37
+ export declare function defineBlock<const F extends Fields>(def: {
38
+ type: string;
39
+ title: string;
40
+ description?: string;
41
+ settings: F;
42
+ parents?: readonly string[];
43
+ requires?: readonly string[];
44
+ }): BlockSchema<F>;
45
+ export interface SectionPresetDef {
46
+ name: string;
47
+ settings?: Record<string, unknown>;
48
+ blocks?: {
49
+ type: string;
50
+ settings?: Record<string, unknown>;
51
+ }[];
52
+ }
53
+ export interface SectionDefinition<F extends Fields> {
54
+ /** The persisted section type: never changes once stores have saved it. */
55
+ type: string;
56
+ title: string;
57
+ /** One line for the editor's "Add section" picker. */
58
+ description?: string;
59
+ /** The picker group. */
60
+ category?: string;
61
+ /** Where the page places it: the header zone, the page body (default), the footer zone. */
62
+ zone?: SectionZone;
63
+ settings: F;
64
+ /** The blocks this section accepts. */
65
+ blocks?: readonly BlockSchema[];
66
+ /** "Add section" starting points; `presets[0]` is what the editor previews. */
67
+ presets?: readonly SectionPresetDef[];
68
+ /** Route kinds the section needs (`product`, `collection`, `cart`, …): offered only on those templates. */
69
+ requires?: readonly string[];
70
+ /** The roles it plays for Tanqory AI (`ai.hero`, `ai.headerMenu`, …). */
71
+ ai?: AiRole | readonly AiRole[];
72
+ component: ComponentType<SectionComponentProps<F>>;
73
+ }
74
+ /** A section as the runtime and the build read it. */
75
+ export type HeadlessSection<F extends Fields = Fields> = ThemeSection<SectionComponentProps<F>> & {
76
+ readonly ai?: AiRole | readonly AiRole[];
77
+ readonly headless: true;
78
+ };
79
+ /** Declare a section of a headless theme. */
80
+ export declare function defineSection<const F extends Fields>(def: SectionDefinition<F>): HeadlessSection<F>;
81
+ export interface HeadlessThemeDefinition {
82
+ name: string;
83
+ /** Every section, in the editor picker's order. A section of type `not-found` renders the 404 body. */
84
+ sections: readonly HeadlessSection<any>[];
85
+ /** The page layout around the sections (the runtime renders header zone → `<main id="main">` → footer zone inside it); a place for `<CartDrawer>` and `<ConsentBanner>`. */
86
+ Layout?: ComponentType<{
87
+ children: ReactNode;
88
+ }>;
89
+ /** Wraps every page and the editor's single-section preview. */
90
+ Provider?: ThemeDefinition['Provider'];
91
+ /** Words for the breadcrumb trail; the kit's strings by default. */
92
+ labels?: ThemeDefinition['labels'];
93
+ /** External stylesheets every page loads (web fonts). */
94
+ stylesheets?: readonly string[];
95
+ /** English words the theme ships in defaults and presets, rendered in the page's language (`copy.*` locale keys). */
96
+ shippedCopy?: ThemeDefinition['shippedCopy'];
97
+ }
98
+ /** Declare a headless theme. */
99
+ export declare function defineTheme(def: HeadlessThemeDefinition): ThemeDefinition;