blacksmith-cli 0.1.5 → 0.1.7
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/index.js +1989 -690
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/src/templates/frontend/package.json.hbs +13 -5
- package/src/templates/frontend/src/__tests__/setup.ts.hbs +21 -0
- package/src/templates/frontend/src/__tests__/test-utils.tsx.hbs +81 -0
- package/src/templates/frontend/src/app.tsx.hbs +13 -9
- package/src/templates/frontend/src/features/auth/adapter.ts.hbs +7 -7
- package/src/templates/frontend/src/features/auth/components/auth-provider.tsx.hbs +91 -11
- package/src/templates/frontend/src/features/auth/hooks/use-auth.ts.hbs +3 -4
- package/src/templates/frontend/src/features/auth/pages/forgot-password-page.tsx.hbs +76 -12
- package/src/templates/frontend/src/features/auth/pages/login-page.tsx.hbs +84 -11
- package/src/templates/frontend/src/features/auth/pages/register-page.tsx.hbs +85 -14
- package/src/templates/frontend/src/features/auth/pages/reset-password-page.tsx.hbs +63 -12
- package/src/templates/frontend/src/features/auth/types.ts.hbs +32 -0
- package/src/templates/frontend/src/pages/dashboard/components/quick-start-card.tsx.hbs +19 -18
- package/src/templates/frontend/src/pages/dashboard/components/stack-cards.tsx.hbs +33 -31
- package/src/templates/frontend/src/pages/dashboard/components/welcome-header.tsx.hbs +5 -5
- package/src/templates/frontend/src/pages/dashboard/dashboard.tsx.hbs +5 -5
- package/src/templates/frontend/src/pages/home/home.tsx.hbs +48 -52
- package/src/templates/frontend/src/router/auth-guard.tsx.hbs +10 -7
- package/src/templates/frontend/src/router/error-boundary.tsx.hbs +16 -12
- package/src/templates/frontend/src/router/layouts/auth-layout.tsx.hbs +12 -12
- package/src/templates/frontend/src/router/layouts/main-layout.tsx.hbs +62 -55
- package/src/templates/frontend/src/shared/components/loading-spinner.tsx.hbs +6 -6
- package/src/templates/frontend/src/shared/components/not-found-page.tsx.hbs +1 -1
- package/src/templates/frontend/src/shared/hooks/use-debounce.ts.hbs +18 -2
- package/src/templates/frontend/src/styles/globals.css.hbs +3 -1
- package/src/templates/frontend/tailwind.config.js.hbs +1 -1
- package/src/templates/frontend/tsconfig.app.json.hbs +1 -0
- package/src/templates/frontend/vite.config.ts.hbs +8 -0
- package/src/templates/resource/frontend/components/{{kebab}}-form.tsx.hbs +3 -2
- package/src/templates/resource/frontend/pages/{{kebabs}}-page.tsx.hbs +3 -2
- package/src/templates/resource/frontend/pages/{{kebab}}-detail-page.tsx.hbs +5 -3
- package/src/templates/resource/pages/components/{{kebab}}-form.tsx.hbs +3 -2
- package/src/templates/resource/pages/{{kebabs}}-page.tsx.hbs +3 -2
- package/src/templates/resource/pages/{{kebab}}-detail-page.tsx.hbs +5 -3
package/dist/index.js
CHANGED
|
@@ -1,7 +1,768 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
+
var __esm = (fn, res) => function __init() {
|
|
4
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
5
|
+
};
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// node_modules/tsup/assets/esm_shims.js
|
|
12
|
+
import path from "path";
|
|
13
|
+
import { fileURLToPath } from "url";
|
|
14
|
+
var init_esm_shims = __esm({
|
|
15
|
+
"node_modules/tsup/assets/esm_shims.js"() {
|
|
16
|
+
"use strict";
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// node_modules/is-docker/index.js
|
|
21
|
+
import fs10 from "fs";
|
|
22
|
+
function hasDockerEnv() {
|
|
23
|
+
try {
|
|
24
|
+
fs10.statSync("/.dockerenv");
|
|
25
|
+
return true;
|
|
26
|
+
} catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function hasDockerCGroup() {
|
|
31
|
+
try {
|
|
32
|
+
return fs10.readFileSync("/proc/self/cgroup", "utf8").includes("docker");
|
|
33
|
+
} catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function isDocker() {
|
|
38
|
+
if (isDockerCached === void 0) {
|
|
39
|
+
isDockerCached = hasDockerEnv() || hasDockerCGroup();
|
|
40
|
+
}
|
|
41
|
+
return isDockerCached;
|
|
42
|
+
}
|
|
43
|
+
var isDockerCached;
|
|
44
|
+
var init_is_docker = __esm({
|
|
45
|
+
"node_modules/is-docker/index.js"() {
|
|
46
|
+
"use strict";
|
|
47
|
+
init_esm_shims();
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// node_modules/is-inside-container/index.js
|
|
52
|
+
import fs11 from "fs";
|
|
53
|
+
function isInsideContainer() {
|
|
54
|
+
if (cachedResult === void 0) {
|
|
55
|
+
cachedResult = hasContainerEnv() || isDocker();
|
|
56
|
+
}
|
|
57
|
+
return cachedResult;
|
|
58
|
+
}
|
|
59
|
+
var cachedResult, hasContainerEnv;
|
|
60
|
+
var init_is_inside_container = __esm({
|
|
61
|
+
"node_modules/is-inside-container/index.js"() {
|
|
62
|
+
"use strict";
|
|
63
|
+
init_esm_shims();
|
|
64
|
+
init_is_docker();
|
|
65
|
+
hasContainerEnv = () => {
|
|
66
|
+
try {
|
|
67
|
+
fs11.statSync("/run/.containerenv");
|
|
68
|
+
return true;
|
|
69
|
+
} catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// node_modules/is-wsl/index.js
|
|
77
|
+
import process2 from "process";
|
|
78
|
+
import os from "os";
|
|
79
|
+
import fs12 from "fs";
|
|
80
|
+
var isWsl, is_wsl_default;
|
|
81
|
+
var init_is_wsl = __esm({
|
|
82
|
+
"node_modules/is-wsl/index.js"() {
|
|
83
|
+
"use strict";
|
|
84
|
+
init_esm_shims();
|
|
85
|
+
init_is_inside_container();
|
|
86
|
+
isWsl = () => {
|
|
87
|
+
if (process2.platform !== "linux") {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
if (os.release().toLowerCase().includes("microsoft")) {
|
|
91
|
+
if (isInsideContainer()) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
if (fs12.readFileSync("/proc/version", "utf8").toLowerCase().includes("microsoft")) {
|
|
98
|
+
return !isInsideContainer();
|
|
99
|
+
}
|
|
100
|
+
} catch {
|
|
101
|
+
}
|
|
102
|
+
if (fs12.existsSync("/proc/sys/fs/binfmt_misc/WSLInterop") || fs12.existsSync("/run/WSL")) {
|
|
103
|
+
return !isInsideContainer();
|
|
104
|
+
}
|
|
105
|
+
return false;
|
|
106
|
+
};
|
|
107
|
+
is_wsl_default = process2.env.__IS_WSL_TEST__ ? isWsl : isWsl();
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// node_modules/powershell-utils/index.js
|
|
112
|
+
import process3 from "process";
|
|
113
|
+
import { Buffer as Buffer2 } from "buffer";
|
|
114
|
+
import { promisify } from "util";
|
|
115
|
+
import childProcess from "child_process";
|
|
116
|
+
import fs13, { constants as fsConstants } from "fs/promises";
|
|
117
|
+
var execFile, powerShellPath, executePowerShell;
|
|
118
|
+
var init_powershell_utils = __esm({
|
|
119
|
+
"node_modules/powershell-utils/index.js"() {
|
|
120
|
+
"use strict";
|
|
121
|
+
init_esm_shims();
|
|
122
|
+
execFile = promisify(childProcess.execFile);
|
|
123
|
+
powerShellPath = () => `${process3.env.SYSTEMROOT || process3.env.windir || String.raw`C:\Windows`}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe`;
|
|
124
|
+
executePowerShell = async (command, options = {}) => {
|
|
125
|
+
const {
|
|
126
|
+
powerShellPath: psPath,
|
|
127
|
+
...execFileOptions
|
|
128
|
+
} = options;
|
|
129
|
+
const encodedCommand = executePowerShell.encodeCommand(command);
|
|
130
|
+
return execFile(
|
|
131
|
+
psPath ?? powerShellPath(),
|
|
132
|
+
[
|
|
133
|
+
...executePowerShell.argumentsPrefix,
|
|
134
|
+
encodedCommand
|
|
135
|
+
],
|
|
136
|
+
{
|
|
137
|
+
encoding: "utf8",
|
|
138
|
+
...execFileOptions
|
|
139
|
+
}
|
|
140
|
+
);
|
|
141
|
+
};
|
|
142
|
+
executePowerShell.argumentsPrefix = [
|
|
143
|
+
"-NoProfile",
|
|
144
|
+
"-NonInteractive",
|
|
145
|
+
"-ExecutionPolicy",
|
|
146
|
+
"Bypass",
|
|
147
|
+
"-EncodedCommand"
|
|
148
|
+
];
|
|
149
|
+
executePowerShell.encodeCommand = (command) => Buffer2.from(command, "utf16le").toString("base64");
|
|
150
|
+
executePowerShell.escapeArgument = (value) => `'${String(value).replaceAll("'", "''")}'`;
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// node_modules/wsl-utils/utilities.js
|
|
155
|
+
function parseMountPointFromConfig(content) {
|
|
156
|
+
for (const line of content.split("\n")) {
|
|
157
|
+
if (/^\s*#/.test(line)) {
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
const match = /^\s*root\s*=\s*(?<mountPoint>"[^"]*"|'[^']*'|[^#]*)/.exec(line);
|
|
161
|
+
if (!match) {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
return match.groups.mountPoint.trim().replaceAll(/^["']|["']$/g, "");
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
var init_utilities = __esm({
|
|
168
|
+
"node_modules/wsl-utils/utilities.js"() {
|
|
169
|
+
"use strict";
|
|
170
|
+
init_esm_shims();
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// node_modules/wsl-utils/index.js
|
|
175
|
+
import { promisify as promisify2 } from "util";
|
|
176
|
+
import childProcess2 from "child_process";
|
|
177
|
+
import fs14, { constants as fsConstants2 } from "fs/promises";
|
|
178
|
+
var execFile2, wslDrivesMountPoint, powerShellPathFromWsl, powerShellPath2, canAccessPowerShellPromise, canAccessPowerShell, wslDefaultBrowser, convertWslPathToWindows;
|
|
179
|
+
var init_wsl_utils = __esm({
|
|
180
|
+
"node_modules/wsl-utils/index.js"() {
|
|
181
|
+
"use strict";
|
|
182
|
+
init_esm_shims();
|
|
183
|
+
init_is_wsl();
|
|
184
|
+
init_powershell_utils();
|
|
185
|
+
init_utilities();
|
|
186
|
+
init_is_wsl();
|
|
187
|
+
execFile2 = promisify2(childProcess2.execFile);
|
|
188
|
+
wslDrivesMountPoint = /* @__PURE__ */ (() => {
|
|
189
|
+
const defaultMountPoint = "/mnt/";
|
|
190
|
+
let mountPoint;
|
|
191
|
+
return async function() {
|
|
192
|
+
if (mountPoint) {
|
|
193
|
+
return mountPoint;
|
|
194
|
+
}
|
|
195
|
+
const configFilePath = "/etc/wsl.conf";
|
|
196
|
+
let isConfigFileExists = false;
|
|
197
|
+
try {
|
|
198
|
+
await fs14.access(configFilePath, fsConstants2.F_OK);
|
|
199
|
+
isConfigFileExists = true;
|
|
200
|
+
} catch {
|
|
201
|
+
}
|
|
202
|
+
if (!isConfigFileExists) {
|
|
203
|
+
return defaultMountPoint;
|
|
204
|
+
}
|
|
205
|
+
const configContent = await fs14.readFile(configFilePath, { encoding: "utf8" });
|
|
206
|
+
const parsedMountPoint = parseMountPointFromConfig(configContent);
|
|
207
|
+
if (parsedMountPoint === void 0) {
|
|
208
|
+
return defaultMountPoint;
|
|
209
|
+
}
|
|
210
|
+
mountPoint = parsedMountPoint;
|
|
211
|
+
mountPoint = mountPoint.endsWith("/") ? mountPoint : `${mountPoint}/`;
|
|
212
|
+
return mountPoint;
|
|
213
|
+
};
|
|
214
|
+
})();
|
|
215
|
+
powerShellPathFromWsl = async () => {
|
|
216
|
+
const mountPoint = await wslDrivesMountPoint();
|
|
217
|
+
return `${mountPoint}c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe`;
|
|
218
|
+
};
|
|
219
|
+
powerShellPath2 = is_wsl_default ? powerShellPathFromWsl : powerShellPath;
|
|
220
|
+
canAccessPowerShell = async () => {
|
|
221
|
+
canAccessPowerShellPromise ??= (async () => {
|
|
222
|
+
try {
|
|
223
|
+
const psPath = await powerShellPath2();
|
|
224
|
+
await fs14.access(psPath, fsConstants2.X_OK);
|
|
225
|
+
return true;
|
|
226
|
+
} catch {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
})();
|
|
230
|
+
return canAccessPowerShellPromise;
|
|
231
|
+
};
|
|
232
|
+
wslDefaultBrowser = async () => {
|
|
233
|
+
const psPath = await powerShellPath2();
|
|
234
|
+
const command = String.raw`(Get-ItemProperty -Path "HKCU:\Software\Microsoft\Windows\Shell\Associations\UrlAssociations\http\UserChoice").ProgId`;
|
|
235
|
+
const { stdout } = await executePowerShell(command, { powerShellPath: psPath });
|
|
236
|
+
return stdout.trim();
|
|
237
|
+
};
|
|
238
|
+
convertWslPathToWindows = async (path12) => {
|
|
239
|
+
if (/^[a-z]+:\/\//i.test(path12)) {
|
|
240
|
+
return path12;
|
|
241
|
+
}
|
|
242
|
+
try {
|
|
243
|
+
const { stdout } = await execFile2("wslpath", ["-aw", path12], { encoding: "utf8" });
|
|
244
|
+
return stdout.trim();
|
|
245
|
+
} catch {
|
|
246
|
+
return path12;
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// node_modules/define-lazy-prop/index.js
|
|
253
|
+
function defineLazyProperty(object, propertyName, valueGetter) {
|
|
254
|
+
const define = (value) => Object.defineProperty(object, propertyName, { value, enumerable: true, writable: true });
|
|
255
|
+
Object.defineProperty(object, propertyName, {
|
|
256
|
+
configurable: true,
|
|
257
|
+
enumerable: true,
|
|
258
|
+
get() {
|
|
259
|
+
const result = valueGetter();
|
|
260
|
+
define(result);
|
|
261
|
+
return result;
|
|
262
|
+
},
|
|
263
|
+
set(value) {
|
|
264
|
+
define(value);
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
return object;
|
|
268
|
+
}
|
|
269
|
+
var init_define_lazy_prop = __esm({
|
|
270
|
+
"node_modules/define-lazy-prop/index.js"() {
|
|
271
|
+
"use strict";
|
|
272
|
+
init_esm_shims();
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// node_modules/default-browser-id/index.js
|
|
277
|
+
import { promisify as promisify3 } from "util";
|
|
278
|
+
import process4 from "process";
|
|
279
|
+
import { execFile as execFile3 } from "child_process";
|
|
280
|
+
async function defaultBrowserId() {
|
|
281
|
+
if (process4.platform !== "darwin") {
|
|
282
|
+
throw new Error("macOS only");
|
|
283
|
+
}
|
|
284
|
+
const { stdout } = await execFileAsync("defaults", ["read", "com.apple.LaunchServices/com.apple.launchservices.secure", "LSHandlers"]);
|
|
285
|
+
const match = /LSHandlerRoleAll = "(?!-)(?<id>[^"]+?)";\s+?LSHandlerURLScheme = (?:http|https);/.exec(stdout);
|
|
286
|
+
const browserId = match?.groups.id ?? "com.apple.Safari";
|
|
287
|
+
if (browserId === "com.apple.safari") {
|
|
288
|
+
return "com.apple.Safari";
|
|
289
|
+
}
|
|
290
|
+
return browserId;
|
|
291
|
+
}
|
|
292
|
+
var execFileAsync;
|
|
293
|
+
var init_default_browser_id = __esm({
|
|
294
|
+
"node_modules/default-browser-id/index.js"() {
|
|
295
|
+
"use strict";
|
|
296
|
+
init_esm_shims();
|
|
297
|
+
execFileAsync = promisify3(execFile3);
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
// node_modules/run-applescript/index.js
|
|
302
|
+
import process5 from "process";
|
|
303
|
+
import { promisify as promisify4 } from "util";
|
|
304
|
+
import { execFile as execFile4, execFileSync } from "child_process";
|
|
305
|
+
async function runAppleScript(script, { humanReadableOutput = true, signal } = {}) {
|
|
306
|
+
if (process5.platform !== "darwin") {
|
|
307
|
+
throw new Error("macOS only");
|
|
308
|
+
}
|
|
309
|
+
const outputArguments = humanReadableOutput ? [] : ["-ss"];
|
|
310
|
+
const execOptions = {};
|
|
311
|
+
if (signal) {
|
|
312
|
+
execOptions.signal = signal;
|
|
313
|
+
}
|
|
314
|
+
const { stdout } = await execFileAsync2("osascript", ["-e", script, outputArguments], execOptions);
|
|
315
|
+
return stdout.trim();
|
|
316
|
+
}
|
|
317
|
+
var execFileAsync2;
|
|
318
|
+
var init_run_applescript = __esm({
|
|
319
|
+
"node_modules/run-applescript/index.js"() {
|
|
320
|
+
"use strict";
|
|
321
|
+
init_esm_shims();
|
|
322
|
+
execFileAsync2 = promisify4(execFile4);
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
// node_modules/bundle-name/index.js
|
|
327
|
+
async function bundleName(bundleId) {
|
|
328
|
+
return runAppleScript(`tell application "Finder" to set app_path to application file id "${bundleId}" as string
|
|
329
|
+
tell application "System Events" to get value of property list item "CFBundleName" of property list file (app_path & ":Contents:Info.plist")`);
|
|
330
|
+
}
|
|
331
|
+
var init_bundle_name = __esm({
|
|
332
|
+
"node_modules/bundle-name/index.js"() {
|
|
333
|
+
"use strict";
|
|
334
|
+
init_esm_shims();
|
|
335
|
+
init_run_applescript();
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
// node_modules/default-browser/windows.js
|
|
340
|
+
import { promisify as promisify5 } from "util";
|
|
341
|
+
import { execFile as execFile5 } from "child_process";
|
|
342
|
+
async function defaultBrowser(_execFileAsync = execFileAsync3) {
|
|
343
|
+
const { stdout } = await _execFileAsync("reg", [
|
|
344
|
+
"QUERY",
|
|
345
|
+
" HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\Shell\\Associations\\UrlAssociations\\http\\UserChoice",
|
|
346
|
+
"/v",
|
|
347
|
+
"ProgId"
|
|
348
|
+
]);
|
|
349
|
+
const match = /ProgId\s*REG_SZ\s*(?<id>\S+)/.exec(stdout);
|
|
350
|
+
if (!match) {
|
|
351
|
+
throw new UnknownBrowserError(`Cannot find Windows browser in stdout: ${JSON.stringify(stdout)}`);
|
|
352
|
+
}
|
|
353
|
+
const { id } = match.groups;
|
|
354
|
+
const dotIndex = id.lastIndexOf(".");
|
|
355
|
+
const hyphenIndex = id.lastIndexOf("-");
|
|
356
|
+
const baseIdByDot = dotIndex === -1 ? void 0 : id.slice(0, dotIndex);
|
|
357
|
+
const baseIdByHyphen = hyphenIndex === -1 ? void 0 : id.slice(0, hyphenIndex);
|
|
358
|
+
return windowsBrowserProgIds[id] ?? windowsBrowserProgIds[baseIdByDot] ?? windowsBrowserProgIds[baseIdByHyphen] ?? { name: id, id };
|
|
359
|
+
}
|
|
360
|
+
var execFileAsync3, windowsBrowserProgIds, _windowsBrowserProgIdMap, UnknownBrowserError;
|
|
361
|
+
var init_windows = __esm({
|
|
362
|
+
"node_modules/default-browser/windows.js"() {
|
|
363
|
+
"use strict";
|
|
364
|
+
init_esm_shims();
|
|
365
|
+
execFileAsync3 = promisify5(execFile5);
|
|
366
|
+
windowsBrowserProgIds = {
|
|
367
|
+
MSEdgeHTM: { name: "Edge", id: "com.microsoft.edge" },
|
|
368
|
+
// The missing `L` is correct.
|
|
369
|
+
MSEdgeBHTML: { name: "Edge Beta", id: "com.microsoft.edge.beta" },
|
|
370
|
+
MSEdgeDHTML: { name: "Edge Dev", id: "com.microsoft.edge.dev" },
|
|
371
|
+
AppXq0fevzme2pys62n3e0fbqa7peapykr8v: { name: "Edge", id: "com.microsoft.edge.old" },
|
|
372
|
+
ChromeHTML: { name: "Chrome", id: "com.google.chrome" },
|
|
373
|
+
ChromeBHTML: { name: "Chrome Beta", id: "com.google.chrome.beta" },
|
|
374
|
+
ChromeDHTML: { name: "Chrome Dev", id: "com.google.chrome.dev" },
|
|
375
|
+
ChromiumHTM: { name: "Chromium", id: "org.chromium.Chromium" },
|
|
376
|
+
BraveHTML: { name: "Brave", id: "com.brave.Browser" },
|
|
377
|
+
BraveBHTML: { name: "Brave Beta", id: "com.brave.Browser.beta" },
|
|
378
|
+
BraveDHTML: { name: "Brave Dev", id: "com.brave.Browser.dev" },
|
|
379
|
+
BraveSSHTM: { name: "Brave Nightly", id: "com.brave.Browser.nightly" },
|
|
380
|
+
FirefoxURL: { name: "Firefox", id: "org.mozilla.firefox" },
|
|
381
|
+
OperaStable: { name: "Opera", id: "com.operasoftware.Opera" },
|
|
382
|
+
VivaldiHTM: { name: "Vivaldi", id: "com.vivaldi.Vivaldi" },
|
|
383
|
+
"IE.HTTP": { name: "Internet Explorer", id: "com.microsoft.ie" }
|
|
384
|
+
};
|
|
385
|
+
_windowsBrowserProgIdMap = new Map(Object.entries(windowsBrowserProgIds));
|
|
386
|
+
UnknownBrowserError = class extends Error {
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
// node_modules/default-browser/index.js
|
|
392
|
+
import { promisify as promisify6 } from "util";
|
|
393
|
+
import process6 from "process";
|
|
394
|
+
import { execFile as execFile6 } from "child_process";
|
|
395
|
+
async function defaultBrowser2() {
|
|
396
|
+
if (process6.platform === "darwin") {
|
|
397
|
+
const id = await defaultBrowserId();
|
|
398
|
+
const name = await bundleName(id);
|
|
399
|
+
return { name, id };
|
|
400
|
+
}
|
|
401
|
+
if (process6.platform === "linux") {
|
|
402
|
+
const { stdout } = await execFileAsync4("xdg-mime", ["query", "default", "x-scheme-handler/http"]);
|
|
403
|
+
const id = stdout.trim();
|
|
404
|
+
const name = titleize(id.replace(/.desktop$/, "").replace("-", " "));
|
|
405
|
+
return { name, id };
|
|
406
|
+
}
|
|
407
|
+
if (process6.platform === "win32") {
|
|
408
|
+
return defaultBrowser();
|
|
409
|
+
}
|
|
410
|
+
throw new Error("Only macOS, Linux, and Windows are supported");
|
|
411
|
+
}
|
|
412
|
+
var execFileAsync4, titleize;
|
|
413
|
+
var init_default_browser = __esm({
|
|
414
|
+
"node_modules/default-browser/index.js"() {
|
|
415
|
+
"use strict";
|
|
416
|
+
init_esm_shims();
|
|
417
|
+
init_default_browser_id();
|
|
418
|
+
init_bundle_name();
|
|
419
|
+
init_windows();
|
|
420
|
+
init_windows();
|
|
421
|
+
execFileAsync4 = promisify6(execFile6);
|
|
422
|
+
titleize = (string) => string.toLowerCase().replaceAll(/(?:^|\s|-)\S/g, (x) => x.toUpperCase());
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
// node_modules/is-in-ssh/index.js
|
|
427
|
+
import process7 from "process";
|
|
428
|
+
var isInSsh, is_in_ssh_default;
|
|
429
|
+
var init_is_in_ssh = __esm({
|
|
430
|
+
"node_modules/is-in-ssh/index.js"() {
|
|
431
|
+
"use strict";
|
|
432
|
+
init_esm_shims();
|
|
433
|
+
isInSsh = Boolean(process7.env.SSH_CONNECTION || process7.env.SSH_CLIENT || process7.env.SSH_TTY);
|
|
434
|
+
is_in_ssh_default = isInSsh;
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
// node_modules/open/index.js
|
|
439
|
+
var open_exports = {};
|
|
440
|
+
__export(open_exports, {
|
|
441
|
+
apps: () => apps,
|
|
442
|
+
default: () => open_default,
|
|
443
|
+
openApp: () => openApp
|
|
444
|
+
});
|
|
445
|
+
import process8 from "process";
|
|
446
|
+
import path11 from "path";
|
|
447
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
448
|
+
import childProcess3 from "child_process";
|
|
449
|
+
import fs15, { constants as fsConstants3 } from "fs/promises";
|
|
450
|
+
function detectArchBinary(binary) {
|
|
451
|
+
if (typeof binary === "string" || Array.isArray(binary)) {
|
|
452
|
+
return binary;
|
|
453
|
+
}
|
|
454
|
+
const { [arch]: archBinary } = binary;
|
|
455
|
+
if (!archBinary) {
|
|
456
|
+
throw new Error(`${arch} is not supported`);
|
|
457
|
+
}
|
|
458
|
+
return archBinary;
|
|
459
|
+
}
|
|
460
|
+
function detectPlatformBinary({ [platform]: platformBinary }, { wsl } = {}) {
|
|
461
|
+
if (wsl && is_wsl_default) {
|
|
462
|
+
return detectArchBinary(wsl);
|
|
463
|
+
}
|
|
464
|
+
if (!platformBinary) {
|
|
465
|
+
throw new Error(`${platform} is not supported`);
|
|
466
|
+
}
|
|
467
|
+
return detectArchBinary(platformBinary);
|
|
468
|
+
}
|
|
469
|
+
var fallbackAttemptSymbol, __dirname3, localXdgOpenPath, platform, arch, tryEachApp, baseOpen, open, openApp, apps, open_default;
|
|
470
|
+
var init_open = __esm({
|
|
471
|
+
"node_modules/open/index.js"() {
|
|
472
|
+
"use strict";
|
|
473
|
+
init_esm_shims();
|
|
474
|
+
init_wsl_utils();
|
|
475
|
+
init_powershell_utils();
|
|
476
|
+
init_define_lazy_prop();
|
|
477
|
+
init_default_browser();
|
|
478
|
+
init_is_inside_container();
|
|
479
|
+
init_is_in_ssh();
|
|
480
|
+
fallbackAttemptSymbol = /* @__PURE__ */ Symbol("fallbackAttempt");
|
|
481
|
+
__dirname3 = import.meta.url ? path11.dirname(fileURLToPath3(import.meta.url)) : "";
|
|
482
|
+
localXdgOpenPath = path11.join(__dirname3, "xdg-open");
|
|
483
|
+
({ platform, arch } = process8);
|
|
484
|
+
tryEachApp = async (apps2, opener) => {
|
|
485
|
+
if (apps2.length === 0) {
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
const errors = [];
|
|
489
|
+
for (const app of apps2) {
|
|
490
|
+
try {
|
|
491
|
+
return await opener(app);
|
|
492
|
+
} catch (error) {
|
|
493
|
+
errors.push(error);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
throw new AggregateError(errors, "Failed to open in all supported apps");
|
|
497
|
+
};
|
|
498
|
+
baseOpen = async (options) => {
|
|
499
|
+
options = {
|
|
500
|
+
wait: false,
|
|
501
|
+
background: false,
|
|
502
|
+
newInstance: false,
|
|
503
|
+
allowNonzeroExitCode: false,
|
|
504
|
+
...options
|
|
505
|
+
};
|
|
506
|
+
const isFallbackAttempt = options[fallbackAttemptSymbol] === true;
|
|
507
|
+
delete options[fallbackAttemptSymbol];
|
|
508
|
+
if (Array.isArray(options.app)) {
|
|
509
|
+
return tryEachApp(options.app, (singleApp) => baseOpen({
|
|
510
|
+
...options,
|
|
511
|
+
app: singleApp,
|
|
512
|
+
[fallbackAttemptSymbol]: true
|
|
513
|
+
}));
|
|
514
|
+
}
|
|
515
|
+
let { name: app, arguments: appArguments = [] } = options.app ?? {};
|
|
516
|
+
appArguments = [...appArguments];
|
|
517
|
+
if (Array.isArray(app)) {
|
|
518
|
+
return tryEachApp(app, (appName) => baseOpen({
|
|
519
|
+
...options,
|
|
520
|
+
app: {
|
|
521
|
+
name: appName,
|
|
522
|
+
arguments: appArguments
|
|
523
|
+
},
|
|
524
|
+
[fallbackAttemptSymbol]: true
|
|
525
|
+
}));
|
|
526
|
+
}
|
|
527
|
+
if (app === "browser" || app === "browserPrivate") {
|
|
528
|
+
const ids = {
|
|
529
|
+
"com.google.chrome": "chrome",
|
|
530
|
+
"google-chrome.desktop": "chrome",
|
|
531
|
+
"com.brave.browser": "brave",
|
|
532
|
+
"org.mozilla.firefox": "firefox",
|
|
533
|
+
"firefox.desktop": "firefox",
|
|
534
|
+
"com.microsoft.msedge": "edge",
|
|
535
|
+
"com.microsoft.edge": "edge",
|
|
536
|
+
"com.microsoft.edgemac": "edge",
|
|
537
|
+
"microsoft-edge.desktop": "edge",
|
|
538
|
+
"com.apple.safari": "safari"
|
|
539
|
+
};
|
|
540
|
+
const flags = {
|
|
541
|
+
chrome: "--incognito",
|
|
542
|
+
brave: "--incognito",
|
|
543
|
+
firefox: "--private-window",
|
|
544
|
+
edge: "--inPrivate"
|
|
545
|
+
// Safari doesn't support private mode via command line
|
|
546
|
+
};
|
|
547
|
+
let browser;
|
|
548
|
+
if (is_wsl_default) {
|
|
549
|
+
const progId = await wslDefaultBrowser();
|
|
550
|
+
const browserInfo = _windowsBrowserProgIdMap.get(progId);
|
|
551
|
+
browser = browserInfo ?? {};
|
|
552
|
+
} else {
|
|
553
|
+
browser = await defaultBrowser2();
|
|
554
|
+
}
|
|
555
|
+
if (browser.id in ids) {
|
|
556
|
+
const browserName = ids[browser.id.toLowerCase()];
|
|
557
|
+
if (app === "browserPrivate") {
|
|
558
|
+
if (browserName === "safari") {
|
|
559
|
+
throw new Error("Safari doesn't support opening in private mode via command line");
|
|
560
|
+
}
|
|
561
|
+
appArguments.push(flags[browserName]);
|
|
562
|
+
}
|
|
563
|
+
return baseOpen({
|
|
564
|
+
...options,
|
|
565
|
+
app: {
|
|
566
|
+
name: apps[browserName],
|
|
567
|
+
arguments: appArguments
|
|
568
|
+
}
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
throw new Error(`${browser.name} is not supported as a default browser`);
|
|
572
|
+
}
|
|
573
|
+
let command;
|
|
574
|
+
const cliArguments = [];
|
|
575
|
+
const childProcessOptions = {};
|
|
576
|
+
let shouldUseWindowsInWsl = false;
|
|
577
|
+
if (is_wsl_default && !isInsideContainer() && !is_in_ssh_default && !app) {
|
|
578
|
+
shouldUseWindowsInWsl = await canAccessPowerShell();
|
|
579
|
+
}
|
|
580
|
+
if (platform === "darwin") {
|
|
581
|
+
command = "open";
|
|
582
|
+
if (options.wait) {
|
|
583
|
+
cliArguments.push("--wait-apps");
|
|
584
|
+
}
|
|
585
|
+
if (options.background) {
|
|
586
|
+
cliArguments.push("--background");
|
|
587
|
+
}
|
|
588
|
+
if (options.newInstance) {
|
|
589
|
+
cliArguments.push("--new");
|
|
590
|
+
}
|
|
591
|
+
if (app) {
|
|
592
|
+
cliArguments.push("-a", app);
|
|
593
|
+
}
|
|
594
|
+
} else if (platform === "win32" || shouldUseWindowsInWsl) {
|
|
595
|
+
command = await powerShellPath2();
|
|
596
|
+
cliArguments.push(...executePowerShell.argumentsPrefix);
|
|
597
|
+
if (!is_wsl_default) {
|
|
598
|
+
childProcessOptions.windowsVerbatimArguments = true;
|
|
599
|
+
}
|
|
600
|
+
if (is_wsl_default && options.target) {
|
|
601
|
+
options.target = await convertWslPathToWindows(options.target);
|
|
602
|
+
}
|
|
603
|
+
const encodedArguments = ["$ProgressPreference = 'SilentlyContinue';", "Start"];
|
|
604
|
+
if (options.wait) {
|
|
605
|
+
encodedArguments.push("-Wait");
|
|
606
|
+
}
|
|
607
|
+
if (app) {
|
|
608
|
+
encodedArguments.push(executePowerShell.escapeArgument(app));
|
|
609
|
+
if (options.target) {
|
|
610
|
+
appArguments.push(options.target);
|
|
611
|
+
}
|
|
612
|
+
} else if (options.target) {
|
|
613
|
+
encodedArguments.push(executePowerShell.escapeArgument(options.target));
|
|
614
|
+
}
|
|
615
|
+
if (appArguments.length > 0) {
|
|
616
|
+
appArguments = appArguments.map((argument) => executePowerShell.escapeArgument(argument));
|
|
617
|
+
encodedArguments.push("-ArgumentList", appArguments.join(","));
|
|
618
|
+
}
|
|
619
|
+
options.target = executePowerShell.encodeCommand(encodedArguments.join(" "));
|
|
620
|
+
if (!options.wait) {
|
|
621
|
+
childProcessOptions.stdio = "ignore";
|
|
622
|
+
}
|
|
623
|
+
} else {
|
|
624
|
+
if (app) {
|
|
625
|
+
command = app;
|
|
626
|
+
} else {
|
|
627
|
+
const isBundled = !__dirname3 || __dirname3 === "/";
|
|
628
|
+
let exeLocalXdgOpen = false;
|
|
629
|
+
try {
|
|
630
|
+
await fs15.access(localXdgOpenPath, fsConstants3.X_OK);
|
|
631
|
+
exeLocalXdgOpen = true;
|
|
632
|
+
} catch {
|
|
633
|
+
}
|
|
634
|
+
const useSystemXdgOpen = process8.versions.electron ?? (platform === "android" || isBundled || !exeLocalXdgOpen);
|
|
635
|
+
command = useSystemXdgOpen ? "xdg-open" : localXdgOpenPath;
|
|
636
|
+
}
|
|
637
|
+
if (appArguments.length > 0) {
|
|
638
|
+
cliArguments.push(...appArguments);
|
|
639
|
+
}
|
|
640
|
+
if (!options.wait) {
|
|
641
|
+
childProcessOptions.stdio = "ignore";
|
|
642
|
+
childProcessOptions.detached = true;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
if (platform === "darwin" && appArguments.length > 0) {
|
|
646
|
+
cliArguments.push("--args", ...appArguments);
|
|
647
|
+
}
|
|
648
|
+
if (options.target) {
|
|
649
|
+
cliArguments.push(options.target);
|
|
650
|
+
}
|
|
651
|
+
const subprocess = childProcess3.spawn(command, cliArguments, childProcessOptions);
|
|
652
|
+
if (options.wait) {
|
|
653
|
+
return new Promise((resolve, reject) => {
|
|
654
|
+
subprocess.once("error", reject);
|
|
655
|
+
subprocess.once("close", (exitCode) => {
|
|
656
|
+
if (!options.allowNonzeroExitCode && exitCode !== 0) {
|
|
657
|
+
reject(new Error(`Exited with code ${exitCode}`));
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
resolve(subprocess);
|
|
661
|
+
});
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
if (isFallbackAttempt) {
|
|
665
|
+
return new Promise((resolve, reject) => {
|
|
666
|
+
subprocess.once("error", reject);
|
|
667
|
+
subprocess.once("spawn", () => {
|
|
668
|
+
subprocess.once("close", (exitCode) => {
|
|
669
|
+
subprocess.off("error", reject);
|
|
670
|
+
if (exitCode !== 0) {
|
|
671
|
+
reject(new Error(`Exited with code ${exitCode}`));
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
subprocess.unref();
|
|
675
|
+
resolve(subprocess);
|
|
676
|
+
});
|
|
677
|
+
});
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
subprocess.unref();
|
|
681
|
+
return new Promise((resolve, reject) => {
|
|
682
|
+
subprocess.once("error", reject);
|
|
683
|
+
subprocess.once("spawn", () => {
|
|
684
|
+
subprocess.off("error", reject);
|
|
685
|
+
resolve(subprocess);
|
|
686
|
+
});
|
|
687
|
+
});
|
|
688
|
+
};
|
|
689
|
+
open = (target, options) => {
|
|
690
|
+
if (typeof target !== "string") {
|
|
691
|
+
throw new TypeError("Expected a `target`");
|
|
692
|
+
}
|
|
693
|
+
return baseOpen({
|
|
694
|
+
...options,
|
|
695
|
+
target
|
|
696
|
+
});
|
|
697
|
+
};
|
|
698
|
+
openApp = (name, options) => {
|
|
699
|
+
if (typeof name !== "string" && !Array.isArray(name)) {
|
|
700
|
+
throw new TypeError("Expected a valid `name`");
|
|
701
|
+
}
|
|
702
|
+
const { arguments: appArguments = [] } = options ?? {};
|
|
703
|
+
if (appArguments !== void 0 && appArguments !== null && !Array.isArray(appArguments)) {
|
|
704
|
+
throw new TypeError("Expected `appArguments` as Array type");
|
|
705
|
+
}
|
|
706
|
+
return baseOpen({
|
|
707
|
+
...options,
|
|
708
|
+
app: {
|
|
709
|
+
name,
|
|
710
|
+
arguments: appArguments
|
|
711
|
+
}
|
|
712
|
+
});
|
|
713
|
+
};
|
|
714
|
+
apps = {
|
|
715
|
+
browser: "browser",
|
|
716
|
+
browserPrivate: "browserPrivate"
|
|
717
|
+
};
|
|
718
|
+
defineLazyProperty(apps, "chrome", () => detectPlatformBinary({
|
|
719
|
+
darwin: "google chrome",
|
|
720
|
+
win32: "chrome",
|
|
721
|
+
// `chromium-browser` is the older deb package name used by Ubuntu/Debian before snap.
|
|
722
|
+
linux: ["google-chrome", "google-chrome-stable", "chromium", "chromium-browser"]
|
|
723
|
+
}, {
|
|
724
|
+
wsl: {
|
|
725
|
+
ia32: "/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe",
|
|
726
|
+
x64: ["/mnt/c/Program Files/Google/Chrome/Application/chrome.exe", "/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe"]
|
|
727
|
+
}
|
|
728
|
+
}));
|
|
729
|
+
defineLazyProperty(apps, "brave", () => detectPlatformBinary({
|
|
730
|
+
darwin: "brave browser",
|
|
731
|
+
win32: "brave",
|
|
732
|
+
linux: ["brave-browser", "brave"]
|
|
733
|
+
}, {
|
|
734
|
+
wsl: {
|
|
735
|
+
ia32: "/mnt/c/Program Files (x86)/BraveSoftware/Brave-Browser/Application/brave.exe",
|
|
736
|
+
x64: ["/mnt/c/Program Files/BraveSoftware/Brave-Browser/Application/brave.exe", "/mnt/c/Program Files (x86)/BraveSoftware/Brave-Browser/Application/brave.exe"]
|
|
737
|
+
}
|
|
738
|
+
}));
|
|
739
|
+
defineLazyProperty(apps, "firefox", () => detectPlatformBinary({
|
|
740
|
+
darwin: "firefox",
|
|
741
|
+
win32: String.raw`C:\Program Files\Mozilla Firefox\firefox.exe`,
|
|
742
|
+
linux: "firefox"
|
|
743
|
+
}, {
|
|
744
|
+
wsl: "/mnt/c/Program Files/Mozilla Firefox/firefox.exe"
|
|
745
|
+
}));
|
|
746
|
+
defineLazyProperty(apps, "edge", () => detectPlatformBinary({
|
|
747
|
+
darwin: "microsoft edge",
|
|
748
|
+
win32: "msedge",
|
|
749
|
+
linux: ["microsoft-edge", "microsoft-edge-dev"]
|
|
750
|
+
}, {
|
|
751
|
+
wsl: "/mnt/c/Program Files (x86)/Microsoft/Edge/Application/msedge.exe"
|
|
752
|
+
}));
|
|
753
|
+
defineLazyProperty(apps, "safari", () => detectPlatformBinary({
|
|
754
|
+
darwin: "Safari"
|
|
755
|
+
}));
|
|
756
|
+
open_default = open;
|
|
757
|
+
}
|
|
758
|
+
});
|
|
759
|
+
|
|
1
760
|
// src/index.ts
|
|
761
|
+
init_esm_shims();
|
|
2
762
|
import { Command } from "commander";
|
|
3
763
|
|
|
4
764
|
// src/utils/logger.ts
|
|
765
|
+
init_esm_shims();
|
|
5
766
|
import chalk from "chalk";
|
|
6
767
|
import ora from "ora";
|
|
7
768
|
import { createInterface } from "readline";
|
|
@@ -114,13 +875,15 @@ function printNextSteps(projectName, backendPort = 8e3, frontendPort = 5173) {
|
|
|
114
875
|
}
|
|
115
876
|
|
|
116
877
|
// src/commands/init.ts
|
|
117
|
-
|
|
878
|
+
init_esm_shims();
|
|
879
|
+
import path5 from "path";
|
|
118
880
|
import fs4 from "fs";
|
|
119
881
|
import { spawn } from "child_process";
|
|
120
882
|
|
|
121
883
|
// src/utils/template.ts
|
|
884
|
+
init_esm_shims();
|
|
122
885
|
import fs from "fs";
|
|
123
|
-
import
|
|
886
|
+
import path2 from "path";
|
|
124
887
|
import Handlebars from "handlebars";
|
|
125
888
|
Handlebars.registerHelper("eq", (a, b) => a === b);
|
|
126
889
|
Handlebars.registerHelper("ne", (a, b) => a !== b);
|
|
@@ -138,7 +901,7 @@ function renderTemplateFile(templatePath, context) {
|
|
|
138
901
|
}
|
|
139
902
|
function renderToFile(templatePath, destPath, context) {
|
|
140
903
|
const rendered = renderTemplateFile(templatePath, context);
|
|
141
|
-
const destDir =
|
|
904
|
+
const destDir = path2.dirname(destPath);
|
|
142
905
|
if (!fs.existsSync(destDir)) {
|
|
143
906
|
fs.mkdirSync(destDir, { recursive: true });
|
|
144
907
|
}
|
|
@@ -151,17 +914,17 @@ function renderDirectory(srcDir, destDir, context) {
|
|
|
151
914
|
const entries = fs.readdirSync(srcDir, { withFileTypes: true });
|
|
152
915
|
for (const entry of entries) {
|
|
153
916
|
const renderedName = renderTemplate(entry.name, context);
|
|
154
|
-
const srcPath =
|
|
917
|
+
const srcPath = path2.join(srcDir, entry.name);
|
|
155
918
|
if (entry.isDirectory()) {
|
|
156
|
-
const destSubDir =
|
|
919
|
+
const destSubDir = path2.join(destDir, renderedName);
|
|
157
920
|
renderDirectory(srcPath, destSubDir, context);
|
|
158
921
|
} else if (entry.name.endsWith(".hbs")) {
|
|
159
922
|
const outputName = renderedName.replace(/\.hbs$/, "");
|
|
160
|
-
const destPath =
|
|
923
|
+
const destPath = path2.join(destDir, outputName);
|
|
161
924
|
renderToFile(srcPath, destPath, context);
|
|
162
925
|
} else {
|
|
163
|
-
const destPath =
|
|
164
|
-
const destDirPath =
|
|
926
|
+
const destPath = path2.join(destDir, renderedName);
|
|
927
|
+
const destDirPath = path2.dirname(destPath);
|
|
165
928
|
if (!fs.existsSync(destDirPath)) {
|
|
166
929
|
fs.mkdirSync(destDirPath, { recursive: true });
|
|
167
930
|
}
|
|
@@ -191,6 +954,7 @@ function insertBeforeMarker(filePath, marker, content) {
|
|
|
191
954
|
}
|
|
192
955
|
|
|
193
956
|
// src/utils/exec.ts
|
|
957
|
+
init_esm_shims();
|
|
194
958
|
import { execa } from "execa";
|
|
195
959
|
async function exec(command, args, options = {}) {
|
|
196
960
|
const { cwd, silent = false, env } = options;
|
|
@@ -229,25 +993,26 @@ async function execPip(args, cwd, silent = false) {
|
|
|
229
993
|
}
|
|
230
994
|
|
|
231
995
|
// src/utils/paths.ts
|
|
232
|
-
|
|
996
|
+
init_esm_shims();
|
|
997
|
+
import path3 from "path";
|
|
233
998
|
import fs2 from "fs";
|
|
234
|
-
import { fileURLToPath } from "url";
|
|
235
|
-
var __filename2 =
|
|
236
|
-
var __dirname2 =
|
|
999
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1000
|
+
var __filename2 = fileURLToPath2(import.meta.url);
|
|
1001
|
+
var __dirname2 = path3.dirname(__filename2);
|
|
237
1002
|
function getTemplatesDir() {
|
|
238
|
-
const devPath =
|
|
239
|
-
const prodPath =
|
|
1003
|
+
const devPath = path3.resolve(__dirname2, "..", "templates");
|
|
1004
|
+
const prodPath = path3.resolve(__dirname2, "..", "src", "templates");
|
|
240
1005
|
if (fs2.existsSync(devPath)) return devPath;
|
|
241
1006
|
if (fs2.existsSync(prodPath)) return prodPath;
|
|
242
1007
|
throw new Error("Templates directory not found. Make sure the CLI is properly installed.");
|
|
243
1008
|
}
|
|
244
1009
|
function findProjectRoot(startDir) {
|
|
245
1010
|
let dir = startDir || process.cwd();
|
|
246
|
-
while (dir !==
|
|
247
|
-
if (fs2.existsSync(
|
|
1011
|
+
while (dir !== path3.dirname(dir)) {
|
|
1012
|
+
if (fs2.existsSync(path3.join(dir, "blacksmith.config.json"))) {
|
|
248
1013
|
return dir;
|
|
249
1014
|
}
|
|
250
|
-
dir =
|
|
1015
|
+
dir = path3.dirname(dir);
|
|
251
1016
|
}
|
|
252
1017
|
throw new Error(
|
|
253
1018
|
'Not inside a Blacksmith project. Run "blacksmith init <name>" to create one, or navigate to an existing Blacksmith project.'
|
|
@@ -255,23 +1020,25 @@ function findProjectRoot(startDir) {
|
|
|
255
1020
|
}
|
|
256
1021
|
function getBackendDir(projectRoot) {
|
|
257
1022
|
const root = projectRoot || findProjectRoot();
|
|
258
|
-
return
|
|
1023
|
+
return path3.join(root, "backend");
|
|
259
1024
|
}
|
|
260
1025
|
function getFrontendDir(projectRoot) {
|
|
261
1026
|
const root = projectRoot || findProjectRoot();
|
|
262
|
-
return
|
|
1027
|
+
return path3.join(root, "frontend");
|
|
263
1028
|
}
|
|
264
1029
|
function loadConfig(projectRoot) {
|
|
265
1030
|
const root = projectRoot || findProjectRoot();
|
|
266
|
-
const configPath =
|
|
1031
|
+
const configPath = path3.join(root, "blacksmith.config.json");
|
|
267
1032
|
return JSON.parse(fs2.readFileSync(configPath, "utf-8"));
|
|
268
1033
|
}
|
|
269
1034
|
|
|
270
1035
|
// src/commands/ai-setup.ts
|
|
271
|
-
|
|
1036
|
+
init_esm_shims();
|
|
1037
|
+
import path4 from "path";
|
|
272
1038
|
import fs3 from "fs";
|
|
273
1039
|
|
|
274
1040
|
// src/skills/core-rules.ts
|
|
1041
|
+
init_esm_shims();
|
|
275
1042
|
var coreRulesSkill = {
|
|
276
1043
|
id: "core-rules",
|
|
277
1044
|
// No `name` → content is inlined directly into CLAUDE.md, not a separate file
|
|
@@ -280,12 +1047,12 @@ var coreRulesSkill = {
|
|
|
280
1047
|
|
|
281
1048
|
> **These rules are mandatory. Violating them produces broken, inconsistent code.**
|
|
282
1049
|
|
|
283
|
-
### 1. Use \`@
|
|
284
|
-
- **Layout**: Use \`
|
|
285
|
-
- **Typography**: Use \`
|
|
286
|
-
- **Separators**: Use \`Divider\` \u2014 NEVER \`<hr>\`
|
|
287
|
-
- **Everything else**: \`Button\`, \`Card\`, \`Badge\`, \`Input\`, \`Table\`, \`
|
|
288
|
-
- See the \`
|
|
1050
|
+
### 1. Use \`@chakra-ui/react\` for ALL UI
|
|
1051
|
+
- **Layout**: Use \`VStack\`, \`HStack\`, \`Flex\`, \`SimpleGrid\`, \`Box\`, \`Container\` \u2014 NEVER \`<div className="flex ...">\` or \`<div className="grid ...">\`
|
|
1052
|
+
- **Typography**: Use \`Heading\` and \`Text\` \u2014 NEVER raw \`<h1>\`\u2013\`<h6>\`, \`<p>\`, or \`<span>\` with text classes
|
|
1053
|
+
- **Separators**: Use \`Divider\` \u2014 NEVER \`<hr>\`
|
|
1054
|
+
- **Everything else**: \`Button\`, \`Card\`, \`Badge\`, \`Input\`, \`Table\`, \`Modal\`, \`Alert\`, \`Skeleton\`, \`Stat\`, etc.
|
|
1055
|
+
- See the \`chakra-ui-react\` skill for the full component list
|
|
289
1056
|
|
|
290
1057
|
### 2. Pages Are Thin Orchestrators
|
|
291
1058
|
- A page file should be ~20-30 lines: import components, call hooks, compose JSX
|
|
@@ -319,6 +1086,7 @@ pages/<page>/
|
|
|
319
1086
|
};
|
|
320
1087
|
|
|
321
1088
|
// src/skills/project-overview.ts
|
|
1089
|
+
init_esm_shims();
|
|
322
1090
|
var projectOverviewSkill = {
|
|
323
1091
|
id: "project-overview",
|
|
324
1092
|
name: "Project Overview",
|
|
@@ -374,6 +1142,7 @@ ${ctx.projectName}/
|
|
|
374
1142
|
};
|
|
375
1143
|
|
|
376
1144
|
// src/skills/django.ts
|
|
1145
|
+
init_esm_shims();
|
|
377
1146
|
var djangoSkill = {
|
|
378
1147
|
id: "django",
|
|
379
1148
|
name: "Django Backend Conventions",
|
|
@@ -455,6 +1224,7 @@ var djangoSkill = {
|
|
|
455
1224
|
};
|
|
456
1225
|
|
|
457
1226
|
// src/skills/django-rest-advanced.ts
|
|
1227
|
+
init_esm_shims();
|
|
458
1228
|
var djangoRestAdvancedSkill = {
|
|
459
1229
|
id: "django-rest-advanced",
|
|
460
1230
|
name: "Advanced Django REST Framework",
|
|
@@ -982,6 +1752,7 @@ class OrderViewSet(ModelViewSet):
|
|
|
982
1752
|
};
|
|
983
1753
|
|
|
984
1754
|
// src/skills/api-documentation.ts
|
|
1755
|
+
init_esm_shims();
|
|
985
1756
|
var apiDocumentationSkill = {
|
|
986
1757
|
id: "api-documentation",
|
|
987
1758
|
name: "API Documentation",
|
|
@@ -1299,6 +2070,7 @@ def internal_health_check(self, request):
|
|
|
1299
2070
|
};
|
|
1300
2071
|
|
|
1301
2072
|
// src/skills/react.ts
|
|
2073
|
+
init_esm_shims();
|
|
1302
2074
|
var reactSkill = {
|
|
1303
2075
|
id: "react",
|
|
1304
2076
|
name: "React Frontend Conventions",
|
|
@@ -1312,7 +2084,8 @@ var reactSkill = {
|
|
|
1312
2084
|
- TanStack React Query for server state management
|
|
1313
2085
|
- React Router v7 for client-side routing
|
|
1314
2086
|
- React Hook Form + Zod for forms and validation
|
|
1315
|
-
-
|
|
2087
|
+
- Chakra UI v2 for component library and theming
|
|
2088
|
+
- Tailwind CSS for additional styling
|
|
1316
2089
|
- \`@hey-api/openapi-ts\` for auto-generating API client from Django's OpenAPI schema
|
|
1317
2090
|
- \`lucide-react\` for icons
|
|
1318
2091
|
|
|
@@ -1341,11 +2114,11 @@ var reactSkill = {
|
|
|
1341
2114
|
- **Pages must be thin orchestrators** \u2014 break into child components in \`components/\`, extract logic into \`hooks/\`. See the \`page-structure\` skill for the full pattern
|
|
1342
2115
|
|
|
1343
2116
|
### UI Components
|
|
1344
|
-
- **All UI must use \`@
|
|
1345
|
-
- Use \`
|
|
1346
|
-
- Use \`
|
|
1347
|
-
- Use \`Divider\` instead of \`<
|
|
1348
|
-
- Use \`
|
|
2117
|
+
- **All UI must use \`@chakra-ui/react\` components** \u2014 see the \`chakra-ui-react\` skill for the full component list
|
|
2118
|
+
- Use \`VStack\`, \`HStack\`, \`Flex\`, \`SimpleGrid\`, \`Box\` for layout \u2014 never raw \`<div>\` with flex/grid classes
|
|
2119
|
+
- Use \`Heading\` and \`Text\` for headings and text \u2014 never raw \`<h1>\`\u2013\`<h6>\` or \`<p>\`
|
|
2120
|
+
- Use \`Divider\` instead of \`<hr>\`
|
|
2121
|
+
- Use \`Stat\`, \`Skeleton\` instead of building custom equivalents
|
|
1349
2122
|
|
|
1350
2123
|
### Route Paths
|
|
1351
2124
|
- All route paths live in the \`Path\` enum at \`src/router/paths.ts\` \u2014 **never hardcode path strings**
|
|
@@ -1353,12 +2126,12 @@ var reactSkill = {
|
|
|
1353
2126
|
- Use \`buildPath()\` for dynamic segments \u2014 see the \`page-structure\` skill for details
|
|
1354
2127
|
|
|
1355
2128
|
### Styling
|
|
1356
|
-
- Use
|
|
1357
|
-
- Use
|
|
1358
|
-
- Theming via
|
|
1359
|
-
-
|
|
1360
|
-
- Use responsive
|
|
1361
|
-
- Avoid inline \`style\` attributes \u2014 use Tailwind classes instead
|
|
2129
|
+
- Use Chakra UI style props as the primary styling approach
|
|
2130
|
+
- Use Tailwind CSS utility classes for additional styling needs
|
|
2131
|
+
- Theming via Chakra UI \`extendTheme()\` and design tokens
|
|
2132
|
+
- Color mode is supported via Chakra UI \`useColorMode()\` hook
|
|
2133
|
+
- Use responsive props (\`{{ base: ..., md: ..., lg: ... }}\`) for responsive layouts
|
|
2134
|
+
- Avoid inline \`style\` attributes \u2014 use Chakra style props or Tailwind classes instead
|
|
1362
2135
|
|
|
1363
2136
|
### Path Aliases
|
|
1364
2137
|
- \`@/\` maps to \`frontend/src/\`
|
|
@@ -1368,17 +2141,19 @@ var reactSkill = {
|
|
|
1368
2141
|
### Error Handling
|
|
1369
2142
|
- Use React Error Boundary (\`frontend/src/router/error-boundary.tsx\`) for render errors
|
|
1370
2143
|
- API errors are handled by \`useApiQuery\` / \`useApiMutation\` \u2014 see the \`react-query\` skill for error display patterns
|
|
1371
|
-
- Display user-facing errors using the project's feedback components (Alert,
|
|
2144
|
+
- Display user-facing errors using the project's feedback components (Alert, useToast)
|
|
1372
2145
|
|
|
1373
2146
|
### Testing
|
|
1374
|
-
-
|
|
1375
|
-
-
|
|
1376
|
-
-
|
|
2147
|
+
- See the \`frontend-testing\` skill for full conventions on test placement, utilities, mocking, and what to test
|
|
2148
|
+
- **Every code change must include corresponding tests** \u2014 see the \`frontend-testing\` skill for the complete rules
|
|
2149
|
+
- Tests use \`.spec.tsx\` / \`.spec.ts\` and live in \`__tests__/\` folders co-located with source code
|
|
2150
|
+
- Always use \`renderWithProviders\` from \`@/__tests__/test-utils\` \u2014 never import \`render\` from \`@testing-library/react\` directly
|
|
1377
2151
|
`;
|
|
1378
2152
|
}
|
|
1379
2153
|
};
|
|
1380
2154
|
|
|
1381
2155
|
// src/skills/react-query.ts
|
|
2156
|
+
init_esm_shims();
|
|
1382
2157
|
var reactQuerySkill = {
|
|
1383
2158
|
id: "react-query",
|
|
1384
2159
|
name: "TanStack React Query",
|
|
@@ -1605,6 +2380,7 @@ export function useDeletePost() {
|
|
|
1605
2380
|
};
|
|
1606
2381
|
|
|
1607
2382
|
// src/skills/page-structure.ts
|
|
2383
|
+
init_esm_shims();
|
|
1608
2384
|
var pageStructureSkill = {
|
|
1609
2385
|
id: "page-structure",
|
|
1610
2386
|
name: "Page & Route Structure",
|
|
@@ -1778,8 +2554,8 @@ pages/dashboard/
|
|
|
1778
2554
|
\`\`\`
|
|
1779
2555
|
|
|
1780
2556
|
\`\`\`tsx
|
|
1781
|
-
// dashboard.tsx \u2014 thin orchestrator using @
|
|
1782
|
-
import {
|
|
2557
|
+
// dashboard.tsx \u2014 thin orchestrator using @chakra-ui/react layout
|
|
2558
|
+
import { VStack, SimpleGrid, Divider } from '@chakra-ui/react'
|
|
1783
2559
|
import { StatsCards } from './components/stats-cards'
|
|
1784
2560
|
import { RecentActivity } from './components/recent-activity'
|
|
1785
2561
|
import { QuickActions } from './components/quick-actions'
|
|
@@ -1789,14 +2565,14 @@ export default function DashboardPage() {
|
|
|
1789
2565
|
const { stats, activity, isLoading } = useDashboardData()
|
|
1790
2566
|
|
|
1791
2567
|
return (
|
|
1792
|
-
<
|
|
2568
|
+
<VStack spacing={6} align="stretch">
|
|
1793
2569
|
<StatsCards stats={stats} isLoading={isLoading} />
|
|
1794
2570
|
<Divider />
|
|
1795
|
-
<
|
|
1796
|
-
<RecentActivity items={activity} isLoading={isLoading}
|
|
2571
|
+
<SimpleGrid columns={{ base: 1, lg: 3 }} spacing={6}>
|
|
2572
|
+
<RecentActivity items={activity} isLoading={isLoading} gridColumn={{ lg: 'span 2' }} />
|
|
1797
2573
|
<QuickActions />
|
|
1798
|
-
</
|
|
1799
|
-
</
|
|
2574
|
+
</SimpleGrid>
|
|
2575
|
+
</VStack>
|
|
1800
2576
|
)
|
|
1801
2577
|
}
|
|
1802
2578
|
\`\`\`
|
|
@@ -1856,7 +2632,7 @@ export function useOrdersPage() {
|
|
|
1856
2632
|
}
|
|
1857
2633
|
|
|
1858
2634
|
// orders-page.tsx
|
|
1859
|
-
import {
|
|
2635
|
+
import { VStack } from '@chakra-ui/react'
|
|
1860
2636
|
import { useOrdersPage } from './hooks/use-orders-page'
|
|
1861
2637
|
import { OrdersTable } from './components/orders-table'
|
|
1862
2638
|
import { OrdersToolbar } from './components/orders-toolbar'
|
|
@@ -1865,10 +2641,10 @@ export default function OrdersPage() {
|
|
|
1865
2641
|
const { orders, total, isLoading, page, setPage, search, setSearch, deleteOrder } = useOrdersPage()
|
|
1866
2642
|
|
|
1867
2643
|
return (
|
|
1868
|
-
<
|
|
2644
|
+
<VStack spacing={4} align="stretch">
|
|
1869
2645
|
<OrdersToolbar search={search} onSearchChange={setSearch} />
|
|
1870
2646
|
<OrdersTable orders={orders} isLoading={isLoading} onDelete={(id) => deleteOrder.mutate({ path: { id } })} />
|
|
1871
|
-
</
|
|
2647
|
+
</VStack>
|
|
1872
2648
|
)
|
|
1873
2649
|
}
|
|
1874
2650
|
\`\`\`
|
|
@@ -1901,217 +2677,176 @@ export default function OrdersPage() {
|
|
|
1901
2677
|
}
|
|
1902
2678
|
};
|
|
1903
2679
|
|
|
1904
|
-
// src/skills/
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
2680
|
+
// src/skills/chakra-ui-react.ts
|
|
2681
|
+
init_esm_shims();
|
|
2682
|
+
var chakraUiReactSkill = {
|
|
2683
|
+
id: "chakra-ui-react",
|
|
2684
|
+
name: "Chakra UI React",
|
|
2685
|
+
description: "Core UI component library \u2014 Chakra UI v2 components for layout, typography, inputs, data display, overlays, feedback, media, and navigation.",
|
|
1909
2686
|
render(_ctx) {
|
|
1910
|
-
return `##
|
|
2687
|
+
return `## Chakra UI React \u2014 Core UI Components
|
|
1911
2688
|
|
|
1912
|
-
> **CRITICAL RULE: Every UI element MUST be built using \`@
|
|
1913
|
-
> Do NOT use raw HTML elements when a
|
|
1914
|
-
> This includes layout: use \`
|
|
1915
|
-
> This includes typography: use \`
|
|
2689
|
+
> **CRITICAL RULE: Every UI element MUST be built using \`@chakra-ui/react\` components \u2014 including layout and typography.**
|
|
2690
|
+
> Do NOT use raw HTML elements when a Chakra UI component exists for that purpose.
|
|
2691
|
+
> This includes layout: use \`HStack\`, \`VStack\`, \`SimpleGrid\`, \`Box\`, \`Container\` instead of \`<div>\` with flex/grid classes.
|
|
2692
|
+
> This includes typography: use \`Heading\` and \`Text\` instead of raw \`<h1>\`\u2013\`<h6>\`, \`<p>\`, \`<span>\`.
|
|
1916
2693
|
|
|
1917
2694
|
### Layout
|
|
1918
2695
|
|
|
1919
2696
|
| Component | Use instead of | Description |
|
|
1920
2697
|
|-----------|---------------|-------------|
|
|
1921
2698
|
| \`Box\` | \`<div>\` | Base layout primitive with style props |
|
|
1922
|
-
| \`Flex\` | \`<div className="flex ...">\` | Flexbox container with style props (\`direction\`, \`align\`, \`justify\`, \`gap\`, \`wrap\`) |
|
|
1923
|
-
| \`
|
|
1924
|
-
| \`
|
|
2699
|
+
| \`Flex\` / \`HStack\` | \`<div className="flex ...">\` | Flexbox container with style props (\`direction\`, \`align\`, \`justify\`, \`gap\`, \`wrap\`) |
|
|
2700
|
+
| \`SimpleGrid\` | \`<div className="grid ...">\` | CSS Grid container (\`columns\`, \`spacing\`) |
|
|
2701
|
+
| \`VStack\` | \`<div className="flex flex-col gap-...">\` | Vertical stack (\`spacing\`) |
|
|
2702
|
+
| \`HStack\` | \`<div className="flex flex-row gap-...">\` | Horizontal stack (\`spacing\`) |
|
|
1925
2703
|
| \`Container\` | \`<div className="max-w-7xl mx-auto px-...">\` | Max-width centered container |
|
|
1926
2704
|
| \`Divider\` | \`<hr>\` or border hacks | Visual separator (horizontal/vertical) |
|
|
1927
2705
|
| \`AspectRatio\` | padding-bottom trick | Maintain aspect ratio for content |
|
|
1928
|
-
| \`Resizable\` | custom resize logic | Resizable panel groups |
|
|
1929
|
-
| \`ScrollArea\` | \`overflow-auto\` divs | Custom scrollbar container |
|
|
1930
2706
|
|
|
1931
2707
|
### Typography
|
|
1932
2708
|
|
|
1933
2709
|
| Component | Use instead of | Description |
|
|
1934
2710
|
|-----------|---------------|-------------|
|
|
1935
|
-
| \`Text\` | \`<p>\`, \`<span>\` | Text display with style props (\`
|
|
1936
|
-
| \`
|
|
1937
|
-
| \`
|
|
2711
|
+
| \`Text\` | \`<p>\`, \`<span>\` | Text display with style props (\`fontSize\`, \`fontWeight\`, \`color\`, \`textAlign\`) |
|
|
2712
|
+
| \`Heading\` | \`<h1>\`\u2013\`<h6>\`, \`<p>\` | Semantic heading elements (\`as\`: h1\u2013h6, \`size\`: 2xl, xl, lg, md, sm, xs) |
|
|
2713
|
+
| \`FormLabel\` | \`<label>\` | Form label with accessibility support |
|
|
1938
2714
|
|
|
1939
2715
|
### Cards & Containers
|
|
1940
2716
|
|
|
1941
|
-
- \`Card\`, \`CardHeader\`, \`
|
|
1942
|
-
- \`StatCard\` \u2014 Use for metric/stat display (value, label, trend)
|
|
1943
|
-
- \`EmptyState\` \u2014 Use for empty content placeholders instead of custom empty divs
|
|
2717
|
+
- \`Card\`, \`CardHeader\`, \`CardBody\`, \`CardFooter\` \u2014 Use instead of styled \`<div>\` containers. Use \`Heading\` and \`Text\` inside for title/description.
|
|
1944
2718
|
|
|
1945
2719
|
### Actions
|
|
1946
2720
|
|
|
1947
2721
|
- \`Button\` \u2014 Use instead of \`<button>\` or \`<a>\` styled as buttons
|
|
1948
|
-
- Variants: \`
|
|
1949
|
-
- Sizes: \`
|
|
1950
|
-
-
|
|
1951
|
-
- \`
|
|
1952
|
-
- \`
|
|
1953
|
-
- \`
|
|
1954
|
-
- \`AlertDialog\`, \`AlertDialogTrigger\`, \`AlertDialogContent\`, \`AlertDialogAction\`, \`AlertDialogCancel\` \u2014 Use for destructive action confirmations
|
|
2722
|
+
- Variants: \`solid\`, \`outline\`, \`ghost\`, \`link\`
|
|
2723
|
+
- Sizes: \`xs\`, \`sm\`, \`md\`, \`lg\`
|
|
2724
|
+
- Use \`colorScheme\` for color: \`blue\`, \`red\`, \`green\`, \`gray\`, etc.
|
|
2725
|
+
- \`IconButton\` \u2014 Use for icon-only buttons
|
|
2726
|
+
- \`Menu\`, \`MenuButton\`, \`MenuList\`, \`MenuItem\`, \`MenuDivider\`, \`MenuGroup\` \u2014 Use for action menus
|
|
2727
|
+
- \`AlertDialog\`, \`AlertDialogOverlay\`, \`AlertDialogContent\`, \`AlertDialogHeader\`, \`AlertDialogBody\`, \`AlertDialogFooter\` \u2014 Use for destructive action confirmations
|
|
1955
2728
|
|
|
1956
2729
|
### Data Entry
|
|
1957
2730
|
|
|
1958
2731
|
- \`Input\` \u2014 Use instead of \`<input>\`
|
|
1959
|
-
- \`
|
|
2732
|
+
- \`InputGroup\`, \`InputLeftElement\`, \`InputRightElement\` \u2014 Use for input with icons/addons
|
|
1960
2733
|
- \`Textarea\` \u2014 Use instead of \`<textarea>\`
|
|
1961
|
-
- \`NumberInput\` \u2014 Use for numeric inputs
|
|
1962
|
-
- \`Select
|
|
1963
|
-
- \`Checkbox\` \u2014 Use instead of \`<input type="checkbox">\`
|
|
1964
|
-
- \`
|
|
2734
|
+
- \`NumberInput\`, \`NumberInputField\`, \`NumberInputStepper\`, \`NumberIncrementStepper\`, \`NumberDecrementStepper\` \u2014 Use for numeric inputs
|
|
2735
|
+
- \`Select\` \u2014 Use instead of \`<select>\`
|
|
2736
|
+
- \`Checkbox\`, \`CheckboxGroup\` \u2014 Use instead of \`<input type="checkbox">\`
|
|
2737
|
+
- \`Radio\`, \`RadioGroup\` \u2014 Use instead of \`<input type="radio">\`
|
|
1965
2738
|
- \`Switch\` \u2014 Use for toggle switches
|
|
1966
|
-
- \`Slider\` \u2014 Use for
|
|
1967
|
-
- \`
|
|
1968
|
-
- \`
|
|
1969
|
-
- \`PinInput\` / \`InputOTP\` \u2014 Use for PIN/OTP code entry
|
|
1970
|
-
- \`ColorPicker\` \u2014 Use for color selection
|
|
1971
|
-
- \`FileUpload\` \u2014 Use for file upload with drag & drop
|
|
1972
|
-
- \`TagInput\` \u2014 Use for tag/chip input with add/remove
|
|
1973
|
-
- \`Rating\` \u2014 Use for star/icon rating selection
|
|
1974
|
-
- \`Label\` \u2014 Use instead of \`<label>\`
|
|
2739
|
+
- \`Slider\`, \`SliderTrack\`, \`SliderFilledTrack\`, \`SliderThumb\` \u2014 Use for range inputs
|
|
2740
|
+
- \`PinInput\`, \`PinInputField\` \u2014 Use for PIN/OTP code entry
|
|
2741
|
+
- \`FormLabel\` \u2014 Use instead of \`<label>\`
|
|
1975
2742
|
|
|
1976
2743
|
### Data Display
|
|
1977
2744
|
|
|
1978
|
-
- \`Table\`, \`
|
|
1979
|
-
- \`
|
|
1980
|
-
- \`
|
|
1981
|
-
- \`
|
|
1982
|
-
- \`
|
|
1983
|
-
- \`
|
|
1984
|
-
- \`Calendar\` \u2014 Use for full calendar display
|
|
1985
|
-
- \`Chart\` \u2014 Use for data visualization (powered by Recharts)
|
|
1986
|
-
- \`Timeline\` \u2014 Use for chronological event display
|
|
1987
|
-
- \`Tree\` \u2014 Use for hierarchical tree views
|
|
1988
|
-
- \`List\` \u2014 Use for structured list display instead of \`<ul>\`/\`<ol>\`
|
|
1989
|
-
- \`Skeleton\` \u2014 Use for loading placeholders
|
|
2745
|
+
- \`Table\`, \`Thead\`, \`Tbody\`, \`Tr\`, \`Th\`, \`Td\`, \`TableContainer\` \u2014 Use instead of \`<table>\` elements
|
|
2746
|
+
- \`Badge\` \u2014 Use for status indicators, tags, counts (\`colorScheme\`: \`green\`, \`red\`, \`blue\`, \`gray\`, etc.)
|
|
2747
|
+
- \`Avatar\` \u2014 Use for user profile images (use \`name\` prop for fallback initials)
|
|
2748
|
+
- \`Tooltip\` \u2014 Use for hover hints (\`label\` prop for content)
|
|
2749
|
+
- \`Stat\`, \`StatLabel\`, \`StatNumber\`, \`StatHelpText\`, \`StatArrow\` \u2014 Use for metric/stat display
|
|
2750
|
+
- \`Skeleton\`, \`SkeletonText\`, \`SkeletonCircle\` \u2014 Use for loading placeholders
|
|
1990
2751
|
- \`Spinner\` \u2014 Use for loading indicators
|
|
1991
2752
|
- \`Progress\` \u2014 Use for progress bars
|
|
1992
|
-
- \`
|
|
2753
|
+
- \`Tag\`, \`TagLabel\`, \`TagCloseButton\` \u2014 Use for removable tags
|
|
1993
2754
|
|
|
1994
2755
|
### Tabs & Accordion
|
|
1995
2756
|
|
|
1996
|
-
- \`Tabs\`, \`
|
|
1997
|
-
- \`Accordion\`, \`AccordionItem\`, \`
|
|
2757
|
+
- \`Tabs\`, \`TabList\`, \`Tab\`, \`TabPanels\`, \`TabPanel\` \u2014 Use for tabbed interfaces
|
|
2758
|
+
- \`Accordion\`, \`AccordionItem\`, \`AccordionButton\`, \`AccordionPanel\`, \`AccordionIcon\` \u2014 Use for collapsible sections
|
|
1998
2759
|
|
|
1999
2760
|
### Overlays
|
|
2000
2761
|
|
|
2001
|
-
- \`
|
|
2762
|
+
- \`Modal\`, \`ModalOverlay\`, \`ModalContent\`, \`ModalHeader\`, \`ModalBody\`, \`ModalFooter\`, \`ModalCloseButton\` \u2014 Use for modals
|
|
2002
2763
|
- \`AlertDialog\` \u2014 Use for confirmation dialogs
|
|
2003
|
-
- \`Drawer
|
|
2004
|
-
- \`Popover\` \u2014 Use for floating content panels
|
|
2005
|
-
- \`CommandPalette\` \u2014 Use for searchable command menus (cmdk-based)
|
|
2764
|
+
- \`Drawer\`, \`DrawerOverlay\`, \`DrawerContent\`, \`DrawerHeader\`, \`DrawerBody\`, \`DrawerFooter\`, \`DrawerCloseButton\` \u2014 Use for slide-out panels
|
|
2765
|
+
- \`Popover\`, \`PopoverTrigger\`, \`PopoverContent\`, \`PopoverHeader\`, \`PopoverBody\`, \`PopoverArrow\`, \`PopoverCloseButton\` \u2014 Use for floating content panels
|
|
2006
2766
|
|
|
2007
2767
|
### Navigation
|
|
2008
2768
|
|
|
2009
|
-
- \`Breadcrumb\`, \`
|
|
2010
|
-
- \`NavigationMenu\`, \`NavigationMenuList\`, \`NavigationMenuItem\`, \`NavigationMenuTrigger\`, \`NavigationMenuContent\` \u2014 Use for site navigation
|
|
2011
|
-
- \`Sidebar\` \u2014 Use for app sidebars
|
|
2012
|
-
- \`Dock\` \u2014 Use for macOS-style dock navigation
|
|
2013
|
-
- \`BackToTop\` \u2014 Use for scroll-to-top buttons
|
|
2769
|
+
- \`Breadcrumb\`, \`BreadcrumbItem\`, \`BreadcrumbLink\` \u2014 Use for breadcrumb trails
|
|
2014
2770
|
|
|
2015
2771
|
### Feedback
|
|
2016
2772
|
|
|
2017
|
-
- \`Alert\`, \`AlertTitle\`, \`AlertDescription\` \u2014 Use for inline messages/warnings
|
|
2018
|
-
- \`
|
|
2019
|
-
- \`Toast\` / \`Toaster\` / \`useToast\` \u2014 Use for transient notifications
|
|
2020
|
-
- \`SonnerToaster\` \u2014 Sonner-based toast notifications
|
|
2021
|
-
|
|
2022
|
-
### Media
|
|
2023
|
-
|
|
2024
|
-
- \`Image\` \u2014 Use instead of \`<img>\` for optimized image display
|
|
2025
|
-
- \`VideoPlayer\` \u2014 Use for video playback
|
|
2026
|
-
- \`CodeBlock\` \u2014 Use for syntax-highlighted code (Shiki-powered)
|
|
2027
|
-
- \`Carousel\` \u2014 Use for image/content carousels
|
|
2028
|
-
- \`Lightbox\` \u2014 Use for full-screen media viewers
|
|
2029
|
-
|
|
2030
|
-
### Specialized
|
|
2031
|
-
|
|
2032
|
-
- \`Stepper\` / \`Wizard\` \u2014 Use for multi-step workflows
|
|
2033
|
-
- \`NotificationCenter\` / \`useNotificationCenter\` \u2014 Use for notification management
|
|
2034
|
-
- \`SpotlightTour\` \u2014 Use for guided feature tours
|
|
2773
|
+
- \`Alert\`, \`AlertIcon\`, \`AlertTitle\`, \`AlertDescription\` \u2014 Use for inline messages/warnings (\`status\`: \`error\`, \`warning\`, \`success\`, \`info\`)
|
|
2774
|
+
- \`useToast\` \u2014 Use for transient notifications
|
|
2035
2775
|
|
|
2036
2776
|
### Utilities & Hooks
|
|
2037
2777
|
|
|
2038
|
-
- \`
|
|
2778
|
+
- \`useColorMode()\` \u2014 Color mode toggle. Returns \`{ colorMode, toggleColorMode }\`
|
|
2779
|
+
- \`useDisclosure()\` \u2014 Open/close state for modals, drawers, etc. Returns \`{ isOpen, onOpen, onClose, onToggle }\`
|
|
2780
|
+
- \`useBreakpointValue()\` \u2014 Responsive values based on breakpoint
|
|
2039
2781
|
- \`useToast()\` \u2014 Programmatic toast notifications
|
|
2040
|
-
- \`
|
|
2041
|
-
- \`useDarkMode()\` \u2014 Dark mode toggle. Returns \`{ isDark, toggle }\`
|
|
2782
|
+
- \`useMediaQuery()\` \u2014 CSS media query matching
|
|
2042
2783
|
|
|
2043
2784
|
---
|
|
2044
2785
|
|
|
2045
2786
|
### Component-First Rules
|
|
2046
2787
|
|
|
2047
|
-
1. **Layout**: NEVER use \`<div className="flex ...">\` or \`<div className="grid ...">\`. Use \`<Flex>\`, \`<
|
|
2788
|
+
1. **Layout**: NEVER use \`<div className="flex ...">\` or \`<div className="grid ...">\`. Use \`<Flex>\`, \`<SimpleGrid>\`, \`<VStack>\`, \`<HStack>\`, \`<Box>\` from \`@chakra-ui/react\`.
|
|
2048
2789
|
2. **Centering/max-width**: NEVER use \`<div className="max-w-7xl mx-auto px-...">\`. Use \`<Container>\`.
|
|
2049
|
-
3. **Typography**: NEVER use raw \`<h1>\`\u2013\`<h6>\` or \`<p>\` with Tailwind text classes. Use \`<
|
|
2790
|
+
3. **Typography**: NEVER use raw \`<h1>\`\u2013\`<h6>\` or \`<p>\` with Tailwind text classes. Use \`<Heading as="h2" size="lg">\` or \`<Text>\`.
|
|
2050
2791
|
4. **Separators**: NEVER use \`<hr>\` or border hacks. Use \`<Divider>\`.
|
|
2051
|
-
5. **
|
|
2052
|
-
6. **
|
|
2053
|
-
7. **
|
|
2054
|
-
8. **
|
|
2055
|
-
9. **
|
|
2056
|
-
10. **
|
|
2057
|
-
11. **
|
|
2058
|
-
12. **Modals**: NEVER build custom modals. Use \`Dialog\`, \`AlertDialog\`, \`Drawer\`, or \`Sheet\`.
|
|
2059
|
-
13. **Feedback**: NEVER use plain styled text for errors/warnings. Use \`Alert\` or \`useToast\`.
|
|
2060
|
-
14. **Empty states**: NEVER build custom empty-state UIs. Use \`EmptyState\`.
|
|
2061
|
-
15. **Metrics**: NEVER build custom stat/metric cards. Use \`StatCard\`.
|
|
2792
|
+
5. **Buttons**: NEVER use \`<button>\` or \`<a>\` styled as a button. Use \`<Button>\`.
|
|
2793
|
+
6. **Inputs**: NEVER use \`<input>\`, \`<textarea>\`, \`<select>\` directly. Use the Chakra UI equivalents.
|
|
2794
|
+
7. **Cards**: NEVER use a styled \`<div>\` as a card. Use \`Card\` + sub-components.
|
|
2795
|
+
8. **Tables**: NEVER use raw \`<table>\` HTML. Use Chakra UI \`Table\` components.
|
|
2796
|
+
9. **Loading**: NEVER use custom \`animate-pulse\` divs. Use \`Skeleton\` or \`Spinner\`.
|
|
2797
|
+
10. **Modals**: NEVER build custom modals. Use \`Modal\`, \`AlertDialog\`, or \`Drawer\`.
|
|
2798
|
+
11. **Feedback**: NEVER use plain styled text for errors/warnings. Use \`Alert\` or \`useToast\`.
|
|
2062
2799
|
|
|
2063
2800
|
### When Raw HTML IS Acceptable
|
|
2064
2801
|
|
|
2065
|
-
- \`<main>\`, \`<section>\`, \`<header>\`, \`<footer>\`, \`<nav>\`, \`<article>\`, \`<aside>\` \u2014 semantic HTML landmarks for page structure (but use \`Flex\`/\`
|
|
2066
|
-
- \`<Link>\` from react-router-dom \u2014 for page navigation (
|
|
2802
|
+
- \`<main>\`, \`<section>\`, \`<header>\`, \`<footer>\`, \`<nav>\`, \`<article>\`, \`<aside>\` \u2014 semantic HTML landmarks for page structure (but use \`Flex\`/\`VStack\`/\`SimpleGrid\` inside them for layout)
|
|
2803
|
+
- \`<Link>\` from react-router-dom \u2014 for page navigation (wrap with \`<Button as={Link}>...\` if it needs button styling)
|
|
2067
2804
|
- Icon components from \`lucide-react\`
|
|
2068
|
-
- \`<form>\` element when used with React Hook Form (but use
|
|
2805
|
+
- \`<form>\` element when used with React Hook Form (but use Chakra UI form components inside)
|
|
2069
2806
|
|
|
2070
2807
|
### Design Tokens & Theming
|
|
2071
2808
|
|
|
2072
|
-
- \`
|
|
2073
|
-
-
|
|
2074
|
-
- All components use
|
|
2075
|
-
-
|
|
2076
|
-
-
|
|
2077
|
-
- Extend with \`className\` prop + \`cn()\` utility for custom styles
|
|
2078
|
-
- Global styles: \`@import '@blacksmith-ui/react/styles.css'\` in app entry
|
|
2809
|
+
- \`ChakraProvider\` \u2014 Wrap app with \`theme\` prop to apply preset or custom theme
|
|
2810
|
+
- Extend theme with \`extendTheme()\` from \`@chakra-ui/react\`
|
|
2811
|
+
- All components use design tokens from the theme
|
|
2812
|
+
- Color mode: \`useColorMode()\` hook, or \`ColorModeScript\` in document head
|
|
2813
|
+
- Extend with \`sx\` prop or \`style props\` for custom styles
|
|
2079
2814
|
|
|
2080
2815
|
### Example: HowItWorks Section (Correct Way)
|
|
2081
2816
|
|
|
2082
2817
|
\`\`\`tsx
|
|
2083
|
-
import { Container,
|
|
2818
|
+
import { Container, VStack, Flex, SimpleGrid, Text, Heading, Box } from '@chakra-ui/react'
|
|
2084
2819
|
import { howItWorksSteps } from '../data'
|
|
2085
2820
|
|
|
2086
2821
|
export function HowItWorks() {
|
|
2087
2822
|
return (
|
|
2088
|
-
<Box as="section"
|
|
2089
|
-
<Container>
|
|
2090
|
-
<
|
|
2091
|
-
<
|
|
2092
|
-
<Text color="
|
|
2093
|
-
</
|
|
2094
|
-
|
|
2095
|
-
<
|
|
2823
|
+
<Box as="section" py={{ base: 16, sm: 20 }}>
|
|
2824
|
+
<Container maxW="container.xl">
|
|
2825
|
+
<VStack spacing={3} align="center" mb={12}>
|
|
2826
|
+
<Heading as="h2" size="xl">How It Works</Heading>
|
|
2827
|
+
<Text color="gray.500">Book your stay in three simple steps</Text>
|
|
2828
|
+
</VStack>
|
|
2829
|
+
|
|
2830
|
+
<SimpleGrid columns={{ base: 1, md: 3 }} spacing={8} maxW="4xl" mx="auto">
|
|
2096
2831
|
{howItWorksSteps.map((item) => (
|
|
2097
|
-
<
|
|
2098
|
-
<Box
|
|
2099
|
-
<Flex align="center" justify="center"
|
|
2100
|
-
<item.icon
|
|
2832
|
+
<VStack key={item.step} align="center" spacing={4}>
|
|
2833
|
+
<Box position="relative">
|
|
2834
|
+
<Flex align="center" justify="center" h={16} w={16} rounded="full" bg="blue.500" color="white" shadow="lg">
|
|
2835
|
+
<item.icon size={28} />
|
|
2101
2836
|
</Flex>
|
|
2102
|
-
<Flex align="center" justify="center"
|
|
2103
|
-
<Text
|
|
2837
|
+
<Flex align="center" justify="center" position="absolute" top={-1} right={-1} h={6} w={6} rounded="full" bg="white" borderWidth={2} borderColor="blue.500">
|
|
2838
|
+
<Text fontSize="xs" fontWeight="bold" color="blue.500">{item.step}</Text>
|
|
2104
2839
|
</Flex>
|
|
2105
2840
|
</Box>
|
|
2106
|
-
<
|
|
2107
|
-
<Text
|
|
2108
|
-
<Text
|
|
2841
|
+
<VStack spacing={2} align="center">
|
|
2842
|
+
<Text fontSize="lg" fontWeight="bold">{item.title}</Text>
|
|
2843
|
+
<Text fontSize="sm" color="gray.500" textAlign="center" maxW="xs">
|
|
2109
2844
|
{item.description}
|
|
2110
2845
|
</Text>
|
|
2111
|
-
</
|
|
2112
|
-
</
|
|
2846
|
+
</VStack>
|
|
2847
|
+
</VStack>
|
|
2113
2848
|
))}
|
|
2114
|
-
</
|
|
2849
|
+
</SimpleGrid>
|
|
2115
2850
|
</Container>
|
|
2116
2851
|
</Box>
|
|
2117
2852
|
)
|
|
@@ -2122,28 +2857,34 @@ export function HowItWorks() {
|
|
|
2122
2857
|
|
|
2123
2858
|
\`\`\`tsx
|
|
2124
2859
|
import {
|
|
2125
|
-
|
|
2126
|
-
Card, CardHeader,
|
|
2127
|
-
Button, Badge, Skeleton,
|
|
2128
|
-
Table,
|
|
2129
|
-
|
|
2130
|
-
AlertDialog,
|
|
2131
|
-
|
|
2132
|
-
|
|
2860
|
+
VStack, Flex, Box,
|
|
2861
|
+
Card, CardHeader, CardBody,
|
|
2862
|
+
Button, Badge, Skeleton, Heading,
|
|
2863
|
+
Table, Thead, Tbody, Tr, Th, Td, TableContainer,
|
|
2864
|
+
Menu, MenuButton, MenuList, MenuItem, MenuDivider,
|
|
2865
|
+
AlertDialog, AlertDialogOverlay, AlertDialogContent,
|
|
2866
|
+
AlertDialogHeader, AlertDialogBody, AlertDialogFooter,
|
|
2867
|
+
IconButton, useDisclosure,
|
|
2868
|
+
} from '@chakra-ui/react'
|
|
2133
2869
|
import { MoreHorizontal, Plus, Trash2, Edit } from 'lucide-react'
|
|
2134
2870
|
import { Link } from 'react-router-dom'
|
|
2871
|
+
import { useRef } from 'react'
|
|
2135
2872
|
|
|
2136
2873
|
function ResourceListPage({ resources, isLoading, onDelete }) {
|
|
2874
|
+
const { isOpen, onOpen, onClose } = useDisclosure()
|
|
2875
|
+
const cancelRef = useRef()
|
|
2876
|
+
const [deleteId, setDeleteId] = useState(null)
|
|
2877
|
+
|
|
2137
2878
|
if (isLoading) {
|
|
2138
2879
|
return (
|
|
2139
2880
|
<Card>
|
|
2140
|
-
<
|
|
2141
|
-
<
|
|
2881
|
+
<CardBody p={6}>
|
|
2882
|
+
<VStack spacing={4}>
|
|
2142
2883
|
{Array.from({ length: 5 }).map((_, i) => (
|
|
2143
|
-
<Skeleton key={i}
|
|
2884
|
+
<Skeleton key={i} h="48px" w="full" />
|
|
2144
2885
|
))}
|
|
2145
|
-
</
|
|
2146
|
-
</
|
|
2886
|
+
</VStack>
|
|
2887
|
+
</CardBody>
|
|
2147
2888
|
</Card>
|
|
2148
2889
|
)
|
|
2149
2890
|
}
|
|
@@ -2151,61 +2892,63 @@ function ResourceListPage({ resources, isLoading, onDelete }) {
|
|
|
2151
2892
|
return (
|
|
2152
2893
|
<Card>
|
|
2153
2894
|
<CardHeader>
|
|
2154
|
-
<Flex align="center" justify="between">
|
|
2155
|
-
<
|
|
2156
|
-
<Button
|
|
2157
|
-
|
|
2895
|
+
<Flex align="center" justify="space-between">
|
|
2896
|
+
<Heading size="md">Resources</Heading>
|
|
2897
|
+
<Button as={Link} to="/resources/new" leftIcon={<Plus size={16} />} colorScheme="blue">
|
|
2898
|
+
Create
|
|
2158
2899
|
</Button>
|
|
2159
2900
|
</Flex>
|
|
2160
2901
|
</CardHeader>
|
|
2161
|
-
<
|
|
2162
|
-
<
|
|
2163
|
-
<
|
|
2164
|
-
<
|
|
2165
|
-
<
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
<
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
<
|
|
2177
|
-
<
|
|
2178
|
-
<
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
<
|
|
2185
|
-
|
|
2186
|
-
</
|
|
2187
|
-
</
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2902
|
+
<CardBody>
|
|
2903
|
+
<TableContainer>
|
|
2904
|
+
<Table>
|
|
2905
|
+
<Thead>
|
|
2906
|
+
<Tr>
|
|
2907
|
+
<Th>Title</Th>
|
|
2908
|
+
<Th>Status</Th>
|
|
2909
|
+
<Th w="48px" />
|
|
2910
|
+
</Tr>
|
|
2911
|
+
</Thead>
|
|
2912
|
+
<Tbody>
|
|
2913
|
+
{resources.map((r) => (
|
|
2914
|
+
<Tr key={r.id}>
|
|
2915
|
+
<Td>{r.title}</Td>
|
|
2916
|
+
<Td><Badge variant="outline">{r.status}</Badge></Td>
|
|
2917
|
+
<Td>
|
|
2918
|
+
<Menu>
|
|
2919
|
+
<MenuButton as={IconButton} icon={<MoreHorizontal size={16} />} variant="ghost" size="sm" />
|
|
2920
|
+
<MenuList>
|
|
2921
|
+
<MenuItem as={Link} to={\`/resources/\${r.id}/edit\`} icon={<Edit size={16} />}>
|
|
2922
|
+
Edit
|
|
2923
|
+
</MenuItem>
|
|
2924
|
+
<MenuDivider />
|
|
2925
|
+
<MenuItem icon={<Trash2 size={16} />} color="red.500" onClick={() => { setDeleteId(r.id); onOpen() }}>
|
|
2926
|
+
Delete
|
|
2927
|
+
</MenuItem>
|
|
2928
|
+
</MenuList>
|
|
2929
|
+
</Menu>
|
|
2930
|
+
</Td>
|
|
2931
|
+
</Tr>
|
|
2932
|
+
))}
|
|
2933
|
+
</Tbody>
|
|
2934
|
+
</Table>
|
|
2935
|
+
</TableContainer>
|
|
2936
|
+
</CardBody>
|
|
2937
|
+
|
|
2938
|
+
<AlertDialog isOpen={isOpen} leastDestructiveRef={cancelRef} onClose={onClose}>
|
|
2939
|
+
<AlertDialogOverlay>
|
|
2940
|
+
<AlertDialogContent>
|
|
2941
|
+
<AlertDialogHeader>Delete Resource</AlertDialogHeader>
|
|
2942
|
+
<AlertDialogBody>Are you sure? This cannot be undone.</AlertDialogBody>
|
|
2943
|
+
<AlertDialogFooter>
|
|
2944
|
+
<Button ref={cancelRef} onClick={onClose}>Cancel</Button>
|
|
2945
|
+
<Button colorScheme="red" onClick={() => { onDelete(deleteId); onClose() }} ml={3}>
|
|
2946
|
+
Delete
|
|
2947
|
+
</Button>
|
|
2948
|
+
</AlertDialogFooter>
|
|
2949
|
+
</AlertDialogContent>
|
|
2950
|
+
</AlertDialogOverlay>
|
|
2951
|
+
</AlertDialog>
|
|
2209
2952
|
</Card>
|
|
2210
2953
|
)
|
|
2211
2954
|
}
|
|
@@ -2214,42 +2957,50 @@ function ResourceListPage({ resources, isLoading, onDelete }) {
|
|
|
2214
2957
|
}
|
|
2215
2958
|
};
|
|
2216
2959
|
|
|
2217
|
-
// src/skills/
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2960
|
+
// src/skills/chakra-ui-forms.ts
|
|
2961
|
+
init_esm_shims();
|
|
2962
|
+
var chakraUiFormsSkill = {
|
|
2963
|
+
id: "chakra-ui-forms",
|
|
2964
|
+
name: "Chakra UI Forms",
|
|
2965
|
+
description: "Form components using Chakra UI + React Hook Form + Zod for validation and submission.",
|
|
2222
2966
|
render(_ctx) {
|
|
2223
|
-
return `##
|
|
2967
|
+
return `## Chakra UI Forms \u2014 Form Components (React Hook Form + Zod)
|
|
2224
2968
|
|
|
2225
|
-
> **RULE: ALWAYS use
|
|
2969
|
+
> **RULE: ALWAYS use Chakra UI form components for forms.** Do NOT build forms with raw \`<form>\`, \`<input>\`, \`<label>\`, or manual error display.
|
|
2226
2970
|
|
|
2227
2971
|
\`\`\`tsx
|
|
2228
|
-
import {
|
|
2972
|
+
import {
|
|
2973
|
+
FormControl, FormLabel, FormErrorMessage, FormHelperText,
|
|
2974
|
+
Input, Textarea, Select, Checkbox, Switch, NumberInput,
|
|
2975
|
+
NumberInputField, NumberInputStepper, NumberIncrementStepper, NumberDecrementStepper,
|
|
2976
|
+
Radio, RadioGroup, Button,
|
|
2977
|
+
} from '@chakra-ui/react'
|
|
2229
2978
|
\`\`\`
|
|
2230
2979
|
|
|
2231
2980
|
### Components
|
|
2232
2981
|
|
|
2233
|
-
- \`
|
|
2234
|
-
- \`
|
|
2235
|
-
- \`
|
|
2236
|
-
- \`
|
|
2237
|
-
- \`
|
|
2238
|
-
- \`
|
|
2239
|
-
- \`
|
|
2240
|
-
- \`
|
|
2241
|
-
- \`
|
|
2242
|
-
- \`
|
|
2243
|
-
- \`
|
|
2982
|
+
- \`FormControl\` \u2014 Wraps each field. Props: \`isInvalid\`, \`isRequired\`, \`isDisabled\`
|
|
2983
|
+
- \`FormLabel\` \u2014 Label for a form field
|
|
2984
|
+
- \`FormErrorMessage\` \u2014 Displays field-level validation error (shown when \`FormControl\` has \`isInvalid\`)
|
|
2985
|
+
- \`FormHelperText\` \u2014 Displays helper text below a field
|
|
2986
|
+
- \`Input\` \u2014 Text input. Props: \`type\`, \`placeholder\`, \`size\`
|
|
2987
|
+
- \`Textarea\` \u2014 Textarea. Props: \`rows\`, \`placeholder\`
|
|
2988
|
+
- \`Select\` \u2014 Select dropdown. Props: \`placeholder\`
|
|
2989
|
+
- \`Checkbox\` \u2014 Checkbox input
|
|
2990
|
+
- \`Switch\` \u2014 Toggle switch
|
|
2991
|
+
- \`RadioGroup\` + \`Radio\` \u2014 Radio group
|
|
2992
|
+
- \`NumberInput\` \u2014 Numeric input with stepper
|
|
2244
2993
|
|
|
2245
2994
|
### Rules
|
|
2246
|
-
- NEVER use raw \`<form>\` with manual \`<label>\` and error \`<p>\` tags. Always use \`
|
|
2247
|
-
- NEVER use \`<input>\`, \`<textarea>\`, \`<select>\` inside forms. Use \`
|
|
2995
|
+
- NEVER use raw \`<form>\` with manual \`<label>\` and error \`<p>\` tags. Always use \`FormControl\` + \`FormLabel\` + \`FormErrorMessage\`.
|
|
2996
|
+
- NEVER use \`<input>\`, \`<textarea>\`, \`<select>\` inside forms. Use Chakra UI \`Input\`, \`Textarea\`, \`Select\`.
|
|
2248
2997
|
|
|
2249
2998
|
### Form Pattern \u2014 ALWAYS follow this:
|
|
2250
2999
|
\`\`\`tsx
|
|
2251
|
-
import {
|
|
2252
|
-
|
|
3000
|
+
import {
|
|
3001
|
+
FormControl, FormLabel, FormErrorMessage,
|
|
3002
|
+
Input, Textarea, Select, Button, VStack,
|
|
3003
|
+
} from '@chakra-ui/react'
|
|
2253
3004
|
import { useForm } from 'react-hook-form'
|
|
2254
3005
|
import { zodResolver } from '@hookform/resolvers/zod'
|
|
2255
3006
|
import { z } from 'zod'
|
|
@@ -2263,29 +3014,40 @@ const schema = z.object({
|
|
|
2263
3014
|
type FormData = z.infer<typeof schema>
|
|
2264
3015
|
|
|
2265
3016
|
function ResourceForm({ defaultValues, onSubmit, isSubmitting }: Props) {
|
|
2266
|
-
const
|
|
3017
|
+
const { register, handleSubmit, formState: { errors } } = useForm<FormData>({
|
|
2267
3018
|
resolver: zodResolver(schema),
|
|
2268
3019
|
defaultValues: { title: '', description: '', status: 'draft', ...defaultValues },
|
|
2269
3020
|
})
|
|
2270
3021
|
|
|
2271
3022
|
return (
|
|
2272
|
-
<
|
|
2273
|
-
<
|
|
2274
|
-
<
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
<
|
|
2281
|
-
|
|
2282
|
-
{
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
{
|
|
2287
|
-
|
|
2288
|
-
|
|
3023
|
+
<form onSubmit={handleSubmit(onSubmit)}>
|
|
3024
|
+
<VStack spacing={4} align="stretch">
|
|
3025
|
+
<FormControl isInvalid={!!errors.title} isRequired>
|
|
3026
|
+
<FormLabel>Title</FormLabel>
|
|
3027
|
+
<Input placeholder="Enter title" {...register('title')} />
|
|
3028
|
+
<FormErrorMessage>{errors.title?.message}</FormErrorMessage>
|
|
3029
|
+
</FormControl>
|
|
3030
|
+
|
|
3031
|
+
<FormControl isInvalid={!!errors.description}>
|
|
3032
|
+
<FormLabel>Description</FormLabel>
|
|
3033
|
+
<Textarea rows={4} placeholder="Enter description" {...register('description')} />
|
|
3034
|
+
<FormErrorMessage>{errors.description?.message}</FormErrorMessage>
|
|
3035
|
+
</FormControl>
|
|
3036
|
+
|
|
3037
|
+
<FormControl isInvalid={!!errors.status}>
|
|
3038
|
+
<FormLabel>Status</FormLabel>
|
|
3039
|
+
<Select {...register('status')}>
|
|
3040
|
+
<option value="draft">Draft</option>
|
|
3041
|
+
<option value="published">Published</option>
|
|
3042
|
+
</Select>
|
|
3043
|
+
<FormErrorMessage>{errors.status?.message}</FormErrorMessage>
|
|
3044
|
+
</FormControl>
|
|
3045
|
+
|
|
3046
|
+
<Button type="submit" colorScheme="blue" isLoading={isSubmitting}>
|
|
3047
|
+
Save
|
|
3048
|
+
</Button>
|
|
3049
|
+
</VStack>
|
|
3050
|
+
</form>
|
|
2289
3051
|
)
|
|
2290
3052
|
}
|
|
2291
3053
|
\`\`\`
|
|
@@ -2293,12 +3055,13 @@ function ResourceForm({ defaultValues, onSubmit, isSubmitting }: Props) {
|
|
|
2293
3055
|
### Example: Detail Page with Edit Dialog
|
|
2294
3056
|
\`\`\`tsx
|
|
2295
3057
|
import {
|
|
2296
|
-
Card, CardHeader,
|
|
2297
|
-
Button, Badge,
|
|
2298
|
-
|
|
2299
|
-
Alert, AlertTitle, AlertDescription,
|
|
2300
|
-
|
|
2301
|
-
|
|
3058
|
+
Card, CardHeader, CardBody, CardFooter,
|
|
3059
|
+
Button, Badge, Divider, Heading, Text,
|
|
3060
|
+
Modal, ModalOverlay, ModalContent, ModalHeader, ModalBody, ModalFooter, ModalCloseButton,
|
|
3061
|
+
Alert, AlertIcon, AlertTitle, AlertDescription,
|
|
3062
|
+
FormControl, FormLabel, FormErrorMessage,
|
|
3063
|
+
Input, Textarea, Flex, HStack, useDisclosure,
|
|
3064
|
+
} from '@chakra-ui/react'
|
|
2302
3065
|
import { useForm } from 'react-hook-form'
|
|
2303
3066
|
import { zodResolver } from '@hookform/resolvers/zod'
|
|
2304
3067
|
import { z } from 'zod'
|
|
@@ -2311,7 +3074,8 @@ const editSchema = z.object({
|
|
|
2311
3074
|
})
|
|
2312
3075
|
|
|
2313
3076
|
function ResourceDetailPage({ resource, onUpdate, error }) {
|
|
2314
|
-
const
|
|
3077
|
+
const { isOpen, onOpen, onClose } = useDisclosure()
|
|
3078
|
+
const { register, handleSubmit, formState: { errors } } = useForm({
|
|
2315
3079
|
resolver: zodResolver(editSchema),
|
|
2316
3080
|
defaultValues: { title: resource.title, description: resource.description },
|
|
2317
3081
|
})
|
|
@@ -2319,52 +3083,62 @@ function ResourceDetailPage({ resource, onUpdate, error }) {
|
|
|
2319
3083
|
return (
|
|
2320
3084
|
<Card>
|
|
2321
3085
|
<CardHeader>
|
|
2322
|
-
<
|
|
2323
|
-
<
|
|
2324
|
-
<
|
|
2325
|
-
<
|
|
2326
|
-
</
|
|
2327
|
-
<
|
|
2328
|
-
<Button variant="outline"
|
|
2329
|
-
|
|
3086
|
+
<Flex align="center" justify="space-between">
|
|
3087
|
+
<Box>
|
|
3088
|
+
<Heading size="md">{resource.title}</Heading>
|
|
3089
|
+
<Text fontSize="sm" color="gray.500">Created {new Date(resource.created_at).toLocaleDateString()}</Text>
|
|
3090
|
+
</Box>
|
|
3091
|
+
<HStack spacing={2}>
|
|
3092
|
+
<Button variant="outline" as={Link} to="/resources" leftIcon={<ArrowLeft size={16} />}>
|
|
3093
|
+
Back
|
|
3094
|
+
</Button>
|
|
3095
|
+
<Button leftIcon={<Edit size={16} />} colorScheme="blue" onClick={onOpen}>
|
|
3096
|
+
Edit
|
|
2330
3097
|
</Button>
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
<Button><Edit className="mr-2 h-4 w-4" /> Edit</Button>
|
|
2334
|
-
</DialogTrigger>
|
|
2335
|
-
<DialogContent>
|
|
2336
|
-
<DialogHeader>
|
|
2337
|
-
<DialogTitle>Edit Resource</DialogTitle>
|
|
2338
|
-
</DialogHeader>
|
|
2339
|
-
<Form form={form} onSubmit={onUpdate}>
|
|
2340
|
-
<FormField name="title" label="Title">
|
|
2341
|
-
<FormInput />
|
|
2342
|
-
</FormField>
|
|
2343
|
-
<FormField name="description" label="Description">
|
|
2344
|
-
<FormTextarea rows={4} />
|
|
2345
|
-
</FormField>
|
|
2346
|
-
<DialogFooter>
|
|
2347
|
-
<Button type="submit">Save Changes</Button>
|
|
2348
|
-
</DialogFooter>
|
|
2349
|
-
</Form>
|
|
2350
|
-
</DialogContent>
|
|
2351
|
-
</Dialog>
|
|
2352
|
-
</div>
|
|
2353
|
-
</div>
|
|
3098
|
+
</HStack>
|
|
3099
|
+
</Flex>
|
|
2354
3100
|
</CardHeader>
|
|
2355
|
-
<
|
|
2356
|
-
<
|
|
3101
|
+
<Divider />
|
|
3102
|
+
<CardBody>
|
|
2357
3103
|
{error && (
|
|
2358
|
-
<Alert
|
|
3104
|
+
<Alert status="error" mb={4}>
|
|
3105
|
+
<AlertIcon />
|
|
2359
3106
|
<AlertTitle>Error</AlertTitle>
|
|
2360
3107
|
<AlertDescription>{error}</AlertDescription>
|
|
2361
3108
|
</Alert>
|
|
2362
3109
|
)}
|
|
2363
|
-
<
|
|
2364
|
-
</
|
|
3110
|
+
<Text>{resource.description || 'No description provided.'}</Text>
|
|
3111
|
+
</CardBody>
|
|
2365
3112
|
<CardFooter>
|
|
2366
3113
|
<Badge>{resource.status}</Badge>
|
|
2367
3114
|
</CardFooter>
|
|
3115
|
+
|
|
3116
|
+
<Modal isOpen={isOpen} onClose={onClose}>
|
|
3117
|
+
<ModalOverlay />
|
|
3118
|
+
<ModalContent>
|
|
3119
|
+
<ModalHeader>Edit Resource</ModalHeader>
|
|
3120
|
+
<ModalCloseButton />
|
|
3121
|
+
<form onSubmit={handleSubmit(onUpdate)}>
|
|
3122
|
+
<ModalBody>
|
|
3123
|
+
<VStack spacing={4}>
|
|
3124
|
+
<FormControl isInvalid={!!errors.title}>
|
|
3125
|
+
<FormLabel>Title</FormLabel>
|
|
3126
|
+
<Input {...register('title')} />
|
|
3127
|
+
<FormErrorMessage>{errors.title?.message}</FormErrorMessage>
|
|
3128
|
+
</FormControl>
|
|
3129
|
+
<FormControl isInvalid={!!errors.description}>
|
|
3130
|
+
<FormLabel>Description</FormLabel>
|
|
3131
|
+
<Textarea rows={4} {...register('description')} />
|
|
3132
|
+
<FormErrorMessage>{errors.description?.message}</FormErrorMessage>
|
|
3133
|
+
</FormControl>
|
|
3134
|
+
</VStack>
|
|
3135
|
+
</ModalBody>
|
|
3136
|
+
<ModalFooter>
|
|
3137
|
+
<Button type="submit" colorScheme="blue">Save Changes</Button>
|
|
3138
|
+
</ModalFooter>
|
|
3139
|
+
</form>
|
|
3140
|
+
</ModalContent>
|
|
3141
|
+
</Modal>
|
|
2368
3142
|
</Card>
|
|
2369
3143
|
)
|
|
2370
3144
|
}
|
|
@@ -2373,232 +3147,222 @@ function ResourceDetailPage({ resource, onUpdate, error }) {
|
|
|
2373
3147
|
}
|
|
2374
3148
|
};
|
|
2375
3149
|
|
|
2376
|
-
// src/skills/
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
3150
|
+
// src/skills/chakra-ui-auth.ts
|
|
3151
|
+
init_esm_shims();
|
|
3152
|
+
var chakraUiAuthSkill = {
|
|
3153
|
+
id: "chakra-ui-auth",
|
|
3154
|
+
name: "Custom Auth System",
|
|
3155
|
+
description: "Custom authentication context, hooks, and types for login, registration, and password reset.",
|
|
2381
3156
|
render(_ctx) {
|
|
2382
|
-
return `##
|
|
3157
|
+
return `## Custom Auth System \u2014 Authentication Context & Hooks
|
|
2383
3158
|
|
|
2384
|
-
> **RULE:
|
|
3159
|
+
> **RULE: Use the local AuthProvider and useAuth hook for all auth functionality.**
|
|
3160
|
+
> Auth components (LoginForm, RegisterForm, etc.) are custom implementations in \`frontend/src/features/auth/\`.
|
|
2385
3161
|
|
|
2386
3162
|
\`\`\`tsx
|
|
2387
|
-
import { AuthProvider
|
|
3163
|
+
import { AuthProvider } from '@/features/auth/context'
|
|
3164
|
+
import { useAuth } from '@/features/auth/hooks/use-auth'
|
|
3165
|
+
import type { User, AuthState } from '@/features/auth/types'
|
|
2388
3166
|
\`\`\`
|
|
2389
3167
|
|
|
2390
|
-
###
|
|
3168
|
+
### Context & Provider
|
|
3169
|
+
|
|
3170
|
+
- \`AuthProvider\` \u2014 Context provider wrapping the app. Manages auth state, token storage, and session lifecycle.
|
|
3171
|
+
- Place at the app root, inside \`ChakraProvider\`.
|
|
3172
|
+
- Props: \`config?: { adapter?: AuthAdapter }\`
|
|
3173
|
+
|
|
3174
|
+
### Types
|
|
3175
|
+
|
|
3176
|
+
\`\`\`tsx
|
|
3177
|
+
interface User {
|
|
3178
|
+
id: string
|
|
3179
|
+
email: string
|
|
3180
|
+
displayName?: string
|
|
3181
|
+
avatar?: string
|
|
3182
|
+
}
|
|
2391
3183
|
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
- \`ResetPasswordForm\` \u2014 Set new password form
|
|
2400
|
-
- Props: \`onSubmit: (data: { password, code }) => void\`, \`code\`, \`onLoginClick\`, \`error\`, \`loading\`
|
|
3184
|
+
interface AuthState {
|
|
3185
|
+
user: User | null
|
|
3186
|
+
loading: boolean
|
|
3187
|
+
error: string | null
|
|
3188
|
+
isAuthenticated: boolean
|
|
3189
|
+
}
|
|
3190
|
+
\`\`\`
|
|
2401
3191
|
|
|
2402
3192
|
### Hooks
|
|
2403
3193
|
|
|
2404
|
-
- \`useAuth\` \u2014 Hook for auth state and actions
|
|
2405
|
-
- Returns: \`user\`, \`loading\`, \`error\`, \`signInWithEmail(email, password)\`, \`signUpWithEmail(email, password, displayName?)\`, \`signOut()\`, \`sendPasswordResetEmail(email)\`, \`confirmPasswordReset(code, newPassword)
|
|
3194
|
+
- \`useAuth()\` \u2014 Hook for auth state and actions
|
|
3195
|
+
- Returns: \`user\`, \`loading\`, \`error\`, \`isAuthenticated\`, \`signInWithEmail(email, password)\`, \`signUpWithEmail(email, password, displayName?)\`, \`signOut()\`, \`sendPasswordResetEmail(email)\`, \`confirmPasswordReset(code, newPassword)\`
|
|
3196
|
+
|
|
3197
|
+
### Auth Pages
|
|
3198
|
+
|
|
3199
|
+
Auth pages are custom implementations in \`frontend/src/features/auth/pages/\`:
|
|
3200
|
+
|
|
3201
|
+
- \`LoginPage\` \u2014 Login form with email/password, validation, and navigation links
|
|
3202
|
+
- \`RegisterPage\` \u2014 Registration form with email, password, and display name
|
|
3203
|
+
- \`ForgotPasswordPage\` \u2014 Password reset email request
|
|
3204
|
+
- \`ResetPasswordPage\` \u2014 Set new password form
|
|
3205
|
+
|
|
3206
|
+
Each auth page uses Chakra UI form components (\`FormControl\`, \`FormLabel\`, \`Input\`, \`Button\`, etc.) with React Hook Form + Zod validation.
|
|
2406
3207
|
|
|
2407
3208
|
### Adapter
|
|
2408
3209
|
|
|
2409
|
-
- \`AuthAdapter\` \u2014 Interface for custom auth backends (Django JWT adapter
|
|
3210
|
+
- \`AuthAdapter\` \u2014 Interface for custom auth backends (Django JWT adapter in \`frontend/src/features/auth/adapter.ts\`)
|
|
2410
3211
|
|
|
2411
3212
|
### Rules
|
|
2412
|
-
- NEVER
|
|
2413
|
-
-
|
|
3213
|
+
- NEVER manage auth state manually. Use \`useAuth()\` hook.
|
|
3214
|
+
- Auth pages live in \`frontend/src/features/auth/pages/\` and use Chakra UI components.
|
|
3215
|
+
- Use the \`Path\` enum for auth route paths (\`Path.Login\`, \`Path.Register\`, etc.).
|
|
2414
3216
|
`;
|
|
2415
3217
|
}
|
|
2416
3218
|
};
|
|
2417
3219
|
|
|
2418
3220
|
// src/skills/blacksmith-hooks.ts
|
|
3221
|
+
init_esm_shims();
|
|
2419
3222
|
var blacksmithHooksSkill = {
|
|
2420
3223
|
id: "blacksmith-hooks",
|
|
2421
|
-
name: "
|
|
2422
|
-
description: "
|
|
3224
|
+
name: "Custom Hooks & Chakra UI Hooks",
|
|
3225
|
+
description: "Custom React hooks (local implementations) and Chakra UI built-in hooks for state, UI, layout, and responsiveness.",
|
|
2423
3226
|
render(_ctx) {
|
|
2424
|
-
return `##
|
|
3227
|
+
return `## Custom Hooks & Chakra UI Hooks
|
|
2425
3228
|
|
|
2426
|
-
A
|
|
3229
|
+
A combination of local custom hooks and Chakra UI built-in hooks for common UI patterns.
|
|
2427
3230
|
|
|
2428
|
-
> **RULE: Use
|
|
3231
|
+
> **RULE: Use Chakra UI hooks when available, and local custom hooks for additional utilities.**
|
|
2429
3232
|
> Before creating a new hook, check if one already exists below.
|
|
2430
3233
|
|
|
3234
|
+
### Chakra UI Built-in Hooks
|
|
3235
|
+
|
|
2431
3236
|
\`\`\`tsx
|
|
2432
|
-
import {
|
|
3237
|
+
import { useColorMode, useDisclosure, useBreakpointValue, useMediaQuery, useToast } from '@chakra-ui/react'
|
|
2433
3238
|
\`\`\`
|
|
2434
3239
|
|
|
2435
|
-
### State & Data
|
|
2436
|
-
|
|
2437
3240
|
| Hook | Description |
|
|
2438
3241
|
|------|-------------|
|
|
2439
|
-
| \`
|
|
2440
|
-
| \`
|
|
2441
|
-
| \`
|
|
2442
|
-
| \`
|
|
2443
|
-
| \`
|
|
2444
|
-
| \`
|
|
2445
|
-
| \`
|
|
2446
|
-
| \`
|
|
2447
|
-
| \`
|
|
2448
|
-
| \`
|
|
2449
|
-
| \`
|
|
2450
|
-
| \`
|
|
2451
|
-
|
|
3242
|
+
| \`useColorMode\` | Color mode toggle. Returns \`{ colorMode, toggleColorMode, setColorMode }\` |
|
|
3243
|
+
| \`useColorModeValue\` | Returns a value based on current color mode: \`useColorModeValue(lightValue, darkValue)\` |
|
|
3244
|
+
| \`useDisclosure\` | Open/close/toggle state for modals, drawers, popovers. Returns \`{ isOpen, onOpen, onClose, onToggle }\` |
|
|
3245
|
+
| \`useBreakpointValue\` | Responsive values: \`useBreakpointValue({ base: 1, md: 2, lg: 3 })\` |
|
|
3246
|
+
| \`useMediaQuery\` | CSS media query matching: \`useMediaQuery('(min-width: 768px)')\` |
|
|
3247
|
+
| \`useToast\` | Programmatic toast notifications |
|
|
3248
|
+
| \`useClipboard\` | Copy text to clipboard with status feedback |
|
|
3249
|
+
| \`useBoolean\` | Boolean state with \`on\`, \`off\`, \`toggle\` actions |
|
|
3250
|
+
| \`useOutsideClick\` | Detect clicks outside a ref element |
|
|
3251
|
+
| \`useControllable\` | Controlled/uncontrolled component pattern helper |
|
|
3252
|
+
| \`useMergeRefs\` | Merge multiple refs into one |
|
|
3253
|
+
| \`useTheme\` | Access the current Chakra UI theme object |
|
|
3254
|
+
|
|
3255
|
+
### Custom Local Hooks
|
|
3256
|
+
|
|
3257
|
+
These are implemented locally in the project (e.g., in \`frontend/src/shared/hooks/\`):
|
|
2452
3258
|
|
|
2453
|
-
|
|
3259
|
+
\`\`\`tsx
|
|
3260
|
+
import { useDebounce } from '@/shared/hooks/use-debounce'
|
|
3261
|
+
import { useLocalStorage } from '@/shared/hooks/use-local-storage'
|
|
3262
|
+
\`\`\`
|
|
2454
3263
|
|
|
2455
3264
|
| Hook | Description |
|
|
2456
3265
|
|------|-------------|
|
|
2457
3266
|
| \`useDebounce\` | Debounce a value with configurable delay |
|
|
2458
3267
|
| \`useDebouncedCallback\` | Debounce a callback function |
|
|
2459
|
-
| \`
|
|
2460
|
-
| \`
|
|
3268
|
+
| \`useLocalStorage\` | Persist state to localStorage with JSON serialization |
|
|
3269
|
+
| \`useSessionStorage\` | Persist state to sessionStorage with JSON serialization |
|
|
2461
3270
|
| \`usePrevious\` | Track the previous value of a variable |
|
|
2462
|
-
| \`
|
|
2463
|
-
| \`
|
|
2464
|
-
| \`useSyncedRef\` | Keep a ref synchronized with the latest value |
|
|
2465
|
-
|
|
2466
|
-
### DOM & Browser
|
|
2467
|
-
|
|
2468
|
-
| Hook | Description |
|
|
2469
|
-
|------|-------------|
|
|
2470
|
-
| \`useClickOutside\` | Detect clicks outside a ref element |
|
|
3271
|
+
| \`useInterval\` | setInterval wrapper with pause support |
|
|
3272
|
+
| \`useTimeout\` | setTimeout wrapper with manual clear |
|
|
2471
3273
|
| \`useEventListener\` | Attach event listeners to window or elements |
|
|
2472
3274
|
| \`useElementSize\` | Track element width/height via ResizeObserver |
|
|
2473
3275
|
| \`useHover\` | Track mouse hover state |
|
|
2474
3276
|
| \`useKeyPress\` | Listen for a specific key press |
|
|
2475
|
-
| \`useKeyCombo\` | Listen for key + modifier combinations |
|
|
2476
|
-
| \`useLongPress\` | Detect long press gestures |
|
|
2477
|
-
| \`useFullscreen\` | Manage the Fullscreen API |
|
|
2478
|
-
| \`useTextSelection\` | Track currently selected text |
|
|
2479
|
-
| \`useFocusWithin\` | Track whether focus is inside a container |
|
|
2480
|
-
| \`useFocusTrap\` | Trap Tab/Shift+Tab focus within a container |
|
|
2481
|
-
| \`useBoundingClientRect\` | Track element bounding rect via ResizeObserver |
|
|
2482
|
-
| \`useSwipe\` | Detect touch swipe direction |
|
|
2483
|
-
| \`useDrag\` | Track mouse drag with position and delta |
|
|
2484
|
-
| \`useElementVisibility\` | Check if an element is in the viewport |
|
|
2485
3277
|
| \`useScrollPosition\` | Track window scroll position |
|
|
2486
3278
|
| \`useScrollLock\` | Lock/unlock body scroll |
|
|
2487
|
-
| \`useMutationObserver\` | Observe DOM mutations |
|
|
2488
|
-
| \`useIntersectionObserver\` | Observe element intersection with viewport |
|
|
2489
|
-
|
|
2490
|
-
### Timers & Lifecycle
|
|
2491
|
-
|
|
2492
|
-
| Hook | Description |
|
|
2493
|
-
|------|-------------|
|
|
2494
|
-
| \`useInterval\` | setInterval wrapper with pause support |
|
|
2495
|
-
| \`useTimeout\` | setTimeout wrapper with manual clear |
|
|
2496
|
-
| \`useCountdown\` | Countdown timer with start/pause/reset |
|
|
2497
|
-
| \`useStopwatch\` | Stopwatch with lap support |
|
|
2498
|
-
| \`useIdleTimer\` | Detect user idle time |
|
|
2499
|
-
| \`useUpdateEffect\` | useEffect that skips the initial render |
|
|
2500
|
-
| \`useIsomorphicLayoutEffect\` | SSR-safe useLayoutEffect |
|
|
2501
|
-
| \`useIsMounted\` | Check if component is currently mounted |
|
|
2502
|
-
| \`useIsFirstRender\` | Check if this is the first render |
|
|
2503
|
-
|
|
2504
|
-
### Async & Network
|
|
2505
|
-
|
|
2506
|
-
| Hook | Description |
|
|
2507
|
-
|------|-------------|
|
|
2508
|
-
| \`useFetch\` | Declarative data fetching with loading/error states (use for external URLs; use TanStack Query for API calls) |
|
|
2509
|
-
| \`useAsync\` | Execute async functions with status tracking |
|
|
2510
|
-
| \`useScript\` | Dynamically load external scripts |
|
|
2511
|
-
| \`useWebSocket\` | WebSocket connection with auto-reconnect |
|
|
2512
|
-
| \`useSSE\` | Server-Sent Events (EventSource) wrapper |
|
|
2513
|
-
| \`usePolling\` | Poll an async function at a fixed interval |
|
|
2514
|
-
| \`useAbortController\` | Manage AbortController lifecycle |
|
|
2515
|
-
| \`useRetry\` | Retry async operations with exponential backoff |
|
|
2516
|
-
| \`useSearch\` | Filter arrays with debounced search |
|
|
2517
|
-
|
|
2518
|
-
### Browser APIs
|
|
2519
|
-
|
|
2520
|
-
| Hook | Description |
|
|
2521
|
-
|------|-------------|
|
|
2522
|
-
| \`useMediaQuery\` | Reactive CSS media query matching |
|
|
2523
|
-
| \`useColorScheme\` | Detect system color scheme preference |
|
|
2524
|
-
| \`useCopyToClipboard\` | Copy text to clipboard with status feedback |
|
|
2525
3279
|
| \`useOnline\` | Track network connectivity |
|
|
2526
3280
|
| \`useWindowSize\` | Track window dimensions |
|
|
2527
3281
|
| \`usePageVisibility\` | Detect page visibility state |
|
|
2528
|
-
| \`
|
|
2529
|
-
| \`
|
|
2530
|
-
| \`
|
|
2531
|
-
| \`
|
|
2532
|
-
| \`useIsClient\` | SSR-safe client-side detection |
|
|
2533
|
-
|
|
2534
|
-
### Layout & UI
|
|
2535
|
-
|
|
2536
|
-
| Hook | Description |
|
|
2537
|
-
|------|-------------|
|
|
2538
|
-
| \`useStickyHeader\` | Detect when header should be sticky |
|
|
2539
|
-
| \`useVirtualList\` | Virtualized list rendering for large datasets |
|
|
3282
|
+
| \`useIsMounted\` | Check if component is currently mounted |
|
|
3283
|
+
| \`useIsFirstRender\` | Check if this is the first render |
|
|
3284
|
+
| \`useUpdateEffect\` | useEffect that skips the initial render |
|
|
3285
|
+
| \`useIntersectionObserver\` | Observe element intersection with viewport |
|
|
2540
3286
|
| \`useInfiniteScroll\` | Infinite scroll with threshold detection |
|
|
2541
|
-
| \`useCollapse\` | Collapse/expand animation with prop getters |
|
|
2542
|
-
| \`useSteps\` | Multi-step flow navigation |
|
|
2543
3287
|
|
|
2544
3288
|
### Common Patterns
|
|
2545
3289
|
|
|
2546
|
-
**Modal with
|
|
3290
|
+
**Modal with Chakra disclosure:**
|
|
2547
3291
|
\`\`\`tsx
|
|
2548
|
-
import { useDisclosure
|
|
3292
|
+
import { useDisclosure } from '@chakra-ui/react'
|
|
3293
|
+
import { Modal, ModalOverlay, ModalContent, ModalHeader, ModalBody, ModalCloseButton, Button } from '@chakra-ui/react'
|
|
2549
3294
|
|
|
2550
3295
|
function MyComponent() {
|
|
2551
|
-
const
|
|
2552
|
-
const ref = useClickOutside<HTMLDivElement>(close)
|
|
3296
|
+
const { isOpen, onOpen, onClose } = useDisclosure()
|
|
2553
3297
|
|
|
2554
3298
|
return (
|
|
2555
3299
|
<>
|
|
2556
|
-
<Button onClick={
|
|
2557
|
-
|
|
3300
|
+
<Button onClick={onOpen}>Open</Button>
|
|
3301
|
+
<Modal isOpen={isOpen} onClose={onClose}>
|
|
3302
|
+
<ModalOverlay />
|
|
3303
|
+
<ModalContent>
|
|
3304
|
+
<ModalHeader>Modal Title</ModalHeader>
|
|
3305
|
+
<ModalCloseButton />
|
|
3306
|
+
<ModalBody>Modal content here</ModalBody>
|
|
3307
|
+
</ModalContent>
|
|
3308
|
+
</Modal>
|
|
2558
3309
|
</>
|
|
2559
3310
|
)
|
|
2560
3311
|
}
|
|
2561
3312
|
\`\`\`
|
|
2562
3313
|
|
|
2563
|
-
**Debounced search:**
|
|
3314
|
+
**Debounced search (custom hook):**
|
|
2564
3315
|
\`\`\`tsx
|
|
2565
|
-
import { useDebounce
|
|
3316
|
+
import { useDebounce } from '@/shared/hooks/use-debounce'
|
|
3317
|
+
import { Input } from '@chakra-ui/react'
|
|
2566
3318
|
|
|
2567
3319
|
function SearchPage({ items }) {
|
|
2568
3320
|
const [query, setQuery] = useState('')
|
|
2569
3321
|
const debouncedQuery = useDebounce(query, 300)
|
|
2570
|
-
|
|
3322
|
+
// Use debouncedQuery for API calls or filtering
|
|
2571
3323
|
|
|
2572
3324
|
return (
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
{
|
|
2576
|
-
|
|
3325
|
+
<Input
|
|
3326
|
+
value={query}
|
|
3327
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
3328
|
+
placeholder="Search..."
|
|
3329
|
+
/>
|
|
2577
3330
|
)
|
|
2578
3331
|
}
|
|
2579
3332
|
\`\`\`
|
|
2580
3333
|
|
|
2581
|
-
**
|
|
3334
|
+
**Responsive layout with Chakra hook:**
|
|
2582
3335
|
\`\`\`tsx
|
|
2583
|
-
import {
|
|
3336
|
+
import { useBreakpointValue } from '@chakra-ui/react'
|
|
2584
3337
|
|
|
2585
|
-
function
|
|
2586
|
-
const
|
|
2587
|
-
const
|
|
3338
|
+
function Layout({ children }) {
|
|
3339
|
+
const columns = useBreakpointValue({ base: 1, md: 2, lg: 3 })
|
|
3340
|
+
const isMobile = useBreakpointValue({ base: true, md: false })
|
|
2588
3341
|
|
|
2589
|
-
|
|
3342
|
+
return isMobile ? <MobileLayout>{children}</MobileLayout> : <DesktopLayout>{children}</DesktopLayout>
|
|
2590
3343
|
}
|
|
2591
3344
|
\`\`\`
|
|
2592
3345
|
|
|
2593
|
-
**
|
|
3346
|
+
**Color mode toggle:**
|
|
2594
3347
|
\`\`\`tsx
|
|
2595
|
-
import {
|
|
3348
|
+
import { useColorMode, useColorModeValue, IconButton } from '@chakra-ui/react'
|
|
3349
|
+
import { Sun, Moon } from 'lucide-react'
|
|
2596
3350
|
|
|
2597
|
-
function
|
|
2598
|
-
const
|
|
2599
|
-
const
|
|
3351
|
+
function ColorModeToggle() {
|
|
3352
|
+
const { colorMode, toggleColorMode } = useColorMode()
|
|
3353
|
+
const icon = colorMode === 'light' ? <Moon size={16} /> : <Sun size={16} />
|
|
2600
3354
|
|
|
2601
|
-
return
|
|
3355
|
+
return <IconButton aria-label="Toggle color mode" icon={icon} onClick={toggleColorMode} variant="ghost" />
|
|
3356
|
+
}
|
|
3357
|
+
\`\`\`
|
|
3358
|
+
|
|
3359
|
+
**Persisted state with local storage:**
|
|
3360
|
+
\`\`\`tsx
|
|
3361
|
+
import { useLocalStorage } from '@/shared/hooks/use-local-storage'
|
|
3362
|
+
|
|
3363
|
+
function Editor() {
|
|
3364
|
+
const [saved, setSaved] = useLocalStorage('draft', '')
|
|
3365
|
+
// saved persists across page refreshes
|
|
2602
3366
|
}
|
|
2603
3367
|
\`\`\`
|
|
2604
3368
|
`;
|
|
@@ -2606,6 +3370,7 @@ function Layout({ children }) {
|
|
|
2606
3370
|
};
|
|
2607
3371
|
|
|
2608
3372
|
// src/skills/blacksmith-cli.ts
|
|
3373
|
+
init_esm_shims();
|
|
2609
3374
|
var blacksmithCliSkill = {
|
|
2610
3375
|
id: "blacksmith-cli",
|
|
2611
3376
|
name: "Blacksmith CLI",
|
|
@@ -2697,21 +3462,22 @@ blacksmith init my-app -b 9000 -f 3000 --ai
|
|
|
2697
3462
|
| \`-b, --backend-port <port>\` | Django port (default: 8000) |
|
|
2698
3463
|
| \`-f, --frontend-port <port>\` | Vite port (default: 5173) |
|
|
2699
3464
|
| \`--ai\` | Generate CLAUDE.md with project skills |
|
|
2700
|
-
| \`--no-
|
|
3465
|
+
| \`--no-chakra-ui-skill\` | Exclude Chakra UI skill from CLAUDE.md |
|
|
2701
3466
|
`;
|
|
2702
3467
|
}
|
|
2703
3468
|
};
|
|
2704
3469
|
|
|
2705
3470
|
// src/skills/ui-design.ts
|
|
3471
|
+
init_esm_shims();
|
|
2706
3472
|
var uiDesignSkill = {
|
|
2707
3473
|
id: "ui-design",
|
|
2708
3474
|
name: "UI/UX Design System",
|
|
2709
|
-
description: "Modern flat design principles, spacing, typography, color, layout patterns, and interaction guidelines aligned with the
|
|
3475
|
+
description: "Modern flat design principles, spacing, typography, color, layout patterns, and interaction guidelines aligned with the Chakra UI design language.",
|
|
2710
3476
|
render(_ctx) {
|
|
2711
3477
|
return `## UI/UX Design System \u2014 Modern Flat Design
|
|
2712
3478
|
|
|
2713
3479
|
> **Design philosophy: Clean, flat, content-first.**
|
|
2714
|
-
>
|
|
3480
|
+
> Chakra UI follows a clean design language similar to Anthropic, Apple, Linear, Vercel, and OpenAI \u2014 minimal chrome, generous whitespace, subtle depth, and purposeful motion. Every UI you build must conform to this standard.
|
|
2715
3481
|
|
|
2716
3482
|
### Core Principles
|
|
2717
3483
|
|
|
@@ -2723,7 +3489,7 @@ var uiDesignSkill = {
|
|
|
2723
3489
|
|
|
2724
3490
|
### Spacing System
|
|
2725
3491
|
|
|
2726
|
-
Use
|
|
3492
|
+
Use Chakra UI's spacing scale consistently. Chakra uses a numeric spacing scale (1 = 4px, 2 = 8px, etc.).
|
|
2727
3493
|
|
|
2728
3494
|
| Scale | Value | Use for |
|
|
2729
3495
|
|-------|-------|---------|
|
|
@@ -2735,57 +3501,51 @@ Use Tailwind's spacing scale consistently. Do NOT use arbitrary values (\`p-[13p
|
|
|
2735
3501
|
| \`16\`\u2013\`20\` | 64\u201380px | Page-level vertical padding (hero, landing sections) |
|
|
2736
3502
|
|
|
2737
3503
|
**Rules:**
|
|
2738
|
-
- Use \`
|
|
2739
|
-
- Use \`
|
|
2740
|
-
- Page content padding:
|
|
2741
|
-
- Card body padding: \`p
|
|
2742
|
-
- Never mix spacing approaches in the same context \u2014 pick
|
|
3504
|
+
- Use \`spacing\` (via \`VStack\`, \`HStack\`, \`SimpleGrid\`) for spacing between siblings \u2014 not margin on individual items
|
|
3505
|
+
- Use \`VStack spacing={...}\` for vertical rhythm within a section
|
|
3506
|
+
- Page content padding: use \`Container\` which handles responsive horizontal padding
|
|
3507
|
+
- Card body padding: \`p={6}\` standard, \`p={4}\` for compact cards
|
|
3508
|
+
- Never mix spacing approaches in the same context \u2014 pick spacing OR margin, not both
|
|
2743
3509
|
|
|
2744
3510
|
### Typography
|
|
2745
3511
|
|
|
2746
|
-
Use \`
|
|
3512
|
+
Use \`Heading\` and \`Text\` components from \`@chakra-ui/react\`. Do NOT style raw HTML headings.
|
|
2747
3513
|
|
|
2748
3514
|
**Hierarchy:**
|
|
2749
3515
|
| Level | Component | Use for |
|
|
2750
3516
|
|-------|-----------|---------|
|
|
2751
|
-
| Page title | \`<
|
|
2752
|
-
| Section title | \`<
|
|
2753
|
-
| Sub-section | \`<
|
|
2754
|
-
| Card title | \`<
|
|
3517
|
+
| Page title | \`<Heading as="h1" size="2xl">\` | One per page. The main heading. |
|
|
3518
|
+
| Section title | \`<Heading as="h2" size="xl">\` | Major sections within a page |
|
|
3519
|
+
| Sub-section | \`<Heading as="h3" size="lg">\` | Groups within a section |
|
|
3520
|
+
| Card title | \`<Heading as="h4" size="md">\` | Card headings |
|
|
2755
3521
|
| Body | \`<Text>\` | Paragraphs, descriptions |
|
|
2756
|
-
| Caption/label | \`<Text
|
|
2757
|
-
| Overline | \`<Text
|
|
3522
|
+
| Caption/label | \`<Text fontSize="sm" color="gray.500">\` | Secondary info, metadata, timestamps |
|
|
3523
|
+
| Overline | \`<Text fontSize="xs" fontWeight="medium" textTransform="uppercase" letterSpacing="wide">\` | Category labels, section overlines |
|
|
2758
3524
|
|
|
2759
3525
|
**Rules:**
|
|
2760
3526
|
- One \`h1\` per page \u2014 it's the page title
|
|
2761
|
-
- Headings should never skip levels (h1
|
|
2762
|
-
- Body text: \`
|
|
2763
|
-
-
|
|
2764
|
-
-
|
|
2765
|
-
-
|
|
2766
|
-
- Font weight: \`font-medium\` (500) for labels and emphasis, \`font-semibold\` (600) for headings, \`font-bold\` (700) sparingly
|
|
3527
|
+
- Headings should never skip levels (h1 -> h3 without h2)
|
|
3528
|
+
- Body text: \`fontSize="sm"\` (14px) for dense UIs (tables, sidebars), \`fontSize="md"\` (16px) for reading content
|
|
3529
|
+
- Max reading width: \`maxW="prose"\` (~65ch) for long-form text. Never let paragraphs stretch full-width
|
|
3530
|
+
- Use \`color="gray.500"\` or theme-aware \`useColorModeValue('gray.600', 'gray.400')\` for secondary text
|
|
3531
|
+
- Font weight: \`fontWeight="medium"\` (500) for labels, \`fontWeight="semibold"\` (600) for headings, \`fontWeight="bold"\` (700) sparingly
|
|
2767
3532
|
|
|
2768
3533
|
### Color
|
|
2769
3534
|
|
|
2770
|
-
Use
|
|
3535
|
+
Use Chakra UI's theme-aware color tokens, never hardcoded colors.
|
|
2771
3536
|
|
|
2772
|
-
**Semantic palette:**
|
|
3537
|
+
**Semantic palette (via colorScheme and theme tokens):**
|
|
2773
3538
|
| Token | Usage |
|
|
2774
3539
|
|-------|-------|
|
|
2775
|
-
| \`
|
|
2776
|
-
| \`
|
|
2777
|
-
| \`
|
|
2778
|
-
| \`
|
|
2779
|
-
| \`
|
|
2780
|
-
| \`foreground\` | Primary text |
|
|
2781
|
-
| \`muted-foreground\` | Secondary/helper text |
|
|
2782
|
-
| \`border\` | Borders, dividers |
|
|
2783
|
-
| \`card\` | Card backgrounds |
|
|
2784
|
-
| \`background\` | Page background |
|
|
3540
|
+
| \`blue\` (colorScheme) | Primary actions (buttons, links, active states) |
|
|
3541
|
+
| \`gray\` (colorScheme) | Secondary actions, subtle backgrounds |
|
|
3542
|
+
| \`red\` (colorScheme) | Delete, error, danger states |
|
|
3543
|
+
| \`green\` (colorScheme) | Success states |
|
|
3544
|
+
| \`yellow\` / \`orange\` | Warning states |
|
|
2785
3545
|
|
|
2786
3546
|
**Rules:**
|
|
2787
|
-
-
|
|
2788
|
-
- Status colors: use \`Badge\`
|
|
3547
|
+
- Use \`useColorModeValue()\` for any colors that need to adapt between light and dark mode
|
|
3548
|
+
- Status colors: use \`Badge\` with \`colorScheme\` (\`green\`, \`red\`, \`blue\`, \`gray\`) \u2014 don't hand-roll colored pills.
|
|
2789
3549
|
- Maximum 2\u20133 colors visible at any time (primary + foreground + muted). Colorful UIs feel noisy.
|
|
2790
3550
|
- Every UI must render correctly in both light and dark mode. See the Dark Mode section below for the full rules.
|
|
2791
3551
|
|
|
@@ -2794,66 +3554,66 @@ Use design tokens (CSS variables), never hardcoded colors.
|
|
|
2794
3554
|
**Page layout:**
|
|
2795
3555
|
\`\`\`tsx
|
|
2796
3556
|
<Box as="main">
|
|
2797
|
-
<Container>
|
|
2798
|
-
<
|
|
3557
|
+
<Container maxW="container.xl">
|
|
3558
|
+
<VStack spacing={8} align="stretch">
|
|
2799
3559
|
{/* Page header */}
|
|
2800
|
-
<Flex align="center" justify="between">
|
|
2801
|
-
<
|
|
2802
|
-
<
|
|
2803
|
-
<Text color="
|
|
2804
|
-
</
|
|
2805
|
-
<Button>Primary Action</Button>
|
|
3560
|
+
<Flex align="center" justify="space-between">
|
|
3561
|
+
<VStack spacing={1} align="start">
|
|
3562
|
+
<Heading as="h1" size="2xl">Page Title</Heading>
|
|
3563
|
+
<Text color="gray.500">Brief description of this page</Text>
|
|
3564
|
+
</VStack>
|
|
3565
|
+
<Button colorScheme="blue">Primary Action</Button>
|
|
2806
3566
|
</Flex>
|
|
2807
3567
|
|
|
2808
3568
|
{/* Page content sections */}
|
|
2809
|
-
<
|
|
3569
|
+
<VStack spacing={6} align="stretch">
|
|
2810
3570
|
{/* ... */}
|
|
2811
|
-
</
|
|
2812
|
-
</
|
|
3571
|
+
</VStack>
|
|
3572
|
+
</VStack>
|
|
2813
3573
|
</Container>
|
|
2814
3574
|
</Box>
|
|
2815
3575
|
\`\`\`
|
|
2816
3576
|
|
|
2817
3577
|
**Card-based content:**
|
|
2818
3578
|
\`\`\`tsx
|
|
2819
|
-
<
|
|
3579
|
+
<SimpleGrid columns={{ base: 1, md: 2, lg: 3 }} spacing={6}>
|
|
2820
3580
|
{items.map((item) => (
|
|
2821
3581
|
<Card key={item.id}>
|
|
2822
3582
|
<CardHeader>
|
|
2823
|
-
<
|
|
2824
|
-
<
|
|
3583
|
+
<Heading size="md">{item.title}</Heading>
|
|
3584
|
+
<Text fontSize="sm" color="gray.500">{item.description}</Text>
|
|
2825
3585
|
</CardHeader>
|
|
2826
|
-
<
|
|
3586
|
+
<CardBody>
|
|
2827
3587
|
{/* Content */}
|
|
2828
|
-
</
|
|
3588
|
+
</CardBody>
|
|
2829
3589
|
</Card>
|
|
2830
3590
|
))}
|
|
2831
|
-
</
|
|
3591
|
+
</SimpleGrid>
|
|
2832
3592
|
\`\`\`
|
|
2833
3593
|
|
|
2834
3594
|
**Sidebar + main content:**
|
|
2835
3595
|
\`\`\`tsx
|
|
2836
|
-
<Flex
|
|
2837
|
-
<
|
|
2838
|
-
<Box as="main"
|
|
2839
|
-
<Container>{/* Page content */}</Container>
|
|
3596
|
+
<Flex minH="100vh">
|
|
3597
|
+
<Box as="aside" w="250px">{/* Nav items */}</Box>
|
|
3598
|
+
<Box as="main" flex={1}>
|
|
3599
|
+
<Container maxW="container.xl">{/* Page content */}</Container>
|
|
2840
3600
|
</Box>
|
|
2841
3601
|
</Flex>
|
|
2842
3602
|
\`\`\`
|
|
2843
3603
|
|
|
2844
3604
|
**Section with centered content (landing pages):**
|
|
2845
3605
|
\`\`\`tsx
|
|
2846
|
-
<Box as="section"
|
|
2847
|
-
<Container>
|
|
2848
|
-
<
|
|
2849
|
-
<
|
|
2850
|
-
<Text color="
|
|
3606
|
+
<Box as="section" py={{ base: 16, sm: 20 }}>
|
|
3607
|
+
<Container maxW="container.xl">
|
|
3608
|
+
<VStack spacing={4} align="center" textAlign="center">
|
|
3609
|
+
<Heading as="h2" size="xl">Section Title</Heading>
|
|
3610
|
+
<Text color="gray.500" maxW="2xl">
|
|
2851
3611
|
A concise description that explains the value proposition.
|
|
2852
3612
|
</Text>
|
|
2853
|
-
</
|
|
2854
|
-
<
|
|
3613
|
+
</VStack>
|
|
3614
|
+
<SimpleGrid columns={{ base: 1, md: 3 }} spacing={8} mt={12}>
|
|
2855
3615
|
{/* Feature cards or content */}
|
|
2856
|
-
</
|
|
3616
|
+
</SimpleGrid>
|
|
2857
3617
|
</Container>
|
|
2858
3618
|
</Box>
|
|
2859
3619
|
\`\`\`
|
|
@@ -2862,15 +3622,15 @@ Use design tokens (CSS variables), never hardcoded colors.
|
|
|
2862
3622
|
|
|
2863
3623
|
**Empty states:**
|
|
2864
3624
|
\`\`\`tsx
|
|
2865
|
-
// GOOD \u2014 uses
|
|
2866
|
-
<
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
// BAD \u2014 hand-rolled empty state
|
|
3625
|
+
// GOOD \u2014 uses a well-structured empty state with Chakra components
|
|
3626
|
+
<VStack spacing={4} align="center" py={12} textAlign="center">
|
|
3627
|
+
<Icon as={Inbox} boxSize={12} color="gray.400" />
|
|
3628
|
+
<Heading size="md">No messages yet</Heading>
|
|
3629
|
+
<Text color="gray.500">Messages from your team will appear here.</Text>
|
|
3630
|
+
<Button colorScheme="blue">Send a message</Button>
|
|
3631
|
+
</VStack>
|
|
3632
|
+
|
|
3633
|
+
// BAD \u2014 hand-rolled empty state with raw HTML
|
|
2874
3634
|
<div className="flex flex-col items-center justify-center py-12 text-center">
|
|
2875
3635
|
<Inbox className="h-12 w-12 text-gray-400 mb-4" />
|
|
2876
3636
|
<h3 className="text-lg font-medium">No messages yet</h3>
|
|
@@ -2880,33 +3640,33 @@ Use design tokens (CSS variables), never hardcoded colors.
|
|
|
2880
3640
|
|
|
2881
3641
|
**Stats/metrics:**
|
|
2882
3642
|
\`\`\`tsx
|
|
2883
|
-
// GOOD \u2014 uses
|
|
2884
|
-
<
|
|
2885
|
-
<
|
|
2886
|
-
|
|
2887
|
-
</
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
<
|
|
2891
|
-
|
|
2892
|
-
<
|
|
2893
|
-
<
|
|
2894
|
-
</
|
|
2895
|
-
</
|
|
3643
|
+
// GOOD \u2014 uses Chakra Stat component
|
|
3644
|
+
<SimpleGrid columns={{ base: 1, sm: 2, lg: 4 }} spacing={4}>
|
|
3645
|
+
<Stat>
|
|
3646
|
+
<StatLabel>Total Users</StatLabel>
|
|
3647
|
+
<StatNumber>2,847</StatNumber>
|
|
3648
|
+
<StatHelpText><StatArrow type="increase" />12%</StatHelpText>
|
|
3649
|
+
</Stat>
|
|
3650
|
+
<Stat>
|
|
3651
|
+
<StatLabel>Revenue</StatLabel>
|
|
3652
|
+
<StatNumber>$48,290</StatNumber>
|
|
3653
|
+
<StatHelpText><StatArrow type="increase" />8%</StatHelpText>
|
|
3654
|
+
</Stat>
|
|
3655
|
+
</SimpleGrid>
|
|
2896
3656
|
\`\`\`
|
|
2897
3657
|
|
|
2898
3658
|
**Loading states:**
|
|
2899
3659
|
\`\`\`tsx
|
|
2900
3660
|
// GOOD \u2014 Skeleton matches the layout structure
|
|
2901
|
-
<
|
|
2902
|
-
<Skeleton
|
|
2903
|
-
<
|
|
2904
|
-
<
|
|
3661
|
+
<VStack spacing={4} align="stretch">
|
|
3662
|
+
<Skeleton h="32px" w="200px" />
|
|
3663
|
+
<SkeletonText noOfLines={2} />
|
|
3664
|
+
<SimpleGrid columns={3} spacing={4}>
|
|
2905
3665
|
{Array.from({ length: 3 }).map((_, i) => (
|
|
2906
|
-
<Skeleton key={i}
|
|
3666
|
+
<Skeleton key={i} h="128px" />
|
|
2907
3667
|
))}
|
|
2908
|
-
</
|
|
2909
|
-
</
|
|
3668
|
+
</SimpleGrid>
|
|
3669
|
+
</VStack>
|
|
2910
3670
|
|
|
2911
3671
|
// BAD \u2014 generic spinner with no layout hint
|
|
2912
3672
|
<div className="flex justify-center py-12">
|
|
@@ -2918,87 +3678,52 @@ Use design tokens (CSS variables), never hardcoded colors.
|
|
|
2918
3678
|
|
|
2919
3679
|
> **CRITICAL: Every screen, component, and custom style MUST look correct in both light and dark mode. No exceptions.**
|
|
2920
3680
|
|
|
2921
|
-
|
|
3681
|
+
Chakra UI supports color mode via \`useColorMode()\` and \`useColorModeValue()\`. All built-in components automatically adapt.
|
|
2922
3682
|
|
|
2923
3683
|
**Rules:**
|
|
2924
|
-
-
|
|
2925
|
-
- NEVER
|
|
2926
|
-
- NEVER use \`
|
|
2927
|
-
-
|
|
2928
|
-
- NEVER use opacity-based overlays with hardcoded colors (\`bg-black/50\`). Use \`bg-background/80\` or let overlay components (\`Dialog\`, \`Sheet\`) handle it.
|
|
2929
|
-
- SVG fills and strokes: use \`currentColor\` or \`fill-foreground\` / \`stroke-border\` \u2014 never \`fill-black\` or \`stroke-gray-300\`.
|
|
2930
|
-
- Image assets: if you use decorative images or illustrations, ensure they work on both backgrounds or use \`dark:hidden\` / \`hidden dark:block\` to swap variants.
|
|
2931
|
-
|
|
2932
|
-
**Safe color tokens (always use these):**
|
|
2933
|
-
| Need | Light mode maps to | Dark mode maps to | Use |
|
|
2934
|
-
|------|----|----|-----|
|
|
2935
|
-
| Page background | white/light gray | near-black | \`bg-background\` |
|
|
2936
|
-
| Card/surface | white | dark gray | \`bg-card\` |
|
|
2937
|
-
| Subtle background | light gray | darker gray | \`bg-muted\` |
|
|
2938
|
-
| Primary text | near-black | near-white | \`text-foreground\` |
|
|
2939
|
-
| Secondary text | medium gray | lighter gray | \`text-muted-foreground\` |
|
|
2940
|
-
| Borders | light gray | dark gray | \`border-border\` |
|
|
2941
|
-
| Input borders | light gray | dark gray | \`border-input\` |
|
|
2942
|
-
| Focus ring | brand color | brand color | \`ring-ring\` |
|
|
2943
|
-
| Primary action | brand color | brand color | \`bg-primary text-primary-foreground\` |
|
|
2944
|
-
| Destructive | red | red | \`bg-destructive text-destructive-foreground\` |
|
|
2945
|
-
|
|
2946
|
-
**Testing checklist (mental model):**
|
|
2947
|
-
Before considering any UI complete, verify these in your head:
|
|
2948
|
-
1. Does every text element use \`foreground\`, \`muted-foreground\`, or \`*-foreground\` tokens?
|
|
2949
|
-
2. Does every background use \`background\`, \`card\`, \`muted\`, or \`primary\`/\`secondary\`/\`accent\` tokens?
|
|
2950
|
-
3. Does every border use \`border\`, \`input\`, or \`ring\` tokens?
|
|
2951
|
-
4. Are there ANY hex values, rgb values, or Tailwind color names (gray, slate, blue, etc.) in the code? If yes, replace them.
|
|
2952
|
-
5. Do hover/focus/active states also use semantic tokens? (\`hover:bg-muted\` not \`hover:bg-gray-100\`)
|
|
3684
|
+
- Use \`useColorModeValue()\` for any custom colors that need to differ between modes
|
|
3685
|
+
- NEVER hardcode colors that only work in one mode. Use theme tokens or \`useColorModeValue()\`.
|
|
3686
|
+
- NEVER use \`bg="white"\` or \`bg="black"\`. Use \`bg={useColorModeValue('white', 'gray.800')}\` or Chakra semantic tokens.
|
|
3687
|
+
- All Chakra UI components automatically adapt to color mode \u2014 leverage this.
|
|
2953
3688
|
|
|
2954
3689
|
### Interactions & Feedback
|
|
2955
3690
|
|
|
2956
|
-
- **Hover states**: Subtle background change
|
|
2957
|
-
- **Focus**:
|
|
2958
|
-
- **
|
|
2959
|
-
- **Click feedback**: Use \`active:scale-[0.98]\` only on buttons and interactive cards, never on text or static elements
|
|
2960
|
-
- **Loading feedback**: Show \`Spinner\` on buttons during async actions. Use \`Skeleton\` for content areas. Never leave the user without feedback during loading
|
|
3691
|
+
- **Hover states**: Subtle background change \u2014 Chakra components handle this automatically
|
|
3692
|
+
- **Focus**: Chakra components include accessible focus rings by default
|
|
3693
|
+
- **Loading feedback**: Show \`Spinner\` on buttons via \`isLoading\` prop. Use \`Skeleton\` for content areas. Never leave the user without feedback during loading
|
|
2961
3694
|
- **Success/error feedback**: Use \`useToast()\` for transient confirmations. Use \`Alert\` for persistent messages. Never use \`window.alert()\`
|
|
2962
3695
|
- **Confirmation before destructive actions**: Always use \`AlertDialog\` for delete/remove actions. Never delete on single click
|
|
2963
3696
|
|
|
2964
3697
|
### Responsive Design
|
|
2965
3698
|
|
|
2966
|
-
- **Mobile-first**:
|
|
2967
|
-
- **Breakpoints**: \`sm\` (
|
|
2968
|
-
- **
|
|
2969
|
-
- **Hide/show**: Use \`
|
|
2970
|
-
- **Touch targets**: Minimum
|
|
2971
|
-
- **Stack
|
|
3699
|
+
- **Mobile-first**: Chakra's responsive props use mobile-first breakpoints
|
|
3700
|
+
- **Breakpoints**: \`sm\` (480px), \`md\` (768px), \`lg\` (992px), \`xl\` (1280px), \`2xl\` (1536px)
|
|
3701
|
+
- **Responsive props**: \`columns={{ base: 1, md: 2, lg: 3 }}\` \u2014 single column on mobile, expand on larger screens
|
|
3702
|
+
- **Hide/show**: Use Chakra's \`Show\` and \`Hide\` components or \`display={{ base: 'none', md: 'block' }}\`
|
|
3703
|
+
- **Touch targets**: Minimum 44x44px for interactive elements on mobile. Use \`Button size="lg"\` and adequate padding
|
|
3704
|
+
- **Stack direction**: Use \`Stack direction={{ base: 'column', md: 'row' }}\` for responsive stacking
|
|
2972
3705
|
- **Container**: Always wrap page content in \`<Container>\` \u2014 it handles responsive horizontal padding
|
|
2973
3706
|
|
|
2974
3707
|
### Anti-Patterns \u2014 NEVER Do These
|
|
2975
3708
|
|
|
2976
3709
|
| Anti-pattern | What to do instead |
|
|
2977
3710
|
|---|---|
|
|
2978
|
-
|
|
|
2979
|
-
|
|
|
2980
|
-
|
|
|
2981
|
-
|
|
|
2982
|
-
|
|
|
2983
|
-
| Custom
|
|
2984
|
-
| Animated entrances (fade-in, slide-up
|
|
2985
|
-
|
|
|
2986
|
-
|
|
|
2987
|
-
|
|
|
2988
|
-
| Dense walls of text | Break into sections with headings, cards, or spacing |
|
|
2989
|
-
| Colored backgrounds on every section | Use \`bg-background\` as default, \`bg-muted\` sparingly for contrast |
|
|
2990
|
-
| Over-using badges/tags on everything | Badges are for status and categories, not decoration |
|
|
2991
|
-
| Inline styles (\`style={{ ... }}\`) | Use Tailwind classes via \`className\` |
|
|
2992
|
-
| \`bg-white\` / \`bg-black\` / \`bg-slate-*\` | Use \`bg-background\`, \`bg-card\`, \`bg-muted\` |
|
|
2993
|
-
| \`text-black\` / \`text-white\` / \`text-gray-*\` | Use \`text-foreground\`, \`text-muted-foreground\` |
|
|
2994
|
-
| \`border-gray-*\` / \`border-slate-*\` | Use \`border-border\`, \`border-input\` |
|
|
2995
|
-
| Hex/rgb values in className or style | Use CSS variable tokens exclusively |
|
|
2996
|
-
| UI that only looks right in light mode | Always verify both modes \u2014 use semantic tokens throughout |
|
|
3711
|
+
| Raw \`<div>\` with flex/grid classes | Use \`Flex\`, \`VStack\`, \`HStack\`, \`SimpleGrid\` |
|
|
3712
|
+
| Raw \`<h1>\`-\`<h6>\` tags | Use \`Heading\` with \`as\` and \`size\` props |
|
|
3713
|
+
| Raw \`<p>\` tags | Use \`Text\` |
|
|
3714
|
+
| Heavy box shadows | Use Chakra's built-in shadow prop: \`shadow="sm"\`, \`shadow="md"\` |
|
|
3715
|
+
| Gradient backgrounds on surfaces | Use solid backgrounds |
|
|
3716
|
+
| Custom scrollbar CSS hacks | Use Chakra's styling system |
|
|
3717
|
+
| Animated entrances (fade-in, slide-up) | Content should appear instantly. Only animate user-triggered changes |
|
|
3718
|
+
| Using \`<br />\` for spacing | Use \`VStack spacing={...}\` or Chakra spacing props |
|
|
3719
|
+
| Inline styles (\`style={{ ... }}\`) | Use Chakra style props (\`p\`, \`m\`, \`bg\`, \`color\`, etc.) |
|
|
3720
|
+
| Hardcoded color values | Use theme tokens and \`useColorModeValue()\` |
|
|
2997
3721
|
`;
|
|
2998
3722
|
}
|
|
2999
3723
|
};
|
|
3000
3724
|
|
|
3001
3725
|
// src/skills/programming-paradigms.ts
|
|
3726
|
+
init_esm_shims();
|
|
3002
3727
|
var programmingParadigmsSkill = {
|
|
3003
3728
|
id: "programming-paradigms",
|
|
3004
3729
|
name: "Programming Paradigms",
|
|
@@ -3427,7 +4152,249 @@ class OrderService:
|
|
|
3427
4152
|
}
|
|
3428
4153
|
};
|
|
3429
4154
|
|
|
4155
|
+
// src/skills/frontend-testing.ts
|
|
4156
|
+
init_esm_shims();
|
|
4157
|
+
var frontendTestingSkill = {
|
|
4158
|
+
id: "frontend-testing",
|
|
4159
|
+
name: "Frontend Testing Conventions",
|
|
4160
|
+
description: "Test infrastructure, file placement, test utilities, and rules for when and how to write frontend tests.",
|
|
4161
|
+
render(_ctx) {
|
|
4162
|
+
return `## Frontend Testing Conventions
|
|
4163
|
+
|
|
4164
|
+
### Stack
|
|
4165
|
+
- **Vitest** \u2014 test runner (configured in \`vite.config.ts\`)
|
|
4166
|
+
- **jsdom** \u2014 browser environment
|
|
4167
|
+
- **React Testing Library** \u2014 component rendering and queries
|
|
4168
|
+
- **\`@testing-library/user-event\`** \u2014 user interaction simulation
|
|
4169
|
+
- **\`@testing-library/jest-dom\`** \u2014 DOM assertion matchers (e.g. \`toBeInTheDocument\`)
|
|
4170
|
+
|
|
4171
|
+
### Running Tests
|
|
4172
|
+
- Run all tests: \`cd frontend && npm test\`
|
|
4173
|
+
- Watch mode: \`cd frontend && npm run test:watch\`
|
|
4174
|
+
- Run a specific file: \`cd frontend && npx vitest run src/pages/home/__tests__/home.spec.tsx\`
|
|
4175
|
+
- Coverage: \`cd frontend && npm run test:coverage\`
|
|
4176
|
+
|
|
4177
|
+
### File Placement \u2014 Tests Live Next to the Code
|
|
4178
|
+
|
|
4179
|
+
> **RULE: Every test file goes in a \`__tests__/\` folder co-located with the code it tests. Never put tests in a top-level \`tests/\` directory.**
|
|
4180
|
+
|
|
4181
|
+
\`\`\`
|
|
4182
|
+
pages/customers/
|
|
4183
|
+
\u251C\u2500\u2500 customers-page.tsx
|
|
4184
|
+
\u251C\u2500\u2500 customer-detail-page.tsx
|
|
4185
|
+
\u251C\u2500\u2500 __tests__/ # Page integration tests
|
|
4186
|
+
\u2502 \u251C\u2500\u2500 customers-page.spec.tsx
|
|
4187
|
+
\u2502 \u2514\u2500\u2500 customer-detail-page.spec.tsx
|
|
4188
|
+
\u251C\u2500\u2500 components/
|
|
4189
|
+
\u2502 \u251C\u2500\u2500 customer-card.tsx
|
|
4190
|
+
\u2502 \u251C\u2500\u2500 customer-list.tsx
|
|
4191
|
+
\u2502 \u251C\u2500\u2500 customer-form.tsx
|
|
4192
|
+
\u2502 \u2514\u2500\u2500 __tests__/ # Component unit tests
|
|
4193
|
+
\u2502 \u251C\u2500\u2500 customer-card.spec.tsx
|
|
4194
|
+
\u2502 \u251C\u2500\u2500 customer-list.spec.tsx
|
|
4195
|
+
\u2502 \u2514\u2500\u2500 customer-form.spec.tsx
|
|
4196
|
+
\u2514\u2500\u2500 hooks/
|
|
4197
|
+
\u251C\u2500\u2500 use-customers-page.ts
|
|
4198
|
+
\u2514\u2500\u2500 __tests__/ # Hook tests
|
|
4199
|
+
\u2514\u2500\u2500 use-customers-page.spec.ts
|
|
4200
|
+
\`\`\`
|
|
4201
|
+
|
|
4202
|
+
The same pattern applies to \`features/\`, \`shared/\`, and \`router/\`:
|
|
4203
|
+
\`\`\`
|
|
4204
|
+
features/auth/
|
|
4205
|
+
\u251C\u2500\u2500 pages/
|
|
4206
|
+
\u2502 \u251C\u2500\u2500 login-page.tsx
|
|
4207
|
+
\u2502 \u2514\u2500\u2500 __tests__/
|
|
4208
|
+
\u2502 \u2514\u2500\u2500 login-page.spec.tsx
|
|
4209
|
+
\u251C\u2500\u2500 hooks/
|
|
4210
|
+
\u2502 \u2514\u2500\u2500 __tests__/
|
|
4211
|
+
shared/
|
|
4212
|
+
\u251C\u2500\u2500 components/
|
|
4213
|
+
\u2502 \u2514\u2500\u2500 __tests__/
|
|
4214
|
+
\u251C\u2500\u2500 hooks/
|
|
4215
|
+
\u2502 \u2514\u2500\u2500 __tests__/
|
|
4216
|
+
router/
|
|
4217
|
+
\u251C\u2500\u2500 __tests__/
|
|
4218
|
+
\u2502 \u251C\u2500\u2500 paths.spec.ts
|
|
4219
|
+
\u2502 \u2514\u2500\u2500 auth-guard.spec.tsx
|
|
4220
|
+
\`\`\`
|
|
4221
|
+
|
|
4222
|
+
### Test File Naming
|
|
4223
|
+
- Use \`.spec.tsx\` for component/page tests (JSX)
|
|
4224
|
+
- Use \`.spec.ts\` for pure logic tests (hooks, utilities, no JSX)
|
|
4225
|
+
- Name matches the source file: \`customer-card.tsx\` \u2192 \`customer-card.spec.tsx\`
|
|
4226
|
+
|
|
4227
|
+
### Always Use \`renderWithProviders\`
|
|
4228
|
+
|
|
4229
|
+
> **RULE: Never import \`render\` from \`@testing-library/react\` directly. Always use \`renderWithProviders\` from \`@/__tests__/test-utils\`.**
|
|
4230
|
+
|
|
4231
|
+
\`renderWithProviders\` wraps components with all app providers (ChakraProvider, QueryClientProvider, MemoryRouter) so tests match the real app environment.
|
|
4232
|
+
|
|
4233
|
+
\`\`\`tsx
|
|
4234
|
+
import { screen } from '@/__tests__/test-utils'
|
|
4235
|
+
import { renderWithProviders } from '@/__tests__/test-utils'
|
|
4236
|
+
import { MyComponent } from '../my-component'
|
|
4237
|
+
|
|
4238
|
+
describe('MyComponent', () => {
|
|
4239
|
+
it('renders correctly', () => {
|
|
4240
|
+
renderWithProviders(<MyComponent />)
|
|
4241
|
+
expect(screen.getByText('Hello')).toBeInTheDocument()
|
|
4242
|
+
})
|
|
4243
|
+
})
|
|
4244
|
+
\`\`\`
|
|
4245
|
+
|
|
4246
|
+
**Options:**
|
|
4247
|
+
\`\`\`tsx
|
|
4248
|
+
renderWithProviders(<MyComponent />, {
|
|
4249
|
+
routerEntries: ['/customers/1'], // Set initial route
|
|
4250
|
+
queryClient: customQueryClient, // Custom query client
|
|
4251
|
+
})
|
|
4252
|
+
\`\`\`
|
|
4253
|
+
|
|
4254
|
+
**User interactions:**
|
|
4255
|
+
\`\`\`tsx
|
|
4256
|
+
const { user } = renderWithProviders(<MyComponent />)
|
|
4257
|
+
await user.click(screen.getByRole('button', { name: 'Submit' }))
|
|
4258
|
+
await user.type(screen.getByLabelText('Email'), 'test@example.com')
|
|
4259
|
+
\`\`\`
|
|
4260
|
+
|
|
4261
|
+
### When to Write Tests
|
|
4262
|
+
|
|
4263
|
+
> **RULE: Every code change that touches pages, components, hooks, or utilities must include corresponding test updates.**
|
|
4264
|
+
|
|
4265
|
+
| What changed | Test required |
|
|
4266
|
+
|---|---|
|
|
4267
|
+
| New page | Add \`__tests__/<page>.spec.tsx\` with integration test |
|
|
4268
|
+
| New component | Add \`__tests__/<component>.spec.tsx\` |
|
|
4269
|
+
| New hook (with logic) | Add \`__tests__/<hook>.spec.ts\` |
|
|
4270
|
+
| New utility function | Add \`__tests__/<util>.spec.ts\` |
|
|
4271
|
+
| Modified page/component | Update existing test or add new test cases |
|
|
4272
|
+
| Bug fix | Add regression test that would have caught the bug |
|
|
4273
|
+
| Deleted page/component | Delete corresponding test file |
|
|
4274
|
+
|
|
4275
|
+
### What to Test
|
|
4276
|
+
|
|
4277
|
+
**Page integration tests** \u2014 test the page as a whole:
|
|
4278
|
+
- Renders correct heading/title
|
|
4279
|
+
- Loading state shows skeleton or spinner
|
|
4280
|
+
- Error state shows error message
|
|
4281
|
+
- Data renders correctly (mock the API hooks)
|
|
4282
|
+
- User interactions (navigation, form submission, delete confirmation)
|
|
4283
|
+
|
|
4284
|
+
**Component unit tests** \u2014 test the component in isolation:
|
|
4285
|
+
- Renders with required props
|
|
4286
|
+
- Handles optional props correctly (present vs absent)
|
|
4287
|
+
- Displays correct content based on props
|
|
4288
|
+
- User interactions trigger correct callbacks
|
|
4289
|
+
- Conditional rendering (empty state, loading state)
|
|
4290
|
+
|
|
4291
|
+
**Hook tests** \u2014 test custom hooks with logic:
|
|
4292
|
+
- Returns correct initial state
|
|
4293
|
+
- Transforms data correctly
|
|
4294
|
+
- Side effects fire as expected
|
|
4295
|
+
|
|
4296
|
+
**Utility/pure function tests** \u2014 test input/output:
|
|
4297
|
+
- Happy path
|
|
4298
|
+
- Edge cases (empty input, null, special characters)
|
|
4299
|
+
- Error cases
|
|
4300
|
+
|
|
4301
|
+
### Mocking Patterns
|
|
4302
|
+
|
|
4303
|
+
**Mock hooks (for page tests):**
|
|
4304
|
+
\`\`\`tsx
|
|
4305
|
+
vi.mock('@/api/hooks/customers')
|
|
4306
|
+
vi.mock('@/features/auth/hooks/use-auth')
|
|
4307
|
+
|
|
4308
|
+
import { useCustomers } from '@/api/hooks/customers'
|
|
4309
|
+
|
|
4310
|
+
beforeEach(() => {
|
|
4311
|
+
vi.mocked(useCustomers).mockReturnValue({
|
|
4312
|
+
data: { customers: mockCustomers, total: 2 },
|
|
4313
|
+
isLoading: false,
|
|
4314
|
+
errorMessage: null,
|
|
4315
|
+
} as any)
|
|
4316
|
+
})
|
|
4317
|
+
\`\`\`
|
|
4318
|
+
|
|
4319
|
+
**Mock auth hook (for auth page tests):**
|
|
4320
|
+
\`\`\`tsx
|
|
4321
|
+
vi.mock('@/features/auth/hooks/use-auth', () => ({
|
|
4322
|
+
useAuth: () => ({
|
|
4323
|
+
user: null,
|
|
4324
|
+
loading: false,
|
|
4325
|
+
error: null,
|
|
4326
|
+
isAuthenticated: false,
|
|
4327
|
+
signInWithEmail: vi.fn(),
|
|
4328
|
+
signOut: vi.fn(),
|
|
4329
|
+
}),
|
|
4330
|
+
}))
|
|
4331
|
+
\`\`\`
|
|
4332
|
+
|
|
4333
|
+
**Mock react-router-dom hooks (for detail pages):**
|
|
4334
|
+
\`\`\`tsx
|
|
4335
|
+
const mockNavigate = vi.fn()
|
|
4336
|
+
vi.mock('react-router-dom', async () => {
|
|
4337
|
+
const actual = await vi.importActual('react-router-dom')
|
|
4338
|
+
return { ...actual, useParams: () => ({ id: '1' }), useNavigate: () => mockNavigate }
|
|
4339
|
+
})
|
|
4340
|
+
\`\`\`
|
|
4341
|
+
|
|
4342
|
+
### Test Structure
|
|
4343
|
+
\`\`\`tsx
|
|
4344
|
+
import { screen, waitFor } from '@/__tests__/test-utils'
|
|
4345
|
+
import { renderWithProviders } from '@/__tests__/test-utils'
|
|
4346
|
+
|
|
4347
|
+
// Mocks at the top, before imports of modules that use them
|
|
4348
|
+
vi.mock('@/api/hooks/customers')
|
|
4349
|
+
|
|
4350
|
+
import { useCustomers } from '@/api/hooks/customers'
|
|
4351
|
+
import CustomersPage from '../customers-page'
|
|
4352
|
+
|
|
4353
|
+
const mockCustomers = [
|
|
4354
|
+
{ id: '1', title: 'Acme Corp', created_at: '2024-01-15T10:00:00Z' },
|
|
4355
|
+
]
|
|
4356
|
+
|
|
4357
|
+
describe('CustomersPage', () => {
|
|
4358
|
+
beforeEach(() => {
|
|
4359
|
+
vi.mocked(useCustomers).mockReturnValue({ ... } as any)
|
|
4360
|
+
})
|
|
4361
|
+
|
|
4362
|
+
it('renders page heading', () => {
|
|
4363
|
+
renderWithProviders(<CustomersPage />)
|
|
4364
|
+
expect(screen.getByText('Customers')).toBeInTheDocument()
|
|
4365
|
+
})
|
|
4366
|
+
|
|
4367
|
+
it('shows error message when API fails', () => {
|
|
4368
|
+
vi.mocked(useCustomers).mockReturnValue({
|
|
4369
|
+
data: undefined,
|
|
4370
|
+
isLoading: false,
|
|
4371
|
+
errorMessage: 'Failed to load',
|
|
4372
|
+
} as any)
|
|
4373
|
+
|
|
4374
|
+
renderWithProviders(<CustomersPage />)
|
|
4375
|
+
expect(screen.getByText('Failed to load')).toBeInTheDocument()
|
|
4376
|
+
})
|
|
4377
|
+
})
|
|
4378
|
+
\`\`\`
|
|
4379
|
+
|
|
4380
|
+
### Key Rules
|
|
4381
|
+
|
|
4382
|
+
1. **Tests live next to code** \u2014 \`__tests__/\` folder alongside the source, not in a separate top-level directory
|
|
4383
|
+
2. **Always use \`renderWithProviders\`** \u2014 never import render from \`@testing-library/react\` directly
|
|
4384
|
+
3. **Every page gets an integration test** \u2014 at minimum: renders heading, handles loading, handles errors
|
|
4385
|
+
4. **Every component gets a unit test** \u2014 at minimum: renders with required props, handles optional props
|
|
4386
|
+
5. **Mock at the hook level** \u2014 mock \`useCustomers\`, not \`fetch\`. Mock \`useAuth\`, not the auth adapter
|
|
4387
|
+
6. **Test behavior, not implementation** \u2014 query by role, text, or label, not by class names or internal state
|
|
4388
|
+
7. **No test-only IDs unless necessary** \u2014 prefer \`getByRole\`, \`getByText\`, \`getByLabelText\` over \`getByTestId\`
|
|
4389
|
+
8. **Keep tests focused** \u2014 each \`it()\` tests one behavior. Don't assert 10 things in one test
|
|
4390
|
+
9. **Clean up mocks** \u2014 use \`beforeEach\` to reset mock return values so tests don't leak state
|
|
4391
|
+
10. **Update tests when code changes** \u2014 if you modify a component, update its tests. If you delete a component, delete its tests
|
|
4392
|
+
`;
|
|
4393
|
+
}
|
|
4394
|
+
};
|
|
4395
|
+
|
|
3430
4396
|
// src/skills/clean-code.ts
|
|
4397
|
+
init_esm_shims();
|
|
3431
4398
|
var cleanCodeSkill = {
|
|
3432
4399
|
id: "clean-code",
|
|
3433
4400
|
name: "Clean Code Principles",
|
|
@@ -3462,7 +4429,7 @@ Write code that is easy to read, easy to change, and easy to delete. Treat clari
|
|
|
3462
4429
|
- Props interfaces should be explicit and narrow \u2014 accept only what the component needs, not entire objects
|
|
3463
4430
|
- Avoid prop drilling beyond 2 levels \u2014 use context or restructure the component tree
|
|
3464
4431
|
- Destructure props in the function signature for clarity
|
|
3465
|
-
- Use \`@
|
|
4432
|
+
- Use \`@chakra-ui/react\` layout components (\`VStack\`, \`HStack\`, \`Flex\`, \`SimpleGrid\`) \u2014 never raw \`<div>\` with flex/grid classes
|
|
3466
4433
|
|
|
3467
4434
|
### File Organization
|
|
3468
4435
|
- Keep files short. If a file exceeds 200 lines, it is likely doing too much \u2014 split it
|
|
@@ -3531,6 +4498,7 @@ Write code that is easy to read, easy to change, and easy to delete. Treat clari
|
|
|
3531
4498
|
};
|
|
3532
4499
|
|
|
3533
4500
|
// src/skills/ai-guidelines.ts
|
|
4501
|
+
init_esm_shims();
|
|
3534
4502
|
var aiGuidelinesSkill = {
|
|
3535
4503
|
id: "ai-guidelines",
|
|
3536
4504
|
name: "AI Development Guidelines",
|
|
@@ -3549,7 +4517,7 @@ var aiGuidelinesSkill = {
|
|
|
3549
4517
|
- Use existing patterns in the codebase as reference before inventing new ones
|
|
3550
4518
|
|
|
3551
4519
|
### Frontend Architecture (Mandatory)
|
|
3552
|
-
- **Use \`@
|
|
4520
|
+
- **Use \`@chakra-ui/react\` for ALL UI** \u2014 \`VStack\`, \`HStack\`, \`Flex\`, \`SimpleGrid\` for layout; \`Heading\`, \`Text\` for text; \`Card\`, \`Button\`, \`Badge\`, etc. for all elements. Never use raw HTML (\`<div>\`, \`<h1>\`, \`<p>\`, \`<button>\`) when a Chakra UI component exists
|
|
3553
4521
|
- **Pages are thin orchestrators** \u2014 compose child components from \`components/\`, extract logic into \`hooks/\`. A page file should be ~20-30 lines, not a monolith
|
|
3554
4522
|
- **Use the \`Path\` enum** \u2014 all route paths come from \`src/router/paths.ts\`. Never hardcode path strings like \`'/login'\` or \`'/dashboard'\`
|
|
3555
4523
|
- **Add new paths to the enum** \u2014 when creating a new page, add its path to the \`Path\` enum before the \`// blacksmith:path\` marker
|
|
@@ -3563,20 +4531,22 @@ var aiGuidelinesSkill = {
|
|
|
3563
4531
|
|
|
3564
4532
|
### Checklist Before Finishing a Task
|
|
3565
4533
|
1. Backend tests pass: \`cd backend && ./venv/bin/python manage.py test\`
|
|
3566
|
-
2. Frontend
|
|
3567
|
-
3.
|
|
3568
|
-
4.
|
|
3569
|
-
5.
|
|
3570
|
-
6.
|
|
3571
|
-
7.
|
|
3572
|
-
8.
|
|
3573
|
-
9.
|
|
4534
|
+
2. Frontend tests pass: \`cd frontend && npm test\`
|
|
4535
|
+
3. Frontend builds: \`cd frontend && npm run build\`
|
|
4536
|
+
4. API types are in sync: \`blacksmith sync\`
|
|
4537
|
+
5. No lint errors in modified files
|
|
4538
|
+
6. All UI uses \`@chakra-ui/react\` components \u2014 no raw \`<div>\` for layout, no raw \`<h1>\`-\`<h6>\` for text
|
|
4539
|
+
7. Pages are modular \u2014 page file is a thin orchestrator, sections are in \`components/\`, logic in \`hooks/\`
|
|
4540
|
+
8. Logic is in hooks \u2014 no \`useApiQuery\`, \`useApiMutation\`, \`useEffect\`, or multi-\`useState\` in component bodies
|
|
4541
|
+
9. No hardcoded route paths \u2014 all paths use the \`Path\` enum from \`@/router/paths\`
|
|
4542
|
+
10. New routes have a corresponding \`Path\` enum entry
|
|
4543
|
+
11. **Tests are co-located** \u2014 every new or modified page, component, or hook has a corresponding \`.spec.tsx\` / \`.spec.ts\` in a \`__tests__/\` folder next to the source file (see the \`frontend-testing\` skill)
|
|
3574
4544
|
`;
|
|
3575
4545
|
}
|
|
3576
4546
|
};
|
|
3577
4547
|
|
|
3578
4548
|
// src/commands/ai-setup.ts
|
|
3579
|
-
async function setupAiDev({ projectDir, projectName,
|
|
4549
|
+
async function setupAiDev({ projectDir, projectName, includeChakraUiSkill }) {
|
|
3580
4550
|
const aiSpinner = spinner("Setting up AI development environment...");
|
|
3581
4551
|
try {
|
|
3582
4552
|
const skills = [
|
|
@@ -3589,24 +4559,25 @@ async function setupAiDev({ projectDir, projectName, includeBlacksmithUiSkill })
|
|
|
3589
4559
|
reactQuerySkill,
|
|
3590
4560
|
pageStructureSkill
|
|
3591
4561
|
];
|
|
3592
|
-
if (
|
|
3593
|
-
skills.push(
|
|
3594
|
-
skills.push(
|
|
3595
|
-
skills.push(
|
|
4562
|
+
if (includeChakraUiSkill) {
|
|
4563
|
+
skills.push(chakraUiReactSkill);
|
|
4564
|
+
skills.push(chakraUiFormsSkill);
|
|
4565
|
+
skills.push(chakraUiAuthSkill);
|
|
3596
4566
|
skills.push(blacksmithHooksSkill);
|
|
3597
4567
|
skills.push(uiDesignSkill);
|
|
3598
4568
|
}
|
|
3599
4569
|
skills.push(blacksmithCliSkill);
|
|
4570
|
+
skills.push(frontendTestingSkill);
|
|
3600
4571
|
skills.push(programmingParadigmsSkill);
|
|
3601
4572
|
skills.push(cleanCodeSkill);
|
|
3602
4573
|
skills.push(aiGuidelinesSkill);
|
|
3603
4574
|
const ctx = { projectName };
|
|
3604
4575
|
const inlineSkills = skills.filter((s) => !s.name);
|
|
3605
4576
|
const fileSkills = skills.filter((s) => s.name);
|
|
3606
|
-
const skillsDir =
|
|
4577
|
+
const skillsDir = path4.join(projectDir, ".claude", "skills");
|
|
3607
4578
|
if (fs3.existsSync(skillsDir)) {
|
|
3608
4579
|
for (const entry of fs3.readdirSync(skillsDir)) {
|
|
3609
|
-
const entryPath =
|
|
4580
|
+
const entryPath = path4.join(skillsDir, entry);
|
|
3610
4581
|
const stat = fs3.statSync(entryPath);
|
|
3611
4582
|
if (stat.isDirectory()) {
|
|
3612
4583
|
fs3.rmSync(entryPath, { recursive: true });
|
|
@@ -3617,7 +4588,7 @@ async function setupAiDev({ projectDir, projectName, includeBlacksmithUiSkill })
|
|
|
3617
4588
|
}
|
|
3618
4589
|
fs3.mkdirSync(skillsDir, { recursive: true });
|
|
3619
4590
|
for (const skill of fileSkills) {
|
|
3620
|
-
const skillDir =
|
|
4591
|
+
const skillDir = path4.join(skillsDir, skill.id);
|
|
3621
4592
|
fs3.mkdirSync(skillDir, { recursive: true });
|
|
3622
4593
|
const frontmatter = `---
|
|
3623
4594
|
name: ${skill.name}
|
|
@@ -3626,7 +4597,7 @@ description: ${skill.description}
|
|
|
3626
4597
|
|
|
3627
4598
|
`;
|
|
3628
4599
|
const content = skill.render(ctx).trim();
|
|
3629
|
-
fs3.writeFileSync(
|
|
4600
|
+
fs3.writeFileSync(path4.join(skillDir, "SKILL.md"), frontmatter + content + "\n", "utf-8");
|
|
3630
4601
|
}
|
|
3631
4602
|
const inlineContent = inlineSkills.map((s) => s.render(ctx)).join("\n");
|
|
3632
4603
|
const skillsList = fileSkills.map((s) => `- \`.claude/skills/${s.id}/SKILL.md\` \u2014 ${s.name}`).join("\n");
|
|
@@ -3642,7 +4613,7 @@ description: ${skill.description}
|
|
|
3642
4613
|
"These files are auto-loaded by Claude Code. Run `blacksmith setup:ai` to regenerate.",
|
|
3643
4614
|
""
|
|
3644
4615
|
].join("\n");
|
|
3645
|
-
fs3.writeFileSync(
|
|
4616
|
+
fs3.writeFileSync(path4.join(projectDir, "CLAUDE.md"), claudeMd, "utf-8");
|
|
3646
4617
|
const skillNames = skills.filter((s) => s.id !== "project-overview" && s.id !== "ai-guidelines").map((s) => s.id).join(" + ");
|
|
3647
4618
|
aiSpinner.succeed(`AI dev environment ready (${skillNames} skills)`);
|
|
3648
4619
|
} catch (error) {
|
|
@@ -3691,9 +4662,9 @@ async function init(name, options) {
|
|
|
3691
4662
|
"Theme": themePreset,
|
|
3692
4663
|
"AI support": options.ai ? "Yes" : "No"
|
|
3693
4664
|
});
|
|
3694
|
-
const projectDir =
|
|
3695
|
-
const backendDir =
|
|
3696
|
-
const frontendDir =
|
|
4665
|
+
const projectDir = path5.resolve(process.cwd(), name);
|
|
4666
|
+
const backendDir = path5.join(projectDir, "backend");
|
|
4667
|
+
const frontendDir = path5.join(projectDir, "frontend");
|
|
3697
4668
|
const templatesDir = getTemplatesDir();
|
|
3698
4669
|
if (fs4.existsSync(projectDir)) {
|
|
3699
4670
|
log.error(`Directory "${name}" already exists.`);
|
|
@@ -3720,7 +4691,7 @@ async function init(name, options) {
|
|
|
3720
4691
|
};
|
|
3721
4692
|
fs4.mkdirSync(projectDir, { recursive: true });
|
|
3722
4693
|
fs4.writeFileSync(
|
|
3723
|
-
|
|
4694
|
+
path5.join(projectDir, "blacksmith.config.json"),
|
|
3724
4695
|
JSON.stringify(
|
|
3725
4696
|
{
|
|
3726
4697
|
name,
|
|
@@ -3735,13 +4706,13 @@ async function init(name, options) {
|
|
|
3735
4706
|
const backendSpinner = spinner("Generating Django backend...");
|
|
3736
4707
|
try {
|
|
3737
4708
|
renderDirectory(
|
|
3738
|
-
|
|
4709
|
+
path5.join(templatesDir, "backend"),
|
|
3739
4710
|
backendDir,
|
|
3740
4711
|
context
|
|
3741
4712
|
);
|
|
3742
4713
|
fs4.copyFileSync(
|
|
3743
|
-
|
|
3744
|
-
|
|
4714
|
+
path5.join(backendDir, ".env.example"),
|
|
4715
|
+
path5.join(backendDir, ".env")
|
|
3745
4716
|
);
|
|
3746
4717
|
backendSpinner.succeed("Django backend generated");
|
|
3747
4718
|
} catch (error) {
|
|
@@ -3784,7 +4755,7 @@ async function init(name, options) {
|
|
|
3784
4755
|
const frontendSpinner = spinner("Generating React frontend...");
|
|
3785
4756
|
try {
|
|
3786
4757
|
renderDirectory(
|
|
3787
|
-
|
|
4758
|
+
path5.join(templatesDir, "frontend"),
|
|
3788
4759
|
frontendDir,
|
|
3789
4760
|
context
|
|
3790
4761
|
);
|
|
@@ -3817,7 +4788,7 @@ async function init(name, options) {
|
|
|
3817
4788
|
djangoProcess.unref();
|
|
3818
4789
|
await new Promise((resolve) => setTimeout(resolve, 4e3));
|
|
3819
4790
|
try {
|
|
3820
|
-
await exec(process.execPath, [
|
|
4791
|
+
await exec(process.execPath, [path5.join(frontendDir, "node_modules", ".bin", "openapi-ts")], { cwd: frontendDir, silent: true });
|
|
3821
4792
|
syncSpinner.succeed("OpenAPI types synced");
|
|
3822
4793
|
} catch {
|
|
3823
4794
|
syncSpinner.warn('OpenAPI sync skipped (run "blacksmith sync" after starting Django)');
|
|
@@ -3831,8 +4802,8 @@ async function init(name, options) {
|
|
|
3831
4802
|
} catch {
|
|
3832
4803
|
syncSpinner.warn('OpenAPI sync skipped (run "blacksmith sync" after starting Django)');
|
|
3833
4804
|
}
|
|
3834
|
-
const generatedDir =
|
|
3835
|
-
const stubFile =
|
|
4805
|
+
const generatedDir = path5.join(frontendDir, "src", "api", "generated");
|
|
4806
|
+
const stubFile = path5.join(generatedDir, "client.gen.ts");
|
|
3836
4807
|
if (!fs4.existsSync(stubFile)) {
|
|
3837
4808
|
if (!fs4.existsSync(generatedDir)) {
|
|
3838
4809
|
fs4.mkdirSync(generatedDir, { recursive: true });
|
|
@@ -3862,16 +4833,17 @@ async function init(name, options) {
|
|
|
3862
4833
|
await setupAiDev({
|
|
3863
4834
|
projectDir,
|
|
3864
4835
|
projectName: name,
|
|
3865
|
-
|
|
4836
|
+
includeChakraUiSkill: options.chakraUiSkill !== false
|
|
3866
4837
|
});
|
|
3867
4838
|
}
|
|
3868
4839
|
printNextSteps(name, backendPort, frontendPort);
|
|
3869
4840
|
}
|
|
3870
4841
|
|
|
3871
4842
|
// src/commands/dev.ts
|
|
4843
|
+
init_esm_shims();
|
|
3872
4844
|
import net from "net";
|
|
3873
4845
|
import concurrently from "concurrently";
|
|
3874
|
-
import
|
|
4846
|
+
import path6 from "path";
|
|
3875
4847
|
function isPortAvailable(port) {
|
|
3876
4848
|
return new Promise((resolve) => {
|
|
3877
4849
|
const server = net.createServer();
|
|
@@ -3926,7 +4898,7 @@ async function dev() {
|
|
|
3926
4898
|
log.step(`Swagger \u2192 http://localhost:${backendPort}/api/docs/`);
|
|
3927
4899
|
log.step("OpenAPI sync \u2192 watching backend .py files");
|
|
3928
4900
|
log.blank();
|
|
3929
|
-
const syncCmd = `${process.execPath} ${
|
|
4901
|
+
const syncCmd = `${process.execPath} ${path6.join(frontendDir, "node_modules", ".bin", "openapi-ts")}`;
|
|
3930
4902
|
const watcherCode = [
|
|
3931
4903
|
`const{watch}=require("fs"),{exec}=require("child_process");`,
|
|
3932
4904
|
`let t=null,s=false;`,
|
|
@@ -3986,7 +4958,8 @@ async function dev() {
|
|
|
3986
4958
|
}
|
|
3987
4959
|
|
|
3988
4960
|
// src/commands/sync.ts
|
|
3989
|
-
|
|
4961
|
+
init_esm_shims();
|
|
4962
|
+
import path7 from "path";
|
|
3990
4963
|
import fs5 from "fs";
|
|
3991
4964
|
async function sync() {
|
|
3992
4965
|
let root;
|
|
@@ -4000,9 +4973,9 @@ async function sync() {
|
|
|
4000
4973
|
const frontendDir = getFrontendDir(root);
|
|
4001
4974
|
const s = spinner("Syncing OpenAPI schema to frontend...");
|
|
4002
4975
|
try {
|
|
4003
|
-
const schemaPath =
|
|
4976
|
+
const schemaPath = path7.join(frontendDir, "_schema.yml");
|
|
4004
4977
|
await execPython(["manage.py", "spectacular", "--file", schemaPath], backendDir, true);
|
|
4005
|
-
const configPath =
|
|
4978
|
+
const configPath = path7.join(frontendDir, "openapi-ts.config.ts");
|
|
4006
4979
|
const configBackup = fs5.readFileSync(configPath, "utf-8");
|
|
4007
4980
|
const configWithFile = configBackup.replace(
|
|
4008
4981
|
/path:\s*['"]http[^'"]+['"]/,
|
|
@@ -4010,7 +4983,7 @@ async function sync() {
|
|
|
4010
4983
|
);
|
|
4011
4984
|
fs5.writeFileSync(configPath, configWithFile, "utf-8");
|
|
4012
4985
|
try {
|
|
4013
|
-
await exec(process.execPath, [
|
|
4986
|
+
await exec(process.execPath, [path7.join(frontendDir, "node_modules", ".bin", "openapi-ts")], {
|
|
4014
4987
|
cwd: frontendDir,
|
|
4015
4988
|
silent: true
|
|
4016
4989
|
});
|
|
@@ -4034,10 +5007,12 @@ async function sync() {
|
|
|
4034
5007
|
}
|
|
4035
5008
|
|
|
4036
5009
|
// src/commands/make-resource.ts
|
|
4037
|
-
|
|
5010
|
+
init_esm_shims();
|
|
5011
|
+
import path8 from "path";
|
|
4038
5012
|
import fs6 from "fs";
|
|
4039
5013
|
|
|
4040
5014
|
// src/utils/names.ts
|
|
5015
|
+
init_esm_shims();
|
|
4041
5016
|
import { pascalCase, snakeCase, kebabCase, camelCase } from "change-case";
|
|
4042
5017
|
import pluralize from "pluralize";
|
|
4043
5018
|
function generateNames(input) {
|
|
@@ -4070,12 +5045,12 @@ async function makeResource(name) {
|
|
|
4070
5045
|
const backendDir = getBackendDir(root);
|
|
4071
5046
|
const frontendDir = getFrontendDir(root);
|
|
4072
5047
|
const templatesDir = getTemplatesDir();
|
|
4073
|
-
const backendAppDir =
|
|
5048
|
+
const backendAppDir = path8.join(backendDir, "apps", names.snakes);
|
|
4074
5049
|
if (fs6.existsSync(backendAppDir)) {
|
|
4075
5050
|
log.error(`Backend app "${names.snakes}" already exists.`);
|
|
4076
5051
|
process.exit(1);
|
|
4077
5052
|
}
|
|
4078
|
-
const frontendPageDir =
|
|
5053
|
+
const frontendPageDir = path8.join(frontendDir, "src", "pages", names.kebabs);
|
|
4079
5054
|
if (fs6.existsSync(frontendPageDir)) {
|
|
4080
5055
|
log.error(`Frontend page "${names.kebabs}" already exists.`);
|
|
4081
5056
|
process.exit(1);
|
|
@@ -4084,7 +5059,7 @@ async function makeResource(name) {
|
|
|
4084
5059
|
const backendSpinner = spinner(`Creating backend app: apps/${names.snakes}/`);
|
|
4085
5060
|
try {
|
|
4086
5061
|
renderDirectory(
|
|
4087
|
-
|
|
5062
|
+
path8.join(templatesDir, "resource", "backend"),
|
|
4088
5063
|
backendAppDir,
|
|
4089
5064
|
context
|
|
4090
5065
|
);
|
|
@@ -4096,7 +5071,7 @@ async function makeResource(name) {
|
|
|
4096
5071
|
}
|
|
4097
5072
|
const registerSpinner = spinner("Registering app in Django settings...");
|
|
4098
5073
|
try {
|
|
4099
|
-
const settingsPath =
|
|
5074
|
+
const settingsPath = path8.join(backendDir, "config", "settings", "base.py");
|
|
4100
5075
|
appendAfterMarker(
|
|
4101
5076
|
settingsPath,
|
|
4102
5077
|
"# blacksmith:apps",
|
|
@@ -4110,7 +5085,7 @@ async function makeResource(name) {
|
|
|
4110
5085
|
}
|
|
4111
5086
|
const urlSpinner = spinner("Registering API URLs...");
|
|
4112
5087
|
try {
|
|
4113
|
-
const urlsPath =
|
|
5088
|
+
const urlsPath = path8.join(backendDir, "config", "urls.py");
|
|
4114
5089
|
insertBeforeMarker(
|
|
4115
5090
|
urlsPath,
|
|
4116
5091
|
"# blacksmith:urls",
|
|
@@ -4134,9 +5109,9 @@ async function makeResource(name) {
|
|
|
4134
5109
|
}
|
|
4135
5110
|
const syncSpinner = spinner("Syncing OpenAPI schema...");
|
|
4136
5111
|
try {
|
|
4137
|
-
const schemaPath =
|
|
5112
|
+
const schemaPath = path8.join(frontendDir, "_schema.yml");
|
|
4138
5113
|
await execPython(["manage.py", "spectacular", "--file", schemaPath], backendDir, true);
|
|
4139
|
-
const configPath =
|
|
5114
|
+
const configPath = path8.join(frontendDir, "openapi-ts.config.ts");
|
|
4140
5115
|
const configBackup = fs6.readFileSync(configPath, "utf-8");
|
|
4141
5116
|
const configWithFile = configBackup.replace(
|
|
4142
5117
|
/path:\s*['"]http[^'"]+['"]/,
|
|
@@ -4144,7 +5119,7 @@ async function makeResource(name) {
|
|
|
4144
5119
|
);
|
|
4145
5120
|
fs6.writeFileSync(configPath, configWithFile, "utf-8");
|
|
4146
5121
|
try {
|
|
4147
|
-
await exec(process.execPath, [
|
|
5122
|
+
await exec(process.execPath, [path8.join(frontendDir, "node_modules", ".bin", "openapi-ts")], {
|
|
4148
5123
|
cwd: frontendDir,
|
|
4149
5124
|
silent: true
|
|
4150
5125
|
});
|
|
@@ -4156,11 +5131,11 @@ async function makeResource(name) {
|
|
|
4156
5131
|
} catch {
|
|
4157
5132
|
syncSpinner.warn('Could not sync OpenAPI. Run "blacksmith sync" manually.');
|
|
4158
5133
|
}
|
|
4159
|
-
const apiHooksDir =
|
|
5134
|
+
const apiHooksDir = path8.join(frontendDir, "src", "api", "hooks", names.kebabs);
|
|
4160
5135
|
const apiHooksSpinner = spinner(`Creating API hooks: api/hooks/${names.kebabs}/`);
|
|
4161
5136
|
try {
|
|
4162
5137
|
renderDirectory(
|
|
4163
|
-
|
|
5138
|
+
path8.join(templatesDir, "resource", "api-hooks"),
|
|
4164
5139
|
apiHooksDir,
|
|
4165
5140
|
context
|
|
4166
5141
|
);
|
|
@@ -4173,7 +5148,7 @@ async function makeResource(name) {
|
|
|
4173
5148
|
const frontendSpinner = spinner(`Creating frontend page: pages/${names.kebabs}/`);
|
|
4174
5149
|
try {
|
|
4175
5150
|
renderDirectory(
|
|
4176
|
-
|
|
5151
|
+
path8.join(templatesDir, "resource", "pages"),
|
|
4177
5152
|
frontendPageDir,
|
|
4178
5153
|
context
|
|
4179
5154
|
);
|
|
@@ -4185,7 +5160,7 @@ async function makeResource(name) {
|
|
|
4185
5160
|
}
|
|
4186
5161
|
const pathSpinner = spinner("Registering route path...");
|
|
4187
5162
|
try {
|
|
4188
|
-
const pathsFile =
|
|
5163
|
+
const pathsFile = path8.join(frontendDir, "src", "router", "paths.ts");
|
|
4189
5164
|
insertBeforeMarker(
|
|
4190
5165
|
pathsFile,
|
|
4191
5166
|
"// blacksmith:path",
|
|
@@ -4197,7 +5172,7 @@ async function makeResource(name) {
|
|
|
4197
5172
|
}
|
|
4198
5173
|
const routeSpinner = spinner("Registering frontend routes...");
|
|
4199
5174
|
try {
|
|
4200
|
-
const routesPath =
|
|
5175
|
+
const routesPath = path8.join(frontendDir, "src", "router", "routes.tsx");
|
|
4201
5176
|
insertBeforeMarker(
|
|
4202
5177
|
routesPath,
|
|
4203
5178
|
"// blacksmith:import",
|
|
@@ -4218,6 +5193,7 @@ async function makeResource(name) {
|
|
|
4218
5193
|
}
|
|
4219
5194
|
|
|
4220
5195
|
// src/commands/build.ts
|
|
5196
|
+
init_esm_shims();
|
|
4221
5197
|
async function build() {
|
|
4222
5198
|
let root;
|
|
4223
5199
|
try {
|
|
@@ -4259,8 +5235,9 @@ async function build() {
|
|
|
4259
5235
|
}
|
|
4260
5236
|
|
|
4261
5237
|
// src/commands/eject.ts
|
|
5238
|
+
init_esm_shims();
|
|
4262
5239
|
import fs7 from "fs";
|
|
4263
|
-
import
|
|
5240
|
+
import path9 from "path";
|
|
4264
5241
|
async function eject() {
|
|
4265
5242
|
let root;
|
|
4266
5243
|
try {
|
|
@@ -4269,7 +5246,7 @@ async function eject() {
|
|
|
4269
5246
|
log.error("Not inside a Blacksmith project.");
|
|
4270
5247
|
process.exit(1);
|
|
4271
5248
|
}
|
|
4272
|
-
const configPath =
|
|
5249
|
+
const configPath = path9.join(root, "blacksmith.config.json");
|
|
4273
5250
|
if (fs7.existsSync(configPath)) {
|
|
4274
5251
|
fs7.unlinkSync(configPath);
|
|
4275
5252
|
}
|
|
@@ -4287,6 +5264,7 @@ async function eject() {
|
|
|
4287
5264
|
}
|
|
4288
5265
|
|
|
4289
5266
|
// src/commands/skills.ts
|
|
5267
|
+
init_esm_shims();
|
|
4290
5268
|
import fs8 from "fs";
|
|
4291
5269
|
var allSkills = [
|
|
4292
5270
|
projectOverviewSkill,
|
|
@@ -4294,11 +5272,12 @@ var allSkills = [
|
|
|
4294
5272
|
djangoRestAdvancedSkill,
|
|
4295
5273
|
apiDocumentationSkill,
|
|
4296
5274
|
reactSkill,
|
|
4297
|
-
|
|
4298
|
-
|
|
4299
|
-
|
|
5275
|
+
chakraUiReactSkill,
|
|
5276
|
+
chakraUiFormsSkill,
|
|
5277
|
+
chakraUiAuthSkill,
|
|
4300
5278
|
blacksmithHooksSkill,
|
|
4301
5279
|
blacksmithCliSkill,
|
|
5280
|
+
frontendTestingSkill,
|
|
4302
5281
|
cleanCodeSkill,
|
|
4303
5282
|
aiGuidelinesSkill
|
|
4304
5283
|
];
|
|
@@ -4314,7 +5293,7 @@ async function setupSkills(options) {
|
|
|
4314
5293
|
await setupAiDev({
|
|
4315
5294
|
projectDir: root,
|
|
4316
5295
|
projectName: config.name,
|
|
4317
|
-
|
|
5296
|
+
includeChakraUiSkill: options.chakraUiSkill !== false
|
|
4318
5297
|
});
|
|
4319
5298
|
log.blank();
|
|
4320
5299
|
log.success("AI skills generated:");
|
|
@@ -4352,7 +5331,324 @@ function listSkills() {
|
|
|
4352
5331
|
}
|
|
4353
5332
|
}
|
|
4354
5333
|
|
|
5334
|
+
// src/commands/mcp-setup.ts
|
|
5335
|
+
init_esm_shims();
|
|
5336
|
+
import fs9 from "fs";
|
|
5337
|
+
import path10 from "path";
|
|
5338
|
+
var DONE_OPTION = "Done \u2014 save and exit";
|
|
5339
|
+
function getPresets(projectRoot) {
|
|
5340
|
+
return [
|
|
5341
|
+
{
|
|
5342
|
+
id: "filesystem",
|
|
5343
|
+
name: "Filesystem",
|
|
5344
|
+
description: "Read and write project files",
|
|
5345
|
+
command: "npx",
|
|
5346
|
+
args: ["-y", "@modelcontextprotocol/server-filesystem"],
|
|
5347
|
+
prompts: [
|
|
5348
|
+
{
|
|
5349
|
+
label: "Allowed directory path",
|
|
5350
|
+
defaultValue: projectRoot,
|
|
5351
|
+
target: "args"
|
|
5352
|
+
}
|
|
5353
|
+
]
|
|
5354
|
+
},
|
|
5355
|
+
{
|
|
5356
|
+
id: "postgres",
|
|
5357
|
+
name: "PostgreSQL",
|
|
5358
|
+
description: "Query PostgreSQL databases",
|
|
5359
|
+
command: "npx",
|
|
5360
|
+
args: ["-y", "@modelcontextprotocol/server-postgres"],
|
|
5361
|
+
prompts: [
|
|
5362
|
+
{
|
|
5363
|
+
label: "PostgreSQL connection string (e.g. postgresql://user:pass@localhost:5432/dbname)",
|
|
5364
|
+
target: "args"
|
|
5365
|
+
}
|
|
5366
|
+
]
|
|
5367
|
+
},
|
|
5368
|
+
{
|
|
5369
|
+
id: "fetch",
|
|
5370
|
+
name: "Fetch",
|
|
5371
|
+
description: "Make HTTP requests and fetch web content",
|
|
5372
|
+
command: "npx",
|
|
5373
|
+
args: ["-y", "@modelcontextprotocol/server-fetch"]
|
|
5374
|
+
},
|
|
5375
|
+
{
|
|
5376
|
+
id: "github",
|
|
5377
|
+
name: "GitHub",
|
|
5378
|
+
description: "Interact with GitHub repos, issues, and PRs",
|
|
5379
|
+
command: "npx",
|
|
5380
|
+
args: ["-y", "@modelcontextprotocol/server-github"],
|
|
5381
|
+
prompts: [
|
|
5382
|
+
{
|
|
5383
|
+
label: "GitHub personal access token",
|
|
5384
|
+
target: "env",
|
|
5385
|
+
envVar: "GITHUB_PERSONAL_ACCESS_TOKEN"
|
|
5386
|
+
}
|
|
5387
|
+
]
|
|
5388
|
+
},
|
|
5389
|
+
{
|
|
5390
|
+
id: "chakra-ui-docs",
|
|
5391
|
+
name: "Chakra UI Docs",
|
|
5392
|
+
description: "Chakra UI component documentation for AI assistance",
|
|
5393
|
+
command: "npx",
|
|
5394
|
+
args: [
|
|
5395
|
+
"-y",
|
|
5396
|
+
"@anthropic-ai/mcp-docs-server",
|
|
5397
|
+
"--url",
|
|
5398
|
+
"https://www.chakra-ui.com/docs",
|
|
5399
|
+
"--name",
|
|
5400
|
+
"chakra-ui-docs"
|
|
5401
|
+
]
|
|
5402
|
+
},
|
|
5403
|
+
{
|
|
5404
|
+
id: "sentry",
|
|
5405
|
+
name: "Sentry",
|
|
5406
|
+
description: "Query errors, view issues, and manage releases in Sentry",
|
|
5407
|
+
command: "npx",
|
|
5408
|
+
args: ["-y", "@sentry/mcp-server"],
|
|
5409
|
+
prompts: [
|
|
5410
|
+
{
|
|
5411
|
+
label: "Sentry auth token",
|
|
5412
|
+
target: "env",
|
|
5413
|
+
envVar: "SENTRY_AUTH_TOKEN"
|
|
5414
|
+
},
|
|
5415
|
+
{
|
|
5416
|
+
label: "Sentry organization slug",
|
|
5417
|
+
target: "env",
|
|
5418
|
+
envVar: "SENTRY_ORG"
|
|
5419
|
+
}
|
|
5420
|
+
]
|
|
5421
|
+
},
|
|
5422
|
+
{
|
|
5423
|
+
id: "puppeteer",
|
|
5424
|
+
name: "Puppeteer",
|
|
5425
|
+
description: "Browser automation for testing, screenshots, and scraping",
|
|
5426
|
+
command: "npx",
|
|
5427
|
+
args: ["-y", "@modelcontextprotocol/server-puppeteer"]
|
|
5428
|
+
},
|
|
5429
|
+
{
|
|
5430
|
+
id: "memory",
|
|
5431
|
+
name: "Memory",
|
|
5432
|
+
description: "Persistent knowledge graph for cross-session context",
|
|
5433
|
+
command: "npx",
|
|
5434
|
+
args: ["-y", "@modelcontextprotocol/server-memory"]
|
|
5435
|
+
},
|
|
5436
|
+
{
|
|
5437
|
+
id: "slack",
|
|
5438
|
+
name: "Slack",
|
|
5439
|
+
description: "Read/send messages, search channels, and manage threads",
|
|
5440
|
+
command: "npx",
|
|
5441
|
+
args: ["-y", "@anthropic-ai/mcp-server-slack"],
|
|
5442
|
+
prompts: [
|
|
5443
|
+
{
|
|
5444
|
+
label: "Slack bot token (xoxb-...)",
|
|
5445
|
+
target: "env",
|
|
5446
|
+
envVar: "SLACK_BOT_TOKEN"
|
|
5447
|
+
},
|
|
5448
|
+
{
|
|
5449
|
+
label: "Slack team ID",
|
|
5450
|
+
target: "env",
|
|
5451
|
+
envVar: "SLACK_TEAM_ID"
|
|
5452
|
+
}
|
|
5453
|
+
]
|
|
5454
|
+
},
|
|
5455
|
+
{
|
|
5456
|
+
id: "redis",
|
|
5457
|
+
name: "Redis",
|
|
5458
|
+
description: "Query and manage Redis cache",
|
|
5459
|
+
command: "npx",
|
|
5460
|
+
args: ["-y", "@modelcontextprotocol/server-redis"],
|
|
5461
|
+
prompts: [
|
|
5462
|
+
{
|
|
5463
|
+
label: "Redis URL (e.g. redis://localhost:6379)",
|
|
5464
|
+
defaultValue: "redis://localhost:6379",
|
|
5465
|
+
target: "env",
|
|
5466
|
+
envVar: "REDIS_URL"
|
|
5467
|
+
}
|
|
5468
|
+
]
|
|
5469
|
+
}
|
|
5470
|
+
];
|
|
5471
|
+
}
|
|
5472
|
+
function readSettings(settingsPath) {
|
|
5473
|
+
if (!fs9.existsSync(settingsPath)) {
|
|
5474
|
+
return {};
|
|
5475
|
+
}
|
|
5476
|
+
try {
|
|
5477
|
+
return JSON.parse(fs9.readFileSync(settingsPath, "utf-8"));
|
|
5478
|
+
} catch {
|
|
5479
|
+
return {};
|
|
5480
|
+
}
|
|
5481
|
+
}
|
|
5482
|
+
function buildServerConfig(preset, promptValues) {
|
|
5483
|
+
const args = [...preset.args];
|
|
5484
|
+
const env = { ...preset.env };
|
|
5485
|
+
let valueIndex = 0;
|
|
5486
|
+
for (const prompt of preset.prompts || []) {
|
|
5487
|
+
const value = promptValues[valueIndex++];
|
|
5488
|
+
if (prompt.target === "args") {
|
|
5489
|
+
args.push(value);
|
|
5490
|
+
} else if (prompt.target === "env" && prompt.envVar) {
|
|
5491
|
+
env[prompt.envVar] = value;
|
|
5492
|
+
}
|
|
5493
|
+
}
|
|
5494
|
+
const config = { command: preset.command, args };
|
|
5495
|
+
if (Object.keys(env).length > 0) {
|
|
5496
|
+
config.env = env;
|
|
5497
|
+
}
|
|
5498
|
+
return config;
|
|
5499
|
+
}
|
|
5500
|
+
async function setupMcp() {
|
|
5501
|
+
let root;
|
|
5502
|
+
try {
|
|
5503
|
+
root = findProjectRoot();
|
|
5504
|
+
} catch {
|
|
5505
|
+
log.error('Not inside a Blacksmith project. Run "blacksmith init <name>" first.');
|
|
5506
|
+
process.exit(1);
|
|
5507
|
+
}
|
|
5508
|
+
const settingsPath = path10.join(root, ".claude", "settings.local.json");
|
|
5509
|
+
const settings = readSettings(settingsPath);
|
|
5510
|
+
const existingServers = settings.mcpServers || {};
|
|
5511
|
+
const newServers = {};
|
|
5512
|
+
const presets = getPresets(root);
|
|
5513
|
+
log.blank();
|
|
5514
|
+
log.info("Configure MCP servers for Claude Code.");
|
|
5515
|
+
log.info('Select servers to add, then choose "Done" to save.');
|
|
5516
|
+
log.blank();
|
|
5517
|
+
while (true) {
|
|
5518
|
+
const options = presets.map((p) => {
|
|
5519
|
+
const configured = existingServers[p.id] || newServers[p.id];
|
|
5520
|
+
const suffix = configured ? " (configured)" : "";
|
|
5521
|
+
return `${p.name} \u2014 ${p.description}${suffix}`;
|
|
5522
|
+
});
|
|
5523
|
+
options.push(DONE_OPTION);
|
|
5524
|
+
const choice = await promptSelect("Select an MCP server to configure", options);
|
|
5525
|
+
if (choice === DONE_OPTION) {
|
|
5526
|
+
break;
|
|
5527
|
+
}
|
|
5528
|
+
const selectedIndex = options.indexOf(choice);
|
|
5529
|
+
const preset = presets[selectedIndex];
|
|
5530
|
+
if (!preset) break;
|
|
5531
|
+
if (existingServers[preset.id] || newServers[preset.id]) {
|
|
5532
|
+
const overwrite = await promptYesNo(
|
|
5533
|
+
`${preset.name} is already configured. Overwrite?`,
|
|
5534
|
+
false
|
|
5535
|
+
);
|
|
5536
|
+
if (!overwrite) continue;
|
|
5537
|
+
}
|
|
5538
|
+
const values = [];
|
|
5539
|
+
let skipped = false;
|
|
5540
|
+
for (const prompt of preset.prompts || []) {
|
|
5541
|
+
const value = await promptText(prompt.label, prompt.defaultValue);
|
|
5542
|
+
if (!value && !prompt.defaultValue) {
|
|
5543
|
+
log.warn(`Skipping ${preset.name} \u2014 required value not provided.`);
|
|
5544
|
+
skipped = true;
|
|
5545
|
+
break;
|
|
5546
|
+
}
|
|
5547
|
+
values.push(value);
|
|
5548
|
+
}
|
|
5549
|
+
if (skipped) continue;
|
|
5550
|
+
newServers[preset.id] = buildServerConfig(preset, values);
|
|
5551
|
+
log.success(`${preset.name} configured.`);
|
|
5552
|
+
log.blank();
|
|
5553
|
+
}
|
|
5554
|
+
if (Object.keys(newServers).length === 0) {
|
|
5555
|
+
log.info("No new servers configured.");
|
|
5556
|
+
return;
|
|
5557
|
+
}
|
|
5558
|
+
settings.mcpServers = { ...existingServers, ...newServers };
|
|
5559
|
+
try {
|
|
5560
|
+
fs9.mkdirSync(path10.join(root, ".claude"), { recursive: true });
|
|
5561
|
+
fs9.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
|
|
5562
|
+
} catch (error) {
|
|
5563
|
+
log.error(`Failed to write settings: ${error.message}`);
|
|
5564
|
+
process.exit(1);
|
|
5565
|
+
}
|
|
5566
|
+
log.blank();
|
|
5567
|
+
log.success("MCP servers configured:");
|
|
5568
|
+
for (const id of Object.keys(newServers)) {
|
|
5569
|
+
const preset = presets.find((p) => p.id === id);
|
|
5570
|
+
log.step(` ${preset?.name || id}`);
|
|
5571
|
+
}
|
|
5572
|
+
log.blank();
|
|
5573
|
+
log.info(`Settings written to ${path10.relative(root, settingsPath)}`);
|
|
5574
|
+
log.blank();
|
|
5575
|
+
}
|
|
5576
|
+
|
|
5577
|
+
// src/commands/studio.ts
|
|
5578
|
+
init_esm_shims();
|
|
5579
|
+
import net2 from "net";
|
|
5580
|
+
var DEFAULT_PORT = 3939;
|
|
5581
|
+
function isPortAvailable2(port) {
|
|
5582
|
+
return new Promise((resolve) => {
|
|
5583
|
+
const server = net2.createServer();
|
|
5584
|
+
server.once("error", () => resolve(false));
|
|
5585
|
+
server.once("listening", () => {
|
|
5586
|
+
server.close(() => resolve(true));
|
|
5587
|
+
});
|
|
5588
|
+
server.listen(port);
|
|
5589
|
+
});
|
|
5590
|
+
}
|
|
5591
|
+
async function findAvailablePort2(startPort) {
|
|
5592
|
+
let port = startPort;
|
|
5593
|
+
while (port < startPort + 100) {
|
|
5594
|
+
if (await isPortAvailable2(port)) return port;
|
|
5595
|
+
port++;
|
|
5596
|
+
}
|
|
5597
|
+
throw new Error(`No available port found in range ${startPort}-${port - 1}`);
|
|
5598
|
+
}
|
|
5599
|
+
async function studio(options) {
|
|
5600
|
+
let root;
|
|
5601
|
+
try {
|
|
5602
|
+
root = findProjectRoot();
|
|
5603
|
+
} catch {
|
|
5604
|
+
log.error('Not inside a Blacksmith project. Run "blacksmith init <name>" first.');
|
|
5605
|
+
process.exit(1);
|
|
5606
|
+
}
|
|
5607
|
+
const config = loadConfig(root);
|
|
5608
|
+
const requestedPort = options.port ? parseInt(options.port, 10) : DEFAULT_PORT;
|
|
5609
|
+
let port;
|
|
5610
|
+
try {
|
|
5611
|
+
port = await findAvailablePort2(requestedPort);
|
|
5612
|
+
} catch (err) {
|
|
5613
|
+
log.error(err.message);
|
|
5614
|
+
process.exit(1);
|
|
5615
|
+
}
|
|
5616
|
+
if (port !== requestedPort) {
|
|
5617
|
+
log.step(`Port ${requestedPort} in use, using ${port}`);
|
|
5618
|
+
}
|
|
5619
|
+
log.info(`Starting Blacksmith Studio for "${config.name}"...`);
|
|
5620
|
+
log.blank();
|
|
5621
|
+
try {
|
|
5622
|
+
const { createStudioServer } = await import("@blacksmith/studio");
|
|
5623
|
+
const { server } = await createStudioServer({ projectRoot: root, port });
|
|
5624
|
+
const url = `http://localhost:${port}`;
|
|
5625
|
+
log.success("Blacksmith Studio is running!");
|
|
5626
|
+
log.blank();
|
|
5627
|
+
log.step(`Studio \u2192 ${url}`);
|
|
5628
|
+
log.step(`Project \u2192 ${root}`);
|
|
5629
|
+
log.blank();
|
|
5630
|
+
log.info("Press Ctrl+C to stop.");
|
|
5631
|
+
try {
|
|
5632
|
+
const open2 = (await Promise.resolve().then(() => (init_open(), open_exports))).default;
|
|
5633
|
+
await open2(url);
|
|
5634
|
+
} catch {
|
|
5635
|
+
}
|
|
5636
|
+
const shutdown = () => {
|
|
5637
|
+
log.blank();
|
|
5638
|
+
log.info("Blacksmith Studio stopped.");
|
|
5639
|
+
server.close();
|
|
5640
|
+
process.exit(0);
|
|
5641
|
+
};
|
|
5642
|
+
process.on("SIGINT", shutdown);
|
|
5643
|
+
process.on("SIGTERM", shutdown);
|
|
5644
|
+
} catch (error) {
|
|
5645
|
+
log.error(`Failed to start Studio: ${error.message}`);
|
|
5646
|
+
process.exit(1);
|
|
5647
|
+
}
|
|
5648
|
+
}
|
|
5649
|
+
|
|
4355
5650
|
// src/commands/backend.ts
|
|
5651
|
+
init_esm_shims();
|
|
4356
5652
|
async function backend(args) {
|
|
4357
5653
|
let root;
|
|
4358
5654
|
try {
|
|
@@ -4376,6 +5672,7 @@ async function backend(args) {
|
|
|
4376
5672
|
}
|
|
4377
5673
|
|
|
4378
5674
|
// src/commands/frontend.ts
|
|
5675
|
+
init_esm_shims();
|
|
4379
5676
|
async function frontend(args) {
|
|
4380
5677
|
let root;
|
|
4381
5678
|
try {
|
|
@@ -4403,13 +5700,15 @@ var program = new Command();
|
|
|
4403
5700
|
program.name("blacksmith").description("Fullstack Django + React framework").version("0.1.0").hook("preAction", () => {
|
|
4404
5701
|
banner();
|
|
4405
5702
|
});
|
|
4406
|
-
program.command("init").argument("[name]", "Project name").option("--ai", "Set up AI development skills and documentation (CLAUDE.md)").option("--no-
|
|
5703
|
+
program.command("init").argument("[name]", "Project name").option("--ai", "Set up AI development skills and documentation (CLAUDE.md)").option("--no-chakra-ui-skill", "Disable Chakra UI skill when using --ai").option("-b, --backend-port <port>", "Django backend port (default: 8000)").option("-f, --frontend-port <port>", "Vite frontend port (default: 5173)").option("-t, --theme-color <color>", "Theme color (zinc, slate, blue, green, orange, red, violet)").description("Create a new Blacksmith project").action(init);
|
|
4407
5704
|
program.command("dev").description("Start development servers (Django + Vite + OpenAPI sync)").action(dev);
|
|
4408
5705
|
program.command("sync").description("Sync OpenAPI schema to frontend types, schemas, and hooks").action(sync);
|
|
4409
5706
|
program.command("make:resource").argument("<name>", "Resource name (PascalCase, e.g. BlogPost)").description("Create a new resource (model, serializer, viewset, hooks, pages)").action(makeResource);
|
|
4410
5707
|
program.command("build").description("Build both frontend and backend for production").action(build);
|
|
4411
5708
|
program.command("eject").description("Remove Blacksmith, keep a clean Django + React project").action(eject);
|
|
4412
|
-
program.command("setup:ai").description("Generate CLAUDE.md with AI development skills for the project").option("--no-
|
|
5709
|
+
program.command("setup:ai").description("Generate CLAUDE.md with AI development skills for the project").option("--no-chakra-ui-skill", "Exclude Chakra UI skill").action(setupSkills);
|
|
5710
|
+
program.command("setup:mcp").description("Configure MCP servers for Claude Code AI integration").action(setupMcp);
|
|
5711
|
+
program.command("studio").description("Launch Blacksmith Studio \u2014 web UI for Claude Code").option("-p, --port <port>", "Port for the Studio server (default: 3939)").action(studio);
|
|
4413
5712
|
program.command("skills").description("List all available AI development skills").action(listSkills);
|
|
4414
5713
|
program.command("backend").argument("[args...]", "Django management command and arguments").description("Run a Django management command (e.g. blacksmith backend createsuperuser)").allowUnknownOption().action(backend);
|
|
4415
5714
|
program.command("frontend").argument("[args...]", "npm command and arguments").description("Run an npm command in the frontend (e.g. blacksmith frontend install axios)").allowUnknownOption().action(frontend);
|