create-lumen 1.1.2 → 1.1.4

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 (84) hide show
  1. package/README.md +5 -1
  2. package/package.json +8 -2
  3. package/src/configure.js +23 -13
  4. package/src/css.js +15 -3
  5. package/src/dependencies.js +8 -3
  6. package/src/format.js +28 -0
  7. package/src/injector.js +15 -4
  8. package/src/main.js +17 -1
  9. package/src/readme.js +1 -0
  10. package/templates/architectures/component-based/main.tsx +1 -1
  11. package/templates/architectures/component-based/src/App.jsx +0 -1
  12. package/templates/architectures/component-based/src/App.tsx +0 -1
  13. package/templates/architectures/component-based/src/components/common/Button.jsx +0 -2
  14. package/templates/architectures/component-based/src/components/common/Button.tsx +0 -2
  15. package/templates/architectures/component-based/src/config/constants.js +0 -1
  16. package/templates/architectures/component-based/src/config/constants.ts +0 -1
  17. package/templates/architectures/component-based/src/context/AppContext.jsx +3 -4
  18. package/templates/architectures/component-based/src/context/AppContext.tsx +11 -6
  19. package/templates/architectures/component-based/src/context/app-context.js +5 -0
  20. package/templates/architectures/component-based/src/context/app-context.ts +10 -0
  21. package/templates/architectures/component-based/src/hooks/useLocalStorage.ts +5 -5
  22. package/templates/architectures/component-based/src/layouts/MainLayout.jsx +0 -2
  23. package/templates/architectures/component-based/src/layouts/MainLayout.tsx +0 -2
  24. package/templates/architectures/component-based/src/pages/Home/Home.jsx +0 -1
  25. package/templates/architectures/component-based/src/pages/Home/Home.tsx +0 -1
  26. package/templates/architectures/component-based/src/utils/cn.ts +3 -1
  27. package/templates/architectures/feature-based/main.tsx +1 -1
  28. package/templates/architectures/feature-based/src/app/App.jsx +0 -1
  29. package/templates/architectures/feature-based/src/app/App.tsx +0 -1
  30. package/templates/architectures/feature-based/src/app/providers/ThemeProvider.jsx +0 -2
  31. package/templates/architectures/feature-based/src/app/providers/ThemeProvider.tsx +2 -2
  32. package/templates/architectures/feature-based/src/components/ui/Button.jsx +0 -2
  33. package/templates/architectures/feature-based/src/components/ui/Button.tsx +0 -2
  34. package/templates/architectures/feature-based/src/config/constants.js +0 -1
  35. package/templates/architectures/feature-based/src/config/constants.ts +0 -1
  36. package/templates/architectures/feature-based/src/features/home/pages/HomePage.jsx +0 -1
  37. package/templates/architectures/feature-based/src/features/home/pages/HomePage.tsx +0 -1
  38. package/templates/architectures/feature-based/src/hooks/useLocalStorage.ts +5 -5
  39. package/templates/architectures/feature-based/src/utils/cn.ts +3 -4
  40. package/templates/conditional/formatter/oxfmt/.oxfmtrc.json +1 -1
  41. package/templates/conditional/formatter/prettier/.prettierrc +2 -2
  42. package/templates/conditional/icons/huge/component-based/src/pages/Home/Home.jsx +5 -5
  43. package/templates/conditional/icons/huge/component-based/src/pages/Home/Home.tsx +5 -5
  44. package/templates/conditional/icons/huge/feature-based/src/features/home/pages/HomePage.jsx +5 -5
  45. package/templates/conditional/icons/huge/feature-based/src/features/home/pages/HomePage.tsx +5 -5
  46. package/templates/conditional/icons/lucide/component-based/src/pages/Home/Home.jsx +2 -3
  47. package/templates/conditional/icons/lucide/component-based/src/pages/Home/Home.tsx +2 -3
  48. package/templates/conditional/icons/lucide/feature-based/src/features/home/pages/HomePage.jsx +2 -3
  49. package/templates/conditional/icons/lucide/feature-based/src/features/home/pages/HomePage.tsx +2 -3
  50. package/templates/conditional/linting/eslint/eslint.config.js +24 -2
  51. package/templates/conditional/linting/eslint/eslint.config.ts +25 -2
  52. package/templates/conditional/router/component-based/src/App.jsx +0 -1
  53. package/templates/conditional/router/component-based/src/App.tsx +0 -1
  54. package/templates/conditional/router/component-based/src/routes/index.jsx +0 -1
  55. package/templates/conditional/router/component-based/src/routes/index.tsx +0 -1
  56. package/templates/conditional/router/feature-based/src/app/App.jsx +0 -1
  57. package/templates/conditional/router/feature-based/src/app/App.tsx +0 -1
  58. package/templates/conditional/state/redux/component-based/src/context/StoreProvider.jsx +0 -1
  59. package/templates/conditional/state/redux/component-based/src/context/StoreProvider.tsx +2 -2
  60. package/templates/conditional/state/redux/component-based/src/pages/Home/counterSlice.ts +9 -3
  61. package/templates/conditional/state/redux/component-based/src/store/hooks.ts +4 -2
  62. package/templates/conditional/state/redux/component-based/src/store/index.ts +2 -2
  63. package/templates/conditional/state/redux/feature-based/src/app/providers/StoreProvider.jsx +0 -1
  64. package/templates/conditional/state/redux/feature-based/src/app/providers/StoreProvider.tsx +2 -2
  65. package/templates/conditional/state/redux/feature-based/src/features/home/store/counterSlice.ts +9 -3
  66. package/templates/conditional/state/redux/feature-based/src/stores/hooks.ts +4 -2
  67. package/templates/conditional/state/redux/feature-based/src/stores/index.ts +2 -2
  68. package/templates/conditional/state/zustand/component-based/src/store/useCounterStore.ts +8 -1
  69. package/templates/conditional/state/zustand/feature-based/src/stores/useCounterStore.ts +8 -1
  70. package/templates/conditional/testing/jest/babel.config.cjs +7 -0
  71. package/templates/conditional/testing/jest/component-based/src/__tests__/App.test.jsx +10 -0
  72. package/templates/conditional/testing/jest/component-based/src/__tests__/App.test.tsx +11 -0
  73. package/templates/conditional/testing/jest/{src → feature-based/src}/__tests__/App.test.jsx +2 -1
  74. package/templates/conditional/testing/jest/feature-based/src/__tests__/App.test.tsx +11 -0
  75. package/templates/conditional/testing/jest/jest.config.js +2 -3
  76. package/templates/conditional/testing/jest/jest.config.ts +6 -4
  77. package/templates/conditional/testing/jest/jest.setup.js +6 -0
  78. package/templates/conditional/testing/jest/jest.setup.ts +6 -0
  79. package/templates/conditional/testing/vitest/component-based/src/__tests__/App.test.jsx +10 -0
  80. package/templates/conditional/testing/{jest → vitest/component-based}/src/__tests__/App.test.tsx +2 -1
  81. package/templates/architectures/component-based/src/routes/index.jsx +0 -11
  82. package/templates/architectures/component-based/src/routes/index.tsx +0 -11
  83. /package/templates/conditional/testing/vitest/{src → feature-based/src}/__tests__/App.test.jsx +0 -0
  84. /package/templates/conditional/testing/vitest/{src → feature-based/src}/__tests__/App.test.tsx +0 -0
