create-wakii-app 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -2
- package/dist/index.js +333 -0
- package/package.json +22 -4
- package/starter-templates/template-feats/extras/carousel/src/DemoCarousel.tsx +99 -0
- package/starter-templates/template-feats/extras/chart/src/DemoChart.tsx +84 -0
- package/starter-templates/template-feats/extras/editor/src/DemoEditor.tsx +110 -0
- package/starter-templates/template-feats/extras/schedule/src/DemoSchedule.tsx +81 -0
- package/starter-templates/template-feats/router/src/App.tsx +332 -0
- package/starter-templates/template-feats/router/src/main.tsx +14 -0
- package/starter-templates/vite-js/README.md +72 -0
- package/starter-templates/vite-js/index.html +12 -0
- package/starter-templates/vite-js/jsconfig.json +10 -0
- package/starter-templates/vite-js/package.json +22 -0
- package/starter-templates/vite-js/src/App.jsx +158 -0
- package/starter-templates/vite-js/src/main.jsx +10 -0
- package/starter-templates/vite-js/src/styles.css +15 -0
- package/starter-templates/vite-js/src/theme.js +21 -0
- package/starter-templates/vite-js/vite.config.mjs +8 -0
- package/starter-templates/vite-ts/README.md +53 -0
- package/starter-templates/vite-ts/index.html +12 -0
- package/starter-templates/vite-ts/package.json +29 -0
- package/starter-templates/vite-ts/src/App.tsx +181 -0
- package/starter-templates/vite-ts/src/main.tsx +12 -0
- package/starter-templates/vite-ts/src/styles.css +21 -0
- package/starter-templates/vite-ts/src/theme.ts +16 -0
- package/starter-templates/vite-ts/tsconfig.json +25 -0
- package/starter-templates/vite-ts/tsconfig.node.json +11 -0
- package/starter-templates/vite-ts/vite.config.ts +10 -0
package/README.md
CHANGED
|
@@ -1,3 +1,38 @@
|
|
|
1
|
-
#
|
|
1
|
+
# create-wakii-app
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
CLI scaffold app React + **@wakii/ui-kit** — `pnpm create wakii-app my-app` (spec chung: `docs/` khi publish — hiện `/tmp/starter-spec.md`).
|
|
4
|
+
|
|
5
|
+
## Dùng
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm create wakii-app my-app # interactive (CRA-style)
|
|
9
|
+
pnpm create wakii-app my-app --yes # toàn default (ts + router, 0 extra)
|
|
10
|
+
pnpm create wakii-app my-app --lang js --no-router --extras chart,carousel --skip-install
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
| Flag | Ý nghĩa | Default |
|
|
14
|
+
|---|---|---|
|
|
15
|
+
| `--lang ts\|js` | TypeScript / JavaScript variant | `ts` |
|
|
16
|
+
| `--router` / `--no-router` | react-router v7 | có |
|
|
17
|
+
| `--extras a,b` | demo subpath: `chart · carousel · schedule · editor` | none |
|
|
18
|
+
| `--yes` | bỏ prompt, lấy default | — |
|
|
19
|
+
| `--skip-install` | bỏ gợi ý `pnpm install` ở next-steps | — |
|
|
20
|
+
|
|
21
|
+
CLI KHÔNG tự install — chỉ in next-steps (`cd`, `pnpm install`, `pnpm dev`).
|
|
22
|
+
|
|
23
|
+
## Kiến trúc
|
|
24
|
+
|
|
25
|
+
- `src/options.ts` — parse flags tự viết (0 dep parser) + defaults + validate
|
|
26
|
+
- `src/scaffold.ts` — copy đệ quy template → render placeholder `{{appName}}` (binary bỏ qua) → áp overlays `router`/`extras/<name>` → merge deps vào package.json
|
|
27
|
+
- `src/index.ts` — flow prompt `@clack/prompts` + log + next-steps (wire UI thôi)
|
|
28
|
+
|
|
29
|
+
Nguồn template: `app/starter-templates/vite-ts|vite-js` + `template-feats/` (worker riêng sở hữu). Overlay dùng cho variant JS phải type-free (engine chỉ đổi đuôi `.tsx→.jsx`); nếu overlay có thư mục `vite-js/` riêng thì dùng nguyên thư mục đó.
|
|
30
|
+
|
|
31
|
+
## Phát triển
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pnpm --filter create-wakii-app typecheck
|
|
35
|
+
pnpm --filter create-wakii-app test # fixture templates giả trong tmp — không network
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Build/publish wiring (bin → dist, files field gộp templates) là bước release — xem RELEASE khi publish.
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import * as p from "@clack/prompts";
|
|
5
|
+
import { existsSync as existsSync2, readdirSync } from "fs";
|
|
6
|
+
import { resolve as resolve2 } from "path";
|
|
7
|
+
|
|
8
|
+
// src/options.ts
|
|
9
|
+
var EXTRAS = ["chart", "carousel", "schedule", "editor"];
|
|
10
|
+
function parseExtrasList(raw) {
|
|
11
|
+
const names = raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
12
|
+
const extras = [];
|
|
13
|
+
for (const name of names) {
|
|
14
|
+
if (!EXTRAS.includes(name)) {
|
|
15
|
+
return { ok: false, message: `Extra kh\xF4ng h\u1ED7 tr\u1EE3: '${name}' \u2014 h\u1EE3p l\u1EC7: ${EXTRAS.join(", ")}` };
|
|
16
|
+
}
|
|
17
|
+
if (!extras.includes(name)) extras.push(name);
|
|
18
|
+
}
|
|
19
|
+
return { ok: true, extras };
|
|
20
|
+
}
|
|
21
|
+
function parseArgs(argv) {
|
|
22
|
+
const options = { lang: "ts", extras: [], yes: false, skipInstall: false };
|
|
23
|
+
let i = 0;
|
|
24
|
+
while (i < argv.length) {
|
|
25
|
+
const arg = argv[i];
|
|
26
|
+
if (arg === "--yes") {
|
|
27
|
+
options.yes = true;
|
|
28
|
+
} else if (arg === "--skip-install") {
|
|
29
|
+
options.skipInstall = true;
|
|
30
|
+
} else if (arg === "--router") {
|
|
31
|
+
options.router = true;
|
|
32
|
+
} else if (arg === "--no-router") {
|
|
33
|
+
options.router = false;
|
|
34
|
+
} else if (arg === "--help" || arg === "-h") {
|
|
35
|
+
options.help = true;
|
|
36
|
+
} else if (arg === "--lang") {
|
|
37
|
+
const value = argv[i + 1];
|
|
38
|
+
i += 1;
|
|
39
|
+
if (value !== "ts" && value !== "js") {
|
|
40
|
+
return { ok: false, message: `--lang ch\u1EC9 nh\u1EADn 'ts' | 'js' \u2014 nh\u1EADn: ${value ?? "(thi\u1EBFu gi\xE1 tr\u1ECB)"}` };
|
|
41
|
+
}
|
|
42
|
+
options.lang = value;
|
|
43
|
+
} else if (arg?.startsWith("--lang=")) {
|
|
44
|
+
const value = arg.slice("--lang=".length);
|
|
45
|
+
if (value !== "ts" && value !== "js") {
|
|
46
|
+
return { ok: false, message: `--lang ch\u1EC9 nh\u1EADn 'ts' | 'js' \u2014 nh\u1EADn: ${value}` };
|
|
47
|
+
}
|
|
48
|
+
options.lang = value;
|
|
49
|
+
} else if (arg === "--extras") {
|
|
50
|
+
const value = argv[i + 1];
|
|
51
|
+
i += 1;
|
|
52
|
+
if (!value) return { ok: false, message: "--extras c\u1EA7n danh s\xE1ch ph\u1EA9y: --extras chart,carousel" };
|
|
53
|
+
const parsed = parseExtrasList(value);
|
|
54
|
+
if (!parsed.ok) return parsed;
|
|
55
|
+
options.extras = parsed.extras;
|
|
56
|
+
options.extrasProvided = true;
|
|
57
|
+
} else if (arg?.startsWith("--extras=")) {
|
|
58
|
+
const parsed = parseExtrasList(arg.slice("--extras=".length));
|
|
59
|
+
if (!parsed.ok) return parsed;
|
|
60
|
+
options.extras = parsed.extras;
|
|
61
|
+
options.extrasProvided = true;
|
|
62
|
+
} else if (arg?.startsWith("--")) {
|
|
63
|
+
return { ok: false, message: `Flag kh\xF4ng h\u1ED7 tr\u1EE3: ${arg} (xem --help)` };
|
|
64
|
+
} else if (arg !== void 0) {
|
|
65
|
+
if (options.appName !== void 0) return { ok: false, message: "Ch\u1EC9 nh\u1EADn M\u1ED8T t\xEAn project (positioned \u0111\u1EA7u ti\xEAn)" };
|
|
66
|
+
options.appName = arg;
|
|
67
|
+
}
|
|
68
|
+
i += 1;
|
|
69
|
+
}
|
|
70
|
+
return { ok: true, options };
|
|
71
|
+
}
|
|
72
|
+
function resolveOptions(options) {
|
|
73
|
+
if (options.extras.length > 0 && options.router === false) {
|
|
74
|
+
throw new Error("--no-router m\xE2u thu\u1EABn v\u1EDBi --extras (demo subpath c\u1EA7n route \u0111\u1EC3 m\u1EDF) \u2014 b\u1ECF --no-router ho\u1EB7c b\u1ECF extras");
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
appName: options.appName ?? "my-wakii-app",
|
|
78
|
+
lang: options.lang,
|
|
79
|
+
router: options.extras.length > 0 ? true : options.router ?? true,
|
|
80
|
+
extras: options.extras,
|
|
81
|
+
skipInstall: options.skipInstall
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// src/scaffold.ts
|
|
86
|
+
import { mkdir, readFile, readdir, writeFile } from "fs/promises";
|
|
87
|
+
import { existsSync } from "fs";
|
|
88
|
+
import { dirname, join, resolve } from "path";
|
|
89
|
+
import { fileURLToPath } from "url";
|
|
90
|
+
function resolveTemplatesRoot() {
|
|
91
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
92
|
+
for (const candidate of [
|
|
93
|
+
resolve(here, "../../starter-templates"),
|
|
94
|
+
// repo: app/create-wakii-app/{src,dist}
|
|
95
|
+
resolve(here, "../starter-templates")
|
|
96
|
+
// npm: <pkg>/dist + <pkg>/starter-templates
|
|
97
|
+
]) {
|
|
98
|
+
if (existsSync(candidate)) return candidate;
|
|
99
|
+
}
|
|
100
|
+
return resolve(here, "../../starter-templates");
|
|
101
|
+
}
|
|
102
|
+
var DEFAULT_TEMPLATES_ROOT = resolveTemplatesRoot();
|
|
103
|
+
var EXTRA_DEPS = {
|
|
104
|
+
chart: [{ name: "chart.js", range: "^4.5.1" }],
|
|
105
|
+
carousel: [{ name: "swiper", range: "^14.3.0" }],
|
|
106
|
+
schedule: [
|
|
107
|
+
{ name: "@fullcalendar/core", range: "^6.1.22" },
|
|
108
|
+
{ name: "@fullcalendar/daygrid", range: "^6.1.22" },
|
|
109
|
+
{ name: "@fullcalendar/interaction", range: "^6.1.22" }
|
|
110
|
+
],
|
|
111
|
+
editor: [
|
|
112
|
+
{ name: "@tiptap/react", range: "^3.32.0" },
|
|
113
|
+
{ name: "@tiptap/starter-kit", range: "^3.32.0" }
|
|
114
|
+
]
|
|
115
|
+
};
|
|
116
|
+
function renameForJs(path) {
|
|
117
|
+
return path.replace(/\.tsx$/, ".jsx").replace(/\.ts$/, ".js");
|
|
118
|
+
}
|
|
119
|
+
async function copyTree(srcDir, destDir, opts) {
|
|
120
|
+
await mkdir(destDir, { recursive: true });
|
|
121
|
+
for (const entry of await readdir(srcDir, { withFileTypes: true })) {
|
|
122
|
+
if (entry.name === ".DS_Store") continue;
|
|
123
|
+
const srcPath = join(srcDir, entry.name);
|
|
124
|
+
const outName = opts.renameExt ? renameForJs(entry.name) : entry.name;
|
|
125
|
+
const destPath = join(destDir, outName);
|
|
126
|
+
if (entry.isDirectory()) {
|
|
127
|
+
await copyTree(srcPath, destPath, opts);
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
const buffer = await readFile(srcPath);
|
|
131
|
+
if (buffer.includes(0)) {
|
|
132
|
+
await writeFile(destPath, buffer);
|
|
133
|
+
} else {
|
|
134
|
+
const text2 = buffer.toString("utf8").replaceAll("{{appName}}", opts.appName);
|
|
135
|
+
await writeFile(destPath, text2);
|
|
136
|
+
}
|
|
137
|
+
opts.files.push(destPath);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
async function mergeDeps(dest, plan, log2) {
|
|
141
|
+
const pkgPath = join(dest, "package.json");
|
|
142
|
+
if (!existsSync(pkgPath)) return;
|
|
143
|
+
const pkg = JSON.parse(await readFile(pkgPath, "utf8"));
|
|
144
|
+
pkg.dependencies ??= {};
|
|
145
|
+
const additions = [];
|
|
146
|
+
if (plan.router) {
|
|
147
|
+
pkg.dependencies["react-router"] ??= "^7.9.0";
|
|
148
|
+
additions.push("react-router@^7.9.0");
|
|
149
|
+
}
|
|
150
|
+
for (const extra of plan.extras) {
|
|
151
|
+
for (const dep of EXTRA_DEPS[extra]) {
|
|
152
|
+
pkg.dependencies[dep.name] ??= dep.range;
|
|
153
|
+
additions.push(`${dep.name}@${dep.range}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
await writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}
|
|
157
|
+
`);
|
|
158
|
+
if (additions.length) log2?.(`Merge deps: +${additions.join(" +")}`);
|
|
159
|
+
}
|
|
160
|
+
async function scaffold(input) {
|
|
161
|
+
const { plan, dest, log: log2 } = input;
|
|
162
|
+
const templatesRoot = input.templatesRoot ?? DEFAULT_TEMPLATES_ROOT;
|
|
163
|
+
const variant = plan.lang === "js" ? "vite-js" : "vite-ts";
|
|
164
|
+
const baseDir = join(templatesRoot, variant);
|
|
165
|
+
if (!existsSync(baseDir)) {
|
|
166
|
+
throw new Error(`Kh\xF4ng th\u1EA5y template base: ${baseDir} (c\u1EA7n ${variant} trong starter-templates)`);
|
|
167
|
+
}
|
|
168
|
+
const files = [];
|
|
169
|
+
log2?.(`Copy base template (${variant})`);
|
|
170
|
+
await copyTree(baseDir, dest, { appName: plan.appName, renameExt: false, files });
|
|
171
|
+
const featsDir = join(templatesRoot, "template-feats");
|
|
172
|
+
if (plan.router) {
|
|
173
|
+
const routerDir = join(featsDir, "router");
|
|
174
|
+
if (!existsSync(routerDir)) throw new Error(`Kh\xF4ng th\u1EA5y overlay router: ${routerDir}`);
|
|
175
|
+
const jsVariantDir = join(routerDir, "vite-js");
|
|
176
|
+
const useJsVariant = plan.lang === "js" && existsSync(jsVariantDir);
|
|
177
|
+
log2?.("Apply overlay: router");
|
|
178
|
+
await copyTree(useJsVariant ? jsVariantDir : routerDir, dest, {
|
|
179
|
+
appName: plan.appName,
|
|
180
|
+
renameExt: plan.lang === "js" && !useJsVariant,
|
|
181
|
+
files
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
for (const extra of plan.extras) {
|
|
185
|
+
const extraDir = join(featsDir, "extras", extra);
|
|
186
|
+
if (!existsSync(extraDir)) throw new Error(`Kh\xF4ng th\u1EA5y overlay extra '${extra}': ${extraDir}`);
|
|
187
|
+
const jsVariantDir = join(extraDir, "vite-js");
|
|
188
|
+
const useJsVariant = plan.lang === "js" && existsSync(jsVariantDir);
|
|
189
|
+
log2?.(`Apply overlay: ${extra}`);
|
|
190
|
+
await copyTree(useJsVariant ? jsVariantDir : extraDir, dest, {
|
|
191
|
+
appName: plan.appName,
|
|
192
|
+
renameExt: plan.lang === "js" && !useJsVariant,
|
|
193
|
+
files
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
if (plan.router) {
|
|
197
|
+
log2?.("Render extras markers trong App");
|
|
198
|
+
await renderExtraMarkers(dest, plan, plan.lang === "js");
|
|
199
|
+
}
|
|
200
|
+
await mergeDeps(dest, plan, log2);
|
|
201
|
+
return { files: files.map((f) => f.slice(dest.length + 1)) };
|
|
202
|
+
}
|
|
203
|
+
var EXTRA_COMPONENTS = {
|
|
204
|
+
chart: { component: "ChartDemo", label: "Chart" },
|
|
205
|
+
carousel: { component: "CarouselDemo", label: "Carousel" },
|
|
206
|
+
schedule: { component: "ScheduleDemo", label: "Schedule" },
|
|
207
|
+
editor: { component: "EditorDemo", label: "Editor" }
|
|
208
|
+
};
|
|
209
|
+
async function renderExtraMarkers(dest, plan, isJs) {
|
|
210
|
+
const ext = isJs ? "jsx" : "tsx";
|
|
211
|
+
const appPath = join(dest, `src/App.${ext}`);
|
|
212
|
+
if (!existsSync(appPath)) return;
|
|
213
|
+
const imports = [];
|
|
214
|
+
const links = [];
|
|
215
|
+
const routes = [];
|
|
216
|
+
for (const extra of plan.extras) {
|
|
217
|
+
const { component, label } = EXTRA_COMPONENTS[extra];
|
|
218
|
+
const fileStem = `Demo${extra[0].toUpperCase()}${extra.slice(1)}`;
|
|
219
|
+
imports.push(`import { ${component} } from './${fileStem}';`);
|
|
220
|
+
links.push(` <NavLink className={NAV_LINK} to="/${extra}">
|
|
221
|
+
${label}
|
|
222
|
+
</NavLink>`);
|
|
223
|
+
routes.push(` <Route element={<${component} />} path="/${extra}" />`);
|
|
224
|
+
}
|
|
225
|
+
let text2 = await readFile(appPath, "utf8");
|
|
226
|
+
text2 = text2.replaceAll("{{EXTRA_IMPORTS}}", imports.join("\n")).replaceAll("{{EXTRA_LINKS}}", links.join("\n")).replaceAll("{{EXTRA_ROUTES}}", routes.join("\n"));
|
|
227
|
+
await writeFile(appPath, text2);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// src/index.ts
|
|
231
|
+
var USAGE = `create-wakii-app \u2014 scaffold app React + @wakii/ui-kit
|
|
232
|
+
|
|
233
|
+
Usage: pnpm create wakii-app [appName] [flags]
|
|
234
|
+
|
|
235
|
+
Flags:
|
|
236
|
+
--lang ts|js Ng\xF4n ng\u1EEF (default: ts)
|
|
237
|
+
--router/--no-router react-router v7 (default: c\xF3)
|
|
238
|
+
--extras <list> Extras ph\u1EA9y ng\u0103n c\xE1ch: chart,carousel,schedule,editor (default: none)
|
|
239
|
+
--yes L\u1EA5y to\xE0n b\u1ED9 default, kh\xF4ng prompt
|
|
240
|
+
--skip-install B\u1ECF g\u1EE3i \xFD 'pnpm install' \u1EDF next-steps
|
|
241
|
+
-h, --help In h\u01B0\u1EDBng d\u1EABn
|
|
242
|
+
|
|
243
|
+
V\xED d\u1EE5: pnpm create wakii-app my-app --lang ts --router --extras chart,carousel
|
|
244
|
+
`;
|
|
245
|
+
function printNextSteps(plan) {
|
|
246
|
+
const steps = [`cd ${plan.appName}`];
|
|
247
|
+
if (!plan.skipInstall) steps.push("pnpm install");
|
|
248
|
+
steps.push("pnpm dev");
|
|
249
|
+
p.log.info("Next steps:");
|
|
250
|
+
for (const step of steps) p.log.step(` ${step}`);
|
|
251
|
+
}
|
|
252
|
+
async function main() {
|
|
253
|
+
const parsed = parseArgs(process.argv.slice(2));
|
|
254
|
+
if (!parsed.ok) {
|
|
255
|
+
p.log.error(parsed.message);
|
|
256
|
+
console.error(USAGE);
|
|
257
|
+
process.exitCode = 1;
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (parsed.options.help) {
|
|
261
|
+
console.log(USAGE);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
p.intro("create-wakii-app \u2014 scaffold React + @wakii/ui-kit");
|
|
265
|
+
let options = parsed.options;
|
|
266
|
+
if (options.appName === void 0) {
|
|
267
|
+
const name = await p.text({ message: "T\xEAn project?", placeholder: "my-wakii-app", defaultValue: "my-wakii-app" });
|
|
268
|
+
if (p.isCancel(name)) {
|
|
269
|
+
p.cancel("Hu\u1EF7 \u2014 kh\xF4ng scaffold g\xEC.");
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
options = { ...options, appName: name.trim() };
|
|
273
|
+
}
|
|
274
|
+
if (options.router === void 0 && options.extras.length === 0 && !options.yes) {
|
|
275
|
+
const withRouter = await p.confirm({ message: "Th\xEAm react-router v7?", initialValue: true });
|
|
276
|
+
if (p.isCancel(withRouter)) {
|
|
277
|
+
p.cancel("Hu\u1EF7 \u2014 kh\xF4ng scaffold g\xEC.");
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
options = { ...options, router: withRouter };
|
|
281
|
+
}
|
|
282
|
+
if (!options.yes && !options.extrasProvided) {
|
|
283
|
+
const chosen = await p.multiselect({
|
|
284
|
+
message: "Extras (tu\u1EF3 ch\u1ECDn \u2014 demo subpath component n\u1EB7ng)",
|
|
285
|
+
options: EXTRAS.map((value) => ({ value, label: value })),
|
|
286
|
+
required: false
|
|
287
|
+
});
|
|
288
|
+
if (p.isCancel(chosen)) {
|
|
289
|
+
p.cancel("Hu\u1EF7 \u2014 kh\xF4ng scaffold g\xEC.");
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
options = { ...options, extras: chosen };
|
|
293
|
+
}
|
|
294
|
+
let plan;
|
|
295
|
+
try {
|
|
296
|
+
plan = resolveOptions(options);
|
|
297
|
+
} catch (err) {
|
|
298
|
+
p.log.error(err instanceof Error ? err.message : String(err));
|
|
299
|
+
process.exitCode = 1;
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
const nameIsPath = /[\\/]/.test(plan.appName);
|
|
303
|
+
const dest = nameIsPath ? resolve2(plan.appName) : resolve2(process.cwd(), plan.appName);
|
|
304
|
+
const finalPlan = nameIsPath ? { ...plan, appName: dest.split(/[\\/]/).pop() ?? "my-wakii-app" } : plan;
|
|
305
|
+
if (existsSync2(dest) && readdirSync(dest).length > 0) {
|
|
306
|
+
p.log.error(`Th\u01B0 m\u1EE5c \u0111\xEDch \u0111\xE3 t\u1ED3n t\u1EA1i v\xE0 kh\xF4ng r\u1ED7ng: ${dest}`);
|
|
307
|
+
process.exitCode = 1;
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
const s = p.spinner();
|
|
311
|
+
s.start("Scaffolding\u2026");
|
|
312
|
+
try {
|
|
313
|
+
const result = await scaffold({
|
|
314
|
+
plan: finalPlan,
|
|
315
|
+
dest,
|
|
316
|
+
log: (message) => {
|
|
317
|
+
s.message(message);
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
s.stop(`Xong \u2014 ${result.files.length} file ghi v\xE0o ${dest}`);
|
|
321
|
+
} catch (err) {
|
|
322
|
+
s.stop("Scaffold l\u1ED7i");
|
|
323
|
+
p.log.error(err instanceof Error ? err.message : String(err));
|
|
324
|
+
process.exitCode = 1;
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
p.outro("S\u1EB5n s\xE0ng! \u{1F389}");
|
|
328
|
+
printNextSteps(nameIsPath ? { ...finalPlan, appName: dest } : finalPlan);
|
|
329
|
+
}
|
|
330
|
+
main().catch((err) => {
|
|
331
|
+
p.log.error(err instanceof Error ? err.message : String(err));
|
|
332
|
+
process.exitCode = 1;
|
|
333
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-wakii-app",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "CLI scaffold app React + @wakii/ui-kit — pnpm create wakii-app my-app (spec: /tmp/starter-spec.md → docs khi publish)",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-wakii-app": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"@clack/prompts": "^1.8.1"
|
|
11
|
+
},
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": ">=22"
|
|
14
|
+
},
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"registry": "https://registry.npmjs.org",
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"starter-templates",
|
|
22
|
+
"README.md"
|
|
23
|
+
]
|
|
24
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// Overlay EXTRA: carousel — trang demo <Carousel> qua subpath '@wakii/ui-kit/carousel'.
|
|
2
|
+
// ⚠️ PEER DEP: "swiper": "^14.3.0" (entry subpath tự kéo swiper/css).
|
|
3
|
+
// Redesign 2026-10-25 v2: hero autoplay 1/view + card rail 3/view. Lưu ý engine
|
|
4
|
+
// kit: spaceBetween hardcoded 0 → gutter phải tự tạo bằng padding trong slide;
|
|
5
|
+
// nút navigator absolute left-2/right-2 → container rail cần px đủ rộng để nút
|
|
6
|
+
// không đè card (px-10 > 26px nút + 8px offset); indicators là chip SỐ kiểu
|
|
7
|
+
// Pagination — chỉ bật ở hero, rail tắt cho nhẹ.
|
|
8
|
+
// import CSS swiper tại CONSUMER — liên kết css của dep-optimizer (metadata
|
|
9
|
+
// css:None) không đáng tin sau re-optimize; import trực tiếp luôn hoạt động.
|
|
10
|
+
import 'swiper/css';
|
|
11
|
+
import { Carousel } from '@wakii/ui-kit/carousel';
|
|
12
|
+
import { Icon, Tag, Text, Title, type IconName } from '@wakii/ui-kit';
|
|
13
|
+
|
|
14
|
+
const EYEBROW = 'text-label font-semibold uppercase tracking-[0.1em] text-accent';
|
|
15
|
+
|
|
16
|
+
const HERO_SLIDES: { icon: IconName; name: string; desc: string }[] = [
|
|
17
|
+
{ icon: 'star', name: 'Aurora', desc: 'Hero slides with autoplay, indicators and keyboard arrows.' },
|
|
18
|
+
{ icon: 'heart', name: 'Cascade', desc: 'Pause on hover, loop back to the start with circular mode.' },
|
|
19
|
+
{ icon: 'bell', name: 'Nimbus', desc: 'Navigators fade at the edges — content stays the star.' },
|
|
20
|
+
{ icon: 'users', name: 'Dune', desc: 'Every slide is plain React — style it however you like.' },
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
const heroItems = HERO_SLIDES.map((s) => (
|
|
24
|
+
<div
|
|
25
|
+
key={s.name}
|
|
26
|
+
className="relative flex h-64 flex-col items-start justify-end gap-2 overflow-hidden rounded-xl border border-line bg-gradient-to-br from-accent/20 via-s2 to-s1 p-8"
|
|
27
|
+
>
|
|
28
|
+
<span aria-hidden="true" className="pointer-events-none absolute -top-10 -right-6 size-40 rounded-pill bg-accent/15 blur-2xl" />
|
|
29
|
+
<span aria-hidden="true" className="grid size-10 place-items-center rounded-lg bg-accent">
|
|
30
|
+
<Icon name={s.icon} size={18} className="text-white" />
|
|
31
|
+
</span>
|
|
32
|
+
<Title level={3}>{s.name}</Title>
|
|
33
|
+
<Text type="secondary">{s.desc}</Text>
|
|
34
|
+
</div>
|
|
35
|
+
));
|
|
36
|
+
|
|
37
|
+
const RAIL_CARDS: { icon: IconName; name: string; desc: string }[] = [
|
|
38
|
+
{ icon: 'menu-bars', name: 'Grid view', desc: 'Dense dashboards' },
|
|
39
|
+
{ icon: 'sort', name: 'List view', desc: 'Feeds & digests' },
|
|
40
|
+
{ icon: 'folder', name: 'Files', desc: 'Trees & tables' },
|
|
41
|
+
{ icon: 'calendar', name: 'Calendar', desc: 'Date-driven UI' },
|
|
42
|
+
{ icon: 'clock', name: 'Timeline', desc: 'Activity feeds' },
|
|
43
|
+
{ icon: 'check-circle', name: 'Forms', desc: 'zod-validated' },
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
const railItems = RAIL_CARDS.map((c) => (
|
|
47
|
+
<div key={c.name} className="h-40 px-1.5">
|
|
48
|
+
<div className="flex h-full flex-col items-start justify-between rounded-xl border border-line bg-s1 p-5">
|
|
49
|
+
<span aria-hidden="true" className="grid size-8 place-items-center rounded-lg bg-accent/10">
|
|
50
|
+
<Icon name={c.icon} size={16} className="text-accent" />
|
|
51
|
+
</span>
|
|
52
|
+
<div className="flex flex-col items-start gap-0.5">
|
|
53
|
+
<Text strong>{c.name}</Text>
|
|
54
|
+
<Text type="secondary">{c.desc}</Text>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
));
|
|
59
|
+
|
|
60
|
+
/** Route gợi ý cho engine: '/carousel'. */
|
|
61
|
+
export function CarouselDemo() {
|
|
62
|
+
return (
|
|
63
|
+
<div className="flex flex-col gap-8">
|
|
64
|
+
<div className="flex flex-col gap-1.5">
|
|
65
|
+
<span className={EYEBROW}>Extra · swiper</span>
|
|
66
|
+
<Title level={1}>Carousel, two ways</Title>
|
|
67
|
+
<div className="max-w-2xl">
|
|
68
|
+
<Text type="secondary">
|
|
69
|
+
Swiper-powered — a full-bleed hero loop and a card rail with 3 slides per view. Use ← → keys on the
|
|
70
|
+
hero; the rail navigates with the side arrows.
|
|
71
|
+
</Text>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<Carousel
|
|
76
|
+
autoplay
|
|
77
|
+
autoplayInterval={3500}
|
|
78
|
+
circular
|
|
79
|
+
indicators
|
|
80
|
+
items={heroItems}
|
|
81
|
+
keyboard
|
|
82
|
+
navigators
|
|
83
|
+
pauseOnHover
|
|
84
|
+
height={256}
|
|
85
|
+
/>
|
|
86
|
+
|
|
87
|
+
<div className="flex flex-col gap-3">
|
|
88
|
+
<div className="flex items-center justify-between">
|
|
89
|
+
<Text strong>Component rail</Text>
|
|
90
|
+
<Tag>3 per view</Tag>
|
|
91
|
+
</div>
|
|
92
|
+
{/* px-10 nhường không gian cho nút navigator absolute (left-2, 26px) */}
|
|
93
|
+
<div className="px-10">
|
|
94
|
+
<Carousel items={railItems} itemsPerView={3} indicators={false} navigators height={160} />
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// Overlay EXTRA: chart — trang demo <Chart> qua subpath '@wakii/ui-kit/chart'.
|
|
2
|
+
// ⚠️ PEER DEP: "chart.js": "^4.5.1" (kit lazy-import; thiếu → fallback, không crash).
|
|
3
|
+
// Redesign 2026-10-25: 3 chart (mixed bar+line / radar / line area) + KPI strip,
|
|
4
|
+
// màu mặc định kit tự resolve theo theme light/dark.
|
|
5
|
+
import { Card, CountUp, Statistic, Text, Title } from '@wakii/ui-kit';
|
|
6
|
+
import { Chart } from '@wakii/ui-kit/chart';
|
|
7
|
+
|
|
8
|
+
const EYEBROW = 'text-label font-semibold uppercase tracking-[0.1em] text-accent';
|
|
9
|
+
|
|
10
|
+
/* Flat config — chart.js v4 qua wrapper kit. animation:false — render đồng bộ.
|
|
11
|
+
* Bỏ qua backgroundColor/borderColor → kit gán màu token theo theme (live theo
|
|
12
|
+
* dark toggle nhờ MutationObserver trong useChartTheme). */
|
|
13
|
+
const THROUGHPUT = {
|
|
14
|
+
type: 'bar' as const,
|
|
15
|
+
labels: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
|
|
16
|
+
datasets: [
|
|
17
|
+
{ type: 'line' as const, label: 'Tasks completed', data: [12, 19, 8, 22, 30, 25, 18], tension: 0.35, fill: true },
|
|
18
|
+
{ type: 'bar' as const, label: 'Reviews', data: [4, 7, 3, 9, 12, 8, 6] },
|
|
19
|
+
],
|
|
20
|
+
options: { responsive: true, maintainAspectRatio: false, animation: false },
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const FOCUS = {
|
|
24
|
+
type: 'radar' as const,
|
|
25
|
+
labels: ['Speed', 'Quality', 'Docs', 'Tests', 'Focus', 'Collab'],
|
|
26
|
+
datasets: [
|
|
27
|
+
{ label: 'This sprint', data: [7, 8.5, 6, 9, 7.5, 8] },
|
|
28
|
+
{ label: 'Last sprint', data: [6, 7.5, 5, 7, 8, 6.5] },
|
|
29
|
+
],
|
|
30
|
+
options: { responsive: true, maintainAspectRatio: false, animation: false },
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const BURNDOWN = {
|
|
34
|
+
type: 'line' as const,
|
|
35
|
+
labels: ['S', 'M', 'T', 'W', 'T', 'F', 'S', 'S', 'M', 'T'],
|
|
36
|
+
datasets: [
|
|
37
|
+
{ label: 'Open tasks', data: [40, 36, 33, 27, 22, 18, 15, 11, 6, 0], tension: 0.35, fill: true },
|
|
38
|
+
{ label: 'Ideal', data: [40, 36, 32, 28, 24, 20, 16, 12, 8, 4, 0], borderDash: [6, 4], fill: false },
|
|
39
|
+
],
|
|
40
|
+
options: { responsive: true, maintainAspectRatio: false, animation: false },
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/** Route gợi ý cho engine: '/chart'. */
|
|
44
|
+
export function ChartDemo() {
|
|
45
|
+
return (
|
|
46
|
+
<div className="flex flex-col gap-8">
|
|
47
|
+
<div className="flex flex-col gap-1.5">
|
|
48
|
+
<span className={EYEBROW}>Extra · chart.js</span>
|
|
49
|
+
<Title level={1}>Charts that follow your theme</Title>
|
|
50
|
+
<div className="max-w-2xl">
|
|
51
|
+
<Text type="secondary">
|
|
52
|
+
chart.js v4 wrapped by the kit — mixed datasets, radar and area lines. Colors resolve from design
|
|
53
|
+
tokens and re-render live when you flip light/dark.
|
|
54
|
+
</Text>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<div className="grid grid-cols-3 gap-8 rounded-xl border border-line bg-s1 px-8 py-6 shadow-[var(--shadow-lift)] max-sm:grid-cols-1 max-sm:gap-6">
|
|
59
|
+
<Statistic title="Tasks shipped" value={<CountUp value={134} />} />
|
|
60
|
+
<Statistic title="Review rate" value={<CountUp value={92} />} suffix="%" />
|
|
61
|
+
<Statistic title="Avg cycle time" value={<CountUp value={2.4} decimals={1} />} suffix="d" />
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<div className="grid gap-4 md:grid-cols-2">
|
|
65
|
+
<Card title="Weekly throughput">
|
|
66
|
+
<div className="h-64">
|
|
67
|
+
<Chart config={THROUGHPUT} height={256} />
|
|
68
|
+
</div>
|
|
69
|
+
</Card>
|
|
70
|
+
<Card title="Team focus — this vs last sprint">
|
|
71
|
+
<div className="h-64">
|
|
72
|
+
<Chart config={FOCUS} height={256} />
|
|
73
|
+
</div>
|
|
74
|
+
</Card>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<Card title="Release burndown">
|
|
78
|
+
<div className="h-56">
|
|
79
|
+
<Chart config={BURNDOWN} height={224} />
|
|
80
|
+
</div>
|
|
81
|
+
</Card>
|
|
82
|
+
</div>
|
|
83
|
+
);
|
|
84
|
+
}
|