@cimplify/cli 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{add-HCVYD33F.mjs → add-ZRSE2STJ.mjs} +1 -1
- package/dist/{chunk-VHXZ4SGF.mjs → chunk-3J3CCUMJ.mjs} +8 -2
- package/dist/{chunk-WNZ6N634.mjs → chunk-7CWMGKWE.mjs} +1 -1
- package/dist/{chunk-DE4MI2UK.mjs → chunk-WBNI7PWA.mjs} +18 -18
- package/dist/{dev-OLW755ID.mjs → dev-CCL7H3HC.mjs} +18 -0
- package/dist/dispatcher.mjs +10 -10
- package/dist/{doctor-2OIPFMDS.mjs → doctor-FMQAVFSR.mjs} +2 -2
- package/dist/{explain-JBPAC2ZB.mjs → explain-U65RAIPX.mjs} +1 -1
- package/dist/{introspect-5BX4KGO4.mjs → introspect-HVCWTX7S.mjs} +2 -2
- package/dist/{list-5WLNIKFF.mjs → list-FCBWJ5DY.mjs} +1 -1
- package/dist/preview-config-CHDYYWUY.mjs +120 -0
- package/dist/{update-6EVYEGKD.mjs → update-FT2KGKCY.mjs} +1 -1
- package/package.json +8 -2
- package/templates/storefront-auto/CLAUDE.md +39 -0
- package/templates/storefront-auto/components/mobile-nav.tsx +58 -48
- package/templates/storefront-auto/package.json +1 -1
- package/templates/storefront-bakery/CLAUDE.md +39 -0
- package/templates/storefront-bakery/components/mobile-nav.tsx +58 -48
- package/templates/storefront-bakery/package.json +1 -1
- package/templates/storefront-fashion/CLAUDE.md +39 -0
- package/templates/storefront-fashion/components/mobile-nav.tsx +58 -48
- package/templates/storefront-fashion/package.json +1 -1
- package/templates/storefront-grocery/CLAUDE.md +39 -0
- package/templates/storefront-grocery/components/mobile-nav.tsx +58 -48
- package/templates/storefront-grocery/package.json +1 -1
- package/templates/storefront-pharmacy/CLAUDE.md +39 -0
- package/templates/storefront-pharmacy/components/mobile-nav.tsx +58 -48
- package/templates/storefront-pharmacy/package.json +1 -1
- package/templates/storefront-restaurant/CLAUDE.md +39 -0
- package/templates/storefront-restaurant/components/mobile-nav.tsx +58 -48
- package/templates/storefront-restaurant/package.json +1 -1
- package/templates/storefront-retail/CLAUDE.md +39 -0
- package/templates/storefront-retail/components/mobile-nav.tsx +58 -48
- package/templates/storefront-retail/package.json +1 -1
- package/templates/storefront-services/CLAUDE.md +39 -0
- package/templates/storefront-services/app/book/book-client.tsx +1 -0
- package/templates/storefront-services/bun.lock +454 -8
- package/templates/storefront-services/components/mobile-nav.tsx +58 -48
- package/templates/storefront-services/package.json +1 -1
|
@@ -14,6 +14,22 @@ var PACKAGE_JSON = "package.json";
|
|
|
14
14
|
var ENV_LOCAL_FILE = ".env.local";
|
|
15
15
|
var PUBLIC_API_URL_KEY = "NEXT_PUBLIC_CIMPLIFY_API_URL";
|
|
16
16
|
var SCRIPT_DEV = "dev";
|
|
17
|
+
var PREVIEW_ORIGIN_ENV = "CIMPLIFY_PREVIEW_ORIGIN";
|
|
18
|
+
async function applyPreviewOrigin(cwd, origin) {
|
|
19
|
+
const { ensurePreviewOrigin } = await import('./preview-config-CHDYYWUY.mjs');
|
|
20
|
+
try {
|
|
21
|
+
const result = await ensurePreviewOrigin(cwd, origin);
|
|
22
|
+
if (result.status === "unrecognized") {
|
|
23
|
+
info(
|
|
24
|
+
dim(
|
|
25
|
+
`(preview: could not resolve ${result.file} config shape; hot-reload may be limited)`
|
|
26
|
+
)
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
} catch (err) {
|
|
30
|
+
info(dim(`(preview: dev-origin setup skipped: ${err.message})`));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
17
33
|
var PM_BUN = "bun";
|
|
18
34
|
var PM_PNPM = "pnpm";
|
|
19
35
|
var PM_YARN = "yarn";
|
|
@@ -115,6 +131,8 @@ async function run(argv) {
|
|
|
115
131
|
`No "${SCRIPT_DEV}" script in package.json. Add one (e.g. "next dev") and re-run.`
|
|
116
132
|
);
|
|
117
133
|
}
|
|
134
|
+
const previewOrigin = process.env[PREVIEW_ORIGIN_ENV];
|
|
135
|
+
if (previewOrigin) await applyPreviewOrigin(cwd, previewOrigin);
|
|
118
136
|
const pm = await detectPackageManager(cwd);
|
|
119
137
|
info(dim(`(running: ${pm} run ${SCRIPT_DEV})`));
|
|
120
138
|
const child = spawn(pm, ["run", SCRIPT_DEV], { cwd, stdio: "inherit" });
|
package/dist/dispatcher.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { package_default } from './chunk-
|
|
3
|
-
import { TEMPLATES } from './chunk-
|
|
2
|
+
import { package_default } from './chunk-3J3CCUMJ.mjs';
|
|
3
|
+
import { TEMPLATES } from './chunk-WBNI7PWA.mjs';
|
|
4
4
|
|
|
5
5
|
// src/dispatcher.ts
|
|
6
6
|
var VERSION = package_default.version ?? "unknown";
|
|
@@ -142,18 +142,18 @@ var COMMANDS = {
|
|
|
142
142
|
domains: () => import('./domains-H3K2WW55.mjs'),
|
|
143
143
|
logs: () => import('./logs-2ECJQ3MU.mjs'),
|
|
144
144
|
status: () => import('./status-IA7PJ4TV.mjs'),
|
|
145
|
-
dev: () => import('./dev-
|
|
145
|
+
dev: () => import('./dev-CCL7H3HC.mjs'),
|
|
146
146
|
mock: () => import('./mock-TSUKCLWU.mjs'),
|
|
147
|
-
introspect: () => import('./introspect-
|
|
147
|
+
introspect: () => import('./introspect-HVCWTX7S.mjs'),
|
|
148
148
|
inspect: () => import('./inspect-CGYX4DDF.mjs'),
|
|
149
|
-
doctor: () => import('./doctor-
|
|
150
|
-
explain: () => import('./explain-
|
|
149
|
+
doctor: () => import('./doctor-FMQAVFSR.mjs'),
|
|
150
|
+
explain: () => import('./explain-U65RAIPX.mjs'),
|
|
151
151
|
assets: () => import('./assets-4D7VXRUE.mjs'),
|
|
152
152
|
repo: () => import('./repo-WSUAHXWO.mjs'),
|
|
153
|
-
list: () => import('./list-
|
|
154
|
-
add: () => import('./add-
|
|
155
|
-
update: () => import('./update-
|
|
156
|
-
upgrade: () => import('./update-
|
|
153
|
+
list: () => import('./list-FCBWJ5DY.mjs'),
|
|
154
|
+
add: () => import('./add-ZRSE2STJ.mjs'),
|
|
155
|
+
update: () => import('./update-FT2KGKCY.mjs'),
|
|
156
|
+
upgrade: () => import('./update-FT2KGKCY.mjs'),
|
|
157
157
|
"auth-step-up": () => import('./auth-step-up-NOSM4PUR.mjs')
|
|
158
158
|
};
|
|
159
159
|
var COMMAND_PREFIXES = {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { gatherIntrospection } from './chunk-
|
|
2
|
+
import { gatherIntrospection } from './chunk-7CWMGKWE.mjs';
|
|
3
3
|
import './chunk-K5464A3L.mjs';
|
|
4
4
|
import './chunk-DBZ3UOQ2.mjs';
|
|
5
|
-
import './chunk-
|
|
5
|
+
import './chunk-3J3CCUMJ.mjs';
|
|
6
6
|
import { parseArgs, flagBool } from './chunk-C4M3DXKC.mjs';
|
|
7
7
|
import { SDK_ENV_API_URL, DEFAULT_API_BASE_URL, ApiClient, ENV_API_BASE_URL } from './chunk-F4S5KETY.mjs';
|
|
8
8
|
import { readAuthOrNull } from './chunk-UBAI443T.mjs';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { package_default } from './chunk-
|
|
2
|
+
import { package_default } from './chunk-3J3CCUMJ.mjs';
|
|
3
3
|
import { parseArgs } from './chunk-C4M3DXKC.mjs';
|
|
4
4
|
import { bold, dim, info, result, CliError, CLI_ERROR_CODE } from './chunk-E2T2SBP5.mjs';
|
|
5
5
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
export { run as default, extractMockSeed, gatherIntrospection, renderIntrospection } from './chunk-
|
|
2
|
+
export { run as default, extractMockSeed, gatherIntrospection, renderIntrospection } from './chunk-7CWMGKWE.mjs';
|
|
3
3
|
import './chunk-K5464A3L.mjs';
|
|
4
4
|
import './chunk-DBZ3UOQ2.mjs';
|
|
5
|
-
import './chunk-
|
|
5
|
+
import './chunk-3J3CCUMJ.mjs';
|
|
6
6
|
import './chunk-C4M3DXKC.mjs';
|
|
7
7
|
import './chunk-UBAI443T.mjs';
|
|
8
8
|
import './chunk-E2T2SBP5.mjs';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { REGISTRY_INDEX } from './chunk-
|
|
2
|
+
import { REGISTRY_INDEX } from './chunk-WBNI7PWA.mjs';
|
|
3
3
|
import { parseArgs, flagBool } from './chunk-C4M3DXKC.mjs';
|
|
4
4
|
import { CliError, CLI_ERROR_CODE, info, bold, dim, green, result } from './chunk-E2T2SBP5.mjs';
|
|
5
5
|
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { promises } from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { parse } from '@babel/parser';
|
|
5
|
+
import _traverse from '@babel/traverse';
|
|
6
|
+
import _generate from '@babel/generator';
|
|
7
|
+
import * as t from '@babel/types';
|
|
8
|
+
|
|
9
|
+
var traverse = _traverse.default ?? _traverse;
|
|
10
|
+
var generate = _generate.default ?? _generate;
|
|
11
|
+
var PROPERTY = "allowedDevOrigins";
|
|
12
|
+
var CONFIG_FILENAMES = [
|
|
13
|
+
"next.config.ts",
|
|
14
|
+
"next.config.mts",
|
|
15
|
+
"next.config.cts",
|
|
16
|
+
"next.config.js",
|
|
17
|
+
"next.config.mjs",
|
|
18
|
+
"next.config.cjs"
|
|
19
|
+
];
|
|
20
|
+
function unwrapTs(node) {
|
|
21
|
+
let current = node;
|
|
22
|
+
while (t.isTSAsExpression(current) || t.isTSSatisfiesExpression(current)) {
|
|
23
|
+
current = current.expression;
|
|
24
|
+
}
|
|
25
|
+
return current;
|
|
26
|
+
}
|
|
27
|
+
function returnedObject(body) {
|
|
28
|
+
for (const statement of body.body) {
|
|
29
|
+
if (t.isReturnStatement(statement) && statement.argument) {
|
|
30
|
+
const value = unwrapTs(statement.argument);
|
|
31
|
+
if (t.isObjectExpression(value)) return value;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
function resolveConfigObject(node, scope) {
|
|
37
|
+
const value = unwrapTs(node);
|
|
38
|
+
if (t.isObjectExpression(value)) return value;
|
|
39
|
+
if (t.isIdentifier(value)) {
|
|
40
|
+
const binding = scope.getBinding(value.name);
|
|
41
|
+
const init = binding?.path.isVariableDeclarator() ? binding.path.node.init : null;
|
|
42
|
+
return init ? resolveConfigObject(init, scope) : null;
|
|
43
|
+
}
|
|
44
|
+
if (t.isCallExpression(value)) {
|
|
45
|
+
for (const arg of value.arguments) {
|
|
46
|
+
if (t.isObjectExpression(arg)) return arg;
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
if (t.isArrowFunctionExpression(value) || t.isFunctionExpression(value) || t.isFunctionDeclaration(value)) {
|
|
51
|
+
if (t.isBlockStatement(value.body)) return returnedObject(value.body);
|
|
52
|
+
return resolveConfigObject(value.body, scope);
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
function findConfigObject(ast) {
|
|
57
|
+
let found = null;
|
|
58
|
+
traverse(ast, {
|
|
59
|
+
ExportDefaultDeclaration(path2) {
|
|
60
|
+
found ??= resolveConfigObject(path2.node.declaration, path2.scope);
|
|
61
|
+
},
|
|
62
|
+
AssignmentExpression(path2) {
|
|
63
|
+
const { left, right } = path2.node;
|
|
64
|
+
const isModuleExports = t.isMemberExpression(left) && t.isIdentifier(left.object, { name: "module" }) && t.isIdentifier(left.property, { name: "exports" });
|
|
65
|
+
if (isModuleExports) found ??= resolveConfigObject(right, path2.scope);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
return found;
|
|
69
|
+
}
|
|
70
|
+
function upsertOrigin(config, origin) {
|
|
71
|
+
const existing = config.properties.find(
|
|
72
|
+
(prop) => t.isObjectProperty(prop) && t.isIdentifier(prop.key, { name: PROPERTY })
|
|
73
|
+
);
|
|
74
|
+
if (existing) {
|
|
75
|
+
if (!t.isArrayExpression(existing.value)) return false;
|
|
76
|
+
const already = existing.value.elements.some(
|
|
77
|
+
(el) => t.isStringLiteral(el) && el.value === origin
|
|
78
|
+
);
|
|
79
|
+
if (already) return false;
|
|
80
|
+
existing.value.elements.push(t.stringLiteral(origin));
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
config.properties.unshift(
|
|
84
|
+
t.objectProperty(t.identifier(PROPERTY), t.arrayExpression([t.stringLiteral(origin)]))
|
|
85
|
+
);
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
function injectAllowedDevOrigins(source, origin) {
|
|
89
|
+
const ast = parse(source, {
|
|
90
|
+
sourceType: "module",
|
|
91
|
+
plugins: ["typescript"]
|
|
92
|
+
});
|
|
93
|
+
const config = findConfigObject(ast);
|
|
94
|
+
if (!config) return { code: source, changed: false, resolved: false };
|
|
95
|
+
if (!upsertOrigin(config, origin)) return { code: source, changed: false, resolved: true };
|
|
96
|
+
return {
|
|
97
|
+
code: generate(ast, { retainLines: true }, source).code,
|
|
98
|
+
changed: true,
|
|
99
|
+
resolved: true
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
async function ensurePreviewOrigin(cwd, origin) {
|
|
103
|
+
for (const filename of CONFIG_FILENAMES) {
|
|
104
|
+
const file = path.join(cwd, filename);
|
|
105
|
+
let source;
|
|
106
|
+
try {
|
|
107
|
+
source = await promises.readFile(file, "utf8");
|
|
108
|
+
} catch (err) {
|
|
109
|
+
if (err.code === "ENOENT") continue;
|
|
110
|
+
throw err;
|
|
111
|
+
}
|
|
112
|
+
const { code, changed, resolved } = injectAllowedDevOrigins(source, origin);
|
|
113
|
+
if (!resolved) return { status: "unrecognized", file: filename };
|
|
114
|
+
if (changed) await promises.writeFile(file, code, "utf8");
|
|
115
|
+
return { status: changed ? "injected" : "already-present", file: filename };
|
|
116
|
+
}
|
|
117
|
+
return { status: "no-config" };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export { ensurePreviewOrigin, injectAllowedDevOrigins };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { package_default } from './chunk-
|
|
2
|
+
import { package_default } from './chunk-3J3CCUMJ.mjs';
|
|
3
3
|
import { promptYesNo } from './chunk-ITAFAORS.mjs';
|
|
4
4
|
import { parseArgs, flagBool, flagString } from './chunk-C4M3DXKC.mjs';
|
|
5
5
|
import { success, bold, info, dim, result, failure, CliError, CLI_ERROR_CODE, step, isJsonMode } from './chunk-E2T2SBP5.mjs';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cimplify/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Cimplify CLI — deploy, manage env vars, link projects, and scaffold storefronts",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cimplify",
|
|
@@ -38,6 +38,8 @@
|
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@aws-sdk/client-s3": "^3.1070.0",
|
|
41
|
+
"@types/babel__generator": "^7.27.0",
|
|
42
|
+
"@types/babel__traverse": "^7.28.0",
|
|
41
43
|
"@types/node": "^25.9.3",
|
|
42
44
|
"@typescript/native-preview": "^7.0.0-dev.20260616.1",
|
|
43
45
|
"tsup": "^8.5.1",
|
|
@@ -45,6 +47,10 @@
|
|
|
45
47
|
"vitest": "^4.1.9"
|
|
46
48
|
},
|
|
47
49
|
"dependencies": {
|
|
48
|
-
"@
|
|
50
|
+
"@babel/generator": "^8.0.0",
|
|
51
|
+
"@babel/parser": "^8.0.4",
|
|
52
|
+
"@babel/traverse": "^8.0.4",
|
|
53
|
+
"@babel/types": "^8.0.4",
|
|
54
|
+
"@cimplify/sdk": "^0.96.0"
|
|
49
55
|
}
|
|
50
56
|
}
|
|
@@ -21,3 +21,42 @@ That covers ~95% of what merchants ask for. For anything else, follow the `cimpl
|
|
|
21
21
|
- Add `'use cache'`, `cacheTag`, or `cacheLife` anywhere. Use the SDK's `cacheOptions` instead.
|
|
22
22
|
- Use `unstable_cache` — it's gone in Next 16. Use SDK `cacheOptions` or `fetch.next.{revalidate,tags}`.
|
|
23
23
|
- Run `bun test` (Bun's `vi` shim is incomplete) — use `bun run test:run`.
|
|
24
|
+
|
|
25
|
+
## Design bans
|
|
26
|
+
|
|
27
|
+
Not style preferences — these are the tells of a generated site. Grep for them before calling any piece of work done.
|
|
28
|
+
|
|
29
|
+
- **No scroll-down indicators.** No "scroll" label, chevron, arrow, mouse glyph, or bouncing hint under a hero.
|
|
30
|
+
- **No ornament glyphs** — ◆ ◇ ✦ ✧ ❖ as dividers, bullets, or separators. No `01 / 02 / 03` section numbering.
|
|
31
|
+
- No purple/blue gradient washes, glassmorphism-by-default cards, floating badge pills, or emoji in UI copy.
|
|
32
|
+
- No boxed text-button clusters in the navbar — use icons (search, account, cart with count).
|
|
33
|
+
- No border radius unless the brief asks for softness; prefer hairline borders.
|
|
34
|
+
|
|
35
|
+
Reserve glass for surfaces that earn it (navbar, flyouts, overlays) and execute it properly: deep `backdrop-filter` blur **with saturation**, a specular top edge, a soft falling shadow. Never a flat translucent white card.
|
|
36
|
+
|
|
37
|
+
Ask of each screen: does this read as "template with new colours"? If so, it isn't done.
|
|
38
|
+
|
|
39
|
+
## Known traps
|
|
40
|
+
|
|
41
|
+
- **`backdrop-filter` creates a containing block.** A navbar carrying it becomes the containing block for `position: fixed` children, so a fixed mobile drawer gets clamped to the navbar's box and renders transparent. Portal overlays and drawers to `<body>`.
|
|
42
|
+
- **Google Fonts fetches fail at build time in some environments.** Self-host font files in `app/fonts/` rather than depending on a network fetch mid-build.
|
|
43
|
+
- **Never show a slot the booking engine will reject.** Source the availability grid from the strict bookable endpoint, not the general calendar. If a service verifies unavailable or returns zero slots, render a call-the-shop fallback with the real phone number — never an empty grid.
|
|
44
|
+
- **Route media through `lib/media.ts`.** No hardcoded asset URLs in components. Transcode phone video (usually HEVC, unplayable in most browsers) to H.264, and size images down before shipping them.
|
|
45
|
+
|
|
46
|
+
## Verification loop
|
|
47
|
+
|
|
48
|
+
Continuously, not once at the end:
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
build → start on PORT 3100 → screenshot at 1440px and 390px → fix → repeat
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
- Verify on **port 3100**. Dev holds 3000, and a stray verification server there blocks restarts. Kill your own.
|
|
55
|
+
- Read the **full** build output, unfiltered — a hidden page-generation failure ships as "Could not find a production build".
|
|
56
|
+
- Actually look at the screenshots. "It compiled" is not "it looks right".
|
|
57
|
+
- Screenshot interactive states too: nav open, search flyout, chat open, booking flow, mobile menu.
|
|
58
|
+
- Test once per round with reduced motion emulated.
|
|
59
|
+
|
|
60
|
+
## Escalate rather than loop
|
|
61
|
+
|
|
62
|
+
Stop and ask for: the live API key, go-live approval (`--prod`), anything irreversible (deleting CDN assets, domain changes), and dashboard-side changes you cannot make from code — typically staff-to-service assignment when a service returns zero availability while its API fields look identical to a working one. Present the evidence, then carry on with everything else.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { useEffect, useState } from "react";
|
|
4
|
+
import { createPortal } from "react-dom";
|
|
4
5
|
import Link from "next/link";
|
|
5
6
|
import { brand } from "@/lib/brand";
|
|
6
7
|
|
|
@@ -11,6 +12,9 @@ import { brand } from "@/lib/brand";
|
|
|
11
12
|
*/
|
|
12
13
|
export function MobileNav() {
|
|
13
14
|
const [open, setOpen] = useState(false);
|
|
15
|
+
const [mounted, setMounted] = useState(false);
|
|
16
|
+
|
|
17
|
+
useEffect(() => setMounted(true), []);
|
|
14
18
|
|
|
15
19
|
useEffect(() => {
|
|
16
20
|
if (!open) return;
|
|
@@ -53,61 +57,67 @@ export function MobileNav() {
|
|
|
53
57
|
</svg>
|
|
54
58
|
</button>
|
|
55
59
|
|
|
56
|
-
{
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
className="absolute inset-0 bg-background/80 backdrop-blur-sm"
|
|
63
|
-
/>
|
|
64
|
-
<nav
|
|
65
|
-
id="mobile-nav-drawer"
|
|
66
|
-
aria-label="Mobile navigation"
|
|
67
|
-
className="absolute inset-y-0 right-0 w-[85%] max-w-sm flex flex-col bg-background border-l border-border shadow-2xl"
|
|
68
|
-
>
|
|
69
|
-
<div className="flex items-center justify-between px-6 py-4 border-b border-border">
|
|
70
|
-
<span className="text-xs font-semibold uppercase tracking-[0.16em] text-muted-foreground">
|
|
71
|
-
Menu
|
|
72
|
-
</span>
|
|
60
|
+
{/* Portalled: the header carries `backdrop-filter`, which makes it the
|
|
61
|
+
containing block for `position: fixed`, clamping this drawer to the
|
|
62
|
+
header's box. */}
|
|
63
|
+
{open && mounted
|
|
64
|
+
? createPortal(
|
|
65
|
+
<div className="fixed inset-0 z-50 sm:hidden">
|
|
73
66
|
<button
|
|
74
67
|
type="button"
|
|
75
68
|
onClick={() => setOpen(false)}
|
|
76
69
|
aria-label="Close menu"
|
|
77
|
-
className="
|
|
70
|
+
className="absolute inset-0 bg-background/80 backdrop-blur-sm"
|
|
71
|
+
/>
|
|
72
|
+
<nav
|
|
73
|
+
id="mobile-nav-drawer"
|
|
74
|
+
aria-label="Mobile navigation"
|
|
75
|
+
className="absolute inset-y-0 right-0 w-[85%] max-w-sm flex flex-col bg-background border-l border-border shadow-2xl"
|
|
78
76
|
>
|
|
79
|
-
<
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
strokeWidth="2"
|
|
86
|
-
strokeLinecap="round"
|
|
87
|
-
strokeLinejoin="round"
|
|
88
|
-
aria-hidden="true"
|
|
89
|
-
>
|
|
90
|
-
<line x1="18" y1="6" x2="6" y2="18" />
|
|
91
|
-
<line x1="6" y1="6" x2="18" y2="18" />
|
|
92
|
-
</svg>
|
|
93
|
-
</button>
|
|
94
|
-
</div>
|
|
95
|
-
<ul className="flex flex-col gap-1 px-3 py-4">
|
|
96
|
-
{brand.header.nav.map((link) => (
|
|
97
|
-
<li key={link.href}>
|
|
98
|
-
<Link
|
|
99
|
-
href={link.href}
|
|
77
|
+
<div className="flex items-center justify-between px-6 py-4 border-b border-border">
|
|
78
|
+
<span className="text-xs font-semibold uppercase tracking-[0.16em] text-muted-foreground">
|
|
79
|
+
Menu
|
|
80
|
+
</span>
|
|
81
|
+
<button
|
|
82
|
+
type="button"
|
|
100
83
|
onClick={() => setOpen(false)}
|
|
101
|
-
|
|
84
|
+
aria-label="Close menu"
|
|
85
|
+
className="grid place-items-center w-11 h-11 -mr-2 rounded-md text-foreground hover:bg-muted transition-colors"
|
|
102
86
|
>
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
87
|
+
<svg
|
|
88
|
+
width="20"
|
|
89
|
+
height="20"
|
|
90
|
+
viewBox="0 0 24 24"
|
|
91
|
+
fill="none"
|
|
92
|
+
stroke="currentColor"
|
|
93
|
+
strokeWidth="2"
|
|
94
|
+
strokeLinecap="round"
|
|
95
|
+
strokeLinejoin="round"
|
|
96
|
+
aria-hidden="true"
|
|
97
|
+
>
|
|
98
|
+
<line x1="18" y1="6" x2="6" y2="18" />
|
|
99
|
+
<line x1="6" y1="6" x2="18" y2="18" />
|
|
100
|
+
</svg>
|
|
101
|
+
</button>
|
|
102
|
+
</div>
|
|
103
|
+
<ul className="flex flex-col gap-1 px-3 py-4">
|
|
104
|
+
{brand.header.nav.map((link) => (
|
|
105
|
+
<li key={link.href}>
|
|
106
|
+
<Link
|
|
107
|
+
href={link.href}
|
|
108
|
+
onClick={() => setOpen(false)}
|
|
109
|
+
className="block px-3 py-3 rounded-md text-base font-medium text-foreground hover:bg-muted transition-colors"
|
|
110
|
+
>
|
|
111
|
+
{link.label}
|
|
112
|
+
</Link>
|
|
113
|
+
</li>
|
|
114
|
+
))}
|
|
115
|
+
</ul>
|
|
116
|
+
</nav>
|
|
117
|
+
</div>,
|
|
118
|
+
document.body,
|
|
119
|
+
)
|
|
120
|
+
: null}
|
|
111
121
|
</>
|
|
112
122
|
);
|
|
113
123
|
}
|
|
@@ -21,3 +21,42 @@ That covers ~95% of what merchants ask for. For anything else, follow the `cimpl
|
|
|
21
21
|
- Add `'use cache'`, `cacheTag`, or `cacheLife` anywhere. Use the SDK's `cacheOptions` instead.
|
|
22
22
|
- Use `unstable_cache` — it's gone in Next 16. Use SDK `cacheOptions` or `fetch.next.{revalidate,tags}`.
|
|
23
23
|
- Run `bun test` (Bun's `vi` shim is incomplete) — use `bun run test:run`.
|
|
24
|
+
|
|
25
|
+
## Design bans
|
|
26
|
+
|
|
27
|
+
Not style preferences — these are the tells of a generated site. Grep for them before calling any piece of work done.
|
|
28
|
+
|
|
29
|
+
- **No scroll-down indicators.** No "scroll" label, chevron, arrow, mouse glyph, or bouncing hint under a hero.
|
|
30
|
+
- **No ornament glyphs** — ◆ ◇ ✦ ✧ ❖ as dividers, bullets, or separators. No `01 / 02 / 03` section numbering.
|
|
31
|
+
- No purple/blue gradient washes, glassmorphism-by-default cards, floating badge pills, or emoji in UI copy.
|
|
32
|
+
- No boxed text-button clusters in the navbar — use icons (search, account, cart with count).
|
|
33
|
+
- No border radius unless the brief asks for softness; prefer hairline borders.
|
|
34
|
+
|
|
35
|
+
Reserve glass for surfaces that earn it (navbar, flyouts, overlays) and execute it properly: deep `backdrop-filter` blur **with saturation**, a specular top edge, a soft falling shadow. Never a flat translucent white card.
|
|
36
|
+
|
|
37
|
+
Ask of each screen: does this read as "template with new colours"? If so, it isn't done.
|
|
38
|
+
|
|
39
|
+
## Known traps
|
|
40
|
+
|
|
41
|
+
- **`backdrop-filter` creates a containing block.** A navbar carrying it becomes the containing block for `position: fixed` children, so a fixed mobile drawer gets clamped to the navbar's box and renders transparent. Portal overlays and drawers to `<body>`.
|
|
42
|
+
- **Google Fonts fetches fail at build time in some environments.** Self-host font files in `app/fonts/` rather than depending on a network fetch mid-build.
|
|
43
|
+
- **Never show a slot the booking engine will reject.** Source the availability grid from the strict bookable endpoint, not the general calendar. If a service verifies unavailable or returns zero slots, render a call-the-shop fallback with the real phone number — never an empty grid.
|
|
44
|
+
- **Route media through `lib/media.ts`.** No hardcoded asset URLs in components. Transcode phone video (usually HEVC, unplayable in most browsers) to H.264, and size images down before shipping them.
|
|
45
|
+
|
|
46
|
+
## Verification loop
|
|
47
|
+
|
|
48
|
+
Continuously, not once at the end:
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
build → start on PORT 3100 → screenshot at 1440px and 390px → fix → repeat
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
- Verify on **port 3100**. Dev holds 3000, and a stray verification server there blocks restarts. Kill your own.
|
|
55
|
+
- Read the **full** build output, unfiltered — a hidden page-generation failure ships as "Could not find a production build".
|
|
56
|
+
- Actually look at the screenshots. "It compiled" is not "it looks right".
|
|
57
|
+
- Screenshot interactive states too: nav open, search flyout, chat open, booking flow, mobile menu.
|
|
58
|
+
- Test once per round with reduced motion emulated.
|
|
59
|
+
|
|
60
|
+
## Escalate rather than loop
|
|
61
|
+
|
|
62
|
+
Stop and ask for: the live API key, go-live approval (`--prod`), anything irreversible (deleting CDN assets, domain changes), and dashboard-side changes you cannot make from code — typically staff-to-service assignment when a service returns zero availability while its API fields look identical to a working one. Present the evidence, then carry on with everything else.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { useEffect, useState } from "react";
|
|
4
|
+
import { createPortal } from "react-dom";
|
|
4
5
|
import Link from "next/link";
|
|
5
6
|
import { brand } from "@/lib/brand";
|
|
6
7
|
|
|
@@ -11,6 +12,9 @@ import { brand } from "@/lib/brand";
|
|
|
11
12
|
*/
|
|
12
13
|
export function MobileNav() {
|
|
13
14
|
const [open, setOpen] = useState(false);
|
|
15
|
+
const [mounted, setMounted] = useState(false);
|
|
16
|
+
|
|
17
|
+
useEffect(() => setMounted(true), []);
|
|
14
18
|
|
|
15
19
|
useEffect(() => {
|
|
16
20
|
if (!open) return;
|
|
@@ -53,61 +57,67 @@ export function MobileNav() {
|
|
|
53
57
|
</svg>
|
|
54
58
|
</button>
|
|
55
59
|
|
|
56
|
-
{
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
className="absolute inset-0 bg-background/80 backdrop-blur-sm"
|
|
63
|
-
/>
|
|
64
|
-
<nav
|
|
65
|
-
id="mobile-nav-drawer"
|
|
66
|
-
aria-label="Mobile navigation"
|
|
67
|
-
className="absolute inset-y-0 right-0 w-[85%] max-w-sm flex flex-col bg-background border-l border-border shadow-2xl"
|
|
68
|
-
>
|
|
69
|
-
<div className="flex items-center justify-between px-6 py-4 border-b border-border">
|
|
70
|
-
<span className="text-xs font-semibold uppercase tracking-[0.16em] text-muted-foreground">
|
|
71
|
-
Menu
|
|
72
|
-
</span>
|
|
60
|
+
{/* Portalled: the header carries `backdrop-filter`, which makes it the
|
|
61
|
+
containing block for `position: fixed`, clamping this drawer to the
|
|
62
|
+
header's box. */}
|
|
63
|
+
{open && mounted
|
|
64
|
+
? createPortal(
|
|
65
|
+
<div className="fixed inset-0 z-50 sm:hidden">
|
|
73
66
|
<button
|
|
74
67
|
type="button"
|
|
75
68
|
onClick={() => setOpen(false)}
|
|
76
69
|
aria-label="Close menu"
|
|
77
|
-
className="
|
|
70
|
+
className="absolute inset-0 bg-background/80 backdrop-blur-sm"
|
|
71
|
+
/>
|
|
72
|
+
<nav
|
|
73
|
+
id="mobile-nav-drawer"
|
|
74
|
+
aria-label="Mobile navigation"
|
|
75
|
+
className="absolute inset-y-0 right-0 w-[85%] max-w-sm flex flex-col bg-background border-l border-border shadow-2xl"
|
|
78
76
|
>
|
|
79
|
-
<
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
strokeWidth="2"
|
|
86
|
-
strokeLinecap="round"
|
|
87
|
-
strokeLinejoin="round"
|
|
88
|
-
aria-hidden="true"
|
|
89
|
-
>
|
|
90
|
-
<line x1="18" y1="6" x2="6" y2="18" />
|
|
91
|
-
<line x1="6" y1="6" x2="18" y2="18" />
|
|
92
|
-
</svg>
|
|
93
|
-
</button>
|
|
94
|
-
</div>
|
|
95
|
-
<ul className="flex flex-col gap-1 px-3 py-4">
|
|
96
|
-
{brand.header.nav.map((link) => (
|
|
97
|
-
<li key={link.href}>
|
|
98
|
-
<Link
|
|
99
|
-
href={link.href}
|
|
77
|
+
<div className="flex items-center justify-between px-6 py-4 border-b border-border">
|
|
78
|
+
<span className="text-xs font-semibold uppercase tracking-[0.16em] text-muted-foreground">
|
|
79
|
+
Menu
|
|
80
|
+
</span>
|
|
81
|
+
<button
|
|
82
|
+
type="button"
|
|
100
83
|
onClick={() => setOpen(false)}
|
|
101
|
-
|
|
84
|
+
aria-label="Close menu"
|
|
85
|
+
className="grid place-items-center w-11 h-11 -mr-2 rounded-md text-foreground hover:bg-muted transition-colors"
|
|
102
86
|
>
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
87
|
+
<svg
|
|
88
|
+
width="20"
|
|
89
|
+
height="20"
|
|
90
|
+
viewBox="0 0 24 24"
|
|
91
|
+
fill="none"
|
|
92
|
+
stroke="currentColor"
|
|
93
|
+
strokeWidth="2"
|
|
94
|
+
strokeLinecap="round"
|
|
95
|
+
strokeLinejoin="round"
|
|
96
|
+
aria-hidden="true"
|
|
97
|
+
>
|
|
98
|
+
<line x1="18" y1="6" x2="6" y2="18" />
|
|
99
|
+
<line x1="6" y1="6" x2="18" y2="18" />
|
|
100
|
+
</svg>
|
|
101
|
+
</button>
|
|
102
|
+
</div>
|
|
103
|
+
<ul className="flex flex-col gap-1 px-3 py-4">
|
|
104
|
+
{brand.header.nav.map((link) => (
|
|
105
|
+
<li key={link.href}>
|
|
106
|
+
<Link
|
|
107
|
+
href={link.href}
|
|
108
|
+
onClick={() => setOpen(false)}
|
|
109
|
+
className="block px-3 py-3 rounded-md text-base font-medium text-foreground hover:bg-muted transition-colors"
|
|
110
|
+
>
|
|
111
|
+
{link.label}
|
|
112
|
+
</Link>
|
|
113
|
+
</li>
|
|
114
|
+
))}
|
|
115
|
+
</ul>
|
|
116
|
+
</nav>
|
|
117
|
+
</div>,
|
|
118
|
+
document.body,
|
|
119
|
+
)
|
|
120
|
+
: null}
|
|
111
121
|
</>
|
|
112
122
|
);
|
|
113
123
|
}
|