@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
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/* Blank: the minimum CSS that keeps the storefront usable (layout, focus, touch targets, the drawer and the
|
|
2
|
+
menu on small screens). No brand, no palette, no type scale: this file is yours to replace.
|
|
3
|
+
Keep these three properties when you restyle, the storefront checks rely on them:
|
|
4
|
+
- every standalone link and button is at least 44 × 44 px (touch targets);
|
|
5
|
+
- focus is visible (:focus-visible outline);
|
|
6
|
+
- text is never changed with text-transform (the editor's live edit compares what the canvas shows with what
|
|
7
|
+
the merchant typed; use font-variant-caps for small capitals). */
|
|
8
|
+
:root {
|
|
9
|
+
--t-ink: #111;
|
|
10
|
+
--t-bg: #fff;
|
|
11
|
+
--t-muted: #555;
|
|
12
|
+
--t-line: #ddd;
|
|
13
|
+
--t-panel: #f6f6f6;
|
|
14
|
+
--t-gutter: clamp(16px, 4vw, 32px);
|
|
15
|
+
}
|
|
16
|
+
* { box-sizing: border-box; }
|
|
17
|
+
html, body { margin: 0; }
|
|
18
|
+
body { color: var(--t-ink); background: var(--t-bg); font-family: system-ui, -apple-system, 'Segoe UI', 'Noto Sans Thai', sans-serif; line-height: 1.5; }
|
|
19
|
+
.t { min-height: 100vh; }
|
|
20
|
+
.t--wide .t-pdp, .t--wide .t-plp { max-width: none; }
|
|
21
|
+
.tq-node { display: contents; }
|
|
22
|
+
.t a { color: inherit; }
|
|
23
|
+
.t :focus-visible { outline: 2px solid currentColor; outline-offset: 2px; }
|
|
24
|
+
.t h1, .t h2, .t h3 { line-height: 1.2; margin: 0; }
|
|
25
|
+
.t-muted { color: var(--t-muted); }
|
|
26
|
+
.t-skip { position: absolute; left: -9999px; top: 8px; z-index: 50; padding: 12px 16px; background: var(--t-ink); color: var(--t-bg) !important; }
|
|
27
|
+
.t-skip:focus { left: 8px; }
|
|
28
|
+
main#main:focus { outline: none; }
|
|
29
|
+
|
|
30
|
+
/* buttons, inputs */
|
|
31
|
+
.t-btn { display: inline-flex; align-items: center; justify-content: center; gap: 8px; min-height: 44px; padding: 10px 20px; border: 1px solid var(--t-ink); background: transparent; color: var(--t-ink); font: inherit; text-decoration: none; cursor: pointer; }
|
|
32
|
+
.t-btn--solid, .t a.t-btn--solid { background: var(--t-ink); color: var(--t-bg); }
|
|
33
|
+
.t-btn[disabled], .t-btn[aria-disabled='true'] { opacity: 0.45; cursor: not-allowed; }
|
|
34
|
+
.t-link { min-height: 44px; min-width: 44px; padding: 10px 4px; background: none; border: 0; color: inherit; font: inherit; text-decoration: underline; cursor: pointer; }
|
|
35
|
+
.t-input { min-height: 44px; padding: 8px 12px; border: 1px solid var(--t-line); background: var(--t-bg); color: var(--t-ink); font: inherit; }
|
|
36
|
+
.t-input--qty { width: 72px; text-align: center; }
|
|
37
|
+
.t-label { display: block; margin-bottom: 4px; }
|
|
38
|
+
.t select { min-height: 44px; padding: 8px 12px; border: 1px solid var(--t-line); background: var(--t-bg); color: var(--t-ink); font: inherit; }
|
|
39
|
+
|
|
40
|
+
/* header */
|
|
41
|
+
.t-mast { position: relative; display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 12px 24px; padding: 12px var(--t-gutter); border-bottom: 1px solid var(--t-line); }
|
|
42
|
+
.t-mast__brand { display: inline-flex; align-items: center; min-height: 44px; min-width: 44px; font-weight: 600; text-decoration: none; }
|
|
43
|
+
.t-mast__logo { max-height: 40px; width: auto; height: auto; }
|
|
44
|
+
.t-mast__tagline { display: none; }
|
|
45
|
+
.t-mast__menu { display: none; }
|
|
46
|
+
.t-mast__nav ul { display: flex; flex-wrap: wrap; gap: 4px 16px; list-style: none; margin: 0; padding: 0; }
|
|
47
|
+
.t .t-mast__nav a { display: inline-flex; align-items: center; min-height: 44px; min-width: 44px; text-decoration: none; }
|
|
48
|
+
.t-mast__nav a[aria-current='page'] { text-decoration: underline; }
|
|
49
|
+
.t-mast__tools { display: flex; align-items: center; gap: 12px; }
|
|
50
|
+
.t-mast__cart { display: inline-flex; align-items: center; min-height: 44px; min-width: 44px; padding: 0 4px; text-decoration: none; }
|
|
51
|
+
.t-find { position: relative; }
|
|
52
|
+
.t-find__input { width: 180px; min-height: 44px; padding: 8px; border: 1px solid var(--t-line); background: var(--t-bg); color: var(--t-ink); font: inherit; }
|
|
53
|
+
.t-find__results:not(:empty) { position: absolute; z-index: 30; top: 100%; right: 0; width: min(320px, 90vw); margin-top: 4px; padding: 4px 0; background: var(--t-bg); border: 1px solid var(--t-line); }
|
|
54
|
+
.t-find__results ul { list-style: none; margin: 0; padding: 0; }
|
|
55
|
+
.t .t-find__results a { display: flex; justify-content: space-between; gap: 12px; min-height: 44px; align-items: center; padding: 8px 12px; text-decoration: none; }
|
|
56
|
+
.t-find--page .t-find__input { width: min(100%, 480px); }
|
|
57
|
+
.t-locale { display: flex; gap: 8px; }
|
|
58
|
+
.t-locale__select { max-width: 220px; }
|
|
59
|
+
|
|
60
|
+
/* sections */
|
|
61
|
+
.t-hero { position: relative; display: grid; place-items: center; min-height: 320px; padding: 64px var(--t-gutter); text-align: center; overflow: hidden; background: var(--t-panel); }
|
|
62
|
+
.t-hero__img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; opacity: 0.5; }
|
|
63
|
+
.t-hero__copy { position: relative; max-width: 760px; }
|
|
64
|
+
.t-hero__kicker { margin: 0 0 12px; }
|
|
65
|
+
.t-hero__body { margin: 16px auto 0; max-width: 560px; }
|
|
66
|
+
.t-hero .t-btn { margin-top: 24px; }
|
|
67
|
+
.t-shelf, .t-note, .t-letters, .t-results, .t-cart, .t-page, .t-journal, .t-story, .t-lost, .t-account, .t-contact { padding: 40px var(--t-gutter); }
|
|
68
|
+
.t-shelf--empty { padding: 0; }
|
|
69
|
+
.t-shelf__head { display: flex; align-items: baseline; justify-content: space-between; gap: 16px; margin-bottom: 20px; }
|
|
70
|
+
.t-shelf__all { display: inline-flex; align-items: center; min-height: 44px; min-width: 44px; }
|
|
71
|
+
.t-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 32px 20px; list-style: none; margin: 0; padding: 0; }
|
|
72
|
+
.t-grid[aria-busy='true'] { opacity: 0.55; }
|
|
73
|
+
.t .t-card__link { display: flex; flex-direction: column; gap: 6px; min-height: 44px; text-decoration: none; }
|
|
74
|
+
.t-card__frame { display: block; aspect-ratio: 3 / 4; overflow: hidden; background: var(--t-panel); }
|
|
75
|
+
.t-card__img { display: block; width: 100%; height: 100%; object-fit: cover; }
|
|
76
|
+
.t-card__price { color: var(--t-muted); }
|
|
77
|
+
.t-note { max-width: 720px; margin: 0 auto; }
|
|
78
|
+
.t-letters { display: grid; gap: 12px; border-top: 1px solid var(--t-line); }
|
|
79
|
+
.t-letters__body { margin: 0; }
|
|
80
|
+
.t-letters__form { display: flex; flex-wrap: wrap; gap: 8px; max-width: 520px; }
|
|
81
|
+
.t-letters__form .t-input { flex: 1 1 220px; }
|
|
82
|
+
.t-letters__msg, .t-form__msg { flex-basis: 100%; margin: 0; }
|
|
83
|
+
|
|
84
|
+
/* product */
|
|
85
|
+
.t-pdp { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 40px; max-width: 1200px; padding: 32px var(--t-gutter); }
|
|
86
|
+
.t-pdp__img { display: block; width: 100%; height: auto; aspect-ratio: 4 / 5; object-fit: cover; background: var(--t-panel); }
|
|
87
|
+
.t-pdp__info { display: flex; flex-direction: column; gap: 16px; }
|
|
88
|
+
.t-pdp__vendor { margin: 0; color: var(--t-muted); }
|
|
89
|
+
.t-pdp__was { margin-right: 8px; color: var(--t-muted); }
|
|
90
|
+
.t-pdp__options { display: grid; gap: 12px; }
|
|
91
|
+
.t-opt { margin: 0; padding: 0; border: 0; }
|
|
92
|
+
.t-opt__legend { margin-bottom: 8px; padding: 0; }
|
|
93
|
+
.t-chip { position: relative; display: inline-flex; align-items: center; justify-content: center; min-width: 44px; min-height: 44px; margin: 0 8px 8px 0; padding: 8px 14px; border: 1px solid var(--t-line); cursor: pointer; }
|
|
94
|
+
.t-chip input { position: absolute; inset: 0; margin: 0; opacity: 0; cursor: pointer; }
|
|
95
|
+
.t-chip[data-selected='true'] { border-color: var(--t-ink); font-weight: 600; }
|
|
96
|
+
.t-chip[data-available='false'] span { text-decoration: line-through; }
|
|
97
|
+
.t-chip:has(input:focus-visible) { outline: 2px solid currentColor; outline-offset: 2px; }
|
|
98
|
+
.t-pdp__stock { margin: 0; color: var(--t-muted); }
|
|
99
|
+
.t-pdp__buy { display: flex; gap: 8px; }
|
|
100
|
+
.t-pdp__add { flex: 1; }
|
|
101
|
+
.t-pdp__desc { border-top: 1px solid var(--t-line); padding-top: 16px; }
|
|
102
|
+
|
|
103
|
+
/* collection */
|
|
104
|
+
.t-plp { max-width: 1400px; padding: 32px var(--t-gutter); }
|
|
105
|
+
.t-plp__head { margin-bottom: 24px; }
|
|
106
|
+
.t-plp__kicker { margin: 0 0 4px; color: var(--t-muted); }
|
|
107
|
+
.t-plp__count { color: var(--t-muted); }
|
|
108
|
+
.t-plp__body { display: grid; grid-template-columns: 220px minmax(0, 1fr); gap: 32px; }
|
|
109
|
+
.t-plp__filters { display: grid; align-content: start; gap: 20px; }
|
|
110
|
+
.t-facets { display: grid; gap: 16px; }
|
|
111
|
+
.t-facet { margin: 0; padding: 0; border: 0; }
|
|
112
|
+
.t-facet legend { margin-bottom: 8px; padding: 0; }
|
|
113
|
+
.t-check { display: flex; align-items: center; gap: 8px; min-height: 44px; cursor: pointer; }
|
|
114
|
+
.t-check input { width: 20px; height: 20px; }
|
|
115
|
+
.t-check[data-active='true'] { font-weight: 600; }
|
|
116
|
+
.t-facet input[type='number'] { width: 100%; min-height: 44px; margin: 4px 0 8px; padding: 6px 10px; border: 1px solid var(--t-line); font: inherit; }
|
|
117
|
+
.t-pages { display: flex; justify-content: center; gap: 8px; margin-top: 32px; }
|
|
118
|
+
|
|
119
|
+
/* search, cart */
|
|
120
|
+
.t-results .t-find { display: flex; gap: 8px; margin: 16px 0; }
|
|
121
|
+
.t-cart__empty { display: grid; justify-items: start; gap: 12px; }
|
|
122
|
+
.t-lines { list-style: none; margin: 24px 0 0; padding: 0; border-top: 1px solid var(--t-line); }
|
|
123
|
+
.t-line { display: grid; grid-template-columns: 80px minmax(0, 1fr) auto auto auto; align-items: center; gap: 16px; padding: 16px 0; border-bottom: 1px solid var(--t-line); }
|
|
124
|
+
.t-line[data-busy='true'] { opacity: 0.6; }
|
|
125
|
+
.t-line__img { display: block; width: 80px; height: auto; aspect-ratio: 3 / 4; object-fit: cover; background: var(--t-panel); }
|
|
126
|
+
.t-line__info p { margin: 4px 0 0; }
|
|
127
|
+
.t .t-line__title { display: inline-flex; align-items: center; min-height: 44px; min-width: 44px; text-decoration: none; }
|
|
128
|
+
.t-line__qty { display: flex; align-items: center; gap: 4px; }
|
|
129
|
+
.t-step { min-width: 44px; min-height: 44px; border: 1px solid var(--t-line); background: var(--t-bg); color: var(--t-ink); font: inherit; cursor: pointer; }
|
|
130
|
+
.t-line__total { margin: 0; min-width: 88px; text-align: right; }
|
|
131
|
+
.t-cart__summary { display: grid; justify-items: end; gap: 8px; margin-top: 20px; }
|
|
132
|
+
.t-cart__row { display: flex; gap: 24px; margin: 0; }
|
|
133
|
+
.t-cart__checkout { min-width: 240px; }
|
|
134
|
+
|
|
135
|
+
/* cart drawer */
|
|
136
|
+
.t-drawer__backdrop { position: fixed; inset: 0; z-index: 40; background: rgb(0 0 0 / 0.4); }
|
|
137
|
+
.t-drawer { position: fixed; z-index: 41; top: 0; right: 0; bottom: 0; display: flex; flex-direction: column; gap: 12px; width: min(400px, 100vw); padding: 20px; overflow-y: auto; background: var(--t-bg); border-left: 1px solid var(--t-line); }
|
|
138
|
+
.t-drawer:focus { outline: none; }
|
|
139
|
+
.t-drawer__head { display: flex; align-items: center; justify-content: space-between; }
|
|
140
|
+
.t-drawer__close { min-height: 44px; min-width: 44px; padding: 0 8px; background: none; border: 0; color: inherit; font: inherit; text-decoration: underline; cursor: pointer; }
|
|
141
|
+
.t-drawer__lines { list-style: none; margin: 0; padding: 0; }
|
|
142
|
+
.t-drawer__line { display: grid; grid-template-columns: 64px 1fr auto; gap: 12px; align-items: start; padding: 12px 0; border-bottom: 1px solid var(--t-line); }
|
|
143
|
+
.t-drawer__line p { margin: 0; }
|
|
144
|
+
.t-drawer__img { display: block; width: 64px; height: auto; aspect-ratio: 3 / 4; object-fit: cover; background: var(--t-panel); }
|
|
145
|
+
.t-drawer__total { display: flex; justify-content: space-between; margin: 8px 0; }
|
|
146
|
+
|
|
147
|
+
/* content */
|
|
148
|
+
.t-page, .t-story { max-width: 720px; margin: 0 auto; }
|
|
149
|
+
.t-page__title, .t-story__title, .t-journal__title, .t-lost__title, .t-account__title { margin-bottom: 20px; }
|
|
150
|
+
.t-story__back { display: inline-flex; align-items: center; min-height: 44px; min-width: 44px; margin-bottom: 12px; }
|
|
151
|
+
.t-journal__list { display: grid; list-style: none; margin: 0; padding: 0; border-top: 1px solid var(--t-line); }
|
|
152
|
+
.t .t-journal__link { display: grid; gap: 4px; padding: 16px 0; border-bottom: 1px solid var(--t-line); text-decoration: none; }
|
|
153
|
+
.t-journal__excerpt, .t-journal__more { color: var(--t-muted); }
|
|
154
|
+
.t-lost { display: grid; justify-items: center; gap: 12px; min-height: 50vh; align-content: center; text-align: center; }
|
|
155
|
+
.t-account__frame { display: block; width: 100%; min-height: 640px; border: 1px solid var(--t-line); }
|
|
156
|
+
.t-form { display: grid; gap: 8px; max-width: 520px; }
|
|
157
|
+
.t-form .t-input { width: 100%; margin-bottom: 8px; }
|
|
158
|
+
.t-form textarea.t-input { min-height: 120px; }
|
|
159
|
+
|
|
160
|
+
/* footer */
|
|
161
|
+
.t-foot { padding: 40px var(--t-gutter) 24px; border-top: 1px solid var(--t-line); }
|
|
162
|
+
.t-foot__cols { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 24px; }
|
|
163
|
+
.t-foot__name { margin: 0 0 8px; font-weight: 600; }
|
|
164
|
+
.t-foot__tagline { margin: 0; max-width: 320px; color: var(--t-muted); }
|
|
165
|
+
.t-foot__heading { margin-bottom: 8px; font-size: 1rem; }
|
|
166
|
+
.t-foot ul { list-style: none; margin: 0; padding: 0; }
|
|
167
|
+
.t .t-foot li a { display: inline-flex; align-items: center; min-height: 44px; min-width: 44px; text-decoration: none; }
|
|
168
|
+
.t-foot__legal { margin: 32px 0 0; color: var(--t-muted); }
|
|
169
|
+
|
|
170
|
+
/* consent banner */
|
|
171
|
+
.t-consent { position: fixed; z-index: 35; left: 16px; right: 16px; bottom: 16px; display: flex; flex-wrap: wrap; align-items: center; gap: 12px; max-width: 720px; margin: 0 auto; padding: 16px; background: var(--t-bg); border: 1px solid var(--t-ink); }
|
|
172
|
+
.t-consent p { flex: 1 1 300px; margin: 0; }
|
|
173
|
+
|
|
174
|
+
/* small screens: brand · cart · menu on one line; the menu opens the links, search and region below it */
|
|
175
|
+
@media (max-width: 900px) {
|
|
176
|
+
.t-mast { grid-template-columns: 1fr auto auto; gap: 8px 12px; }
|
|
177
|
+
.t-mast__menu { display: inline-flex; align-items: center; min-height: 44px; min-width: 44px; padding: 0 12px; background: none; border: 1px solid var(--t-line); color: inherit; font: inherit; cursor: pointer; }
|
|
178
|
+
.t-mast__nav { display: none; grid-column: 1 / -1; order: 5; }
|
|
179
|
+
.t-mast__nav.is-open { display: block; }
|
|
180
|
+
.t-mast__nav ul { flex-direction: column; }
|
|
181
|
+
.t-mast__tools { display: contents; }
|
|
182
|
+
.t-mast__cart { grid-row: 1; grid-column: 2; }
|
|
183
|
+
.t-mast__menu { grid-row: 1; grid-column: 3; }
|
|
184
|
+
.t-find, .t-locale { display: none; grid-column: 1 / -1; order: 6; }
|
|
185
|
+
.t-mast__nav.is-open ~ .t-mast__tools .t-find, .t-mast__nav.is-open ~ .t-mast__tools .t-locale { display: flex; }
|
|
186
|
+
.t-find__input { width: 100%; }
|
|
187
|
+
.t-locale { flex-wrap: wrap; }
|
|
188
|
+
.t-pdp { grid-template-columns: 1fr; gap: 20px; }
|
|
189
|
+
.t-plp__body { grid-template-columns: 1fr; }
|
|
190
|
+
.t-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 24px 12px; }
|
|
191
|
+
.t-line { grid-template-columns: 72px 1fr; }
|
|
192
|
+
.t-line__img { width: 72px; }
|
|
193
|
+
.t-line__qty, .t-line__total { grid-column: 2; }
|
|
194
|
+
.t-line__total { text-align: left; }
|
|
195
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Template name → the route kind it renders (templates/<name>.json holds its starter content). */
|
|
2
|
+
export const TEMPLATES = {
|
|
3
|
+
index: { route: 'home', title: 'Home' },
|
|
4
|
+
product: { route: 'product' },
|
|
5
|
+
collection: { route: 'collection' },
|
|
6
|
+
search: { route: 'search' },
|
|
7
|
+
cart: { route: 'cart' },
|
|
8
|
+
page: { route: 'page' },
|
|
9
|
+
'page.contact': { route: 'page', title: 'Contact' },
|
|
10
|
+
blog: { route: 'blog' },
|
|
11
|
+
article: { route: 'article' },
|
|
12
|
+
account: { route: 'customer' },
|
|
13
|
+
'404': { route: 'not_found' },
|
|
14
|
+
} as const
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The theme: its sections and the Layout around every page. Markup and CSS (src/styles/theme.css) are the
|
|
3
|
+
* theme's own; every storefront function comes from @tanqory/theme-kit/headless.
|
|
4
|
+
*/
|
|
5
|
+
import type { ReactNode } from 'react'
|
|
6
|
+
import { CartDrawer, CartLine, CartLineRemove, CheckoutButton, ConsentBanner, Image, SkipLink, defineTheme, formatMoney, useSettings, useTranslate } from '@tanqory/theme-kit/headless'
|
|
7
|
+
import { cx } from './components/parts'
|
|
8
|
+
import { footer, header } from './sections/chrome'
|
|
9
|
+
import { featuredCollection, hero, letters, note } from './sections/editorial'
|
|
10
|
+
import { account, cart, collection, product, recommendations, search } from './sections/commerce'
|
|
11
|
+
import { article, blog, contact, notFound, page } from './sections/content'
|
|
12
|
+
|
|
13
|
+
function Drawer(): JSX.Element {
|
|
14
|
+
const t = useTranslate()
|
|
15
|
+
return (
|
|
16
|
+
<CartDrawer className="t-drawer" backdropClassName="t-drawer__backdrop">
|
|
17
|
+
{(c) => (
|
|
18
|
+
<>
|
|
19
|
+
<div className="t-drawer__head">
|
|
20
|
+
<h2 className="t-drawer__title">{t('cart.title')}</h2>
|
|
21
|
+
<button type="button" className="t-drawer__close" onClick={c.close}>
|
|
22
|
+
{t('cart.close')}
|
|
23
|
+
</button>
|
|
24
|
+
</div>
|
|
25
|
+
{c.lines.length === 0 ? (
|
|
26
|
+
<p className="t-muted">{t('cart.empty')}</p>
|
|
27
|
+
) : (
|
|
28
|
+
<>
|
|
29
|
+
<ul className="t-drawer__lines">
|
|
30
|
+
{c.lines.map((line) => (
|
|
31
|
+
<CartLine key={line.id} line={line} className="t-drawer__line">
|
|
32
|
+
<Image image={line.variant.image} alt="" className="t-drawer__img" fallback={<span className="t-drawer__img t-drawer__img--empty" />} />
|
|
33
|
+
<div>
|
|
34
|
+
<p className="t-drawer__name">{line.product.title}</p>
|
|
35
|
+
<p className="t-muted">
|
|
36
|
+
{line.variant.title !== 'Default Title' ? `${line.variant.title} · ` : ''}
|
|
37
|
+
{t('theme.drawer.qty', { count: line.quantity })}
|
|
38
|
+
</p>
|
|
39
|
+
<p>{formatMoney(line.cost.total)}</p>
|
|
40
|
+
</div>
|
|
41
|
+
<CartLineRemove className="t-link" />
|
|
42
|
+
</CartLine>
|
|
43
|
+
))}
|
|
44
|
+
</ul>
|
|
45
|
+
<p className="t-drawer__total">
|
|
46
|
+
<span>{t('cart.subtotal')}</span> <span>{formatMoney(c.subtotal)}</span>
|
|
47
|
+
</p>
|
|
48
|
+
<CheckoutButton className="t-btn t-btn--solid" />
|
|
49
|
+
<a className="t-btn" href="/cart">
|
|
50
|
+
{t('theme.cart.view')}
|
|
51
|
+
</a>
|
|
52
|
+
</>
|
|
53
|
+
)}
|
|
54
|
+
</>
|
|
55
|
+
)}
|
|
56
|
+
</CartDrawer>
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function Layout({ children }: { children: ReactNode }): JSX.Element {
|
|
61
|
+
const settings = useSettings()
|
|
62
|
+
return (
|
|
63
|
+
<div className={cx('t', settings.width === 'wide' && 't--wide')}>
|
|
64
|
+
<SkipLink className="t-skip" />
|
|
65
|
+
{children}
|
|
66
|
+
<Drawer />
|
|
67
|
+
<ConsentBanner className="t-consent" buttonClassName="t-btn" acceptClassName="t-btn--solid" />
|
|
68
|
+
</div>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export default defineTheme({
|
|
73
|
+
name: '__THEME_TITLE__',
|
|
74
|
+
sections: [header, hero, featuredCollection, note, letters, product, recommendations, collection, search, cart, page, contact, blog, article, account, notFound, footer],
|
|
75
|
+
Layout,
|
|
76
|
+
})
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "blank",
|
|
3
|
+
"title": "Blank",
|
|
4
|
+
"summary": "Unstyled: every storefront function wired through @tanqory/theme-kit/headless, minimal neutral CSS. Start here for a theme of your own design.",
|
|
5
|
+
"api": "@tanqory/theme-kit/headless",
|
|
6
|
+
"layout": [
|
|
7
|
+
["src/theme.tsx", "defineTheme: the list of sections and the Layout around every page (cart drawer, consent banner)"],
|
|
8
|
+
["src/sections/*.tsx", "defineSection / defineBlock, each with its settings schema; the markup is yours"],
|
|
9
|
+
["src/components/parts.tsx", "small shared parts (product card, grid)"],
|
|
10
|
+
["src/styles/theme.css", "all CSS: minimal and neutral, replace it"],
|
|
11
|
+
["src/settings.ts", "theme settings (the editor's Theme panel)"],
|
|
12
|
+
["src/templates.ts", "template name to route kind; templates/<name>.json is its starter content"],
|
|
13
|
+
["templates/, regions/, config/settings.json", "starter content (ABI v2 template and region documents) and setting values"],
|
|
14
|
+
["locales/en.json, locales/th.json", "every visible string the theme adds (laid over the kit's own strings)"],
|
|
15
|
+
["src/entry.server.tsx, src/entry.client.tsx", "createRender() / hydrate(): leave as they are"],
|
|
16
|
+
["scripts/build.mjs", "manifest from your declarations, then the browser and server bundles to dist/abi"],
|
|
17
|
+
["tests/", "vitest: strings, templates and section declarations"]
|
|
18
|
+
],
|
|
19
|
+
"docs": [
|
|
20
|
+
["node_modules/@tanqory/theme-kit/docs/headless-api.md", "the headless API: hooks, primitives, defineSection, settings fields, ai roles, data-tq-* hooks"],
|
|
21
|
+
["node_modules/@tanqory/theme-abi/contracts/theme-abi-v2.md", "the platform contract (routes, data, actions, capabilities, editor protocol, limits)"],
|
|
22
|
+
["node_modules/@tanqory/theme-conformance/docs/storefront-tier.md", "every storefront check, with its fix hint"]
|
|
23
|
+
]
|
|
24
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"contentVersion": 1,
|
|
3
|
+
"template": "index",
|
|
4
|
+
"regions": {
|
|
5
|
+
"main": {
|
|
6
|
+
"sections": [
|
|
7
|
+
{
|
|
8
|
+
"id": "hero-1",
|
|
9
|
+
"type": "hero",
|
|
10
|
+
"settings": {
|
|
11
|
+
"kicker": "Welcome",
|
|
12
|
+
"heading": "Your store, your theme",
|
|
13
|
+
"buttonLabel": "Shop all",
|
|
14
|
+
"buttonLink": "/collections/all"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"id": "featured-1",
|
|
19
|
+
"type": "featured-collection",
|
|
20
|
+
"settings": {
|
|
21
|
+
"heading": "Featured",
|
|
22
|
+
"collection": "all"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"id": "note-1",
|
|
27
|
+
"type": "rich-text"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"id": "newsletter-1",
|
|
31
|
+
"type": "newsletter"
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fast checks on the theme's own declarations (run with `npm test`). The full storefront behaviour is
|
|
3
|
+
* checked by `npm run check` on the built theme.
|
|
4
|
+
*/
|
|
5
|
+
import { readFileSync, readdirSync } from 'node:fs'
|
|
6
|
+
import { join } from 'node:path'
|
|
7
|
+
import { fileURLToPath } from 'node:url'
|
|
8
|
+
import { describe, expect, it } from 'vitest'
|
|
9
|
+
import theme from '../src/theme'
|
|
10
|
+
import { TEMPLATES } from '../src/templates'
|
|
11
|
+
|
|
12
|
+
const root = fileURLToPath(new URL('..', import.meta.url))
|
|
13
|
+
const json = (p: string) => JSON.parse(readFileSync(join(root, p), 'utf8'))
|
|
14
|
+
|
|
15
|
+
describe('locales', () => {
|
|
16
|
+
const en = json('locales/en.json') as Record<string, string>
|
|
17
|
+
const th = json('locales/th.json') as Record<string, string>
|
|
18
|
+
it('every English key has a Thai string, and the reverse', () => {
|
|
19
|
+
expect(Object.keys(th).sort()).toEqual(Object.keys(en).sort())
|
|
20
|
+
})
|
|
21
|
+
it('placeholders match between languages', () => {
|
|
22
|
+
const vars = (s: string) => (s.match(/\{\w+\}/g) ?? []).sort()
|
|
23
|
+
for (const k of Object.keys(en)) expect(vars(th[k] ?? ''), k).toEqual(vars(en[k]!))
|
|
24
|
+
})
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
describe('templates', () => {
|
|
28
|
+
it('every template in src/templates.ts has templates/<name>.json, and the reverse', () => {
|
|
29
|
+
const files = readdirSync(join(root, 'templates')).filter((f) => f.endsWith('.json')).map((f) => f.replace(/\.json$/, ''))
|
|
30
|
+
expect(files.sort()).toEqual(Object.keys(TEMPLATES).sort())
|
|
31
|
+
})
|
|
32
|
+
it('starter content uses only declared section types', () => {
|
|
33
|
+
const types = new Set(theme.sections.map((s) => s.id))
|
|
34
|
+
const docs = [
|
|
35
|
+
...readdirSync(join(root, 'templates')).map((f) => json(`templates/${f}`)),
|
|
36
|
+
...['header', 'footer'].map((r) => ({ regions: { [r]: json(`regions/${r}.json`) } })),
|
|
37
|
+
]
|
|
38
|
+
for (const doc of docs)
|
|
39
|
+
for (const region of Object.values(doc.regions ?? {}) as Array<{ sections?: Array<{ type: string }> }>)
|
|
40
|
+
for (const s of region.sections ?? []) expect(types.has(s.type), s.type).toBe(true)
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
describe('sections', () => {
|
|
45
|
+
it('section types are unique', () => {
|
|
46
|
+
const types = theme.sections.map((s) => s.id)
|
|
47
|
+
expect(new Set(types).size).toBe(types.length)
|
|
48
|
+
})
|
|
49
|
+
})
|