package/README.md CHANGED
@@ -17,9 +17,13 @@ npm run dev
17
17
  Non-interactive Quick Setup (TypeScript + Tailwind + Feature-based + Router + ESLint + Prettier + Vitest), no prompts:
18
18
 
19
19
  ```bash
20
- npm create lumen my-app -y
20
+ npm create lumen my-app -- -y
21
+ # or
22
+ npm create lumen -- -y my-app
21
23
  ```
22
24
 
25
+ The CLI ignores flag arguments when resolving the project name, so `-y` can appear before or after the app name without being mistaken for the folder name.
26
+
23
27
  Run the scaffolder locally (development / testing the generator):
24
28
 
25
29
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-lumen",
3
- "version": "1.1.2",
3
+ "version": "1.1.4",
4
4
  "description": "Scaffold a React + Vite project with architecture choice: feature-based or component-based.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -15,7 +15,13 @@
15
15
  "LICENSE"
16
16
  ],
17
17
  "scripts": {
18
- "test": "node --import ./register.js --test \"tests/unit/*.test.mjs\" \"tests/smoke/*.test.mjs\""
18
+ "test": "node --import ./register.js --test \"tests/unit/*.test.mjs\" \"tests/smoke/*.test.mjs\"",
19
+ "verify:vendor": "node --import ./register.js tests/verify/install-vendor.mjs",
20
+ "verify:offline": "node --import ./register.js tests/verify/verify-offline.mjs",
21
+ "verify:installed": "node --import ./register.js tests/verify/verify-installed.mjs",
22
+ "verify": "npm run verify:vendor && npm run verify:offline",
23
+ "verify:full": "npm run verify:vendor && npm run verify:offline && npm run verify:installed",
24
+ "verify:matrix": "FULL=1 npm run verify:offline"
19
25
  },
