blacksmith-cli 0.1.6 → 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.
Files changed (34) hide show
  1. package/dist/index.js +1745 -689
  2. package/dist/index.js.map +1 -1
  3. package/package.json +2 -1
  4. package/src/templates/frontend/package.json.hbs +4 -4
  5. package/src/templates/frontend/src/__tests__/test-utils.tsx.hbs +5 -4
  6. package/src/templates/frontend/src/app.tsx.hbs +13 -9
  7. package/src/templates/frontend/src/features/auth/adapter.ts.hbs +7 -7
  8. package/src/templates/frontend/src/features/auth/components/auth-provider.tsx.hbs +91 -11
  9. package/src/templates/frontend/src/features/auth/hooks/use-auth.ts.hbs +3 -4
  10. package/src/templates/frontend/src/features/auth/pages/forgot-password-page.tsx.hbs +76 -12
  11. package/src/templates/frontend/src/features/auth/pages/login-page.tsx.hbs +84 -11
  12. package/src/templates/frontend/src/features/auth/pages/register-page.tsx.hbs +85 -14
  13. package/src/templates/frontend/src/features/auth/pages/reset-password-page.tsx.hbs +63 -12
  14. package/src/templates/frontend/src/features/auth/types.ts.hbs +32 -0
  15. package/src/templates/frontend/src/pages/dashboard/components/quick-start-card.tsx.hbs +19 -18
  16. package/src/templates/frontend/src/pages/dashboard/components/stack-cards.tsx.hbs +33 -31
  17. package/src/templates/frontend/src/pages/dashboard/components/welcome-header.tsx.hbs +5 -5
  18. package/src/templates/frontend/src/pages/dashboard/dashboard.tsx.hbs +5 -5
  19. package/src/templates/frontend/src/pages/home/home.tsx.hbs +48 -52
  20. package/src/templates/frontend/src/router/auth-guard.tsx.hbs +10 -7
  21. package/src/templates/frontend/src/router/error-boundary.tsx.hbs +16 -12
  22. package/src/templates/frontend/src/router/layouts/auth-layout.tsx.hbs +12 -12
  23. package/src/templates/frontend/src/router/layouts/main-layout.tsx.hbs +62 -55
  24. package/src/templates/frontend/src/shared/components/loading-spinner.tsx.hbs +6 -6
  25. package/src/templates/frontend/src/shared/components/not-found-page.tsx.hbs +1 -1
  26. package/src/templates/frontend/src/shared/hooks/use-debounce.ts.hbs +18 -2
  27. package/src/templates/frontend/src/styles/globals.css.hbs +3 -1
  28. package/src/templates/frontend/tailwind.config.js.hbs +1 -1
  29. package/src/templates/resource/frontend/components/{{kebab}}-form.tsx.hbs +3 -2
  30. package/src/templates/resource/frontend/pages/{{kebabs}}-page.tsx.hbs +3 -2
  31. package/src/templates/resource/frontend/pages/{{kebab}}-detail-page.tsx.hbs +5 -3
  32. package/src/templates/resource/pages/components/{{kebab}}-form.tsx.hbs +3 -2
  33. package/src/templates/resource/pages/{{kebabs}}-page.tsx.hbs +3 -2
  34. 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
