@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.
- 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 +221 -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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tanqory
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,95 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @tanqory/dev-mcp
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The local [Model Context Protocol](https://modelcontextprotocol.io) server for building Tanqory storefront themes
|
|
4
|
+
(Theme ABI v2, `@tanqory/theme-kit`) with a coding agent: Claude Code, Codex, VS Code / Copilot, Gemini CLI, Cursor.
|
|
5
|
+
|
|
6
|
+
The agent already has a shell, so the Tanqory CLI stays the engine. This server is the knowledge-and-eyes layer:
|
|
7
|
+
|
|
8
|
+
- docs and schemas that match the versions your theme installed;
|
|
9
|
+
- real kit examples;
|
|
10
|
+
- a two-step section scaffold;
|
|
11
|
+
- the CLI's checks as structured JSON;
|
|
12
|
+
- rendered pages and screenshots of your running dev server.
|
|
13
|
+
|
|
14
|
+
It runs on your machine over stdio. It never calls an AI model and never takes a store id: the store comes from
|
|
15
|
+
`tanqory.config.json` and your CLI login. It never publishes anything, and it writes files only inside the open
|
|
16
|
+
theme, only after you confirm a plan.
|
|
17
|
+
|
|
18
|
+
## Setup
|
|
19
|
+
|
|
20
|
+
A theme from `npm create tanqory-theme` is already configured. To configure one by hand, run the server from the
|
|
21
|
+
theme folder:
|
|
22
|
+
|
|
23
|
+
```jsonc
|
|
24
|
+
// .mcp.json (Claude Code)
|
|
25
|
+
{ "mcpServers": { "tanqory": { "command": "npx", "args": ["-y", "@tanqory/cli", "mcp"] } } }
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
```toml
|
|
29
|
+
# .codex/config.toml (Codex)
|
|
30
|
+
[mcp_servers.tanqory]
|
|
31
|
+
command = "npx"
|
|
32
|
+
args = ["-y", "@tanqory/cli", "mcp"]
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`tanqory mcp` (from `@tanqory/cli`) starts this server and passes itself as the CLI to call. To run the server on
|
|
36
|
+
its own, use `npx -y @tanqory/dev-mcp`. It then looks for the CLI in the theme's `node_modules/.bin`, then on
|
|
37
|
+
`PATH`. Node 20 or newer.
|
|
38
|
+
|
|
39
|
+
## Tools
|
|
40
|
+
|
|
41
|
+
Every tool returns text for the model plus `structuredContent` that matches its declared `outputSchema`. A failure
|
|
42
|
+
comes back as `isError: true` with `{ ok: false, error: { code, message, hint } }`. The `hint` always says what to do
|
|
43
|
+
next.
|
|
44
|
+
|
|
45
|
+
| Tool | What it does |
|
|
46
|
+
| --------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
47
|
+
| `search_docs {query, limit?}` | BM25 search, offline and deterministic. It covers the ABI v2 contract, the kit's headless API, the storefront-tier check spec, the field-type registry, the limits, the capabilities, the kit strings and the theme's own `AGENTS.md`. Returns ranked sections, each with a `path`. |
|
|
48
|
+
| `get_doc {path, offset?, maxChars?}` | One document, or one `doc#section`, with pagination. `path: "index"` lists every document. Each answer names its source: `installed` (your `node_modules`) or `bundled` (this package's copy). |
|
|
49
|
+
| `get_schema {kind}` | `manifest`, `section`, `block`, `field-registry`, `theme-settings`, `content-document`, `locale`, `ai-profile`, `storefront-report` or `conformance-report`. |
|
|
50
|
+
| `list_kit_components {kind?}` | Every kit hook, primitive, declaration helper and AI role, checked against the installed kit's exports. |
|
|
51
|
+
| `get_kit_example {name, limit?}` | The real code that uses one kit API, with file and line numbers. It looks in your theme's `src/` first, then in the Blank starter. |
|
|
52
|
+
| `scaffold_section {name, purpose, layout?, template?, titleTh?, confirm?, planDigest?}` | **Two steps.** The first call returns the full plan and a `planDigest`, and writes nothing. The second call (`confirm: true` + `planDigest`) writes exactly that plan, which covers: the section file, its registration in `src/theme.tsx`, en and th locale keys, CSS, a vitest stub and, optionally, starter content. If the theme changed in between, it refuses. |
|
|
53
|
+
| `validate_manifest` | Checks `dist/abi/manifest.json` with the theme's own `validateManifest` plus the JCS byte rule. Problems are traced to the `src/` file and line that declares them. |
|
|
54
|
+
| `check_theme {tier?}` | Runs `tanqory theme check --json`. The tiers are security (the 9 ABI suites), storefront and budgets. Each failure carries `tier`, `checkId`, `message` and `hint`, plus `file:line` when known. |
|
|
55
|
+
| `run_storefront_checks` | Runs `tanqory theme check --tier storefront --json`. |
|
|
56
|
+
| `dev_server_status` | Reads `.tanqory/dev-server.json` and `GET /__tq/status`, then reports the URL, the store, the mode and the last build's errors. It never starts the server for you. |
|
|
57
|
+
| `get_page {path, locale?, market?, viewport?, maxHtmlChars?}` | Gets the server-rendered HTML from `GET /__tq/page`. It also loads the page in headless Chromium and collects console errors. |
|
|
58
|
+
| `screenshot {path, viewport, locale?, market?, fullPage?}` | Returns a PNG, as an MCP image block, from local Playwright. |
|
|
59
|
+
| `list_dev_stores` | Runs `tanqory stores --json` and reports the store this theme is linked to. |
|
|
60
|
+
| `get_store_data_summary` | Calls `GET /__tq/store-summary` on the dev server: the products, collections, markets, languages and content you can design against. |
|
|
61
|
+
| `push_draft`, `get_build_status`, `get_preview_url` | **Always refuse today** with `draft_push_unavailable`. The platform's build lane for developer themes is not live yet. These tools never upload, never install, never publish and never invent a URL. |
|
|
62
|
+
|
|
63
|
+
**Prompts:** `new-theme`, `new-section`, `fix-check-report` and `port-from-another-template-language`.
|
|
64
|
+
|
|
65
|
+
**Resources:** every document, as `tanqory://docs/<id>` (for example `tanqory://docs/abi/contract`).
|
|
66
|
+
|
|
67
|
+
## Environment
|
|
68
|
+
|
|
69
|
+
| Variable | Effect |
|
|
70
|
+
| ----------------------------------- | ---------------------------------------------------------------------------------------------------- |
|
|
71
|
+
| `TANQORY_THEME_DIR` | The theme root, when the server is not started inside the theme |
|
|
72
|
+
| `TANQORY_CLI` | The CLI to call: a path to its binary or `.mjs` file, or a JSON array `["cmd", "arg"]` |
|
|
73
|
+
| `TANQORY_CHROMIUM_PATH` | Use this Chrome or Chromium instead of Playwright's |
|
|
74
|
+
| `TANQORY_MCP_NO_BROWSER_DOWNLOAD=1` | Never download Chromium. By default it is downloaded once, on the first screenshot, if it is missing |
|
|
75
|
+
|
|
76
|
+
## Contract with the CLI
|
|
77
|
+
|
|
78
|
+
`tanqory <cmd> --json` prints exactly one object on stdout:
|
|
79
|
+
`{ ok, command, data?, error?: { code, message, hint? } }`.
|
|
80
|
+
|
|
81
|
+
`theme check` puts `{ tiers, failures: [{ tier, checkId, route?, locale?, message, hint, file?, line? }] }` in
|
|
82
|
+
`data`.
|
|
83
|
+
|
|
84
|
+
`tanqory theme dev` writes `.tanqory/dev-server.json` (`{ pid, url, port, store, mode, startedAt }`) and serves
|
|
85
|
+
three routes:
|
|
86
|
+
|
|
87
|
+
- `/__tq/status`
|
|
88
|
+
- `/__tq/page?path=&locale=&market=`
|
|
89
|
+
- `/__tq/store-summary`
|
|
90
|
+
|
|
91
|
+
Only loopback URLs are used.
|
|
92
|
+
|
|
93
|
+
## License
|
|
94
|
+
|
|
95
|
+
MIT
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// The Tanqory theme-development MCP server on stdio. Configure your agent host to run `npx -y @tanqory/dev-mcp`
|
|
3
|
+
// (or `npx -y @tanqory/cli mcp`) in the theme folder.
|
|
4
|
+
import { main } from '../src/index.mjs'
|
|
5
|
+
|
|
6
|
+
if (process.argv.includes('--version')) {
|
|
7
|
+
const { VERSION } = await import('../src/server.mjs')
|
|
8
|
+
process.stdout.write(`${VERSION}\n`)
|
|
9
|
+
} else if (process.argv.includes('--help') || process.argv.includes('-h')) {
|
|
10
|
+
process.stdout.write('tanqory-dev-mcp: the Tanqory theme-development MCP server (stdio). Run it from an MCP host in the theme folder.\n')
|
|
11
|
+
} else {
|
|
12
|
+
main().catch((e) => {
|
|
13
|
+
process.stderr.write(`[tanqory-mcp] failed to start: ${e?.stack ?? e}\n`)
|
|
14
|
+
process.exit(1)
|
|
15
|
+
})
|
|
16
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"theme.nav.main": "Main menu",
|
|
3
|
+
"theme.footer.menu": "More",
|
|
4
|
+
"theme.footer.rights": "© {name}. All rights reserved.",
|
|
5
|
+
"theme.price.from": "From {price}",
|
|
6
|
+
"theme.shelf.viewAll": "View all",
|
|
7
|
+
"theme.shelf.empty": "No products in this collection yet.",
|
|
8
|
+
"theme.product.missing": "This product is not available.",
|
|
9
|
+
"theme.collection.kicker": "Collection",
|
|
10
|
+
"theme.collection.none": "This collection is empty for now.",
|
|
11
|
+
"theme.cart.continue": "Continue shopping",
|
|
12
|
+
"theme.cart.view": "View cart",
|
|
13
|
+
"theme.cart.taxes": "Taxes and shipping are calculated at checkout.",
|
|
14
|
+
"theme.drawer.qty": "Qty {count}",
|
|
15
|
+
"theme.journal.read": "Read more",
|
|
16
|
+
"theme.journal.back": "Back to {blog}",
|
|
17
|
+
"theme.journal.by": "By {author}",
|
|
18
|
+
"theme.notFound.title": "Page not found",
|
|
19
|
+
"theme.notFound.body": "The link may be old, or the page has moved.",
|
|
20
|
+
"theme.notFound.home": "Back to the home page",
|
|
21
|
+
"seo.homeDescription": "{name}: shop online."
|
|
22
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"theme.nav.main": "เมนูหลัก",
|
|
3
|
+
"theme.footer.menu": "เพิ่มเติม",
|
|
4
|
+
"theme.footer.rights": "© {name} สงวนลิขสิทธิ์",
|
|
5
|
+
"theme.price.from": "เริ่มต้น {price}",
|
|
6
|
+
"theme.shelf.viewAll": "ดูทั้งหมด",
|
|
7
|
+
"theme.shelf.empty": "ยังไม่มีสินค้าในคอลเลกชันนี้",
|
|
8
|
+
"theme.product.missing": "สินค้านี้ไม่มีจำหน่าย",
|
|
9
|
+
"theme.collection.kicker": "คอลเลกชัน",
|
|
10
|
+
"theme.collection.none": "คอลเลกชันนี้ยังไม่มีสินค้า",
|
|
11
|
+
"theme.cart.continue": "เลือกซื้อสินค้าต่อ",
|
|
12
|
+
"theme.cart.view": "ดูตะกร้า",
|
|
13
|
+
"theme.cart.taxes": "ภาษีและค่าจัดส่งคำนวณตอนชำระเงิน",
|
|
14
|
+
"theme.drawer.qty": "จำนวน {count}",
|
|
15
|
+
"theme.journal.read": "อ่านต่อ",
|
|
16
|
+
"theme.journal.back": "กลับไปที่ {blog}",
|
|
17
|
+
"theme.journal.by": "โดย {author}",
|
|
18
|
+
"theme.notFound.title": "ไม่พบหน้าที่คุณกำลังหา",
|
|
19
|
+
"theme.notFound.body": "ลิงก์อาจเก่าแล้ว หรือหน้านี้ถูกย้ายไป",
|
|
20
|
+
"theme.notFound.home": "กลับหน้าแรก",
|
|
21
|
+
"seo.homeDescription": "{name}: ช้อปออนไลน์"
|
|
22
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"contentVersion": 1,
|
|
3
|
+
"region": "footer",
|
|
4
|
+
"sections": [
|
|
5
|
+
{
|
|
6
|
+
"id": "footer",
|
|
7
|
+
"type": "footer",
|
|
8
|
+
"blocks": [
|
|
9
|
+
{
|
|
10
|
+
"id": "footer-brand",
|
|
11
|
+
"type": "footer-brand",
|
|
12
|
+
"settings": {
|
|
13
|
+
"tagline": "Made in small batches."
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"id": "footer-menu",
|
|
18
|
+
"type": "footer-menu",
|
|
19
|
+
"settings": {
|
|
20
|
+
"heading": "Explore",
|
|
21
|
+
"menu": "main-menu"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"id": "footer-menu-2",
|
|
26
|
+
"type": "footer-menu",
|
|
27
|
+
"settings": {
|
|
28
|
+
"heading": "Help",
|
|
29
|
+
"menu": "footer"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** What the kit runtime is handed: the theme and its strings (store-independent, module scope). */
|
|
2
|
+
import type { ThemeBundle } from '@tanqory/theme-kit/headless'
|
|
3
|
+
import theme from './theme'
|
|
4
|
+
|
|
5
|
+
export const bundle: ThemeBundle = {
|
|
6
|
+
theme,
|
|
7
|
+
templates: {},
|
|
8
|
+
groups: {},
|
|
9
|
+
// The build lays locales/*.json over the kit's own strings into generated/locales.
|
|
10
|
+
locales: import.meta.glob('./generated/locales/*.json', { eager: true }),
|
|
11
|
+
settings: {},
|
|
12
|
+
settingsSchema: {},
|
|
13
|
+
customSections: {},
|
|
14
|
+
manifest: null,
|
|
15
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** Small shared parts: the product card and the product grid. */
|
|
2
|
+
import { Image, formatMoney, useTranslate, type Product } from '@tanqory/theme-kit/headless'
|
|
3
|
+
|
|
4
|
+
export const cx = (...names: Array<string | false | null | undefined>): string => names.filter(Boolean).join(' ')
|
|
5
|
+
|
|
6
|
+
export function ProductCard({ product, eager = false }: { product: Product; eager?: boolean }): JSX.Element {
|
|
7
|
+
const t = useTranslate()
|
|
8
|
+
const min = product.priceRange.min
|
|
9
|
+
const ranged = product.priceRange.max.amount > min.amount
|
|
10
|
+
return (
|
|
11
|
+
<li className="t-card">
|
|
12
|
+
<a className="t-card__link" href={product.url}>
|
|
13
|
+
<span className="t-card__frame">
|
|
14
|
+
<Image image={product.featuredImage} alt="" className="t-card__img" loading={eager ? 'eager' : 'lazy'} sizes="(max-width: 720px) 50vw, 25vw" fallback={<span className="t-card__img t-card__img--empty" />} />
|
|
15
|
+
</span>
|
|
16
|
+
<span className="t-card__title">{product.title}</span>
|
|
17
|
+
<span className="t-card__price">
|
|
18
|
+
{ranged ? t('theme.price.from', { price: formatMoney(min) }) : formatMoney(min)}
|
|
19
|
+
{product.available ? null : <span className="t-card__flag"> · {t('product.soldOut')}</span>}
|
|
20
|
+
</span>
|
|
21
|
+
</a>
|
|
22
|
+
</li>
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function Grid({ products, busy, eager = 0 }: { products: readonly Product[]; busy?: boolean; eager?: number }): JSX.Element {
|
|
27
|
+
return (
|
|
28
|
+
<ul className="t-grid" aria-busy={busy || undefined}>
|
|
29
|
+
{products.map((p, i) => <ProductCard key={p.id} product={p} eager={i < eager} />)}
|
|
30
|
+
</ul>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The serve entry (Theme ABI v2 §1): `render(request, ctx)` + `manifest`, built into one file with no imports.
|
|
3
|
+
* The render is the kit's; `storefront` turns on what the headless API needs, and `plan.localization` lists the
|
|
4
|
+
* store's countries for the header's market selector.
|
|
5
|
+
*/
|
|
6
|
+
import type { ThemeManifest } from '@tanqory/theme-abi'
|
|
7
|
+
import { createRender } from '@tanqory/theme-kit/server'
|
|
8
|
+
import { bundle } from './bundle'
|
|
9
|
+
import manifestJson from './generated/manifest.json'
|
|
10
|
+
|
|
11
|
+
export const manifest = manifestJson as unknown as ThemeManifest
|
|
12
|
+
|
|
13
|
+
export const render = createRender({ manifest, bundle, storefront: { plan: { localization: true, recommendations: true } } })
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/** The header and the footer, shared by every page. */
|
|
2
|
+
import { useId, useState } from 'react'
|
|
3
|
+
import {
|
|
4
|
+
CartToggle,
|
|
5
|
+
Image,
|
|
6
|
+
LocalizationForm,
|
|
7
|
+
SearchForm,
|
|
8
|
+
ai,
|
|
9
|
+
defineBlock,
|
|
10
|
+
defineSection,
|
|
11
|
+
field,
|
|
12
|
+
useMenu,
|
|
13
|
+
useShop,
|
|
14
|
+
useTranslate,
|
|
15
|
+
type BlockInstance,
|
|
16
|
+
} from '@tanqory/theme-kit/headless'
|
|
17
|
+
import { cx } from '../components/parts'
|
|
18
|
+
|
|
19
|
+
function Masthead({ settings }: { settings: { menu: string; tagline: string } }): JSX.Element {
|
|
20
|
+
const shop = useShop()
|
|
21
|
+
const menu = useMenu(settings.menu)
|
|
22
|
+
const t = useTranslate()
|
|
23
|
+
const navId = useId()
|
|
24
|
+
const [open, setOpen] = useState(false)
|
|
25
|
+
return (
|
|
26
|
+
<header className="t-mast">
|
|
27
|
+
<a className="t-mast__brand" href="/">
|
|
28
|
+
{shop.logo ? <Image image={shop.logo} alt={shop.name} className="t-mast__logo" loading="eager" /> : shop.name}
|
|
29
|
+
</a>
|
|
30
|
+
{settings.tagline ? <p className="t-mast__tagline">{settings.tagline}</p> : null}
|
|
31
|
+
<button type="button" className="t-mast__menu" data-tq-action="menu:open" aria-expanded={open} aria-controls={navId} onClick={() => setOpen((o) => !o)}>
|
|
32
|
+
{t(open ? 'menu.closeMenu' : 'menu.menu')}
|
|
33
|
+
</button>
|
|
34
|
+
<nav id={navId} className={cx('t-mast__nav', open && 'is-open')} aria-label={t('theme.nav.main')}>
|
|
35
|
+
<ul>
|
|
36
|
+
{(menu?.items ?? []).map((item) => (
|
|
37
|
+
<li key={`${item.title}-${item.url}`}>
|
|
38
|
+
<a href={item.url ?? '/'} aria-current={item.active ? 'page' : undefined}>
|
|
39
|
+
{item.title}
|
|
40
|
+
</a>
|
|
41
|
+
</li>
|
|
42
|
+
))}
|
|
43
|
+
</ul>
|
|
44
|
+
</nav>
|
|
45
|
+
<div className="t-mast__tools">
|
|
46
|
+
<SearchForm className="t-find" inputClassName="t-find__input" submit={false} resultsClassName="t-find__results" limit={5} />
|
|
47
|
+
<LocalizationForm className="t-locale" label="hidden" selectClassName="t-locale__select" />
|
|
48
|
+
<CartToggle className="t-mast__cart" />
|
|
49
|
+
</div>
|
|
50
|
+
</header>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const header = defineSection({
|
|
55
|
+
type: 'header',
|
|
56
|
+
title: 'Header',
|
|
57
|
+
description: 'The shop name, the main menu, search, region and the cart.',
|
|
58
|
+
category: 'chrome',
|
|
59
|
+
zone: 'header',
|
|
60
|
+
settings: {
|
|
61
|
+
menu: field.resource('menu', 'main-menu', { label: 'Menu' }),
|
|
62
|
+
tagline: field.text('', { label: 'Tagline' }),
|
|
63
|
+
// The data plan's convention: the header shows the market selector.
|
|
64
|
+
showLocale: field.boolean(true, { label: 'Show country and language' }),
|
|
65
|
+
},
|
|
66
|
+
ai: ai.headerMenu('menu'),
|
|
67
|
+
component: Masthead,
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
const footerMenu = defineBlock({
|
|
71
|
+
type: 'footer-menu',
|
|
72
|
+
title: 'Menu column',
|
|
73
|
+
settings: { heading: field.text('', { label: 'Heading' }), menu: field.resource('menu', 'footer', { label: 'Menu' }) },
|
|
74
|
+
})
|
|
75
|
+
const footerBrand = defineBlock({
|
|
76
|
+
type: 'footer-brand',
|
|
77
|
+
title: 'Brand',
|
|
78
|
+
settings: { tagline: field.textarea('', { label: 'Tagline' }) },
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
function MenuColumn({ block }: { block: BlockInstance }): JSX.Element | null {
|
|
82
|
+
const menu = useMenu(String(block.settings.menu ?? ''))
|
|
83
|
+
const t = useTranslate()
|
|
84
|
+
if (!menu) return null
|
|
85
|
+
const heading = String(block.settings.heading ?? '') || menu.title
|
|
86
|
+
return (
|
|
87
|
+
<nav className="t-foot__col" aria-label={heading || t('theme.footer.menu')}>
|
|
88
|
+
{heading ? <h2 className="t-foot__heading">{heading}</h2> : null}
|
|
89
|
+
<ul>
|
|
90
|
+
{menu.items.map((i) => (
|
|
91
|
+
<li key={`${i.title}-${i.url}`}>
|
|
92
|
+
<a href={i.url ?? '/'}>{i.title}</a>
|
|
93
|
+
</li>
|
|
94
|
+
))}
|
|
95
|
+
</ul>
|
|
96
|
+
</nav>
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function Colophon({ blocks }: { blocks: BlockInstance[] }): JSX.Element {
|
|
101
|
+
const shop = useShop()
|
|
102
|
+
const t = useTranslate()
|
|
103
|
+
return (
|
|
104
|
+
<footer className="t-foot">
|
|
105
|
+
<div className="t-foot__cols">
|
|
106
|
+
{blocks.map((b, i) =>
|
|
107
|
+
b.type === 'footer-brand' ? (
|
|
108
|
+
<div key={i} className="t-foot__brand">
|
|
109
|
+
<p className="t-foot__name">{shop.name}</p>
|
|
110
|
+
{b.settings.tagline ? <p className="t-foot__tagline">{String(b.settings.tagline)}</p> : null}
|
|
111
|
+
</div>
|
|
112
|
+
) : (
|
|
113
|
+
<MenuColumn key={i} block={b} />
|
|
114
|
+
),
|
|
115
|
+
)}
|
|
116
|
+
</div>
|
|
117
|
+
<p className="t-foot__legal">{t('theme.footer.rights', { name: shop.name })}</p>
|
|
118
|
+
</footer>
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export const footer = defineSection({
|
|
123
|
+
type: 'footer',
|
|
124
|
+
title: 'Footer',
|
|
125
|
+
description: 'The shop name, menu columns and the legal line.',
|
|
126
|
+
category: 'chrome',
|
|
127
|
+
zone: 'footer',
|
|
128
|
+
settings: {},
|
|
129
|
+
blocks: [footerBrand, footerMenu],
|
|
130
|
+
ai: [ai.footerMenu('menu', { block: 'footer-menu' }), ai.footerBrand({ block: 'footer-brand' })],
|
|
131
|
+
component: Colophon,
|
|
132
|
+
})
|