20
26
  "keywords": [
21
27
  "scaffold",
package/src/configure.js CHANGED
@@ -31,8 +31,13 @@ async function configureTsconfig(projectPath) {
31
31
  const config = JSON.parse(content);
32
32
 
33
33
  if (!config.compilerOptions) config.compilerOptions = {};
34
- config.compilerOptions.baseUrl = ".";
34
+ config.compilerOptions.strict = true;
35
35
  config.compilerOptions.paths = paths;
36
+ if (config.compilerOptions.types?.length && !config.compilerOptions.types.includes("vite/client")) {
37
+ config.compilerOptions.types.push("vite/client");
38
+ } else if (!config.compilerOptions.types) {
39
+ config.compilerOptions.types = ["vite/client"];
40
+ }
36
41
 
37
42
  await fsp.writeFile(
38
43
  tsconfigAppPath,
@@ -58,7 +63,7 @@ async function configureTsconfig(projectPath) {
58
63
  noUnusedLocals: true,
59
64
  noUnusedParameters: true,
60
65
  noFallthroughCasesInSwitch: true,
61
- baseUrl: ".",
66
+ types: ["vite/client"],
62
67
  paths,
63
68
  },
64
69
  include: ["src"],
@@ -70,11 +75,19 @@ async function configureTsconfig(projectPath) {
70
75
  );
71
76
  }
72
77
 
73
- // Create/update root tsconfig.json with references
78
+ // Create/update root tsconfig.json with references.
79
+ // Preserve the ./tsconfig.node.json reference (Vite config) when present.
74
80
  const rootTsconfigPath = path.join(projectPath, "tsconfig.json");
81
+ const nodeConfigPath = path.join(projectPath, "tsconfig.node.json");
82
+ const references = [{ path: "./tsconfig.app.json" }];
83
+ try {
84
+ await fsp.access(nodeConfigPath);
85
+ references.push({ path: "./tsconfig.node.json" });
86
+ } catch {}
87
+
75
88
  const rootTsconfig = {
76
89
  files: [],
77
- references: [{ path: "./tsconfig.app.json" }],
90
+ references,
78
91
  };
79
92
  await fsp.writeFile(
80
93
  rootTsconfigPath,
@@ -100,7 +113,7 @@ async function addPathAliasesToViteConfig(projectPath, ext) {
100
113
  try {
101
114
  let content = await readFileContent(viteConfigPath);
102
115
 
103
- if (content.includes("tsconfigPaths")) return;
116
+ if (content.includes("find: '@'")) return;
104
117
 
105
118
  const lines = content.split("\n");
106
119
 
@@ -112,8 +125,8 @@ async function addPathAliasesToViteConfig(projectPath, ext) {
112
125
 
113
126
  // Insert path/fileURLToPath imports after last import
114
127
  const newImports = [
115
- 'import path from "path";',
116
- 'import { fileURLToPath } from "url";',
128
+ "import path from 'path';",
129
+ "import { fileURLToPath } from 'url';",
117
130
  "const __dirname = path.dirname(fileURLToPath(import.meta.url));",
118
131
  ];
119
132
  lines.splice(lastImportIdx + 1, 0, ...newImports);
@@ -121,13 +134,10 @@ async function addPathAliasesToViteConfig(projectPath, ext) {
121
134
  let result = lines.join("\n");
122
135
 
123
136
  const aliasArray = getViteAliases()
124
- .map(
125
- (a) =>
126
- `{ find: "${a.find}", replacement: ${a.replacement} }`
127
- )
128
- .join(",\n ");
137
+ .map((a) => `{ find: '${a.find}', replacement: ${a.replacement} }`)
138
+ .join(",\n ");
129
139
 
130
- const resolveBlock = `resolve: {\n tsconfigPaths: true,\n alias: [\n ${aliasArray}\n ],\n }`;
140
+ const resolveBlock = `resolve: {\n alias: [\n ${aliasArray},\n ],\n }`;
131
141
 
132
142
  if (result.includes("resolve:")) {
133
143
  result = result.replace(/resolve:\s*\{/, resolveBlock);
package/src/css.js CHANGED
@@ -14,6 +14,18 @@ export async function setupCssFramework({
14
14
  process.chdir(projectPath);
15
15
  const cssDir = path.join(templatesDir, "css");
16
16
  const indexCssPath = path.join(projectPath, "src", "index.css");
17
+ // Feature-based main.* imports ./styles/globals.css; component-based main.*
18
+ // imports ./index.css. The chosen framework's styles must land in the file
19
+ // the architecture actually imports.
20
+ const globalsCssPath = path.join(projectPath, "src", "styles", "globals.css");
21
+
22
+ async function writeCss(content) {
23
+ await fsp.writeFile(indexCssPath, content, "utf8");
24
+ try {
25
+ await fsp.access(globalsCssPath);
26
+ await fsp.writeFile(globalsCssPath, content, "utf8");
27
+ } catch {}
28
+ }
17
29
 
18
30
  if (cssFramework === "tailwind") {
19
31
  const viteConfigFile = `vite.config.${language === "ts" ? "ts" : "js"}`;
@@ -28,7 +40,7 @@ export async function setupCssFramework({
28
40
  path.join(cssDir, "tailwind", "src", "index.css"),
29
41
  "utf8"
30
42
  );
31
- await fsp.writeFile(indexCssPath, indexCssContent, "utf8");
43
+ await writeCss(indexCssContent);
32
44
  } else if (cssFramework === "bootstrap") {
33
45
  const mainFile = path.join(projectPath, "src", `main.${ext}`);
34
46
  if (fs.existsSync(mainFile)) {
@@ -45,13 +57,13 @@ export async function setupCssFramework({
45
57
  path.join(cssDir, "bootstrap", "src", "index.css"),
46
58
  "utf8"
47
59
  );
48
- await fsp.writeFile(indexCssPath, indexCssContent, "utf8");
60
+ await writeCss(indexCssContent);
49
61
  } else {
50
62
  const indexCssContent = await fsp.readFile(
51
63
  path.join(cssDir, "none", "src", "index.css"),
52
64
  "utf8"
53
65
  );
54
- await fsp.writeFile(indexCssPath, indexCssContent, "utf8");
66
+ await writeCss(indexCssContent);
55
67
  }
56
68
 
57
69
  // Clean up Vite's default App.css
@@ -71,11 +71,14 @@ export async function installAllDeps(responses, pkg, cwd) {
71
71
  } else if (responses.testing === "jest") {
72
72
  devDeps.push(
73
73
  "jest",
74
- "ts-jest",
75
- "@types/jest",
74
+ "jest-environment-jsdom",
76
75
  "@testing-library/react",
77
76
  "@testing-library/jest-dom",
78
- "jsdom"
77
+ "jsdom",
78
+ "babel-jest",
79
+ "@babel/preset-env",
80
+ "@babel/preset-react",
81
+ "@babel/preset-typescript"
79
82
  );
80
83
  }
81
84
 
@@ -84,6 +87,8 @@ export async function installAllDeps(responses, pkg, cwd) {
84
87
  devDeps.push("eslint", "@eslint/js", "eslint-plugin-react-hooks", "eslint-plugin-react-refresh", "globals");
85
88
  if (responses.language === "ts") {
86
89
  devDeps.push("typescript-eslint");
90
+ // ESLint >= 10 loads TS config files (eslint.config.ts) via jiti.
91
+ devDeps.push("jiti");
87
92
  }
88
93
  } else if (responses.linter === "oxlint") {
89
94
  devDeps.push("oxlint");
package/src/format.js ADDED
@@ -0,0 +1,28 @@
1
+ import path from "path";
2
+ import { execa } from "execa";
3
+ import { promises as fsp } from "fs";
4
+
5
+ async function exists(p) {
6
+ try {
7
+ await fsp.access(p);
8
+ return true;
9
+ } catch {
10
+ return false;
11
+ }
12
+ }
13
+
14
+ // Runs the project-local formatter (installed as a devDependency) so a freshly
15
+ // generated project is canonical under its chosen formatter by construction.
16
+ // Falls back to the bare command name so the same helper works when the binary
17
+ // is already on PATH (e.g. verification harnesses resolving from a vendored
18
+ // node_modules symlink).
19
+ export async function runProjectFormat(projectPath, responses) {
20
+ if (!responses.formatter || responses.formatter === "none") return;
21
+
22
+ const formatter = responses.formatter;
23
+ const bin = path.join(projectPath, "node_modules", ".bin", formatter);
24
+ const command = (await exists(bin)) ? bin : formatter;
25
+ const args = formatter === "prettier" ? ["--write", "."] : ["."];
26
+
27
+ await execa(command, args, { cwd: projectPath, stdio: "pipe" });
28
+ }
package/src/injector.js CHANGED
@@ -72,6 +72,7 @@ export async function injectFormatter(projectPath, templatesDir, responses) {
72
72
  pkg.scripts["format:check"] = "prettier --check .";
73
73
  } else if (responses.formatter === "oxfmt") {
74
74
  pkg.scripts.format = "oxfmt .";
75
+ pkg.scripts["format:check"] = "oxfmt --check .";
75
76
  }
76
77
  await fsp.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
77
78
  } catch {}
@@ -105,11 +106,14 @@ async function wireEslintPrettier(projectPath, language) {
105
106
 
106
107
  let result = lines.join("\n");
107
108
 
108
- // Append ...prettier as the LAST element of the eslint config.
109
+ // Append prettier as the LAST element of the eslint config.
110
+ // eslint-config-prettier exports a flat-config object (not an iterable),
111
+ // so it must be passed by reference, not spread — passing `...prettier`
112
+ // breaks under ESLint's jiti-based TS config loading.
109
113
  // The JS template exports an array (`export default [...]`); the TS
110
114
  // template uses a function call (`export default tseslint.config(...)`).
111
115
  const pushLast = (_, head, tail) =>
112
- head.replace(/,\s*$/, "") + ",\n ...prettier," + tail;
116
+ head.replace(/,\s*$/, "") + ",\n prettier," + tail;
113
117
  if (result.includes("tseslint.config(")) {
114
118
  result = result.replace(
115
119
  /(export default tseslint\.config\([\s\S]*?)(\n\s*\);)/,
@@ -247,11 +251,18 @@ async function injectTesting(projectPath, templatesDir, framework, language, arc
247
251
  await fsp.access(jestSetupSrc);
248
252
  await fsp.copyFile(jestSetupSrc, path.join(projectPath, `jest.setup.${configExt}`));
249
253
  } catch {}
254
+
255
+ // Unified Babel transform config for Jest (env + react + typescript presets).
256
+ const babelSrc = path.join(testingDir, "babel.config.cjs");
257
+ try {
258
+ await fsp.access(babelSrc);
259
+ await fsp.copyFile(babelSrc, path.join(projectPath, "babel.config.cjs"));
260
+ } catch {}
250
261
  }
251
262
 
252
- // Copy test file
263
+ // Copy test file (architecture-scoped so the App import path resolves)
253
264
  const testExt = language === "ts" ? "tsx" : "jsx";
254
- const testSrc = path.join(testingDir, `src/__tests__/App.test.${testExt}`);
265
+ const testSrc = path.join(testingDir, architecture, `src/__tests__/App.test.${testExt}`);
255
266
  try {
256
267
  await fsp.access(testSrc);
257
268
  const testDest = path.join(projectPath, "src", "__tests__", `App.test.${testExt}`);
package/src/main.js CHANGED
@@ -15,6 +15,7 @@ import { getUserInputs, onCancel } from "@/prompts.js";
15
15
  import { runBaseInstall, runGitInit, runViteCreate } from "@/scaffold.js";
16
16
  import { installAllDeps } from "@/dependencies.js";
17
17
  import { generateReadme } from "@/readme.js";
18
+ import { runProjectFormat } from "@/format.js";
18
19
 
19
20
  const __filename = fileURLToPath(import.meta.url);
20
21
  const __dirname = path.dirname(__filename);
@@ -22,7 +23,9 @@ const TEMPLATES_DIR = path.join(__dirname, "../templates");
22
23
  const CURRENT_DIR = process.cwd();
23
24
 
24
25
  export function resolveProjectName({ quickSetup = false, projectName: nameArg, cwd = CURRENT_DIR } = {}) {
25
- const cliArg = nameArg || process.argv[2];
26
+ const cliArgs = (process.argv.slice(2) ?? []).filter((arg) => arg && !arg.startsWith("-"));
27
+ const cliArg = nameArg || cliArgs[0] || null;
28
+
26
29
  if (cliArg && cliArg.trim()) {
27
30
  return cliArg.trim();
28
31
  }
@@ -213,6 +216,19 @@ export async function main(options = {}) {
213
216
  }
214
217
  }
215
218
 
219
+ // 13.5 Format the freshly generated project so it is canonical on init
220
+ if (responses.formatter && responses.formatter !== "none") {
221
+ const fmtPassSpin = spinner();
222
+ fmtPassSpin.start(`Formatting project with ${responses.formatter}...`);
223
+ try {
224
+ await runProjectFormat(projectPath, responses);
225
+ fmtPassSpin.stop(chalk.green("Project formatted."));
226
+ } catch (err) {
227
+ fmtPassSpin.stop(chalk.red("Failed to format project."));
228
+ throw err;
229
+ }
230
+ }
231
+
216
232
  log.step(chalk.green("\nProject setup complete!"));
217
233
  log.message(
218
234
  chalk.gray(
package/src/readme.js CHANGED
@@ -77,6 +77,7 @@ function buildScripts(responses) {
77
77
 
78
78
  if (responses.formatter === "prettier" || responses.formatter === "oxfmt") {
79
79
  rows.push("| `npm run format` | Format code |");
80
+ rows.push("| `npm run format:check` | Verify code is formatted |");
80
81
  }
81
82
 
82
83
  return rows.join("\n");
@@ -3,7 +3,7 @@ import ReactDOM from "react-dom/client";
3
3
  import App from "./App";
4
4
  import "./styles/globals.css";
5
5
 
6
- ReactDOM.createRoot(document.getElementById("root")).render(
6
+ ReactDOM.createRoot(document.getElementById("root")!).render(
7
7
  <React.StrictMode>
8
8
  <App />
9
9
  </React.StrictMode>
@@ -1,4 +1,3 @@
1
- import React from "react";
2
1
  import { MainLayout } from "@/layouts/MainLayout";
3
2
  import { HomePage } from "@/pages/Home/Home";
4
3
 
@@ -1,4 +1,3 @@
1
- import React from "react";
2
1
  import { MainLayout } from "@/layouts/MainLayout";
3
2
  import { HomePage } from "@/pages/Home/Home";
4
3
 
@@ -1,5 +1,3 @@
1
- import React from "react";
2
-
3
1
  export function Button({
4
2
  variant = "primary",
5
3
  children,
@@ -1,5 +1,3 @@
1
- import React from "react";
2
-
3
1
  interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
4
2
  variant?: "primary" | "secondary";
5
3
  children: React.ReactNode;
@@ -1,2 +1 @@
1
- export const APP_NAME = "Lumen";
2
1
  export const API_BASE_URL = import.meta.env.VITE_API_URL || "http://localhost:3000";
@@ -1,2 +1 @@
1
- export const APP_NAME = "Lumen";
2
1
  export const API_BASE_URL = import.meta.env.VITE_API_URL || "http://localhost:3000";
@@ -1,6 +1,5 @@
1
- import React, { createContext, useState } from "react";
2
-
3
- export const AppContext = createContext(null);
1
+ import { useState } from "react";
2
+ import { AppContext } from "./app-context";
4
3
 
5
4
  export function AppProvider({ children }) {
6
5
  const [theme, setTheme] = useState("light");
@@ -10,4 +9,4 @@ export function AppProvider({ children }) {
10
9
  {children}
11
10
  </AppContext.Provider>
12
11
  );
13
- }
12
+ }
@@ -1,13 +1,18 @@
1
- import React, { createContext, useState, useEffect } from "react";
1
+ import { useState, useEffect, type ReactNode } from "react";
2
+ import { AppContext, type AppContextValue } from "./app-context";
2
3
 
3
- export const AppContext = createContext(null);
4
+ export function AppProvider({ children }: { children: ReactNode }) {
5
+ const [theme, setTheme] = useState(() => localStorage.getItem("theme") ?? "light");
4
6
 
5
- export function AppProvider({ children }) {
6
- const [theme, setTheme] = useState("light");
7
+ useEffect(() => {
8
+ localStorage.setItem("theme", theme);
9
+ }, [theme]);
10
+
11
+ const value: AppContextValue = { theme, setTheme };
7
12
 
8
13
  return (
9
- <AppContext.Provider value={{ theme, setTheme }}>
14
+ <AppContext.Provider value={value}>
10
15
  {children}
11
16
  </AppContext.Provider>
12
17
  );
13
- }
18
+ }
@@ -0,0 +1,5 @@
1
+ import { createContext } from "react";
2
+
3
+ // React Fast Refresh requires contexts to live in a separate file from
4
+ // components, so the AppProvider component exports from AppContext.jsx.
5
+ export const AppContext = createContext(null);
@@ -0,0 +1,10 @@
1
+ import { createContext } from "react";
2
+
3
+ // React Fast Refresh requires contexts to live in a separate file from
4
+ // components, so the AppProvider component exports from AppContext.tsx.
5
+ export interface AppContextValue {
6
+ theme: string;
7
+ setTheme: (theme: string) => void;
8
+ }
9
+
10
+ export const AppContext = createContext<AppContextValue | null>(null);
@@ -1,17 +1,17 @@
1
1
  import { useState, useCallback } from "react";
2
2
 
3
- export function useLocalStorage(key, initialValue) {
4
- const [storedValue, setStoredValue] = useState(() => {
3
+ export function useLocalStorage<T>(key: string, initialValue: T) {
4
+ const [storedValue, setStoredValue] = useState<T>(() => {
5
5
  try {
6
6
  const item = window.localStorage.getItem(key);
7
- return item ? JSON.parse(item) : initialValue;
7
+ return item ? (JSON.parse(item) as T) : initialValue;
8
8
  } catch {
9
9
  return initialValue;
10
10
  }
11
11
  });
12
12
 
13
13
  const setValue = useCallback(
14
- (value) => {
14
+ (value: T | ((prev: T) => T)) => {
15
15
  try {
16
16
  const valueToStore =
17
17
  value instanceof Function ? value(storedValue) : value;
@@ -24,5 +24,5 @@ export function useLocalStorage(key, initialValue) {
24
24
  [key, storedValue]
25
25
  );
26
26
 
27
- return [storedValue, setValue];
27
+ return [storedValue, setValue] as const;
28
28
  }
@@ -1,5 +1,3 @@
1
- import React from "react";
2
-
3
1
  export function MainLayout({ children }) {
4
2
  return (
5
3
  <div style={{ minHeight: "100vh", display: "flex", flexDirection: "column" }}>
@@ -1,5 +1,3 @@
1
- import React from "react";
2
-
3
1
  interface MainLayoutProps {
4
2
  children: React.ReactNode;
5
3
  }
@@ -1,4 +1,3 @@
1
- import React from "react";
2
1
  import { Button } from "@/components/common";
3
2
 
4
3
  export function HomePage() {
@@ -1,4 +1,3 @@
1
- import React from "react";
2
1
  import { Button } from "@/components/common";
3
2
 
4
3
  export function HomePage() {
@@ -1,3 +1,5 @@
1
- export function cn(...inputs) {
1
+ type ClassValue = string | false | null | undefined;
2
+
3
+ export function cn(...inputs: ClassValue[]): string {
2
4
  return inputs.filter(Boolean).join(" ");
3
5
  }
@@ -3,7 +3,7 @@ import ReactDOM from "react-dom/client";
3
3
  import App from "@/app/App";
4
4
  import "./styles/globals.css";
5
5
 
6
- ReactDOM.createRoot(document.getElementById("root")).render(
6
+ ReactDOM.createRoot(document.getElementById("root")!).render(
7
7
  <React.StrictMode>
8
8
  <App />
9
9
  </React.StrictMode>
@@ -1,4 +1,3 @@
1
- import React from "react";
2
1
  import { HomePage } from "@/features/home/pages/HomePage";
3
2
 
4
3
  export default function App() {
@@ -1,4 +1,3 @@
1
- import React from "react";
2
1
  import { HomePage } from "@/features/home/pages/HomePage";
3
2
 
4
3
  export default function App() {
@@ -1,5 +1,3 @@
1
- import React from "react";
2
-
3
1
  export function ThemeProvider({ children }) {
4
2
  return <>{children}</>;
5
3
  }
@@ -1,5 +1,5 @@
1
- import React from "react";
1
+ import type { ReactNode } from "react";
2
2
 
3
- export function ThemeProvider({ children }) {
3
+ export function ThemeProvider({ children }: { children: ReactNode }) {
4
4
  return <>{children}</>;
5
5
  }
@@ -1,5 +1,3 @@
1
- import React from "react";
2
-
3
1
  export function Button({
4
2
  variant = "primary",
5
3
  children,
@@ -1,5 +1,3 @@
1
- import React from "react";
2
-
3
1
  interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
4
2
  variant?: "primary" | "secondary";
5
3
  children: React.ReactNode;
@@ -1,2 +1 @@
1
- export const APP_NAME = "Lumen";
2
1
  export const API_BASE_URL = import.meta.env.VITE_API_URL || "http://localhost:3000";
@@ -1,2 +1 @@
1
- export const APP_NAME = "Lumen";
2
1
  export const API_BASE_URL = import.meta.env.VITE_API_URL || "http://localhost:3000";
@@ -1,4 +1,3 @@
1
- import React from "react";
2
1
  import { Button } from "@/components/ui";
3
2
 
4
3
  export function HomePage() {
@@ -1,4 +1,3 @@
1
- import React from "react";
2
1
  import { Button } from "@/components/ui";
3
2
 
4
3
  export function HomePage() {
@@ -1,17 +1,17 @@
1
1
  import { useState, useCallback } from "react";
2
2
 
3
- export function useLocalStorage(key, initialValue) {
4
- const [storedValue, setStoredValue] = useState(() => {
3
+ export function useLocalStorage<T>(key: string, initialValue: T) {
4
+ const [storedValue, setStoredValue] = useState<T>(() => {
5
5
  try {
6
6
  const item = window.localStorage.getItem(key);
7
- return item ? JSON.parse(item) : initialValue;
7
+ return item ? (JSON.parse(item) as T) : initialValue;
8
8
  } catch {
9
9
  return initialValue;
10
10
  }
11
11
  });
12
12
 
13
13
  const setValue = useCallback(
14
- (value) => {
14
+ (value: T | ((prev: T) => T)) => {
15
15
  try {
16
16
  const valueToStore =
17
17
  value instanceof Function ? value(storedValue) : value;
@@ -24,5 +24,5 @@ export function useLocalStorage(key, initialValue) {
24
24
  [key, storedValue]
25
25
  );
26
26
 
27
- return [storedValue, setValue];
27
+ return [storedValue, setValue] as const;
28
28
  }
@@ -1,6 +1,5 @@
1
- import { clsx } from "clsx";
2
- import { twMerge } from "tailwind-merge";
1
+ type ClassValue = string | false | null | undefined;
3
2
 
4
- export function cn(...inputs) {
5
- return twMerge(clsx(inputs));
3
+ export function cn(...inputs: ClassValue[]): string {
4
+ return inputs.filter(Boolean).join(" ");
6
5
  }
@@ -4,4 +4,4 @@
4
4
  "singleQuote": true,
5
5
  "semi": true,
6
6
  "trailingComma": "all"
7
- }
7
+ }
@@ -2,6 +2,6 @@
2
2
  "semi": true,
3
3
  "singleQuote": true,
4
4
  "tabWidth": 2,
5
- "trailingComma": "es5",
5
+ "trailingComma": "all",
6
6
  "printWidth": 80
7
- }
7
+ }
@@ -1,14 +1,14 @@
1
- import React from "react";
2
- import { Sun, Moon, Github } from "@hugeicons/react";
1
+ import { HugeiconsIcon } from "@hugeicons/react";
2
+ import { Sun01Icon, Moon01Icon, Github01Icon } from "@hugeicons/core-free-icons";
3
3
  import { Button } from "@/components/common";
4
4
 
5
5
  export function HomePage() {
6
6
  return (
7
7
  <div className="text-center">
8
8
  <div className="flex justify-center gap-4 mb-6">
9
- <Sun className="w-8 h-8 text-yellow-500" />
10
- <Moon className="w-8 h-8 text-blue-500" />
11
- <Github className="w-8 h-8 text-gray-800" />
9
+ <HugeiconsIcon icon={Sun01Icon} className="w-8 h-8 text-yellow-500" />
10
+ <HugeiconsIcon icon={Moon01Icon} className="w-8 h-8 text-blue-500" />
11
+ <HugeiconsIcon icon={Github01Icon} className="w-8 h-8 text-gray-800" />
12
12
  </div>
13
13
  <h1 className="text-4xl font-bold text-gray-900 mb-4">Welcome to Lumen</h1>
14
14
  <p className="text-gray-600 mb-8">