- import path4 from "path";
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 path from "path";
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 = path.dirname(destPath);
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 = path.join(srcDir, entry.name);
917
+ const srcPath = path2.join(srcDir, entry.name);
155
918
  if (entry.isDirectory()) {
156
- const destSubDir = path.join(destDir, renderedName);
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 = path.join(destDir, outputName);
923
+ const destPath = path2.join(destDir, outputName);
161
924
  renderToFile(srcPath, destPath, context);
162
925
  } else {
163
- const destPath = path.join(destDir, renderedName);
164
- const destDirPath = path.dirname(destPath);
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
- import path2 from "path";
996
+ init_esm_shims();
997
+ import path3 from "path";
233
998
  import fs2 from "fs";
234
- import { fileURLToPath } from "url";
235
- var __filename2 = fileURLToPath(import.meta.url);
236
- var __dirname2 = path2.dirname(__filename2);
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 = path2.resolve(__dirname2, "..", "templates");
239
- const prodPath = path2.resolve(__dirname2, "..", "src", "templates");
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 !== path2.dirname(dir)) {
247
- if (fs2.existsSync(path2.join(dir, "blacksmith.config.json"))) {
1011
+ while (dir !== path3.dirname(dir)) {
1012
+ if (fs2.existsSync(path3.join(dir, "blacksmith.config.json"))) {
248
1013
  return dir;
249
1014
  }
250
- dir = path2.dirname(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 path2.join(root, "backend");
1023
+ return path3.join(root, "backend");
259
1024
  }
260
1025
  function getFrontendDir(projectRoot) {
261
1026
  const root = projectRoot || findProjectRoot();
262
- return path2.join(root, "frontend");
1027
+ return path3.join(root, "frontend");
263
1028
  }
264
1029
  function loadConfig(projectRoot) {
265
1030
  const root = projectRoot || findProjectRoot();
266
- const configPath = path2.join(root, "blacksmith.config.json");
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
- import path3 from "path";
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 \`@blacksmith-ui/react\` for ALL UI
284
- - **Layout**: Use \`Stack\`, \`Flex\`, \`Grid\`, \`Box\`, \`Container\` \u2014 NEVER \`<div className="flex ...">\` or \`<div className="grid ...">\`
285
- - **Typography**: Use \`Typography\` and \`Text\` \u2014 NEVER raw \`<h1>\`\u2013\`<h6>\`, \`<p>\`, or \`<span>\` with text classes
286
- - **Separators**: Use \`Divider\` \u2014 NEVER \`<hr>\` or \`<Separator>\`
287
- - **Everything else**: \`Button\`, \`Card\`, \`Badge\`, \`Input\`, \`Table\`, \`Dialog\`, \`Alert\`, \`Skeleton\`, \`EmptyState\`, \`StatCard\`, etc.
288
- - See the \`blacksmith-ui-react\` skill for the full 60+ component list
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
- - Tailwind CSS for styling
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 \`@blacksmith-ui/react\` components** \u2014 see the \`blacksmith-ui-react\` skill for the full component list
1345
- - Use \`Stack\`, \`Flex\`, \`Grid\`, \`Box\` for layout \u2014 never raw \`<div>\` with flex/grid classes
1346
- - Use \`Typography\` and \`Text\` for headings and text \u2014 never raw \`<h1>\`\u2013\`<h6>\` or \`<p>\`
1347
- - Use \`Divider\` instead of \`<Separator>\` or \`<hr>\`
1348
- - Use \`StatCard\`, \`EmptyState\`, \`Skeleton\` instead of building custom equivalents
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 Tailwind CSS utility classes for all styling
1357
- - Use the \`cn()\` helper (from \`clsx\` + \`tailwind-merge\`) for conditional and merged classes
1358
- - Theming via HSL CSS variables defined in \`frontend/src/styles/globals.css\`
1359
- - Dark mode is supported via the \`class\` strategy on \`<html>\`
1360
- - Use responsive prefixes (\`sm:\`, \`md:\`, \`lg:\`) for responsive layouts
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,7 +2141,7 @@ 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, Toast)
2144
+ - Display user-facing errors using the project's feedback components (Alert, useToast)
1372
2145
 
1373
2146
  ### Testing
1374
2147
  - See the \`frontend-testing\` skill for full conventions on test placement, utilities, mocking, and what to test
@@ -1380,6 +2153,7 @@ var reactSkill = {
1380
2153
  };
1381
2154
 
1382
2155
  // src/skills/react-query.ts
2156
+ init_esm_shims();
1383
2157
  var reactQuerySkill = {
1384
2158
  id: "react-query",
1385
2159
  name: "TanStack React Query",
@@ -1606,6 +2380,7 @@ export function useDeletePost() {
1606
2380
  };
1607
2381
 
1608
2382
  // src/skills/page-structure.ts
2383
+ init_esm_shims();
1609
2384
  var pageStructureSkill = {
1610
2385
  id: "page-structure",
1611
2386
  name: "Page & Route Structure",
@@ -1779,8 +2554,8 @@ pages/dashboard/
1779
2554
  \`\`\`
1780
2555
 
1781
2556
  \`\`\`tsx
1782
- // dashboard.tsx \u2014 thin orchestrator using @blacksmith-ui/react layout
1783
- import { Stack, Grid, Divider } from '@blacksmith-ui/react'
2557
+ // dashboard.tsx \u2014 thin orchestrator using @chakra-ui/react layout
2558
+ import { VStack, SimpleGrid, Divider } from '@chakra-ui/react'
1784
2559
  import { StatsCards } from './components/stats-cards'
1785
2560
  import { RecentActivity } from './components/recent-activity'
1786
2561
  import { QuickActions } from './components/quick-actions'
@@ -1790,14 +2565,14 @@ export default function DashboardPage() {
1790
2565
  const { stats, activity, isLoading } = useDashboardData()
1791
2566
 
1792
2567
  return (
1793
- <Stack gap={6}>
2568
+ <VStack spacing={6} align="stretch">
1794
2569
  <StatsCards stats={stats} isLoading={isLoading} />
1795
2570
  <Divider />
1796
- <Grid columns={{ base: 1, lg: 3 }} gap={6}>
1797
- <RecentActivity items={activity} isLoading={isLoading} className="lg:col-span-2" />
2571
+ <SimpleGrid columns={{ base: 1, lg: 3 }} spacing={6}>
2572
+ <RecentActivity items={activity} isLoading={isLoading} gridColumn={{ lg: 'span 2' }} />
1798
2573
  <QuickActions />
1799
- </Grid>
1800
- </Stack>
2574
+ </SimpleGrid>
2575
+ </VStack>
1801
2576
  )
1802
2577
  }
1803
2578
  \`\`\`
@@ -1857,7 +2632,7 @@ export function useOrdersPage() {
1857
2632
  }
1858
2633
 
1859
2634
  // orders-page.tsx
1860
- import { Stack } from '@blacksmith-ui/react'
2635
+ import { VStack } from '@chakra-ui/react'
1861
2636
  import { useOrdersPage } from './hooks/use-orders-page'
1862
2637
  import { OrdersTable } from './components/orders-table'
1863
2638
  import { OrdersToolbar } from './components/orders-toolbar'
@@ -1866,10 +2641,10 @@ export default function OrdersPage() {
1866
2641
  const { orders, total, isLoading, page, setPage, search, setSearch, deleteOrder } = useOrdersPage()
1867
2642
 
1868
2643
  return (
1869
- <Stack gap={4}>
2644
+ <VStack spacing={4} align="stretch">
1870
2645
  <OrdersToolbar search={search} onSearchChange={setSearch} />
1871
2646
  <OrdersTable orders={orders} isLoading={isLoading} onDelete={(id) => deleteOrder.mutate({ path: { id } })} />
1872
- </Stack>
2647
+ </VStack>
1873
2648
  )
1874
2649
  }
1875
2650
  \`\`\`
@@ -1902,217 +2677,176 @@ export default function OrdersPage() {
1902
2677
  }
1903
2678
  };
1904
2679
 
1905
- // src/skills/blacksmith-ui-react.ts
1906
- var blacksmithUiReactSkill = {
1907
- id: "blacksmith-ui-react",
1908
- name: "@blacksmith-ui/react",
1909
- description: "Core UI component library \u2014 60+ components for layout, typography, inputs, data display, overlays, feedback, media, and navigation.",
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.",
1910
2686
  render(_ctx) {
1911
- return `## @blacksmith-ui/react \u2014 Core UI Components (60+)
2687
+ return `## Chakra UI React \u2014 Core UI Components
1912
2688
 
1913
- > **CRITICAL RULE: Every UI element MUST be built using \`@blacksmith-ui/react\` components \u2014 including layout and typography.**
1914
- > Do NOT use raw HTML elements when a Blacksmith-UI component exists for that purpose.
1915
- > This includes layout: use \`Flex\`, \`Stack\`, \`Grid\`, \`Box\`, \`Container\` instead of \`<div>\` with flex/grid classes.
1916
- > This includes typography: use \`Text\` and \`Typography\` instead of raw \`<h1>\`\u2013\`<h6>\`, \`<p>\`, \`<span>\`.
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>\`.
1917
2693
 
1918
2694
  ### Layout
1919
2695
 
1920
2696
  | Component | Use instead of | Description |
1921
2697
  |-----------|---------------|-------------|
1922
2698
  | \`Box\` | \`<div>\` | Base layout primitive with style props |
1923
- | \`Flex\` | \`<div className="flex ...">\` | Flexbox container with style props (\`direction\`, \`align\`, \`justify\`, \`gap\`, \`wrap\`) |
1924
- | \`Grid\` | \`<div className="grid ...">\` | CSS Grid container (\`columns\`, \`rows\`, \`gap\`) |
1925
- | \`Stack\` | \`<div className="flex flex-col gap-...">\` | Vertical/horizontal stack (\`direction\`, \`gap\`) |
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\`) |
1926
2703
  | \`Container\` | \`<div className="max-w-7xl mx-auto px-...">\` | Max-width centered container |
1927
2704
  | \`Divider\` | \`<hr>\` or border hacks | Visual separator (horizontal/vertical) |
1928
2705
  | \`AspectRatio\` | padding-bottom trick | Maintain aspect ratio for content |
1929
- | \`Resizable\` | custom resize logic | Resizable panel groups |
1930
- | \`ScrollArea\` | \`overflow-auto\` divs | Custom scrollbar container |
1931
2706
 
1932
2707
  ### Typography
1933
2708
 
1934
2709
  | Component | Use instead of | Description |
1935
2710
  |-----------|---------------|-------------|
1936
- | \`Text\` | \`<p>\`, \`<span>\` | Text display with style props (\`size\`, \`weight\`, \`color\`, \`align\`) |
1937
- | \`Typography\` | \`<h1>\`\u2013\`<h6>\`, \`<p>\` | Semantic heading/paragraph elements (\`variant\`: h1\u2013h6, p, lead, muted, etc.) |
1938
- | \`Label\` | \`<label>\` | Form label with accessibility support |
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 |
1939
2714
 
1940
2715
  ### Cards & Containers
1941
2716
 
1942
- - \`Card\`, \`CardHeader\`, \`CardTitle\`, \`CardDescription\`, \`CardContent\`, \`CardFooter\` \u2014 Use instead of styled \`<div>\` containers
1943
- - \`StatCard\` \u2014 Use for metric/stat display (value, label, trend)
1944
- - \`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.
1945
2718
 
1946
2719
  ### Actions
1947
2720
 
1948
2721
  - \`Button\` \u2014 Use instead of \`<button>\` or \`<a>\` styled as buttons
1949
- - Variants: \`default\`, \`secondary\`, \`destructive\`, \`outline\`, \`ghost\`, \`link\`
1950
- - Sizes: \`sm\`, \`default\`, \`lg\`, \`icon\`
1951
- - \`Toggle\`, \`ToggleGroup\` \u2014 Use for toggle buttons
1952
- - \`DropdownMenu\`, \`DropdownMenuTrigger\`, \`DropdownMenuContent\`, \`DropdownMenuItem\`, \`DropdownMenuSeparator\`, \`DropdownMenuLabel\` \u2014 Use for action menus
1953
- - \`ContextMenu\` \u2014 Use for right-click menus
1954
- - \`Menubar\` \u2014 Use for application menu bars
1955
- - \`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
1956
2728
 
1957
2729
  ### Data Entry
1958
2730
 
1959
2731
  - \`Input\` \u2014 Use instead of \`<input>\`
1960
- - \`SearchInput\` \u2014 Use for search fields (has built-in search icon)
2732
+ - \`InputGroup\`, \`InputLeftElement\`, \`InputRightElement\` \u2014 Use for input with icons/addons
1961
2733
  - \`Textarea\` \u2014 Use instead of \`<textarea>\`
1962
- - \`NumberInput\` \u2014 Use for numeric inputs with increment/decrement
1963
- - \`Select\`, \`SelectTrigger\`, \`SelectContent\`, \`SelectItem\`, \`SelectValue\` \u2014 Use instead of \`<select>\`
1964
- - \`Checkbox\` \u2014 Use instead of \`<input type="checkbox">\`
1965
- - \`RadioGroup\`, \`RadioGroupItem\` \u2014 Use instead of \`<input type="radio">\`
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">\`
1966
2738
  - \`Switch\` \u2014 Use for toggle switches
1967
- - \`Slider\` \u2014 Use for single range inputs
1968
- - \`RangeSlider\` \u2014 Use for dual-handle range selection
1969
- - \`DatePicker\` \u2014 Use for date selection with calendar popup
1970
- - \`PinInput\` / \`InputOTP\` \u2014 Use for PIN/OTP code entry
1971
- - \`ColorPicker\` \u2014 Use for color selection
1972
- - \`FileUpload\` \u2014 Use for file upload with drag & drop
1973
- - \`TagInput\` \u2014 Use for tag/chip input with add/remove
1974
- - \`Rating\` \u2014 Use for star/icon rating selection
1975
- - \`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>\`
1976
2742
 
1977
2743
  ### Data Display
1978
2744
 
1979
- - \`Table\`, \`TableHeader\`, \`TableBody\`, \`TableRow\`, \`TableHead\`, \`TableCell\` \u2014 Use instead of \`<table>\` elements
1980
- - \`DataTable\` \u2014 Use for feature-rich tables with sorting, filtering, and pagination
1981
- - \`Badge\` \u2014 Use for status indicators, tags, counts (variants: \`default\`, \`secondary\`, \`destructive\`, \`outline\`)
1982
- - \`Avatar\`, \`AvatarImage\`, \`AvatarFallback\` \u2014 Use for user profile images
1983
- - \`Tooltip\`, \`TooltipTrigger\`, \`TooltipContent\`, \`TooltipProvider\` \u2014 Use for hover hints
1984
- - \`HoverCard\` \u2014 Use for rich hover content
1985
- - \`Calendar\` \u2014 Use for full calendar display
1986
- - \`Chart\` \u2014 Use for data visualization (powered by Recharts)
1987
- - \`Timeline\` \u2014 Use for chronological event display
1988
- - \`Tree\` \u2014 Use for hierarchical tree views
1989
- - \`List\` \u2014 Use for structured list display instead of \`<ul>\`/\`<ol>\`
1990
- - \`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
1991
2751
  - \`Spinner\` \u2014 Use for loading indicators
1992
2752
  - \`Progress\` \u2014 Use for progress bars
1993
- - \`Pagination\`, \`PaginationContent\`, \`PaginationItem\`, \`PaginationLink\`, \`PaginationNext\`, \`PaginationPrevious\` \u2014 Use for paginated lists
2753
+ - \`Tag\`, \`TagLabel\`, \`TagCloseButton\` \u2014 Use for removable tags
1994
2754
 
1995
2755
  ### Tabs & Accordion
1996
2756
 
1997
- - \`Tabs\`, \`TabsList\`, \`TabsTrigger\`, \`TabsContent\` \u2014 Use for tabbed interfaces
1998
- - \`Accordion\`, \`AccordionItem\`, \`AccordionTrigger\`, \`AccordionContent\` \u2014 Use for collapsible sections
2757
+ - \`Tabs\`, \`TabList\`, \`Tab\`, \`TabPanels\`, \`TabPanel\` \u2014 Use for tabbed interfaces
2758
+ - \`Accordion\`, \`AccordionItem\`, \`AccordionButton\`, \`AccordionPanel\`, \`AccordionIcon\` \u2014 Use for collapsible sections
1999
2759
 
2000
2760
  ### Overlays
2001
2761
 
2002
- - \`Dialog\`, \`DialogTrigger\`, \`DialogContent\`, \`DialogHeader\`, \`DialogTitle\`, \`DialogDescription\`, \`DialogFooter\` \u2014 Use for modals
2762
+ - \`Modal\`, \`ModalOverlay\`, \`ModalContent\`, \`ModalHeader\`, \`ModalBody\`, \`ModalFooter\`, \`ModalCloseButton\` \u2014 Use for modals
2003
2763
  - \`AlertDialog\` \u2014 Use for confirmation dialogs
2004
- - \`Drawer\` / \`Sheet\`, \`SheetTrigger\`, \`SheetContent\`, \`SheetHeader\`, \`SheetTitle\`, \`SheetDescription\` \u2014 Use for slide-out panels
2005
- - \`Popover\` \u2014 Use for floating content panels
2006
- - \`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
2007
2766
 
2008
2767
  ### Navigation
2009
2768
 
2010
- - \`Breadcrumb\`, \`BreadcrumbList\`, \`BreadcrumbItem\`, \`BreadcrumbLink\`, \`BreadcrumbSeparator\` \u2014 Use for breadcrumb trails
2011
- - \`NavigationMenu\`, \`NavigationMenuList\`, \`NavigationMenuItem\`, \`NavigationMenuTrigger\`, \`NavigationMenuContent\` \u2014 Use for site navigation
2012
- - \`Sidebar\` \u2014 Use for app sidebars
2013
- - \`Dock\` \u2014 Use for macOS-style dock navigation
2014
- - \`BackToTop\` \u2014 Use for scroll-to-top buttons
2769
+ - \`Breadcrumb\`, \`BreadcrumbItem\`, \`BreadcrumbLink\` \u2014 Use for breadcrumb trails
2015
2770
 
2016
2771
  ### Feedback
2017
2772
 
2018
- - \`Alert\`, \`AlertTitle\`, \`AlertDescription\` \u2014 Use for inline messages/warnings
2019
- - \`AlertBanner\` \u2014 Use for full-width alert banners
2020
- - \`Toast\` / \`Toaster\` / \`useToast\` \u2014 Use for transient notifications
2021
- - \`SonnerToaster\` \u2014 Sonner-based toast notifications
2022
-
2023
- ### Media
2024
-
2025
- - \`Image\` \u2014 Use instead of \`<img>\` for optimized image display
2026
- - \`VideoPlayer\` \u2014 Use for video playback
2027
- - \`CodeBlock\` \u2014 Use for syntax-highlighted code (Shiki-powered)
2028
- - \`Carousel\` \u2014 Use for image/content carousels
2029
- - \`Lightbox\` \u2014 Use for full-screen media viewers
2030
-
2031
- ### Specialized
2032
-
2033
- - \`Stepper\` / \`Wizard\` \u2014 Use for multi-step workflows
2034
- - \`NotificationCenter\` / \`useNotificationCenter\` \u2014 Use for notification management
2035
- - \`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
2036
2775
 
2037
2776
  ### Utilities & Hooks
2038
2777
 
2039
- - \`cn()\` \u2014 Merge class names (clsx + tailwind-merge)
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
2040
2781
  - \`useToast()\` \u2014 Programmatic toast notifications
2041
- - \`useMobile()\` \u2014 Responsive breakpoint detection
2042
- - \`useDarkMode()\` \u2014 Dark mode toggle. Returns \`{ isDark, toggle }\`
2782
+ - \`useMediaQuery()\` \u2014 CSS media query matching
2043
2783
 
2044
2784
  ---
2045
2785
 
2046
2786
  ### Component-First Rules
2047
2787
 
2048
- 1. **Layout**: NEVER use \`<div className="flex ...">\` or \`<div className="grid ...">\`. Use \`<Flex>\`, \`<Grid>\`, \`<Stack>\`, \`<Box>\` from \`@blacksmith-ui/react\`.
2788
+ 1. **Layout**: NEVER use \`<div className="flex ...">\` or \`<div className="grid ...">\`. Use \`<Flex>\`, \`<SimpleGrid>\`, \`<VStack>\`, \`<HStack>\`, \`<Box>\` from \`@chakra-ui/react\`.
2049
2789
  2. **Centering/max-width**: NEVER use \`<div className="max-w-7xl mx-auto px-...">\`. Use \`<Container>\`.
2050
- 3. **Typography**: NEVER use raw \`<h1>\`\u2013\`<h6>\` or \`<p>\` with Tailwind text classes. Use \`<Typography variant="h2">\` or \`<Text>\`.
2790
+ 3. **Typography**: NEVER use raw \`<h1>\`\u2013\`<h6>\` or \`<p>\` with Tailwind text classes. Use \`<Heading as="h2" size="lg">\` or \`<Text>\`.
2051
2791
  4. **Separators**: NEVER use \`<hr>\` or border hacks. Use \`<Divider>\`.
2052
- 5. **Images**: NEVER use raw \`<img>\`. Use \`<Image>\` from \`@blacksmith-ui/react\` (use \`Avatar\` for profile pictures).
2053
- 6. **Lists**: NEVER use \`<ul>\`/\`<ol>\` for structured display lists. Use \`<List>\` from \`@blacksmith-ui/react\`. Plain \`<ul>\`/\`<ol>\` is only acceptable for simple inline content lists.
2054
- 7. **Buttons**: NEVER use \`<button>\` or \`<a>\` styled as a button. Use \`<Button>\`.
2055
- 8. **Inputs**: NEVER use \`<input>\`, \`<textarea>\`, \`<select>\` directly. Use the Blacksmith-UI equivalents.
2056
- 9. **Cards**: NEVER use a styled \`<div>\` as a card. Use \`Card\` + sub-components.
2057
- 10. **Tables**: NEVER use raw \`<table>\` HTML. Use \`Table\` or \`DataTable\`.
2058
- 11. **Loading**: NEVER use custom \`animate-pulse\` divs. Use \`Skeleton\` or \`Spinner\`.
2059
- 12. **Modals**: NEVER build custom modals. Use \`Dialog\`, \`AlertDialog\`, \`Drawer\`, or \`Sheet\`.
2060
- 13. **Feedback**: NEVER use plain styled text for errors/warnings. Use \`Alert\` or \`useToast\`.
2061
- 14. **Empty states**: NEVER build custom empty-state UIs. Use \`EmptyState\`.
2062
- 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\`.
2063
2799
 
2064
2800
  ### When Raw HTML IS Acceptable
2065
2801
 
2066
- - \`<main>\`, \`<section>\`, \`<header>\`, \`<footer>\`, \`<nav>\`, \`<article>\`, \`<aside>\` \u2014 semantic HTML landmarks for page structure (but use \`Flex\`/\`Stack\`/\`Grid\` inside them for layout)
2067
- - \`<Link>\` from react-router-dom \u2014 for page navigation (use \`<Button asChild><Link>...</Link></Button>\` if it needs button styling)
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)
2068
2804
  - Icon components from \`lucide-react\`
2069
- - \`<form>\` element when used with React Hook Form (but use \`@blacksmith-ui/forms\` components inside)
2805
+ - \`<form>\` element when used with React Hook Form (but use Chakra UI form components inside)
2070
2806
 
2071
2807
  ### Design Tokens & Theming
2072
2808
 
2073
- - \`ThemeProvider\` \u2014 Wrap app to apply preset or custom theme
2074
- - Built-in presets: \`default\`, \`blue\`, \`green\`, \`violet\`, \`red\`, \`neutral\`
2075
- - All components use HSL CSS variables (\`--background\`, \`--foreground\`, \`--primary\`, etc.)
2076
- - Dark mode: \`.dark\` class strategy on \`<html>\`, or \`<ThemeProvider mode="dark">\`
2077
- - Border radius: controlled by \`--radius\` CSS variable
2078
- - Extend with \`className\` prop + \`cn()\` utility for custom styles
2079
- - 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
2080
2814
 
2081
2815
  ### Example: HowItWorks Section (Correct Way)
2082
2816
 
2083
2817
  \`\`\`tsx
2084
- import { Container, Stack, Flex, Grid, Text, Typography, Image } from '@blacksmith-ui/react'
2818
+ import { Container, VStack, Flex, SimpleGrid, Text, Heading, Box } from '@chakra-ui/react'
2085
2819
  import { howItWorksSteps } from '../data'
2086
2820
 
2087
2821
  export function HowItWorks() {
2088
2822
  return (
2089
- <Box as="section" className="py-16 sm:py-20">
2090
- <Container>
2091
- <Stack gap={3} align="center" className="mb-12">
2092
- <Typography variant="h2">How It Works</Typography>
2093
- <Text color="muted">Book your stay in three simple steps</Text>
2094
- </Stack>
2095
-
2096
- <Grid columns={{ base: 1, md: 3 }} gap={8} className="max-w-4xl mx-auto">
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">
2097
2831
  {howItWorksSteps.map((item) => (
2098
- <Stack key={item.step} align="center" gap={4}>
2099
- <Box className="relative">
2100
- <Flex align="center" justify="center" className="h-16 w-16 rounded-full bg-primary text-primary-foreground shadow-lg shadow-primary/30">
2101
- <item.icon className="h-7 w-7" />
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} />
2102
2836
  </Flex>
2103
- <Flex align="center" justify="center" className="absolute -top-1 -right-1 h-6 w-6 rounded-full bg-background border-2 border-primary">
2104
- <Text size="xs" weight="bold" color="primary">{item.step}</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>
2105
2839
  </Flex>
2106
2840
  </Box>
2107
- <Stack gap={2} align="center">
2108
- <Text size="lg" weight="bold">{item.title}</Text>
2109
- <Text size="sm" color="muted" align="center" className="max-w-xs">
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">
2110
2844
  {item.description}
2111
2845
  </Text>
2112
- </Stack>
2113
- </Stack>
2846
+ </VStack>
2847
+ </VStack>
2114
2848
  ))}
2115
- </Grid>
2849
+ </SimpleGrid>
2116
2850
  </Container>
2117
2851
  </Box>
2118
2852
  )
@@ -2123,28 +2857,34 @@ export function HowItWorks() {
2123
2857
 
2124
2858
  \`\`\`tsx
2125
2859
  import {
2126
- Stack, Flex,
2127
- Card, CardHeader, CardTitle, CardContent,
2128
- Button, Badge, Skeleton,
2129
- Table, TableHeader, TableBody, TableRow, TableHead, TableCell,
2130
- DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem,
2131
- AlertDialog, AlertDialogTrigger, AlertDialogContent,
2132
- AlertDialogAction, AlertDialogCancel,
2133
- } from '@blacksmith-ui/react'
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'
2134
2869
  import { MoreHorizontal, Plus, Trash2, Edit } from 'lucide-react'
2135
2870
  import { Link } from 'react-router-dom'
2871
+ import { useRef } from 'react'
2136
2872
 
2137
2873
  function ResourceListPage({ resources, isLoading, onDelete }) {
2874
+ const { isOpen, onOpen, onClose } = useDisclosure()
2875
+ const cancelRef = useRef()
2876
+ const [deleteId, setDeleteId] = useState(null)
2877
+
2138
2878
  if (isLoading) {
2139
2879
  return (
2140
2880
  <Card>
2141
- <CardContent className="p-6">
2142
- <Stack gap={4}>
2881
+ <CardBody p={6}>
2882
+ <VStack spacing={4}>
2143
2883
  {Array.from({ length: 5 }).map((_, i) => (
2144
- <Skeleton key={i} className="h-12 w-full" />
2884
+ <Skeleton key={i} h="48px" w="full" />
2145
2885
  ))}
2146
- </Stack>
2147
- </CardContent>
2886
+ </VStack>
2887
+ </CardBody>
2148
2888
  </Card>
2149
2889
  )
2150
2890
  }
@@ -2152,61 +2892,63 @@ function ResourceListPage({ resources, isLoading, onDelete }) {
2152
2892
  return (
2153
2893
  <Card>
2154
2894
  <CardHeader>
2155
- <Flex align="center" justify="between">
2156
- <CardTitle>Resources</CardTitle>
2157
- <Button asChild>
2158
- <Link to="/resources/new"><Plus className="mr-2 h-4 w-4" /> Create</Link>
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
2159
2899
  </Button>
2160
2900
  </Flex>
2161
2901
  </CardHeader>
2162
- <CardContent>
2163
- <Table>
2164
- <TableHeader>
2165
- <TableRow>
2166
- <TableHead>Title</TableHead>
2167
- <TableHead>Status</TableHead>
2168
- <TableHead className="w-12" />
2169
- </TableRow>
2170
- </TableHeader>
2171
- <TableBody>
2172
- {resources.map((r) => (
2173
- <TableRow key={r.id}>
2174
- <TableCell>{r.title}</TableCell>
2175
- <TableCell><Badge variant="outline">{r.status}</Badge></TableCell>
2176
- <TableCell>
2177
- <DropdownMenu>
2178
- <DropdownMenuTrigger asChild>
2179
- <Button variant="ghost" size="icon">
2180
- <MoreHorizontal className="h-4 w-4" />
2181
- </Button>
2182
- </DropdownMenuTrigger>
2183
- <DropdownMenuContent>
2184
- <DropdownMenuItem asChild>
2185
- <Link to={\`/resources/\${r.id}/edit\`}>
2186
- <Edit className="mr-2 h-4 w-4" /> Edit
2187
- </Link>
2188
- </DropdownMenuItem>
2189
- <AlertDialog>
2190
- <AlertDialogTrigger asChild>
2191
- <DropdownMenuItem onSelect={(e) => e.preventDefault()}>
2192
- <Trash2 className="mr-2 h-4 w-4" /> Delete
2193
- </DropdownMenuItem>
2194
- </AlertDialogTrigger>
2195
- <AlertDialogContent>
2196
- <AlertDialogAction onClick={() => onDelete(r.id)}>
2197
- Delete
2198
- </AlertDialogAction>
2199
- <AlertDialogCancel>Cancel</AlertDialogCancel>
2200
- </AlertDialogContent>
2201
- </AlertDialog>
2202
- </DropdownMenuContent>
2203
- </DropdownMenu>
2204
- </TableCell>
2205
- </TableRow>
2206
- ))}
2207
- </TableBody>
2208
- </Table>
2209
- </CardContent>
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>
2210
2952
  </Card>
2211
2953
  )
2212
2954
  }
@@ -2215,42 +2957,50 @@ function ResourceListPage({ resources, isLoading, onDelete }) {
2215
2957
  }
2216
2958
  };
2217
2959
 
2218
- // src/skills/blacksmith-ui-forms.ts
2219
- var blacksmithUiFormsSkill = {
2220
- id: "blacksmith-ui-forms",
2221
- name: "@blacksmith-ui/forms",
2222
- description: "Form components using React Hook Form + Zod for validation and submission.",
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.",
2223
2966
  render(_ctx) {
2224
- return `## @blacksmith-ui/forms \u2014 Form Components (React Hook Form + Zod)
2967
+ return `## Chakra UI Forms \u2014 Form Components (React Hook Form + Zod)
2225
2968
 
2226
- > **RULE: ALWAYS use these for forms.** Do NOT build forms with raw \`<form>\`, \`<input>\`, \`<label>\`, or manual error display.
2969
+ > **RULE: ALWAYS use Chakra UI form components for forms.** Do NOT build forms with raw \`<form>\`, \`<input>\`, \`<label>\`, or manual error display.
2227
2970
 
2228
2971
  \`\`\`tsx
2229
- import { Form, FormField, FormInput, FormTextarea, FormSelect } from '@blacksmith-ui/forms'
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'
2230
2978
  \`\`\`
2231
2979
 
2232
2980
  ### Components
2233
2981
 
2234
- - \`Form\` \u2014 Wraps the entire form. Props: \`form\` (useForm instance), \`onSubmit\`
2235
- - \`FormField\` \u2014 Wraps each field. Props: \`name\`, \`label\`, \`description?\`
2236
- - \`FormInput\` \u2014 Text input within FormField. Props: \`type\`, \`placeholder\`
2237
- - \`FormTextarea\` \u2014 Textarea within FormField. Props: \`rows\`, \`placeholder\`
2238
- - \`FormSelect\` \u2014 Select within FormField. Props: \`options\`, \`placeholder\`
2239
- - \`FormCheckbox\` \u2014 Checkbox within FormField
2240
- - \`FormSwitch\` \u2014 Toggle switch within FormField
2241
- - \`FormRadioGroup\` \u2014 Radio group within FormField. Props: \`options\`
2242
- - \`FormDatePicker\` \u2014 Date picker within FormField
2243
- - \`FormError\` \u2014 Displays field-level validation error (auto-handled by FormField)
2244
- - \`FormDescription\` \u2014 Displays helper text below a field
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
2245
2993
 
2246
2994
  ### Rules
2247
- - NEVER use raw \`<form>\` with manual \`<label>\` and error \`<p>\` tags. Always use \`Form\` + \`FormField\`.
2248
- - NEVER use \`<input>\`, \`<textarea>\`, \`<select>\` inside forms. Use \`FormInput\`, \`FormTextarea\`, \`FormSelect\`.
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\`.
2249
2997
 
2250
2998
  ### Form Pattern \u2014 ALWAYS follow this:
2251
2999
  \`\`\`tsx
2252
- import { Form, FormField, FormInput, FormTextarea, FormSelect } from '@blacksmith-ui/forms'
2253
- import { Button } from '@blacksmith-ui/react'
3000
+ import {
3001
+ FormControl, FormLabel, FormErrorMessage,
3002
+ Input, Textarea, Select, Button, VStack,
3003
+ } from '@chakra-ui/react'
2254
3004
  import { useForm } from 'react-hook-form'
2255
3005
  import { zodResolver } from '@hookform/resolvers/zod'
2256
3006
  import { z } from 'zod'
@@ -2264,29 +3014,40 @@ const schema = z.object({
2264
3014
  type FormData = z.infer<typeof schema>
2265
3015
 
2266
3016
  function ResourceForm({ defaultValues, onSubmit, isSubmitting }: Props) {
2267
- const form = useForm<FormData>({
3017
+ const { register, handleSubmit, formState: { errors } } = useForm<FormData>({
2268
3018
  resolver: zodResolver(schema),
2269
3019
  defaultValues: { title: '', description: '', status: 'draft', ...defaultValues },
2270
3020
  })
2271
3021
 
2272
3022
  return (
2273
- <Form form={form} onSubmit={onSubmit}>
2274
- <FormField name="title" label="Title">
2275
- <FormInput placeholder="Enter title" />
2276
- </FormField>
2277
- <FormField name="description" label="Description">
2278
- <FormTextarea rows={4} placeholder="Enter description" />
2279
- </FormField>
2280
- <FormField name="status" label="Status">
2281
- <FormSelect options={[
2282
- { label: 'Draft', value: 'draft' },
2283
- { label: 'Published', value: 'published' },
2284
- ]} />
2285
- </FormField>
2286
- <Button type="submit" disabled={isSubmitting}>
2287
- {isSubmitting ? 'Saving...' : 'Save'}
2288
- </Button>
2289
- </Form>
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>
2290
3051
  )
2291
3052
  }
2292
3053
  \`\`\`
@@ -2294,12 +3055,13 @@ function ResourceForm({ defaultValues, onSubmit, isSubmitting }: Props) {
2294
3055
  ### Example: Detail Page with Edit Dialog
2295
3056
  \`\`\`tsx
2296
3057
  import {
2297
- Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter,
2298
- Button, Badge, Separator,
2299
- Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogFooter,
2300
- Alert, AlertTitle, AlertDescription,
2301
- } from '@blacksmith-ui/react'
2302
- import { Form, FormField, FormInput, FormTextarea } from '@blacksmith-ui/forms'
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'
2303
3065
  import { useForm } from 'react-hook-form'
2304
3066
  import { zodResolver } from '@hookform/resolvers/zod'
2305
3067
  import { z } from 'zod'
@@ -2312,7 +3074,8 @@ const editSchema = z.object({
2312
3074
  })
2313
3075
 
2314
3076
  function ResourceDetailPage({ resource, onUpdate, error }) {
2315
- const form = useForm({
3077
+ const { isOpen, onOpen, onClose } = useDisclosure()
3078
+ const { register, handleSubmit, formState: { errors } } = useForm({
2316
3079
  resolver: zodResolver(editSchema),
2317
3080
  defaultValues: { title: resource.title, description: resource.description },
2318
3081
  })
@@ -2320,52 +3083,62 @@ function ResourceDetailPage({ resource, onUpdate, error }) {
2320
3083
  return (
2321
3084
  <Card>
2322
3085
  <CardHeader>
2323
- <div className="flex items-center justify-between">
2324
- <div>
2325
- <CardTitle>{resource.title}</CardTitle>
2326
- <CardDescription>Created {new Date(resource.created_at).toLocaleDateString()}</CardDescription>
2327
- </div>
2328
- <div className="flex gap-2">
2329
- <Button variant="outline" asChild>
2330
- <Link to="/resources"><ArrowLeft className="mr-2 h-4 w-4" /> Back</Link>
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
2331
3097
  </Button>
2332
- <Dialog>
2333
- <DialogTrigger asChild>
2334
- <Button><Edit className="mr-2 h-4 w-4" /> Edit</Button>
2335
- </DialogTrigger>
2336
- <DialogContent>
2337
- <DialogHeader>
2338
- <DialogTitle>Edit Resource</DialogTitle>
2339
- </DialogHeader>
2340
- <Form form={form} onSubmit={onUpdate}>
2341
- <FormField name="title" label="Title">
2342
- <FormInput />
2343
- </FormField>
2344
- <FormField name="description" label="Description">
2345
- <FormTextarea rows={4} />
2346
- </FormField>
2347
- <DialogFooter>
2348
- <Button type="submit">Save Changes</Button>
2349
- </DialogFooter>
2350
- </Form>
2351
- </DialogContent>
2352
- </Dialog>
2353
- </div>
2354
- </div>
3098
+ </HStack>
3099
+ </Flex>
2355
3100
  </CardHeader>
2356
- <Separator />
2357
- <CardContent className="pt-6">
3101
+ <Divider />
3102
+ <CardBody>
2358
3103
  {error && (
2359
- <Alert variant="destructive" className="mb-4">
3104
+ <Alert status="error" mb={4}>
3105
+ <AlertIcon />
2360
3106
  <AlertTitle>Error</AlertTitle>
2361
3107
  <AlertDescription>{error}</AlertDescription>
2362
3108
  </Alert>
2363
3109
  )}
2364
- <p>{resource.description || 'No description provided.'}</p>
2365
- </CardContent>
3110
+ <Text>{resource.description || 'No description provided.'}</Text>
3111
+ </CardBody>
2366
3112
  <CardFooter>
2367
3113
  <Badge>{resource.status}</Badge>
2368
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>
2369
3142
  </Card>
2370
3143
  )
2371
3144
  }
@@ -2374,232 +3147,222 @@ function ResourceDetailPage({ resource, onUpdate, error }) {
2374
3147
  }
2375
3148
  };
2376
3149
 
2377
- // src/skills/blacksmith-ui-auth.ts
2378
- var blacksmithUiAuthSkill = {
2379
- id: "blacksmith-ui-auth",
2380
- name: "@blacksmith-ui/auth",
2381
- description: "Authentication UI components and hooks for login, registration, and password reset.",
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.",
2382
3156
  render(_ctx) {
2383
- return `## @blacksmith-ui/auth \u2014 Authentication UI
3157
+ return `## Custom Auth System \u2014 Authentication Context & Hooks
2384
3158
 
2385
- > **RULE: ALWAYS use these for auth pages.** Do NOT build custom login/register forms.
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/\`.
2386
3161
 
2387
3162
  \`\`\`tsx
2388
- import { AuthProvider, LoginForm, RegisterForm, useAuth } from '@blacksmith-ui/auth'
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'
2389
3166
  \`\`\`
2390
3167
 
2391
- ### Components
3168
+ ### Context & Provider
2392
3169
 
2393
- - \`AuthProvider\` \u2014 Context provider wrapping the app. Props: \`config: { adapter, socialProviders? }\`
2394
- - \`LoginForm\` \u2014 Complete login form with email/password fields, validation, and links
2395
- - Props: \`onSubmit: (data: { email, password }) => void\`, \`onRegisterClick\`, \`onForgotPasswordClick\`, \`error\`, \`loading\`
2396
- - \`RegisterForm\` \u2014 Registration form with email, password, and display name
2397
- - Props: \`onSubmit: (data: { email, password, displayName }) => void\`, \`onLoginClick\`, \`error\`, \`loading\`
2398
- - \`ForgotPasswordForm\` \u2014 Password reset email request
2399
- - Props: \`onSubmit: (data: { email }) => void\`, \`onLoginClick\`, \`error\`, \`loading\`
2400
- - \`ResetPasswordForm\` \u2014 Set new password form
2401
- - Props: \`onSubmit: (data: { password, code }) => void\`, \`code\`, \`onLoginClick\`, \`error\`, \`loading\`
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
+ }
3183
+
3184
+ interface AuthState {
3185
+ user: User | null
3186
+ loading: boolean
3187
+ error: string | null
3188
+ isAuthenticated: boolean
3189
+ }
3190
+ \`\`\`
2402
3191
 
2403
3192
  ### Hooks
2404
3193
 
2405
- - \`useAuth\` \u2014 Hook for auth state and actions
2406
- - Returns: \`user\`, \`loading\`, \`error\`, \`signInWithEmail(email, password)\`, \`signUpWithEmail(email, password, displayName?)\`, \`signOut()\`, \`sendPasswordResetEmail(email)\`, \`confirmPasswordReset(code, newPassword)\`, \`socialProviders\`
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.
2407
3207
 
2408
3208
  ### Adapter
2409
3209
 
2410
- - \`AuthAdapter\` \u2014 Interface for custom auth backends (Django JWT adapter already configured in \`frontend/src/features/auth/adapter.ts\`)
3210
+ - \`AuthAdapter\` \u2014 Interface for custom auth backends (Django JWT adapter in \`frontend/src/features/auth/adapter.ts\`)
2411
3211
 
2412
3212
  ### Rules
2413
- - NEVER build custom login/register forms. Use \`LoginForm\`, \`RegisterForm\`, etc. from \`@blacksmith-ui/auth\`.
2414
- - NEVER manage auth state manually. Use \`useAuth\` hook.
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.).
2415
3216
  `;
2416
3217
  }
2417
3218
  };
2418
3219
 
2419
3220
  // src/skills/blacksmith-hooks.ts
3221
+ init_esm_shims();
2420
3222
  var blacksmithHooksSkill = {
2421
3223
  id: "blacksmith-hooks",
2422
- name: "@blacksmith-ui/hooks",
2423
- description: "74 production-ready React hooks for state, DOM, timers, async, browser APIs, and layout.",
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.",
2424
3226
  render(_ctx) {
2425
- return `## @blacksmith-ui/hooks \u2014 React Hooks Library
3227
+ return `## Custom Hooks & Chakra UI Hooks
2426
3228
 
2427
- A collection of 74 production-ready React hooks. SSR-safe, fully typed, zero dependencies, tree-shakeable.
3229
+ A combination of local custom hooks and Chakra UI built-in hooks for common UI patterns.
2428
3230
 
2429
- > **RULE: Use \`@blacksmith-ui/hooks\` instead of writing custom hooks when one exists for that purpose.**
3231
+ > **RULE: Use Chakra UI hooks when available, and local custom hooks for additional utilities.**
2430
3232
  > Before creating a new hook, check if one already exists below.
2431
3233
 
3234
+ ### Chakra UI Built-in Hooks
3235
+
2432
3236
  \`\`\`tsx
2433
- import { useToggle, useLocalStorage, useDebounce, useClickOutside } from '@blacksmith-ui/hooks'
3237
+ import { useColorMode, useDisclosure, useBreakpointValue, useMediaQuery, useToast } from '@chakra-ui/react'
2434
3238
  \`\`\`
2435
3239
 
2436
- ### State & Data
2437
-
2438
3240
  | Hook | Description |
2439
3241
  |------|-------------|
2440
- | \`useToggle\` | Boolean state with \`toggle\`, \`on\`, \`off\` actions |
2441
- | \`useDisclosure\` | Open/close/toggle state for modals, drawers, etc. |
2442
- | \`useCounter\` | Numeric counter with optional min/max clamping |
2443
- | \`useList\` | Array state with push, remove, update, insert, filter, clear |
2444
- | \`useMap\` | Map state with set, remove, clear helpers |
2445
- | \`useSet\` | Set state with add, remove, toggle, has, clear helpers |
2446
- | \`useHistoryState\` | State with undo/redo history |
2447
- | \`useDefault\` | State that falls back to a default when set to null/undefined |
2448
- | \`useQueue\` | FIFO queue data structure |
2449
- | \`useStack\` | LIFO stack data structure |
2450
- | \`useLocalStorage\` | Persist state to localStorage with JSON serialization |
2451
- | \`useSessionStorage\` | Persist state to sessionStorage with JSON serialization |
2452
- | \`useUncontrolled\` | Controlled/uncontrolled component pattern helper |
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/\`):
2453
3258
 
2454
- ### Values & Memoization
3259
+ \`\`\`tsx
3260
+ import { useDebounce } from '@/shared/hooks/use-debounce'
3261
+ import { useLocalStorage } from '@/shared/hooks/use-local-storage'
3262
+ \`\`\`
2455
3263
 
2456
3264
  | Hook | Description |
2457
3265
  |------|-------------|
2458
3266
  | \`useDebounce\` | Debounce a value with configurable delay |
2459
3267
  | \`useDebouncedCallback\` | Debounce a callback function |
2460
- | \`useThrottle\` | Throttle a value with configurable interval |
2461
- | \`useThrottledCallback\` | Throttle a callback function |
3268
+ | \`useLocalStorage\` | Persist state to localStorage with JSON serialization |
3269
+ | \`useSessionStorage\` | Persist state to sessionStorage with JSON serialization |
2462
3270
  | \`usePrevious\` | Track the previous value of a variable |
2463
- | \`useLatest\` | Ref that always points to the latest value |
2464
- | \`useConst\` | Compute a value once and return it on every render |
2465
- | \`useSyncedRef\` | Keep a ref synchronized with the latest value |
2466
-
2467
- ### DOM & Browser
2468
-
2469
- | Hook | Description |
2470
- |------|-------------|
2471
- | \`useClickOutside\` | Detect clicks outside a ref element |
3271
+ | \`useInterval\` | setInterval wrapper with pause support |
3272
+ | \`useTimeout\` | setTimeout wrapper with manual clear |
2472
3273
  | \`useEventListener\` | Attach event listeners to window or elements |
2473
3274
  | \`useElementSize\` | Track element width/height via ResizeObserver |
2474
3275
  | \`useHover\` | Track mouse hover state |
2475
3276
  | \`useKeyPress\` | Listen for a specific key press |
2476
- | \`useKeyCombo\` | Listen for key + modifier combinations |
2477
- | \`useLongPress\` | Detect long press gestures |
2478
- | \`useFullscreen\` | Manage the Fullscreen API |
2479
- | \`useTextSelection\` | Track currently selected text |
2480
- | \`useFocusWithin\` | Track whether focus is inside a container |
2481
- | \`useFocusTrap\` | Trap Tab/Shift+Tab focus within a container |
2482
- | \`useBoundingClientRect\` | Track element bounding rect via ResizeObserver |
2483
- | \`useSwipe\` | Detect touch swipe direction |
2484
- | \`useDrag\` | Track mouse drag with position and delta |
2485
- | \`useElementVisibility\` | Check if an element is in the viewport |
2486
3277
  | \`useScrollPosition\` | Track window scroll position |
2487
3278
  | \`useScrollLock\` | Lock/unlock body scroll |
2488
- | \`useMutationObserver\` | Observe DOM mutations |
2489
- | \`useIntersectionObserver\` | Observe element intersection with viewport |
2490
-
2491
- ### Timers & Lifecycle
2492
-
2493
- | Hook | Description |
2494
- |------|-------------|
2495
- | \`useInterval\` | setInterval wrapper with pause support |
2496
- | \`useTimeout\` | setTimeout wrapper with manual clear |
2497
- | \`useCountdown\` | Countdown timer with start/pause/reset |
2498
- | \`useStopwatch\` | Stopwatch with lap support |
2499
- | \`useIdleTimer\` | Detect user idle time |
2500
- | \`useUpdateEffect\` | useEffect that skips the initial render |
2501
- | \`useIsomorphicLayoutEffect\` | SSR-safe useLayoutEffect |
2502
- | \`useIsMounted\` | Check if component is currently mounted |
2503
- | \`useIsFirstRender\` | Check if this is the first render |
2504
-
2505
- ### Async & Network
2506
-
2507
- | Hook | Description |
2508
- |------|-------------|
2509
- | \`useFetch\` | Declarative data fetching with loading/error states (use for external URLs; use TanStack Query for API calls) |
2510
- | \`useAsync\` | Execute async functions with status tracking |
2511
- | \`useScript\` | Dynamically load external scripts |
2512
- | \`useWebSocket\` | WebSocket connection with auto-reconnect |
2513
- | \`useSSE\` | Server-Sent Events (EventSource) wrapper |
2514
- | \`usePolling\` | Poll an async function at a fixed interval |
2515
- | \`useAbortController\` | Manage AbortController lifecycle |
2516
- | \`useRetry\` | Retry async operations with exponential backoff |
2517
- | \`useSearch\` | Filter arrays with debounced search |
2518
-
2519
- ### Browser APIs
2520
-
2521
- | Hook | Description |
2522
- |------|-------------|
2523
- | \`useMediaQuery\` | Reactive CSS media query matching |
2524
- | \`useColorScheme\` | Detect system color scheme preference |
2525
- | \`useCopyToClipboard\` | Copy text to clipboard with status feedback |
2526
3279
  | \`useOnline\` | Track network connectivity |
2527
3280
  | \`useWindowSize\` | Track window dimensions |
2528
3281
  | \`usePageVisibility\` | Detect page visibility state |
2529
- | \`usePageLeave\` | Detect when the user leaves the page |
2530
- | \`useFavicon\` | Dynamically change the favicon |
2531
- | \`useReducedMotion\` | Respect prefers-reduced-motion |
2532
- | \`useBreakpoint\` | Responsive breakpoint detection |
2533
- | \`useIsClient\` | SSR-safe client-side detection |
2534
-
2535
- ### Layout & UI
2536
-
2537
- | Hook | Description |
2538
- |------|-------------|
2539
- | \`useStickyHeader\` | Detect when header should be sticky |
2540
- | \`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 |
2541
3286
  | \`useInfiniteScroll\` | Infinite scroll with threshold detection |
2542
- | \`useCollapse\` | Collapse/expand animation with prop getters |
2543
- | \`useSteps\` | Multi-step flow navigation |
2544
3287
 
2545
3288
  ### Common Patterns
2546
3289
 
2547
- **Modal with click-outside dismiss:**
3290
+ **Modal with Chakra disclosure:**
2548
3291
  \`\`\`tsx
2549
- import { useDisclosure, useClickOutside } from '@blacksmith-ui/hooks'
3292
+ import { useDisclosure } from '@chakra-ui/react'
3293
+ import { Modal, ModalOverlay, ModalContent, ModalHeader, ModalBody, ModalCloseButton, Button } from '@chakra-ui/react'
2550
3294
 
2551
3295
  function MyComponent() {
2552
- const [opened, { open, close }] = useDisclosure(false)
2553
- const ref = useClickOutside<HTMLDivElement>(close)
3296
+ const { isOpen, onOpen, onClose } = useDisclosure()
2554
3297
 
2555
3298
  return (
2556
3299
  <>
2557
- <Button onClick={open}>Open</Button>
2558
- {opened && <div ref={ref}>Modal content</div>}
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>
2559
3309
  </>
2560
3310
  )
2561
3311
  }
2562
3312
  \`\`\`
2563
3313
 
2564
- **Debounced search:**
3314
+ **Debounced search (custom hook):**
2565
3315
  \`\`\`tsx
2566
- import { useDebounce, useSearch } from '@blacksmith-ui/hooks'
3316
+ import { useDebounce } from '@/shared/hooks/use-debounce'
3317
+ import { Input } from '@chakra-ui/react'
2567
3318
 
2568
3319
  function SearchPage({ items }) {
2569
3320
  const [query, setQuery] = useState('')
2570
3321
  const debouncedQuery = useDebounce(query, 300)
2571
- const results = useSearch(items, debouncedQuery, ['title', 'description'])
3322
+ // Use debouncedQuery for API calls or filtering
2572
3323
 
2573
3324
  return (
2574
- <>
2575
- <Input value={query} onChange={(e) => setQuery(e.target.value)} />
2576
- {results.map(item => <div key={item.id}>{item.title}</div>)}
2577
- </>
3325
+ <Input
3326
+ value={query}
3327
+ onChange={(e) => setQuery(e.target.value)}
3328
+ placeholder="Search..."
3329
+ />
2578
3330
  )
2579
3331
  }
2580
3332
  \`\`\`
2581
3333
 
2582
- **Persisted state with undo:**
3334
+ **Responsive layout with Chakra hook:**
2583
3335
  \`\`\`tsx
2584
- import { useLocalStorage, useHistoryState } from '@blacksmith-ui/hooks'
3336
+ import { useBreakpointValue } from '@chakra-ui/react'
2585
3337
 
2586
- function Editor() {
2587
- const [saved, setSaved] = useLocalStorage('draft', '')
2588
- const [content, { set, undo, redo, canUndo, canRedo }] = useHistoryState(saved)
3338
+ function Layout({ children }) {
3339
+ const columns = useBreakpointValue({ base: 1, md: 2, lg: 3 })
3340
+ const isMobile = useBreakpointValue({ base: true, md: false })
2589
3341
 
2590
- const handleSave = () => setSaved(content)
3342
+ return isMobile ? <MobileLayout>{children}</MobileLayout> : <DesktopLayout>{children}</DesktopLayout>
2591
3343
  }
2592
3344
  \`\`\`
2593
3345
 
2594
- **Responsive layout:**
3346
+ **Color mode toggle:**
2595
3347
  \`\`\`tsx
2596
- import { useBreakpoint, useWindowSize } from '@blacksmith-ui/hooks'
3348
+ import { useColorMode, useColorModeValue, IconButton } from '@chakra-ui/react'
3349
+ import { Sun, Moon } from 'lucide-react'
2597
3350
 
2598
- function Layout({ children }) {
2599
- const breakpoint = useBreakpoint({ sm: 640, md: 768, lg: 1024 })
2600
- const isMobile = breakpoint === 'sm'
3351
+ function ColorModeToggle() {
3352
+ const { colorMode, toggleColorMode } = useColorMode()
3353
+ const icon = colorMode === 'light' ? <Moon size={16} /> : <Sun size={16} />
2601
3354
 
2602
- return isMobile ? <MobileLayout>{children}</MobileLayout> : <DesktopLayout>{children}</DesktopLayout>
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
2603
3366
  }
2604
3367
  \`\`\`
2605
3368
  `;
@@ -2607,6 +3370,7 @@ function Layout({ children }) {
2607
3370
  };
2608
3371
 
2609
3372
  // src/skills/blacksmith-cli.ts
3373
+ init_esm_shims();
2610
3374
  var blacksmithCliSkill = {
2611
3375
  id: "blacksmith-cli",
2612
3376
  name: "Blacksmith CLI",
@@ -2698,21 +3462,22 @@ blacksmith init my-app -b 9000 -f 3000 --ai
2698
3462
  | \`-b, --backend-port <port>\` | Django port (default: 8000) |
2699
3463
  | \`-f, --frontend-port <port>\` | Vite port (default: 5173) |
2700
3464
  | \`--ai\` | Generate CLAUDE.md with project skills |
2701
- | \`--no-blacksmith-ui-skill\` | Exclude blacksmith-ui skill from CLAUDE.md |
3465
+ | \`--no-chakra-ui-skill\` | Exclude Chakra UI skill from CLAUDE.md |
2702
3466
  `;
2703
3467
  }
2704
3468
  };
2705
3469
 
2706
3470
  // src/skills/ui-design.ts
3471
+ init_esm_shims();
2707
3472
  var uiDesignSkill = {
2708
3473
  id: "ui-design",
2709
3474
  name: "UI/UX Design System",
2710
- description: "Modern flat design principles, spacing, typography, color, layout patterns, and interaction guidelines aligned with the BlacksmithUI design language.",
3475
+ description: "Modern flat design principles, spacing, typography, color, layout patterns, and interaction guidelines aligned with the Chakra UI design language.",
2711
3476
  render(_ctx) {
2712
3477
  return `## UI/UX Design System \u2014 Modern Flat Design
2713
3478
 
2714
3479
  > **Design philosophy: Clean, flat, content-first.**
2715
- > BlacksmithUI follows the same design language as 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.
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.
2716
3481
 
2717
3482
  ### Core Principles
2718
3483
 
@@ -2724,7 +3489,7 @@ var uiDesignSkill = {
2724
3489
 
2725
3490
  ### Spacing System
2726
3491
 
2727
- Use Tailwind's spacing scale consistently. Do NOT use arbitrary values (\`p-[13px]\`) \u2014 stick to the system.
3492
+ Use Chakra UI's spacing scale consistently. Chakra uses a numeric spacing scale (1 = 4px, 2 = 8px, etc.).
2728
3493
 
2729
3494
  | Scale | Value | Use for |
2730
3495
  |-------|-------|---------|
@@ -2736,57 +3501,51 @@ Use Tailwind's spacing scale consistently. Do NOT use arbitrary values (\`p-[13p
2736
3501
  | \`16\`\u2013\`20\` | 64\u201380px | Page-level vertical padding (hero, landing sections) |
2737
3502
 
2738
3503
  **Rules:**
2739
- - Use \`gap\` (via \`Flex\`, \`Stack\`, \`Grid\`) for spacing between siblings \u2014 not margin on individual items
2740
- - Use \`Stack gap={...}\` for vertical rhythm within a section
2741
- - Page content padding: \`px-4 sm:px-6 lg:px-8\` (use \`Container\` which handles this)
2742
- - Card body padding: \`p-6\` standard, \`p-4\` for compact cards
2743
- - Never mix spacing approaches in the same context \u2014 pick gap OR margin, not both
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
2744
3509
 
2745
3510
  ### Typography
2746
3511
 
2747
- Use \`Typography\` and \`Text\` components from \`@blacksmith-ui/react\`. Do NOT style raw HTML headings.
3512
+ Use \`Heading\` and \`Text\` components from \`@chakra-ui/react\`. Do NOT style raw HTML headings.
2748
3513
 
2749
3514
  **Hierarchy:**
2750
3515
  | Level | Component | Use for |
2751
3516
  |-------|-----------|---------|
2752
- | Page title | \`<Typography variant="h1">\` | One per page. The main heading. |
2753
- | Section title | \`<Typography variant="h2">\` | Major sections within a page |
2754
- | Sub-section | \`<Typography variant="h3">\` | Groups within a section |
2755
- | Card title | \`<Typography variant="h4">\` or \`CardTitle\` | Card headings |
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 |
2756
3521
  | Body | \`<Text>\` | Paragraphs, descriptions |
2757
- | Caption/label | \`<Text size="sm" color="muted">\` | Secondary info, metadata, timestamps |
2758
- | Overline | \`<Text size="xs" weight="medium" className="uppercase tracking-wide">\` | Category labels, section overlines |
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 |
2759
3524
 
2760
3525
  **Rules:**
2761
3526
  - One \`h1\` per page \u2014 it's the page title
2762
- - Headings should never skip levels (h1 \u2192 h3 without h2)
2763
- - Body text: \`text-sm\` (14px) for dense UIs (tables, sidebars), \`text-base\` (16px) for reading content
2764
- - Line height: use Tailwind defaults (\`leading-relaxed\` for body copy, \`leading-tight\` for headings)
2765
- - Max reading width: \`max-w-prose\` (~65ch) for long-form text. Never let paragraphs stretch full-width
2766
- - Use \`text-muted-foreground\` for secondary text, never gray hardcoded values
2767
- - 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
2768
3532
 
2769
3533
  ### Color
2770
3534
 
2771
- Use design tokens (CSS variables), never hardcoded colors.
3535
+ Use Chakra UI's theme-aware color tokens, never hardcoded colors.
2772
3536
 
2773
- **Semantic palette:**
3537
+ **Semantic palette (via colorScheme and theme tokens):**
2774
3538
  | Token | Usage |
2775
3539
  |-------|-------|
2776
- | \`primary\` | Primary actions (buttons, links, active states) |
2777
- | \`secondary\` | Secondary actions, subtle backgrounds |
2778
- | \`destructive\` | Delete, error, danger states |
2779
- | \`muted\` | Backgrounds for subtle sections, disabled states |
2780
- | \`accent\` | Highlights, hover states, focus rings |
2781
- | \`foreground\` | Primary text |
2782
- | \`muted-foreground\` | Secondary/helper text |
2783
- | \`border\` | Borders, dividers |
2784
- | \`card\` | Card backgrounds |
2785
- | \`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 |
2786
3545
 
2787
3546
  **Rules:**
2788
- - NEVER use Tailwind color literals (\`text-gray-500\`, \`bg-blue-600\`, \`border-slate-200\`, \`bg-white\`, \`bg-black\`). Always use semantic tokens (\`text-muted-foreground\`, \`bg-primary\`, \`border-border\`, \`bg-background\`). This is non-negotiable \u2014 hardcoded colors break dark mode.
2789
- - Status colors: use \`Badge\` variants (\`default\`, \`secondary\`, \`destructive\`, \`outline\`) \u2014 don't hand-roll colored pills.
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.
2790
3549
  - Maximum 2\u20133 colors visible at any time (primary + foreground + muted). Colorful UIs feel noisy.
2791
3550
  - Every UI must render correctly in both light and dark mode. See the Dark Mode section below for the full rules.
2792
3551
 
@@ -2795,66 +3554,66 @@ Use design tokens (CSS variables), never hardcoded colors.
2795
3554
  **Page layout:**
2796
3555
  \`\`\`tsx
2797
3556
  <Box as="main">
2798
- <Container>
2799
- <Stack gap={8}>
3557
+ <Container maxW="container.xl">
3558
+ <VStack spacing={8} align="stretch">
2800
3559
  {/* Page header */}
2801
- <Flex align="center" justify="between">
2802
- <Stack gap={1}>
2803
- <Typography variant="h1">Page Title</Typography>
2804
- <Text color="muted">Brief description of this page</Text>
2805
- </Stack>
2806
- <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>
2807
3566
  </Flex>
2808
3567
 
2809
3568
  {/* Page content sections */}
2810
- <Stack gap={6}>
3569
+ <VStack spacing={6} align="stretch">
2811
3570
  {/* ... */}
2812
- </Stack>
2813
- </Stack>
3571
+ </VStack>
3572
+ </VStack>
2814
3573
  </Container>
2815
3574
  </Box>
2816
3575
  \`\`\`
2817
3576
 
2818
3577
  **Card-based content:**
2819
3578
  \`\`\`tsx
2820
- <Grid columns={{ base: 1, md: 2, lg: 3 }} gap={6}>
3579
+ <SimpleGrid columns={{ base: 1, md: 2, lg: 3 }} spacing={6}>
2821
3580
  {items.map((item) => (
2822
3581
  <Card key={item.id}>
2823
3582
  <CardHeader>
2824
- <CardTitle>{item.title}</CardTitle>
2825
- <CardDescription>{item.description}</CardDescription>
3583
+ <Heading size="md">{item.title}</Heading>
3584
+ <Text fontSize="sm" color="gray.500">{item.description}</Text>
2826
3585
  </CardHeader>
2827
- <CardContent>
3586
+ <CardBody>
2828
3587
  {/* Content */}
2829
- </CardContent>
3588
+ </CardBody>
2830
3589
  </Card>
2831
3590
  ))}
2832
- </Grid>
3591
+ </SimpleGrid>
2833
3592
  \`\`\`
2834
3593
 
2835
3594
  **Sidebar + main content:**
2836
3595
  \`\`\`tsx
2837
- <Flex className="min-h-screen">
2838
- <Sidebar>{/* Nav items */}</Sidebar>
2839
- <Box as="main" className="flex-1">
2840
- <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>
2841
3600
  </Box>
2842
3601
  </Flex>
2843
3602
  \`\`\`
2844
3603
 
2845
3604
  **Section with centered content (landing pages):**
2846
3605
  \`\`\`tsx
2847
- <Box as="section" className="py-16 sm:py-20">
2848
- <Container>
2849
- <Stack gap={4} align="center" className="text-center">
2850
- <Typography variant="h2">Section Title</Typography>
2851
- <Text color="muted" className="max-w-2xl">
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">
2852
3611
  A concise description that explains the value proposition.
2853
3612
  </Text>
2854
- </Stack>
2855
- <Grid columns={{ base: 1, md: 3 }} gap={8} className="mt-12">
3613
+ </VStack>
3614
+ <SimpleGrid columns={{ base: 1, md: 3 }} spacing={8} mt={12}>
2856
3615
  {/* Feature cards or content */}
2857
- </Grid>
3616
+ </SimpleGrid>
2858
3617
  </Container>
2859
3618
  </Box>
2860
3619
  \`\`\`
@@ -2863,15 +3622,15 @@ Use design tokens (CSS variables), never hardcoded colors.
2863
3622
 
2864
3623
  **Empty states:**
2865
3624
  \`\`\`tsx
2866
- // GOOD \u2014 uses EmptyState component
2867
- <EmptyState
2868
- icon={Inbox}
2869
- title="No messages yet"
2870
- description="Messages from your team will appear here."
2871
- action={<Button>Send a message</Button>}
2872
- />
2873
-
2874
- // 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
2875
3634
  <div className="flex flex-col items-center justify-center py-12 text-center">
2876
3635
  <Inbox className="h-12 w-12 text-gray-400 mb-4" />
2877
3636
  <h3 className="text-lg font-medium">No messages yet</h3>
@@ -2881,33 +3640,33 @@ Use design tokens (CSS variables), never hardcoded colors.
2881
3640
 
2882
3641
  **Stats/metrics:**
2883
3642
  \`\`\`tsx
2884
- // GOOD \u2014 uses StatCard
2885
- <Grid columns={{ base: 1, sm: 2, lg: 4 }} gap={4}>
2886
- <StatCard label="Total Users" value="2,847" trend="+12%" />
2887
- <StatCard label="Revenue" value="$48,290" trend="+8%" />
2888
- </Grid>
2889
-
2890
- // BAD \u2014 hand-rolled stat cards
2891
- <div className="grid grid-cols-4 gap-4">
2892
- <div className="bg-white rounded-lg p-6 shadow">
2893
- <p className="text-sm text-gray-500">Total Users</p>
2894
- <p className="text-2xl font-bold">2,847</p>
2895
- </div>
2896
- </div>
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>
2897
3656
  \`\`\`
2898
3657
 
2899
3658
  **Loading states:**
2900
3659
  \`\`\`tsx
2901
3660
  // GOOD \u2014 Skeleton matches the layout structure
2902
- <Stack gap={4}>
2903
- <Skeleton className="h-8 w-48" /> {/* Title */}
2904
- <Skeleton className="h-4 w-96" /> {/* Description */}
2905
- <Grid columns={3} gap={4}>
3661
+ <VStack spacing={4} align="stretch">
3662
+ <Skeleton h="32px" w="200px" />
3663
+ <SkeletonText noOfLines={2} />
3664
+ <SimpleGrid columns={3} spacing={4}>
2906
3665
  {Array.from({ length: 3 }).map((_, i) => (
2907
- <Skeleton key={i} className="h-32" />
3666
+ <Skeleton key={i} h="128px" />
2908
3667
  ))}
2909
- </Grid>
2910
- </Stack>
3668
+ </SimpleGrid>
3669
+ </VStack>
2911
3670
 
2912
3671
  // BAD \u2014 generic spinner with no layout hint
2913
3672
  <div className="flex justify-center py-12">
@@ -2919,87 +3678,52 @@ Use design tokens (CSS variables), never hardcoded colors.
2919
3678
 
2920
3679
  > **CRITICAL: Every screen, component, and custom style MUST look correct in both light and dark mode. No exceptions.**
2921
3680
 
2922
- BlacksmithUI uses the \`.dark\` class strategy on \`<html>\`. All semantic CSS variables automatically switch between light and dark values. Your job is to never break this.
3681
+ Chakra UI supports color mode via \`useColorMode()\` and \`useColorModeValue()\`. All built-in components automatically adapt.
2923
3682
 
2924
3683
  **Rules:**
2925
- - NEVER hardcode colors. \`text-gray-500\`, \`bg-white\`, \`bg-slate-900\`, \`border-gray-200\` \u2014 all of these break in one mode or the other. Use semantic tokens: \`text-muted-foreground\`, \`bg-background\`, \`bg-card\`, \`border-border\`.
2926
- - NEVER use \`bg-white\` or \`bg-black\`. Use \`bg-background\` (page), \`bg-card\` (elevated surfaces), \`bg-muted\` (subtle sections).
2927
- - NEVER use \`text-black\` or \`text-white\`. Use \`text-foreground\` (primary text), \`text-muted-foreground\` (secondary), \`text-primary-foreground\` (text on primary-colored backgrounds).
2928
- - NEVER use hardcoded shadows like \`shadow-[0_2px_8px_rgba(0,0,0,0.1)]\`. Use Tailwind shadow utilities (\`shadow-sm\`, \`shadow-md\`) which respect the theme.
2929
- - NEVER use opacity-based overlays with hardcoded colors (\`bg-black/50\`). Use \`bg-background/80\` or let overlay components (\`Dialog\`, \`Sheet\`) handle it.
2930
- - SVG fills and strokes: use \`currentColor\` or \`fill-foreground\` / \`stroke-border\` \u2014 never \`fill-black\` or \`stroke-gray-300\`.
2931
- - 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.
2932
-
2933
- **Safe color tokens (always use these):**
2934
- | Need | Light mode maps to | Dark mode maps to | Use |
2935
- |------|----|----|-----|
2936
- | Page background | white/light gray | near-black | \`bg-background\` |
2937
- | Card/surface | white | dark gray | \`bg-card\` |
2938
- | Subtle background | light gray | darker gray | \`bg-muted\` |
2939
- | Primary text | near-black | near-white | \`text-foreground\` |
2940
- | Secondary text | medium gray | lighter gray | \`text-muted-foreground\` |
2941
- | Borders | light gray | dark gray | \`border-border\` |
2942
- | Input borders | light gray | dark gray | \`border-input\` |
2943
- | Focus ring | brand color | brand color | \`ring-ring\` |
2944
- | Primary action | brand color | brand color | \`bg-primary text-primary-foreground\` |
2945
- | Destructive | red | red | \`bg-destructive text-destructive-foreground\` |
2946
-
2947
- **Testing checklist (mental model):**
2948
- Before considering any UI complete, verify these in your head:
2949
- 1. Does every text element use \`foreground\`, \`muted-foreground\`, or \`*-foreground\` tokens?
2950
- 2. Does every background use \`background\`, \`card\`, \`muted\`, or \`primary\`/\`secondary\`/\`accent\` tokens?
2951
- 3. Does every border use \`border\`, \`input\`, or \`ring\` tokens?
2952
- 4. Are there ANY hex values, rgb values, or Tailwind color names (gray, slate, blue, etc.) in the code? If yes, replace them.
2953
- 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.
2954
3688
 
2955
3689
  ### Interactions & Feedback
2956
3690
 
2957
- - **Hover states**: Subtle background change (\`hover:bg-muted\`) \u2014 not color shifts or scale transforms
2958
- - **Focus**: Use focus-visible ring (\`focus-visible:ring-2 ring-ring\`). BlacksmithUI components handle this automatically
2959
- - **Transitions**: \`transition-colors duration-150\` for color changes. No bounces, no springs, no dramatic animations
2960
- - **Click feedback**: Use \`active:scale-[0.98]\` only on buttons and interactive cards, never on text or static elements
2961
- - **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
2962
3694
  - **Success/error feedback**: Use \`useToast()\` for transient confirmations. Use \`Alert\` for persistent messages. Never use \`window.alert()\`
2963
3695
  - **Confirmation before destructive actions**: Always use \`AlertDialog\` for delete/remove actions. Never delete on single click
2964
3696
 
2965
3697
  ### Responsive Design
2966
3698
 
2967
- - **Mobile-first**: Write base styles for mobile, add \`sm:\`/\`md:\`/\`lg:\` for larger screens
2968
- - **Breakpoints**: \`sm\` (640px), \`md\` (768px), \`lg\` (1024px), \`xl\` (1280px)
2969
- - **Grid collapse**: \`Grid columns={{ base: 1, md: 2, lg: 3 }}\` \u2014 single column on mobile, expand on larger screens
2970
- - **Hide/show**: Use \`hidden md:block\` / \`md:hidden\` to toggle elements across breakpoints
2971
- - **Touch targets**: Minimum 44\xD744px for interactive elements on mobile. Use \`Button size="lg"\` and adequate padding
2972
- - **Stack on mobile, row on desktop**: Use \`Flex direction={{ base: 'column', md: 'row' }}\` or \`Stack\` that switches direction
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
2973
3705
  - **Container**: Always wrap page content in \`<Container>\` \u2014 it handles responsive horizontal padding
2974
3706
 
2975
3707
  ### Anti-Patterns \u2014 NEVER Do These
2976
3708
 
2977
3709
  | Anti-pattern | What to do instead |
2978
3710
  |---|---|
2979
- | Hardcoded colors (\`text-gray-500\`, \`bg-blue-600\`) | Use semantic tokens (\`text-muted-foreground\`, \`bg-primary\`) |
2980
- | Heavy box shadows (\`shadow-xl\`, \`shadow-2xl\`) | Use \`shadow-sm\` on cards, \`shadow-md\` on elevated overlays only |
2981
- | Rounded pill shapes (\`rounded-full\`) on cards/containers | Use \`rounded-lg\` or \`rounded-md\` (controlled by \`--radius\`) |
2982
- | Gradient backgrounds on surfaces | Use solid \`bg-card\` or \`bg-background\` |
2983
- | Decorative borders (\`border-l-4 border-blue-500\`) | Use \`Divider\` or \`border-border\` |
2984
- | Custom scrollbars with CSS hacks | Use \`ScrollArea\` |
2985
- | Animated entrances (fade-in, slide-up on mount) | Content should appear instantly. Only animate user-triggered changes |
2986
- | Centering with \`absolute inset-0 flex items-center\` | Use \`Flex align="center" justify="center"\` |
2987
- | Using \`<br />\` for spacing | Use \`Stack gap={...}\` or margin utilities |
2988
- | Multiple font sizes in close proximity | Keep nearby text within 1\u20132 size steps |
2989
- | Dense walls of text | Break into sections with headings, cards, or spacing |
2990
- | Colored backgrounds on every section | Use \`bg-background\` as default, \`bg-muted\` sparingly for contrast |
2991
- | Over-using badges/tags on everything | Badges are for status and categories, not decoration |
2992
- | Inline styles (\`style={{ ... }}\`) | Use Tailwind classes via \`className\` |
2993
- | \`bg-white\` / \`bg-black\` / \`bg-slate-*\` | Use \`bg-background\`, \`bg-card\`, \`bg-muted\` |
2994
- | \`text-black\` / \`text-white\` / \`text-gray-*\` | Use \`text-foreground\`, \`text-muted-foreground\` |
2995
- | \`border-gray-*\` / \`border-slate-*\` | Use \`border-border\`, \`border-input\` |
2996
- | Hex/rgb values in className or style | Use CSS variable tokens exclusively |
2997
- | 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()\` |
2998
3721
  `;
2999
3722
  }
3000
3723
  };
3001
3724
 
3002
3725
  // src/skills/programming-paradigms.ts
3726
+ init_esm_shims();
3003
3727
  var programmingParadigmsSkill = {
3004
3728
  id: "programming-paradigms",
3005
3729
  name: "Programming Paradigms",
@@ -3429,6 +4153,7 @@ class OrderService:
3429
4153
  };
3430
4154
 
3431
4155
  // src/skills/frontend-testing.ts
4156
+ init_esm_shims();
3432
4157
  var frontendTestingSkill = {
3433
4158
  id: "frontend-testing",
3434
4159
  name: "Frontend Testing Conventions",
@@ -3503,7 +4228,7 @@ router/
3503
4228
 
3504
4229
  > **RULE: Never import \`render\` from \`@testing-library/react\` directly. Always use \`renderWithProviders\` from \`@/__tests__/test-utils\`.**
3505
4230
 
3506
- \`renderWithProviders\` wraps components with all app providers (ThemeProvider, QueryClientProvider, MemoryRouter) so tests match the real app environment.
4231
+ \`renderWithProviders\` wraps components with all app providers (ChakraProvider, QueryClientProvider, MemoryRouter) so tests match the real app environment.
3507
4232
 
3508
4233
  \`\`\`tsx
3509
4234
  import { screen } from '@/__tests__/test-utils'
@@ -3591,15 +4316,17 @@ beforeEach(() => {
3591
4316
  })
3592
4317
  \`\`\`
3593
4318
 
3594
- **Mock external UI libraries (for auth page tests):**
4319
+ **Mock auth hook (for auth page tests):**
3595
4320
  \`\`\`tsx
3596
- vi.mock('@blacksmith-ui/auth', () => ({
3597
- LoginForm: ({ onSubmit, error, loading }: any) => (
3598
- <form onSubmit={(e: any) => { e.preventDefault(); onSubmit({ email: 'test@test.com', password: 'pass' }) }}>
3599
- {error && <div data-testid="error">{error.message}</div>}
3600
- <button type="submit">Sign In</button>
3601
- </form>
3602
- ),
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
+ }),
3603
4330
  }))
3604
4331
  \`\`\`
3605
4332
 
@@ -3667,6 +4394,7 @@ describe('CustomersPage', () => {
3667
4394
  };
3668
4395
 
3669
4396
  // src/skills/clean-code.ts
4397
+ init_esm_shims();
3670
4398
  var cleanCodeSkill = {
3671
4399
  id: "clean-code",
3672
4400
  name: "Clean Code Principles",
@@ -3701,7 +4429,7 @@ Write code that is easy to read, easy to change, and easy to delete. Treat clari
3701
4429
  - Props interfaces should be explicit and narrow \u2014 accept only what the component needs, not entire objects
3702
4430
  - Avoid prop drilling beyond 2 levels \u2014 use context or restructure the component tree
3703
4431
  - Destructure props in the function signature for clarity
3704
- - Use \`@blacksmith-ui/react\` layout components (\`Stack\`, \`Flex\`, \`Grid\`) \u2014 never raw \`<div>\` with flex/grid classes
4432
+ - Use \`@chakra-ui/react\` layout components (\`VStack\`, \`HStack\`, \`Flex\`, \`SimpleGrid\`) \u2014 never raw \`<div>\` with flex/grid classes
3705
4433
 
3706
4434
  ### File Organization
3707
4435
  - Keep files short. If a file exceeds 200 lines, it is likely doing too much \u2014 split it
@@ -3770,6 +4498,7 @@ Write code that is easy to read, easy to change, and easy to delete. Treat clari
3770
4498
  };
3771
4499
 
3772
4500
  // src/skills/ai-guidelines.ts
4501
+ init_esm_shims();
3773
4502
  var aiGuidelinesSkill = {
3774
4503
  id: "ai-guidelines",
3775
4504
  name: "AI Development Guidelines",
@@ -3788,7 +4517,7 @@ var aiGuidelinesSkill = {
3788
4517
  - Use existing patterns in the codebase as reference before inventing new ones
3789
4518
 
3790
4519
  ### Frontend Architecture (Mandatory)
3791
- - **Use \`@blacksmith-ui/react\` for ALL UI** \u2014 \`Stack\`, \`Flex\`, \`Grid\` for layout; \`Typography\`, \`Text\` for text; \`Card\`, \`Button\`, \`Badge\`, etc. for all elements. Never use raw HTML (\`<div>\`, \`<h1>\`, \`<p>\`, \`<button>\`) when a Blacksmith-UI component exists
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
3792
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
3793
4522
  - **Use the \`Path\` enum** \u2014 all route paths come from \`src/router/paths.ts\`. Never hardcode path strings like \`'/login'\` or \`'/dashboard'\`
3794
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
@@ -3806,7 +4535,7 @@ var aiGuidelinesSkill = {
3806
4535
  3. Frontend builds: \`cd frontend && npm run build\`
3807
4536
  4. API types are in sync: \`blacksmith sync\`
3808
4537
  5. No lint errors in modified files
3809
- 6. All UI uses \`@blacksmith-ui/react\` components \u2014 no raw \`<div>\` for layout, no raw \`<h1>\`-\`<h6>\` for text
4538
+ 6. All UI uses \`@chakra-ui/react\` components \u2014 no raw \`<div>\` for layout, no raw \`<h1>\`-\`<h6>\` for text
3810
4539
  7. Pages are modular \u2014 page file is a thin orchestrator, sections are in \`components/\`, logic in \`hooks/\`
3811
4540
  8. Logic is in hooks \u2014 no \`useApiQuery\`, \`useApiMutation\`, \`useEffect\`, or multi-\`useState\` in component bodies
3812
4541
  9. No hardcoded route paths \u2014 all paths use the \`Path\` enum from \`@/router/paths\`
@@ -3817,7 +4546,7 @@ var aiGuidelinesSkill = {
3817
4546
  };
3818
4547
 
3819
4548
  // src/commands/ai-setup.ts
3820
- async function setupAiDev({ projectDir, projectName, includeBlacksmithUiSkill }) {
4549
+ async function setupAiDev({ projectDir, projectName, includeChakraUiSkill }) {
3821
4550
  const aiSpinner = spinner("Setting up AI development environment...");
3822
4551
  try {
3823
4552
  const skills = [
@@ -3830,10 +4559,10 @@ async function setupAiDev({ projectDir, projectName, includeBlacksmithUiSkill })
3830
4559
  reactQuerySkill,
3831
4560
  pageStructureSkill
3832
4561
  ];
3833
- if (includeBlacksmithUiSkill) {
3834
- skills.push(blacksmithUiReactSkill);
3835
- skills.push(blacksmithUiFormsSkill);
3836
- skills.push(blacksmithUiAuthSkill);
4562
+ if (includeChakraUiSkill) {
4563
+ skills.push(chakraUiReactSkill);
4564
+ skills.push(chakraUiFormsSkill);
4565
+ skills.push(chakraUiAuthSkill);
3837
4566
  skills.push(blacksmithHooksSkill);
3838
4567
  skills.push(uiDesignSkill);
3839
4568
  }
@@ -3845,10 +4574,10 @@ async function setupAiDev({ projectDir, projectName, includeBlacksmithUiSkill })
3845
4574
  const ctx = { projectName };
3846
4575
  const inlineSkills = skills.filter((s) => !s.name);
3847
4576
  const fileSkills = skills.filter((s) => s.name);
3848
- const skillsDir = path3.join(projectDir, ".claude", "skills");
4577
+ const skillsDir = path4.join(projectDir, ".claude", "skills");
3849
4578
  if (fs3.existsSync(skillsDir)) {
3850
4579
  for (const entry of fs3.readdirSync(skillsDir)) {
3851
- const entryPath = path3.join(skillsDir, entry);
4580
+ const entryPath = path4.join(skillsDir, entry);
3852
4581
  const stat = fs3.statSync(entryPath);
3853
4582
  if (stat.isDirectory()) {
3854
4583
  fs3.rmSync(entryPath, { recursive: true });
@@ -3859,7 +4588,7 @@ async function setupAiDev({ projectDir, projectName, includeBlacksmithUiSkill })
3859
4588
  }
3860
4589
  fs3.mkdirSync(skillsDir, { recursive: true });
3861
4590
  for (const skill of fileSkills) {
3862
- const skillDir = path3.join(skillsDir, skill.id);
4591
+ const skillDir = path4.join(skillsDir, skill.id);
3863
4592
  fs3.mkdirSync(skillDir, { recursive: true });
3864
4593
  const frontmatter = `---
3865
4594
  name: ${skill.name}
@@ -3868,7 +4597,7 @@ description: ${skill.description}
3868
4597
 
3869
4598
  `;
3870
4599
  const content = skill.render(ctx).trim();
3871
- fs3.writeFileSync(path3.join(skillDir, "SKILL.md"), frontmatter + content + "\n", "utf-8");
4600
+ fs3.writeFileSync(path4.join(skillDir, "SKILL.md"), frontmatter + content + "\n", "utf-8");
3872
4601
  }
3873
4602
  const inlineContent = inlineSkills.map((s) => s.render(ctx)).join("\n");
3874
4603
  const skillsList = fileSkills.map((s) => `- \`.claude/skills/${s.id}/SKILL.md\` \u2014 ${s.name}`).join("\n");
@@ -3884,7 +4613,7 @@ description: ${skill.description}
3884
4613
  "These files are auto-loaded by Claude Code. Run `blacksmith setup:ai` to regenerate.",
3885
4614
  ""
3886
4615
  ].join("\n");
3887
- fs3.writeFileSync(path3.join(projectDir, "CLAUDE.md"), claudeMd, "utf-8");
4616
+ fs3.writeFileSync(path4.join(projectDir, "CLAUDE.md"), claudeMd, "utf-8");
3888
4617
  const skillNames = skills.filter((s) => s.id !== "project-overview" && s.id !== "ai-guidelines").map((s) => s.id).join(" + ");
3889
4618
  aiSpinner.succeed(`AI dev environment ready (${skillNames} skills)`);
3890
4619
  } catch (error) {
@@ -3933,9 +4662,9 @@ async function init(name, options) {
3933
4662
  "Theme": themePreset,
3934
4663
  "AI support": options.ai ? "Yes" : "No"
3935
4664
  });
3936
- const projectDir = path4.resolve(process.cwd(), name);
3937
- const backendDir = path4.join(projectDir, "backend");
3938
- const frontendDir = path4.join(projectDir, "frontend");
4665
+ const projectDir = path5.resolve(process.cwd(), name);
4666
+ const backendDir = path5.join(projectDir, "backend");
4667
+ const frontendDir = path5.join(projectDir, "frontend");
3939
4668
  const templatesDir = getTemplatesDir();
3940
4669
  if (fs4.existsSync(projectDir)) {
3941
4670
  log.error(`Directory "${name}" already exists.`);
@@ -3962,7 +4691,7 @@ async function init(name, options) {
3962
4691
  };
3963
4692
  fs4.mkdirSync(projectDir, { recursive: true });
3964
4693
  fs4.writeFileSync(
3965
- path4.join(projectDir, "blacksmith.config.json"),
4694
+ path5.join(projectDir, "blacksmith.config.json"),
3966
4695
  JSON.stringify(
3967
4696
  {
3968
4697
  name,
@@ -3977,13 +4706,13 @@ async function init(name, options) {
3977
4706
  const backendSpinner = spinner("Generating Django backend...");
3978
4707
  try {
3979
4708
  renderDirectory(
3980
- path4.join(templatesDir, "backend"),
4709
+ path5.join(templatesDir, "backend"),
3981
4710
  backendDir,
3982
4711
  context
3983
4712
  );
3984
4713
  fs4.copyFileSync(
3985
- path4.join(backendDir, ".env.example"),
3986
- path4.join(backendDir, ".env")
4714
+ path5.join(backendDir, ".env.example"),
4715
+ path5.join(backendDir, ".env")
3987
4716
  );
3988
4717
  backendSpinner.succeed("Django backend generated");
3989
4718
  } catch (error) {
@@ -4026,7 +4755,7 @@ async function init(name, options) {
4026
4755
  const frontendSpinner = spinner("Generating React frontend...");
4027
4756
  try {
4028
4757
  renderDirectory(
4029
- path4.join(templatesDir, "frontend"),
4758
+ path5.join(templatesDir, "frontend"),
4030
4759
  frontendDir,
4031
4760
  context
4032
4761
  );
@@ -4059,7 +4788,7 @@ async function init(name, options) {
4059
4788
  djangoProcess.unref();
4060
4789
  await new Promise((resolve) => setTimeout(resolve, 4e3));
4061
4790
  try {
4062
- await exec(process.execPath, [path4.join(frontendDir, "node_modules", ".bin", "openapi-ts")], { cwd: frontendDir, silent: true });
4791
+ await exec(process.execPath, [path5.join(frontendDir, "node_modules", ".bin", "openapi-ts")], { cwd: frontendDir, silent: true });
4063
4792
  syncSpinner.succeed("OpenAPI types synced");
4064
4793
  } catch {
4065
4794
  syncSpinner.warn('OpenAPI sync skipped (run "blacksmith sync" after starting Django)');
@@ -4073,8 +4802,8 @@ async function init(name, options) {
4073
4802
  } catch {
4074
4803
  syncSpinner.warn('OpenAPI sync skipped (run "blacksmith sync" after starting Django)');
4075
4804
  }
4076
- const generatedDir = path4.join(frontendDir, "src", "api", "generated");
4077
- const stubFile = path4.join(generatedDir, "client.gen.ts");
4805
+ const generatedDir = path5.join(frontendDir, "src", "api", "generated");
4806
+ const stubFile = path5.join(generatedDir, "client.gen.ts");
4078
4807
  if (!fs4.existsSync(stubFile)) {
4079
4808
  if (!fs4.existsSync(generatedDir)) {
4080
4809
  fs4.mkdirSync(generatedDir, { recursive: true });
@@ -4104,16 +4833,17 @@ async function init(name, options) {
4104
4833
  await setupAiDev({
4105
4834
  projectDir,
4106
4835
  projectName: name,
4107
- includeBlacksmithUiSkill: options.blacksmithUiSkill !== false
4836
+ includeChakraUiSkill: options.chakraUiSkill !== false
4108
4837
  });
4109
4838
  }
4110
4839
  printNextSteps(name, backendPort, frontendPort);
4111
4840
  }
4112
4841
 
4113
4842
  // src/commands/dev.ts
4843
+ init_esm_shims();
4114
4844
  import net from "net";
4115
4845
  import concurrently from "concurrently";
4116
- import path5 from "path";
4846
+ import path6 from "path";
4117
4847
  function isPortAvailable(port) {
4118
4848
  return new Promise((resolve) => {
4119
4849
  const server = net.createServer();
@@ -4168,7 +4898,7 @@ async function dev() {
4168
4898
  log.step(`Swagger \u2192 http://localhost:${backendPort}/api/docs/`);
4169
4899
  log.step("OpenAPI sync \u2192 watching backend .py files");
4170
4900
  log.blank();
4171
- const syncCmd = `${process.execPath} ${path5.join(frontendDir, "node_modules", ".bin", "openapi-ts")}`;
4901
+ const syncCmd = `${process.execPath} ${path6.join(frontendDir, "node_modules", ".bin", "openapi-ts")}`;
4172
4902
  const watcherCode = [
4173
4903
  `const{watch}=require("fs"),{exec}=require("child_process");`,
4174
4904
  `let t=null,s=false;`,
@@ -4228,7 +4958,8 @@ async function dev() {
4228
4958
  }
4229
4959
 
4230
4960
  // src/commands/sync.ts
4231
- import path6 from "path";
4961
+ init_esm_shims();
4962
+ import path7 from "path";
4232
4963
  import fs5 from "fs";
4233
4964
  async function sync() {
4234
4965
  let root;
@@ -4242,9 +4973,9 @@ async function sync() {
4242
4973
  const frontendDir = getFrontendDir(root);
4243
4974
  const s = spinner("Syncing OpenAPI schema to frontend...");
4244
4975
  try {
4245
- const schemaPath = path6.join(frontendDir, "_schema.yml");
4976
+ const schemaPath = path7.join(frontendDir, "_schema.yml");
4246
4977
  await execPython(["manage.py", "spectacular", "--file", schemaPath], backendDir, true);
4247
- const configPath = path6.join(frontendDir, "openapi-ts.config.ts");
4978
+ const configPath = path7.join(frontendDir, "openapi-ts.config.ts");
4248
4979
  const configBackup = fs5.readFileSync(configPath, "utf-8");
4249
4980
  const configWithFile = configBackup.replace(
4250
4981
  /path:\s*['"]http[^'"]+['"]/,
@@ -4252,7 +4983,7 @@ async function sync() {
4252
4983
  );
4253
4984
  fs5.writeFileSync(configPath, configWithFile, "utf-8");
4254
4985
  try {
4255
- await exec(process.execPath, [path6.join(frontendDir, "node_modules", ".bin", "openapi-ts")], {
4986
+ await exec(process.execPath, [path7.join(frontendDir, "node_modules", ".bin", "openapi-ts")], {
4256
4987
  cwd: frontendDir,
4257
4988
  silent: true
4258
4989
  });
@@ -4276,10 +5007,12 @@ async function sync() {
4276
5007
  }
4277
5008
 
4278
5009
  // src/commands/make-resource.ts
4279
- import path7 from "path";
5010
+ init_esm_shims();
5011
+ import path8 from "path";
4280
5012
  import fs6 from "fs";
4281
5013
 
4282
5014
  // src/utils/names.ts
5015
+ init_esm_shims();
4283
5016
  import { pascalCase, snakeCase, kebabCase, camelCase } from "change-case";
4284
5017
  import pluralize from "pluralize";
4285
5018
  function generateNames(input) {
@@ -4312,12 +5045,12 @@ async function makeResource(name) {
4312
5045
  const backendDir = getBackendDir(root);
4313
5046
  const frontendDir = getFrontendDir(root);
4314
5047
  const templatesDir = getTemplatesDir();
4315
- const backendAppDir = path7.join(backendDir, "apps", names.snakes);
5048
+ const backendAppDir = path8.join(backendDir, "apps", names.snakes);
4316
5049
  if (fs6.existsSync(backendAppDir)) {
4317
5050
  log.error(`Backend app "${names.snakes}" already exists.`);
4318
5051
  process.exit(1);
4319
5052
  }
4320
- const frontendPageDir = path7.join(frontendDir, "src", "pages", names.kebabs);
5053
+ const frontendPageDir = path8.join(frontendDir, "src", "pages", names.kebabs);
4321
5054
  if (fs6.existsSync(frontendPageDir)) {
4322
5055
  log.error(`Frontend page "${names.kebabs}" already exists.`);
4323
5056
  process.exit(1);
@@ -4326,7 +5059,7 @@ async function makeResource(name) {
4326
5059
  const backendSpinner = spinner(`Creating backend app: apps/${names.snakes}/`);
4327
5060
  try {
4328
5061
  renderDirectory(
4329
- path7.join(templatesDir, "resource", "backend"),
5062
+ path8.join(templatesDir, "resource", "backend"),
4330
5063
  backendAppDir,
4331
5064
  context
4332
5065
  );
@@ -4338,7 +5071,7 @@ async function makeResource(name) {
4338
5071
  }
4339
5072
  const registerSpinner = spinner("Registering app in Django settings...");
4340
5073
  try {
4341
- const settingsPath = path7.join(backendDir, "config", "settings", "base.py");
5074
+ const settingsPath = path8.join(backendDir, "config", "settings", "base.py");
4342
5075
  appendAfterMarker(
4343
5076
  settingsPath,
4344
5077
  "# blacksmith:apps",
@@ -4352,7 +5085,7 @@ async function makeResource(name) {
4352
5085
  }
4353
5086
  const urlSpinner = spinner("Registering API URLs...");
4354
5087
  try {
4355
- const urlsPath = path7.join(backendDir, "config", "urls.py");
5088
+ const urlsPath = path8.join(backendDir, "config", "urls.py");
4356
5089
  insertBeforeMarker(
4357
5090
  urlsPath,
4358
5091
  "# blacksmith:urls",
@@ -4376,9 +5109,9 @@ async function makeResource(name) {
4376
5109
  }
4377
5110
  const syncSpinner = spinner("Syncing OpenAPI schema...");
4378
5111
  try {
4379
- const schemaPath = path7.join(frontendDir, "_schema.yml");
5112
+ const schemaPath = path8.join(frontendDir, "_schema.yml");
4380
5113
  await execPython(["manage.py", "spectacular", "--file", schemaPath], backendDir, true);
4381
- const configPath = path7.join(frontendDir, "openapi-ts.config.ts");
5114
+ const configPath = path8.join(frontendDir, "openapi-ts.config.ts");
4382
5115
  const configBackup = fs6.readFileSync(configPath, "utf-8");
4383
5116
  const configWithFile = configBackup.replace(
4384
5117
  /path:\s*['"]http[^'"]+['"]/,
@@ -4386,7 +5119,7 @@ async function makeResource(name) {
4386
5119
  );
4387
5120
  fs6.writeFileSync(configPath, configWithFile, "utf-8");
4388
5121
  try {
4389
- await exec(process.execPath, [path7.join(frontendDir, "node_modules", ".bin", "openapi-ts")], {
5122
+ await exec(process.execPath, [path8.join(frontendDir, "node_modules", ".bin", "openapi-ts")], {
4390
5123
  cwd: frontendDir,
4391
5124
  silent: true
4392
5125
  });
@@ -4398,11 +5131,11 @@ async function makeResource(name) {
4398
5131
  } catch {
4399
5132
  syncSpinner.warn('Could not sync OpenAPI. Run "blacksmith sync" manually.');
4400
5133
  }
4401
- const apiHooksDir = path7.join(frontendDir, "src", "api", "hooks", names.kebabs);
5134
+ const apiHooksDir = path8.join(frontendDir, "src", "api", "hooks", names.kebabs);
4402
5135
  const apiHooksSpinner = spinner(`Creating API hooks: api/hooks/${names.kebabs}/`);
4403
5136
  try {
4404
5137
  renderDirectory(
4405
- path7.join(templatesDir, "resource", "api-hooks"),
5138
+ path8.join(templatesDir, "resource", "api-hooks"),
4406
5139
  apiHooksDir,
4407
5140
  context
4408
5141
  );
@@ -4415,7 +5148,7 @@ async function makeResource(name) {
4415
5148
  const frontendSpinner = spinner(`Creating frontend page: pages/${names.kebabs}/`);
4416
5149
  try {
4417
5150
  renderDirectory(
4418
- path7.join(templatesDir, "resource", "pages"),
5151
+ path8.join(templatesDir, "resource", "pages"),
4419
5152
  frontendPageDir,
4420
5153
  context
4421
5154
  );
@@ -4427,7 +5160,7 @@ async function makeResource(name) {
4427
5160
  }
4428
5161
  const pathSpinner = spinner("Registering route path...");
4429
5162
  try {
4430
- const pathsFile = path7.join(frontendDir, "src", "router", "paths.ts");
5163
+ const pathsFile = path8.join(frontendDir, "src", "router", "paths.ts");
4431
5164
  insertBeforeMarker(
4432
5165
  pathsFile,
4433
5166
  "// blacksmith:path",
@@ -4439,7 +5172,7 @@ async function makeResource(name) {
4439
5172
  }
4440
5173
  const routeSpinner = spinner("Registering frontend routes...");
4441
5174
  try {
4442
- const routesPath = path7.join(frontendDir, "src", "router", "routes.tsx");
5175
+ const routesPath = path8.join(frontendDir, "src", "router", "routes.tsx");
4443
5176
  insertBeforeMarker(
4444
5177
  routesPath,
4445
5178
  "// blacksmith:import",
@@ -4460,6 +5193,7 @@ async function makeResource(name) {
4460
5193
  }
4461
5194
 
4462
5195
  // src/commands/build.ts
5196
+ init_esm_shims();
4463
5197
  async function build() {
4464
5198
  let root;
4465
5199
  try {
@@ -4501,8 +5235,9 @@ async function build() {
4501
5235
  }
4502
5236
 
4503
5237
  // src/commands/eject.ts
5238
+ init_esm_shims();
4504
5239
  import fs7 from "fs";
4505
- import path8 from "path";
5240
+ import path9 from "path";
4506
5241
  async function eject() {
4507
5242
  let root;
4508
5243
  try {
@@ -4511,7 +5246,7 @@ async function eject() {
4511
5246
  log.error("Not inside a Blacksmith project.");
4512
5247
  process.exit(1);
4513
5248
  }
4514
- const configPath = path8.join(root, "blacksmith.config.json");
5249
+ const configPath = path9.join(root, "blacksmith.config.json");
4515
5250
  if (fs7.existsSync(configPath)) {
4516
5251
  fs7.unlinkSync(configPath);
4517
5252
  }
@@ -4529,6 +5264,7 @@ async function eject() {
4529
5264
  }
4530
5265
 
4531
5266
  // src/commands/skills.ts
5267
+ init_esm_shims();
4532
5268
  import fs8 from "fs";
4533
5269
  var allSkills = [
4534
5270
  projectOverviewSkill,
@@ -4536,9 +5272,9 @@ var allSkills = [
4536
5272
  djangoRestAdvancedSkill,
4537
5273
  apiDocumentationSkill,
4538
5274
  reactSkill,
4539
- blacksmithUiReactSkill,
4540
- blacksmithUiFormsSkill,
4541
- blacksmithUiAuthSkill,
5275
+ chakraUiReactSkill,
5276
+ chakraUiFormsSkill,
5277
+ chakraUiAuthSkill,
4542
5278
  blacksmithHooksSkill,
4543
5279
  blacksmithCliSkill,
4544
5280
  frontendTestingSkill,
@@ -4557,7 +5293,7 @@ async function setupSkills(options) {
4557
5293
  await setupAiDev({
4558
5294
  projectDir: root,
4559
5295
  projectName: config.name,
4560
- includeBlacksmithUiSkill: options.blacksmithUiSkill !== false
5296
+ includeChakraUiSkill: options.chakraUiSkill !== false
4561
5297
  });
4562
5298
  log.blank();
4563
5299
  log.success("AI skills generated:");
@@ -4595,7 +5331,324 @@ function listSkills() {
4595
5331
  }
4596
5332
  }
4597
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
+
4598
5650
  // src/commands/backend.ts
5651
+ init_esm_shims();
4599
5652
  async function backend(args) {
4600
5653
  let root;
4601
5654
  try {
@@ -4619,6 +5672,7 @@ async function backend(args) {
4619
5672
  }
4620
5673
 
4621
5674
  // src/commands/frontend.ts
5675
+ init_esm_shims();
4622
5676
  async function frontend(args) {
4623
5677
  let root;
4624
5678
  try {
@@ -4646,13 +5700,15 @@ var program = new Command();
4646
5700
  program.name("blacksmith").description("Fullstack Django + React framework").version("0.1.0").hook("preAction", () => {
4647
5701
  banner();
4648
5702
  });
4649
- program.command("init").argument("[name]", "Project name").option("--ai", "Set up AI development skills and documentation (CLAUDE.md)").option("--no-blacksmith-ui-skill", "Disable blacksmith-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);
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);
4650
5704
  program.command("dev").description("Start development servers (Django + Vite + OpenAPI sync)").action(dev);
4651
5705
  program.command("sync").description("Sync OpenAPI schema to frontend types, schemas, and hooks").action(sync);
4652
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);
4653
5707
  program.command("build").description("Build both frontend and backend for production").action(build);
4654
5708
  program.command("eject").description("Remove Blacksmith, keep a clean Django + React project").action(eject);
4655
- program.command("setup:ai").description("Generate CLAUDE.md with AI development skills for the project").option("--no-blacksmith-ui-skill", "Exclude blacksmith-ui skill").action(setupSkills);
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);
4656
5712
  program.command("skills").description("List all available AI development skills").action(listSkills);
4657
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);
4658
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);