create-cronus-app 0.6.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/LICENSE +21 -0
- package/README.md +173 -0
- package/dist/compose.d.ts +56 -0
- package/dist/compose.js +92 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +255 -0
- package/dist/scaffold.d.ts +31 -0
- package/dist/scaffold.js +118 -0
- package/dist/utils.d.ts +121 -0
- package/dist/utils.js +285 -0
- package/dist/version.d.ts +3 -0
- package/dist/version.js +3 -0
- package/package.json +56 -0
- package/templates/dashboard/README.md +61 -0
- package/templates/dashboard/app/globals.css +16 -0
- package/templates/dashboard/app/layout.tsx +40 -0
- package/templates/dashboard/app/page.tsx +77 -0
- package/templates/dashboard/app/settings/page.tsx +21 -0
- package/templates/dashboard/components/dashboard-shell.tsx +120 -0
- package/templates/dashboard/components/orders-table.tsx +132 -0
- package/templates/dashboard/components/revenue-chart.tsx +50 -0
- package/templates/dashboard/components/settings-form.tsx +167 -0
- package/templates/dashboard/cronus-ui.json +17 -0
- package/templates/dashboard/gitignore +34 -0
- package/templates/dashboard/next.config.mjs +11 -0
- package/templates/dashboard/package.json +30 -0
- package/templates/dashboard/postcss.config.mjs +7 -0
- package/templates/dashboard/tsconfig.json +23 -0
- package/templates/default/README.md +46 -0
- package/templates/default/app/globals.css +16 -0
- package/templates/default/app/layout.tsx +39 -0
- package/templates/default/app/page.tsx +112 -0
- package/templates/default/cronus-ui.json +17 -0
- package/templates/default/gitignore +34 -0
- package/templates/default/next.config.mjs +11 -0
- package/templates/default/package.json +27 -0
- package/templates/default/postcss.config.mjs +7 -0
- package/templates/default/tsconfig.json +23 -0
- package/templates/marketing/README.md +65 -0
- package/templates/marketing/app/globals.css +16 -0
- package/templates/marketing/app/layout.tsx +39 -0
- package/templates/marketing/app/page.tsx +25 -0
- package/templates/marketing/components/faq.tsx +62 -0
- package/templates/marketing/components/feature-grid.tsx +67 -0
- package/templates/marketing/components/hero.tsx +61 -0
- package/templates/marketing/components/pricing.tsx +116 -0
- package/templates/marketing/components/site-footer.tsx +91 -0
- package/templates/marketing/components/site-header.tsx +47 -0
- package/templates/marketing/components/testimonials.tsx +91 -0
- package/templates/marketing/components/waitlist-cta.tsx +93 -0
- package/templates/marketing/cronus-ui.json +17 -0
- package/templates/marketing/gitignore +34 -0
- package/templates/marketing/next.config.mjs +11 -0
- package/templates/marketing/package.json +28 -0
- package/templates/marketing/postcss.config.mjs +7 -0
- package/templates/marketing/tsconfig.json +23 -0
package/dist/scaffold.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync, } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { DEFAULT_MODE, DEFAULT_TEMPLATE, DEFAULT_THEME, templateBaseDir, } from "./utils.js";
|
|
6
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
/**
|
|
8
|
+
* Locate the bundled dir for `template` under `templates/`. In the published
|
|
9
|
+
* package, sources live in `dist/` and templates are a sibling (`../templates`).
|
|
10
|
+
* When running from source (tests/ts-node), they're one level up from `src/` as
|
|
11
|
+
* well.
|
|
12
|
+
*/
|
|
13
|
+
function templateRoot(template) {
|
|
14
|
+
const candidates = [
|
|
15
|
+
join(HERE, "..", "templates", template),
|
|
16
|
+
join(HERE, "..", "..", "templates", template),
|
|
17
|
+
];
|
|
18
|
+
const found = candidates.find((p) => existsSync(p));
|
|
19
|
+
if (!found) {
|
|
20
|
+
throw new Error(`Could not locate the bundled "${template}" template (looked in: ${candidates.join(", ")}).`);
|
|
21
|
+
}
|
|
22
|
+
return found;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Files whose dot-prefixed name npm strips when packing a template. We store
|
|
26
|
+
* them undotted in the template and restore the leading "." on scaffold.
|
|
27
|
+
* `gitignore` → `.gitignore`; a `_`-prefixed file → its dotted form.
|
|
28
|
+
*/
|
|
29
|
+
const DOTFILE_RENAMES = {
|
|
30
|
+
gitignore: ".gitignore",
|
|
31
|
+
npmrc: ".npmrc",
|
|
32
|
+
};
|
|
33
|
+
function restoreDotfileName(base) {
|
|
34
|
+
if (base in DOTFILE_RENAMES)
|
|
35
|
+
return DOTFILE_RENAMES[base];
|
|
36
|
+
if (base.startsWith("_") && base.length > 1)
|
|
37
|
+
return `.${base.slice(1)}`;
|
|
38
|
+
return base;
|
|
39
|
+
}
|
|
40
|
+
/** Text extensions that get token replacement. Everything else is copied raw. */
|
|
41
|
+
const TEXT_EXTENSIONS = new Set([
|
|
42
|
+
".ts",
|
|
43
|
+
".tsx",
|
|
44
|
+
".js",
|
|
45
|
+
".jsx",
|
|
46
|
+
".mjs",
|
|
47
|
+
".cjs",
|
|
48
|
+
".json",
|
|
49
|
+
".css",
|
|
50
|
+
".md",
|
|
51
|
+
".txt",
|
|
52
|
+
".html",
|
|
53
|
+
]);
|
|
54
|
+
function isTextFile(path) {
|
|
55
|
+
const dot = path.lastIndexOf(".");
|
|
56
|
+
return dot !== -1 && TEXT_EXTENSIONS.has(path.slice(dot));
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Replace template tokens. `__APP_NAME__` is the package name; `__THEME__` /
|
|
60
|
+
* `__MODE__` are the chosen theme preset + color mode (baked into layout.tsx and
|
|
61
|
+
* cronus-ui.json). Theme/mode default to the ship defaults so existing callers
|
|
62
|
+
* (and tests) that pass only a name keep working.
|
|
63
|
+
*/
|
|
64
|
+
export function applyTokens(content, name, theme = DEFAULT_THEME, mode = DEFAULT_MODE) {
|
|
65
|
+
return content
|
|
66
|
+
.replaceAll("__APP_NAME__", name)
|
|
67
|
+
.replaceAll("__THEME__", theme)
|
|
68
|
+
.replaceAll("__MODE__", mode);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Recursively copy the chosen template into `targetDir`, replacing tokens in
|
|
72
|
+
* text files and restoring stripped dotfile names. Returns the file count.
|
|
73
|
+
*/
|
|
74
|
+
export function scaffold(options) {
|
|
75
|
+
// Composed templates (store/landing) have no bundled dir — copy the base they
|
|
76
|
+
// build on (`default`), then the post-scaffold composeApp() step generates the
|
|
77
|
+
// pages/chrome from validated blocks.
|
|
78
|
+
const src = templateRoot(templateBaseDir(options.template ?? DEFAULT_TEMPLATE));
|
|
79
|
+
const { targetDir, name, theme = DEFAULT_THEME, mode = DEFAULT_MODE } = options;
|
|
80
|
+
mkdirSync(targetDir, { recursive: true });
|
|
81
|
+
const fileCount = copyDir(src, targetDir, name, theme, mode);
|
|
82
|
+
return { fileCount };
|
|
83
|
+
}
|
|
84
|
+
function copyDir(srcDir, destDir, name, theme, mode) {
|
|
85
|
+
mkdirSync(destDir, { recursive: true });
|
|
86
|
+
let count = 0;
|
|
87
|
+
for (const entry of readdirSync(srcDir)) {
|
|
88
|
+
const srcPath = join(srcDir, entry);
|
|
89
|
+
const destPath = join(destDir, restoreDotfileName(entry));
|
|
90
|
+
if (statSync(srcPath).isDirectory()) {
|
|
91
|
+
count += copyDir(srcPath, destPath, name, theme, mode);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (isTextFile(srcPath)) {
|
|
95
|
+
const content = applyTokens(readFileSync(srcPath, "utf8"), name, theme, mode);
|
|
96
|
+
writeFileSync(destPath, content);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
cpSync(srcPath, destPath);
|
|
100
|
+
}
|
|
101
|
+
count += 1;
|
|
102
|
+
}
|
|
103
|
+
return count;
|
|
104
|
+
}
|
|
105
|
+
/** Run the package manager's install in `cwd`. Throws if it exits non-zero. */
|
|
106
|
+
export function runInstall(pm, cwd) {
|
|
107
|
+
const result = spawnSync(pm, ["install"], {
|
|
108
|
+
cwd,
|
|
109
|
+
stdio: "inherit",
|
|
110
|
+
shell: process.platform === "win32",
|
|
111
|
+
});
|
|
112
|
+
if (result.error)
|
|
113
|
+
throw result.error;
|
|
114
|
+
if (typeof result.status === "number" && result.status !== 0) {
|
|
115
|
+
throw new Error(`${pm} install exited with code ${result.status}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=scaffold.js.map
|
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/** Supported package managers, in detection-fallback order. */
|
|
2
|
+
export declare const PACKAGE_MANAGERS: readonly ["bun", "npm", "pnpm", "yarn"];
|
|
3
|
+
export type PackageManager = (typeof PACKAGE_MANAGERS)[number];
|
|
4
|
+
export declare const c: {
|
|
5
|
+
bold: (s: string) => string;
|
|
6
|
+
dim: (s: string) => string;
|
|
7
|
+
green: (s: string) => string;
|
|
8
|
+
yellow: (s: string) => string;
|
|
9
|
+
red: (s: string) => string;
|
|
10
|
+
cyan: (s: string) => string;
|
|
11
|
+
magenta: (s: string) => string;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Next-step lines printed after a successful scaffold. Pure so tests can assert
|
|
15
|
+
* the copy without scraping stdout. `template` is optional for back-compat: omit
|
|
16
|
+
* it (or pass a bundled template) to keep the component-add path; composed
|
|
17
|
+
* templates (`saas`/`store`/`landing`) get add-page / theme / upgrade instead.
|
|
18
|
+
*/
|
|
19
|
+
export declare function outroLines(name: string, pm: PackageManager, installed: boolean, template?: TemplateName): string[];
|
|
20
|
+
export declare const log: {
|
|
21
|
+
intro(): void;
|
|
22
|
+
step(msg: string): void;
|
|
23
|
+
ok(msg: string): void;
|
|
24
|
+
warn(msg: string): void;
|
|
25
|
+
error(msg: string): void;
|
|
26
|
+
outro(name: string, pm: PackageManager, installed: boolean, template?: TemplateName): void;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* The starter templates a user can pick. `default`/`dashboard`/`marketing` copy
|
|
30
|
+
* a bundled directory under `templates/`; `store`/`landing`/`saas` have NO
|
|
31
|
+
* template dir — they scaffold the `default` base and then compose validated
|
|
32
|
+
* blocks on top (see {@link TEMPLATE_BASE_DIR} and the post-scaffold
|
|
33
|
+
* `composeApp()` step).
|
|
34
|
+
*/
|
|
35
|
+
/**
|
|
36
|
+
* Product landing flavors (compose apps). Each is a distinctive stack of
|
|
37
|
+
* validated marketing blocks + a theme/mode default — not a bundled directory.
|
|
38
|
+
* `landing` stays the generic marketing page; these are the named flavors.
|
|
39
|
+
*/
|
|
40
|
+
export declare const LANDING_FLAVORS: readonly ["landing-studio", "landing-ops", "landing-secure", "landing-care", "landing-shop", "landing-docs", "landing-premium", "landing-agents", "landing-coverage", "landing-broadcast", "landing-agency", "landing-glass"];
|
|
41
|
+
export type LandingFlavor = (typeof LANDING_FLAVORS)[number];
|
|
42
|
+
/**
|
|
43
|
+
* Pro compose apps. Additive product surfaces (mail, chat, finance) — not
|
|
44
|
+
* landings. OSS keeps saas/store/landing; these are the Pro pack.
|
|
45
|
+
*/
|
|
46
|
+
export declare const PRO_APPS: readonly ["mail", "chat", "finance"];
|
|
47
|
+
export type ProApp = (typeof PRO_APPS)[number];
|
|
48
|
+
export declare const TEMPLATES: readonly ["default", "dashboard", "marketing", "store", "landing", "saas", "landing-studio", "landing-ops", "landing-secure", "landing-care", "landing-shop", "landing-docs", "landing-premium", "landing-agents", "landing-coverage", "landing-broadcast", "landing-agency", "landing-glass", "mail", "chat", "finance"];
|
|
49
|
+
export type TemplateName = (typeof TEMPLATES)[number];
|
|
50
|
+
export declare const DEFAULT_TEMPLATE: TemplateName;
|
|
51
|
+
/**
|
|
52
|
+
* The template names generated by the Cronus Compose app generator (no bundled
|
|
53
|
+
* dir): they scaffold the `default` base, then `composeApp()` from the bundled
|
|
54
|
+
* manifest renders the pages + chrome from validated blocks. The value is the
|
|
55
|
+
* manifest/template name passed to `composeApp`.
|
|
56
|
+
*/
|
|
57
|
+
export declare const COMPOSED_TEMPLATES: {
|
|
58
|
+
readonly store: "store";
|
|
59
|
+
readonly landing: "landing";
|
|
60
|
+
readonly saas: "saas";
|
|
61
|
+
readonly "landing-studio": "landing-studio";
|
|
62
|
+
readonly "landing-ops": "landing-ops";
|
|
63
|
+
readonly "landing-secure": "landing-secure";
|
|
64
|
+
readonly "landing-care": "landing-care";
|
|
65
|
+
readonly "landing-shop": "landing-shop";
|
|
66
|
+
readonly "landing-docs": "landing-docs";
|
|
67
|
+
readonly "landing-premium": "landing-premium";
|
|
68
|
+
readonly "landing-agents": "landing-agents";
|
|
69
|
+
readonly "landing-coverage": "landing-coverage";
|
|
70
|
+
readonly "landing-broadcast": "landing-broadcast";
|
|
71
|
+
readonly "landing-agency": "landing-agency";
|
|
72
|
+
readonly "landing-glass": "landing-glass";
|
|
73
|
+
readonly mail: "mail";
|
|
74
|
+
readonly chat: "chat";
|
|
75
|
+
readonly finance: "finance";
|
|
76
|
+
};
|
|
77
|
+
export type ComposedTemplateName = keyof typeof COMPOSED_TEMPLATES;
|
|
78
|
+
/** True when `template` is generated by the composer (no bundled dir). */
|
|
79
|
+
export declare function isComposedTemplate(template: TemplateName): template is ComposedTemplateName;
|
|
80
|
+
/**
|
|
81
|
+
* The bundled base directory each template copies before any composition.
|
|
82
|
+
* Composed templates (`store`/`landing`/`saas`) reuse the `default` base; the
|
|
83
|
+
* others are their own name.
|
|
84
|
+
*/
|
|
85
|
+
export declare function templateBaseDir(template: TemplateName): TemplateName;
|
|
86
|
+
/** One-line summary per template, shown in the picker. */
|
|
87
|
+
export declare const TEMPLATE_HINTS: Record<TemplateName, string>;
|
|
88
|
+
/** The five shipped theme presets (must match @cronus-ui/tokens `ThemeName`). */
|
|
89
|
+
export declare const THEMES: readonly ["aurora", "neutral", "midnight", "sunset", "emerald"];
|
|
90
|
+
export type Theme = (typeof THEMES)[number];
|
|
91
|
+
export declare const DEFAULT_THEME: Theme;
|
|
92
|
+
/** One-line vibe per theme, shown in the picker. */
|
|
93
|
+
export declare const THEME_HINTS: Record<Theme, string>;
|
|
94
|
+
/** Color modes the provider supports natively (no built-in "system"). */
|
|
95
|
+
export declare const MODES: readonly ["dark", "light"];
|
|
96
|
+
export type ModeName = (typeof MODES)[number];
|
|
97
|
+
export declare const DEFAULT_MODE: ModeName;
|
|
98
|
+
/** Theme/mode baked when the user picks a landing flavor without `--theme`/`--mode`. */
|
|
99
|
+
export declare const TEMPLATE_APPEARANCE: Partial<Record<TemplateName, {
|
|
100
|
+
theme: Theme;
|
|
101
|
+
mode: ModeName;
|
|
102
|
+
}>>;
|
|
103
|
+
/**
|
|
104
|
+
* A single-select TTY prompt: prints a numbered list and returns the option the
|
|
105
|
+
* user picks (by number or name), the default on empty input, or the default
|
|
106
|
+
* verbatim when stdin is not a TTY (piped/CI) so scaffolding never blocks.
|
|
107
|
+
*/
|
|
108
|
+
export declare function promptSelect<T extends string>(label: string, options: readonly T[], defaultValue: T, hints?: Partial<Record<T, string>>): Promise<T>;
|
|
109
|
+
/**
|
|
110
|
+
* A yes/no TTY prompt. Returns `defaultValue` on empty input or when stdin is
|
|
111
|
+
* not a TTY (piped/CI), so scaffolding never blocks on a confirmation.
|
|
112
|
+
*/
|
|
113
|
+
export declare function promptConfirm(label: string, defaultValue?: boolean): Promise<boolean>;
|
|
114
|
+
/** Validate that `name` is safe to use as an npm package name and a dir name. */
|
|
115
|
+
export declare function isValidProjectName(name: string): boolean;
|
|
116
|
+
/**
|
|
117
|
+
* Derive the on-disk directory name from a (possibly scoped) package name:
|
|
118
|
+
* "@acme/widget" → "widget", "my-app" → "my-app".
|
|
119
|
+
*/
|
|
120
|
+
export declare function dirNameFromProjectName(name: string): string;
|
|
121
|
+
//# sourceMappingURL=utils.d.ts.map
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/** Supported package managers, in detection-fallback order. */
|
|
2
|
+
export const PACKAGE_MANAGERS = ["bun", "npm", "pnpm", "yarn"];
|
|
3
|
+
// Minimal ANSI helpers — no dependency needed. Colors are dropped when stdout
|
|
4
|
+
// is not a TTY or NO_COLOR is set, so piped/CI output stays clean.
|
|
5
|
+
const useColor = Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;
|
|
6
|
+
const wrap = (open, close) => (s) => useColor ? `[${open}m${s}[${close}m` : s;
|
|
7
|
+
export const c = {
|
|
8
|
+
bold: wrap(1, 22),
|
|
9
|
+
dim: wrap(2, 22),
|
|
10
|
+
green: wrap(32, 39),
|
|
11
|
+
yellow: wrap(33, 39),
|
|
12
|
+
red: wrap(31, 39),
|
|
13
|
+
cyan: wrap(36, 39),
|
|
14
|
+
magenta: wrap(35, 39),
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Next-step lines printed after a successful scaffold. Pure so tests can assert
|
|
18
|
+
* the copy without scraping stdout. `template` is optional for back-compat: omit
|
|
19
|
+
* it (or pass a bundled template) to keep the component-add path; composed
|
|
20
|
+
* templates (`saas`/`store`/`landing`) get add-page / theme / upgrade instead.
|
|
21
|
+
*/
|
|
22
|
+
export function outroLines(name, pm, installed, template) {
|
|
23
|
+
const dev = pm === "npm" ? "npm run dev" : `${pm} dev`;
|
|
24
|
+
const install = pm === "yarn" ? "yarn" : `${pm} install`;
|
|
25
|
+
const dollar = c.dim("$");
|
|
26
|
+
const composed = template !== undefined && isComposedTemplate(template);
|
|
27
|
+
const grow = composed
|
|
28
|
+
? [
|
|
29
|
+
"Grow the app anytime:",
|
|
30
|
+
` ${dollar} npx cronus-ui add-page --route /pricing --blocks pricing,cta --nav Pricing`,
|
|
31
|
+
` ${dollar} npx cronus-ui theme set aurora --mode dark`,
|
|
32
|
+
` ${dollar} npx cronus-ui upgrade --all --dry-run`,
|
|
33
|
+
]
|
|
34
|
+
: [
|
|
35
|
+
`Add more components anytime: ${dollar} npx cronus-ui add dialog table tabs`,
|
|
36
|
+
`Want a multi-page app? ${dollar} npx cronus-ui compose saas`,
|
|
37
|
+
];
|
|
38
|
+
return [
|
|
39
|
+
"",
|
|
40
|
+
`${c.green(c.bold("Done!"))} Your Cronus UI app is ready in ${c.cyan(name)}.`,
|
|
41
|
+
"",
|
|
42
|
+
"Next steps:",
|
|
43
|
+
` ${dollar} cd ${name}`,
|
|
44
|
+
...(installed ? [] : [` ${dollar} ${install}`]),
|
|
45
|
+
` ${dollar} ${dev}`,
|
|
46
|
+
"",
|
|
47
|
+
`Then open ${c.cyan("http://localhost:3000")}.`,
|
|
48
|
+
"",
|
|
49
|
+
...grow,
|
|
50
|
+
"",
|
|
51
|
+
];
|
|
52
|
+
}
|
|
53
|
+
export const log = {
|
|
54
|
+
intro() {
|
|
55
|
+
process.stdout.write(`\n${c.magenta(c.bold("create-cronus-app"))}\n\n`);
|
|
56
|
+
},
|
|
57
|
+
step(msg) {
|
|
58
|
+
process.stdout.write(`${c.cyan("›")} ${msg}\n`);
|
|
59
|
+
},
|
|
60
|
+
ok(msg) {
|
|
61
|
+
process.stdout.write(`${c.green("✓")} ${msg}\n`);
|
|
62
|
+
},
|
|
63
|
+
warn(msg) {
|
|
64
|
+
process.stdout.write(`${c.yellow("!")} ${msg}\n`);
|
|
65
|
+
},
|
|
66
|
+
error(msg) {
|
|
67
|
+
process.stderr.write(`${c.red("✗")} ${msg}\n`);
|
|
68
|
+
},
|
|
69
|
+
outro(name, pm, installed, template) {
|
|
70
|
+
process.stdout.write(`${outroLines(name, pm, installed, template).join("\n")}\n`);
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* The starter templates a user can pick. `default`/`dashboard`/`marketing` copy
|
|
75
|
+
* a bundled directory under `templates/`; `store`/`landing`/`saas` have NO
|
|
76
|
+
* template dir — they scaffold the `default` base and then compose validated
|
|
77
|
+
* blocks on top (see {@link TEMPLATE_BASE_DIR} and the post-scaffold
|
|
78
|
+
* `composeApp()` step).
|
|
79
|
+
*/
|
|
80
|
+
/**
|
|
81
|
+
* Product landing flavors (compose apps). Each is a distinctive stack of
|
|
82
|
+
* validated marketing blocks + a theme/mode default — not a bundled directory.
|
|
83
|
+
* `landing` stays the generic marketing page; these are the named flavors.
|
|
84
|
+
*/
|
|
85
|
+
export const LANDING_FLAVORS = [
|
|
86
|
+
"landing-studio",
|
|
87
|
+
"landing-ops",
|
|
88
|
+
"landing-secure",
|
|
89
|
+
"landing-care",
|
|
90
|
+
"landing-shop",
|
|
91
|
+
"landing-docs",
|
|
92
|
+
"landing-premium",
|
|
93
|
+
"landing-agents",
|
|
94
|
+
"landing-coverage",
|
|
95
|
+
"landing-broadcast",
|
|
96
|
+
"landing-agency",
|
|
97
|
+
"landing-glass",
|
|
98
|
+
];
|
|
99
|
+
/**
|
|
100
|
+
* Pro compose apps. Additive product surfaces (mail, chat, finance) — not
|
|
101
|
+
* landings. OSS keeps saas/store/landing; these are the Pro pack.
|
|
102
|
+
*/
|
|
103
|
+
export const PRO_APPS = ["mail", "chat", "finance"];
|
|
104
|
+
export const TEMPLATES = [
|
|
105
|
+
"default",
|
|
106
|
+
"dashboard",
|
|
107
|
+
"marketing",
|
|
108
|
+
"store",
|
|
109
|
+
"landing",
|
|
110
|
+
"saas",
|
|
111
|
+
...LANDING_FLAVORS,
|
|
112
|
+
...PRO_APPS,
|
|
113
|
+
];
|
|
114
|
+
export const DEFAULT_TEMPLATE = "default";
|
|
115
|
+
/**
|
|
116
|
+
* The template names generated by the Cronus Compose app generator (no bundled
|
|
117
|
+
* dir): they scaffold the `default` base, then `composeApp()` from the bundled
|
|
118
|
+
* manifest renders the pages + chrome from validated blocks. The value is the
|
|
119
|
+
* manifest/template name passed to `composeApp`.
|
|
120
|
+
*/
|
|
121
|
+
export const COMPOSED_TEMPLATES = {
|
|
122
|
+
store: "store",
|
|
123
|
+
landing: "landing",
|
|
124
|
+
saas: "saas",
|
|
125
|
+
"landing-studio": "landing-studio",
|
|
126
|
+
"landing-ops": "landing-ops",
|
|
127
|
+
"landing-secure": "landing-secure",
|
|
128
|
+
"landing-care": "landing-care",
|
|
129
|
+
"landing-shop": "landing-shop",
|
|
130
|
+
"landing-docs": "landing-docs",
|
|
131
|
+
"landing-premium": "landing-premium",
|
|
132
|
+
"landing-agents": "landing-agents",
|
|
133
|
+
"landing-coverage": "landing-coverage",
|
|
134
|
+
"landing-broadcast": "landing-broadcast",
|
|
135
|
+
"landing-agency": "landing-agency",
|
|
136
|
+
"landing-glass": "landing-glass",
|
|
137
|
+
mail: "mail",
|
|
138
|
+
chat: "chat",
|
|
139
|
+
finance: "finance",
|
|
140
|
+
};
|
|
141
|
+
/** True when `template` is generated by the composer (no bundled dir). */
|
|
142
|
+
export function isComposedTemplate(template) {
|
|
143
|
+
return template in COMPOSED_TEMPLATES;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* The bundled base directory each template copies before any composition.
|
|
147
|
+
* Composed templates (`store`/`landing`/`saas`) reuse the `default` base; the
|
|
148
|
+
* others are their own name.
|
|
149
|
+
*/
|
|
150
|
+
export function templateBaseDir(template) {
|
|
151
|
+
return isComposedTemplate(template) ? "default" : template;
|
|
152
|
+
}
|
|
153
|
+
/** One-line summary per template, shown in the picker. */
|
|
154
|
+
export const TEMPLATE_HINTS = {
|
|
155
|
+
default: "single-page starter — metrics, table & cards (default)",
|
|
156
|
+
dashboard: "multi-page app — sidebar shell, KPIs, chart, data table, settings",
|
|
157
|
+
marketing: "landing site — hero, features, pricing, testimonials, FAQ, waitlist",
|
|
158
|
+
store: "generated storefront — 9 navigable pages, real nav, from validated blocks",
|
|
159
|
+
landing: "generated landing page — hero, features, pricing, testimonials, FAQ, CTA",
|
|
160
|
+
saas: "recommended for a full product — split auth + shell dashboard, team, billing, settings",
|
|
161
|
+
"landing-studio": "dark AI studio landing — atmosphere hero, marquee, bento, stats, pricing",
|
|
162
|
+
"landing-ops": "ops/workflow landing — split hero, logos, features, integrations",
|
|
163
|
+
"landing-secure": "security/infra landing — compact hero, metrics, usage pricing, split FAQ",
|
|
164
|
+
"landing-care": "healthcare/conversion landing — waitlist hero, bento, proof (emerald light)",
|
|
165
|
+
"landing-shop": "storefront landing — compact hero + editorial product showcase (sunset light)",
|
|
166
|
+
"landing-docs": "developer-tool landing — compact hero, logos, features, integrations",
|
|
167
|
+
"landing-premium": "full SaaS marketing page — split hero, toggle pricing, FAQ (aurora light)",
|
|
168
|
+
"landing-agents": "automation landing — split hero, marquee, bento, stats (emerald light)",
|
|
169
|
+
"landing-coverage": "services landing — hero, stats, testimonial grid, FAQ (sunset light)",
|
|
170
|
+
"landing-broadcast": "studio/show landing — atmosphere hero, marquee logos, features, pricing",
|
|
171
|
+
"landing-agency": "agency landing — split hero, about, services, stats (midnight dark)",
|
|
172
|
+
"landing-glass": "glass-dark landing — atmosphere hero, bento, split FAQ, split CTA",
|
|
173
|
+
mail: "Pro inbox — notification panel, activity feed, compose, preferences (midnight dark)",
|
|
174
|
+
chat: "Pro assistant — chat thread, prompt box, replies, settings (aurora dark)",
|
|
175
|
+
finance: "Pro money app — payouts, invoices, billing, analytics (emerald light)",
|
|
176
|
+
};
|
|
177
|
+
/** The five shipped theme presets (must match @cronus-ui/tokens `ThemeName`). */
|
|
178
|
+
export const THEMES = ["aurora", "neutral", "midnight", "sunset", "emerald"];
|
|
179
|
+
export const DEFAULT_THEME = "aurora";
|
|
180
|
+
/** One-line vibe per theme, shown in the picker. */
|
|
181
|
+
export const THEME_HINTS = {
|
|
182
|
+
aurora: "sky → cyan premium gradient (default)",
|
|
183
|
+
neutral: "clean black & white — docs-site chrome, achromatic",
|
|
184
|
+
midnight: "deep indigo / violet",
|
|
185
|
+
sunset: "warm amber / rose",
|
|
186
|
+
emerald: "fresh green / teal",
|
|
187
|
+
};
|
|
188
|
+
/** Color modes the provider supports natively (no built-in "system"). */
|
|
189
|
+
export const MODES = ["dark", "light"];
|
|
190
|
+
export const DEFAULT_MODE = "dark";
|
|
191
|
+
/** Theme/mode baked when the user picks a landing flavor without `--theme`/`--mode`. */
|
|
192
|
+
export const TEMPLATE_APPEARANCE = {
|
|
193
|
+
"landing-studio": { theme: "midnight", mode: "dark" },
|
|
194
|
+
"landing-ops": { theme: "aurora", mode: "dark" },
|
|
195
|
+
"landing-secure": { theme: "midnight", mode: "dark" },
|
|
196
|
+
"landing-care": { theme: "emerald", mode: "light" },
|
|
197
|
+
"landing-shop": { theme: "sunset", mode: "light" },
|
|
198
|
+
"landing-docs": { theme: "aurora", mode: "dark" },
|
|
199
|
+
"landing-premium": { theme: "aurora", mode: "light" },
|
|
200
|
+
"landing-agents": { theme: "emerald", mode: "light" },
|
|
201
|
+
"landing-coverage": { theme: "sunset", mode: "light" },
|
|
202
|
+
"landing-broadcast": { theme: "aurora", mode: "dark" },
|
|
203
|
+
"landing-agency": { theme: "midnight", mode: "dark" },
|
|
204
|
+
"landing-glass": { theme: "midnight", mode: "dark" },
|
|
205
|
+
mail: { theme: "midnight", mode: "dark" },
|
|
206
|
+
chat: { theme: "aurora", mode: "dark" },
|
|
207
|
+
finance: { theme: "emerald", mode: "light" },
|
|
208
|
+
};
|
|
209
|
+
/**
|
|
210
|
+
* A single-select TTY prompt: prints a numbered list and returns the option the
|
|
211
|
+
* user picks (by number or name), the default on empty input, or the default
|
|
212
|
+
* verbatim when stdin is not a TTY (piped/CI) so scaffolding never blocks.
|
|
213
|
+
*/
|
|
214
|
+
export async function promptSelect(label, options, defaultValue, hints) {
|
|
215
|
+
if (!process.stdin.isTTY)
|
|
216
|
+
return defaultValue;
|
|
217
|
+
const { createInterface } = await import("node:readline/promises");
|
|
218
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
219
|
+
try {
|
|
220
|
+
process.stdout.write(`${c.bold(label)}\n`);
|
|
221
|
+
options.forEach((opt, i) => {
|
|
222
|
+
const marker = opt === defaultValue ? c.green("●") : c.dim("○");
|
|
223
|
+
const hint = hints?.[opt] ? ` ${c.dim(hints[opt])}` : "";
|
|
224
|
+
process.stdout.write(` ${marker} ${c.cyan(String(i + 1))} ${opt}${hint}\n`);
|
|
225
|
+
});
|
|
226
|
+
const answer = (await rl.question(`${c.dim(`(1-${options.length} or name, default ${defaultValue})`)} `)).trim();
|
|
227
|
+
if (!answer)
|
|
228
|
+
return defaultValue;
|
|
229
|
+
const asNum = Number.parseInt(answer, 10);
|
|
230
|
+
if (Number.isInteger(asNum) && asNum >= 1 && asNum <= options.length) {
|
|
231
|
+
return options[asNum - 1];
|
|
232
|
+
}
|
|
233
|
+
const byName = options.find((o) => o === answer.toLowerCase());
|
|
234
|
+
return byName ?? defaultValue;
|
|
235
|
+
}
|
|
236
|
+
finally {
|
|
237
|
+
rl.close();
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* A yes/no TTY prompt. Returns `defaultValue` on empty input or when stdin is
|
|
242
|
+
* not a TTY (piped/CI), so scaffolding never blocks on a confirmation.
|
|
243
|
+
*/
|
|
244
|
+
export async function promptConfirm(label, defaultValue = true) {
|
|
245
|
+
if (!process.stdin.isTTY)
|
|
246
|
+
return defaultValue;
|
|
247
|
+
const { createInterface } = await import("node:readline/promises");
|
|
248
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
249
|
+
try {
|
|
250
|
+
const hint = defaultValue ? "Y/n" : "y/N";
|
|
251
|
+
const answer = (await rl.question(`${c.bold(label)} ${c.dim(`(${hint})`)} `))
|
|
252
|
+
.trim()
|
|
253
|
+
.toLowerCase();
|
|
254
|
+
if (!answer)
|
|
255
|
+
return defaultValue;
|
|
256
|
+
return answer === "y" || answer === "yes";
|
|
257
|
+
}
|
|
258
|
+
finally {
|
|
259
|
+
rl.close();
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
// npm's package-name rules (the spec subset we care about): optionally scoped,
|
|
263
|
+
// lowercase, URL-safe, may not start with "." or "_", <= 214 chars.
|
|
264
|
+
const SCOPED = /^(?:@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
|
|
265
|
+
/** Validate that `name` is safe to use as an npm package name and a dir name. */
|
|
266
|
+
export function isValidProjectName(name) {
|
|
267
|
+
if (!name || name.length > 214)
|
|
268
|
+
return false;
|
|
269
|
+
if (name.trim() !== name)
|
|
270
|
+
return false;
|
|
271
|
+
// Reject path separators outright (a scoped name's single "/" is allowed by
|
|
272
|
+
// the regex, but the literal dir is the unscoped trailing segment).
|
|
273
|
+
if (name.includes("\\"))
|
|
274
|
+
return false;
|
|
275
|
+
return SCOPED.test(name);
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Derive the on-disk directory name from a (possibly scoped) package name:
|
|
279
|
+
* "@acme/widget" → "widget", "my-app" → "my-app".
|
|
280
|
+
*/
|
|
281
|
+
export function dirNameFromProjectName(name) {
|
|
282
|
+
const slash = name.lastIndexOf("/");
|
|
283
|
+
return slash === -1 ? name : name.slice(slash + 1);
|
|
284
|
+
}
|
|
285
|
+
//# sourceMappingURL=utils.js.map
|
package/dist/version.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-cronus-app",
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"description": "Scaffold a Next.js + Cronus UI app: npx create-cronus-app my-app --template saas.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Cronus",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/pedrogbraz/cronus-ui.git",
|
|
11
|
+
"directory": "packages/create-cronus-app"
|
|
12
|
+
},
|
|
13
|
+
"homepage": "https://aicronus.com",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/pedrogbraz/cronus-ui/issues"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"cronus",
|
|
19
|
+
"create-cronus-app",
|
|
20
|
+
"design-system",
|
|
21
|
+
"next",
|
|
22
|
+
"scaffold",
|
|
23
|
+
"starter",
|
|
24
|
+
"tailwind",
|
|
25
|
+
"ui"
|
|
26
|
+
],
|
|
27
|
+
"bin": {
|
|
28
|
+
"create-cronus-app": "./dist/index.js"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"dist",
|
|
32
|
+
"templates",
|
|
33
|
+
"LICENSE",
|
|
34
|
+
"README.md",
|
|
35
|
+
"!dist/**/*.map"
|
|
36
|
+
],
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsc -p tsconfig.json",
|
|
42
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
43
|
+
"prepublishOnly": "tsc -p tsconfig.json"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@cronus-ui/ai-kit": "0.6.0",
|
|
47
|
+
"cronus-ui": "0.6.0"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@types/node": "^22.10.0",
|
|
51
|
+
"typescript": "^6.0.3"
|
|
52
|
+
},
|
|
53
|
+
"engines": {
|
|
54
|
+
"node": ">=20"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# __APP_NAME__
|
|
2
|
+
|
|
3
|
+
A multi-page dashboard built with [Next.js](https://nextjs.org) (App Router) and
|
|
4
|
+
[Cronus UI](https://www.npmjs.com/package/@cronus-ui/ui) — themeable, accessible React
|
|
5
|
+
components on Tailwind v4.
|
|
6
|
+
|
|
7
|
+
> **Screenshot placeholder** — run `npm run dev`, open http://localhost:3000, and drop a
|
|
8
|
+
> capture here (e.g. `docs/screenshot.png`) so your repo landing page shows the app.
|
|
9
|
+
|
|
10
|
+
## Getting started
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
npm install # or: pnpm install / yarn / bun install
|
|
14
|
+
npm run dev
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Open [http://localhost:3000](http://localhost:3000) — the pages hot-reload as you save.
|
|
18
|
+
|
|
19
|
+
## What's inside
|
|
20
|
+
|
|
21
|
+
| Route | File | Contents |
|
|
22
|
+
| --- | --- | --- |
|
|
23
|
+
| `/` | `app/page.tsx` | KPI metric cards, a revenue bar chart, and a searchable, sortable orders `DataTable`. |
|
|
24
|
+
| `/settings` | `app/settings/page.tsx` | Profile, workspace, and notification forms (`Field`, `Input`, `Select`, `Switch`). |
|
|
25
|
+
|
|
26
|
+
The persistent chrome lives in `components/dashboard-shell.tsx`: an `AppShell` with an
|
|
27
|
+
icon-collapsible `Sidebar` (route-aware via `usePathname`) and a sticky topbar with a
|
|
28
|
+
light/dark toggle.
|
|
29
|
+
|
|
30
|
+
## How it's wired
|
|
31
|
+
|
|
32
|
+
- **`app/globals.css`** imports Tailwind and the Cronus token CSS, plus the required
|
|
33
|
+
`@source "../node_modules/@cronus-ui/ui/dist/**/*.js";` line so Tailwind v4 emits the
|
|
34
|
+
component utility classes (it skips `node_modules` by default).
|
|
35
|
+
- **`app/layout.tsx`** mounts `<CronusUIProvider>` and the anti-flash `<CronusThemeScript>`
|
|
36
|
+
so the right theme is applied before hydration, then wraps every page in the shell.
|
|
37
|
+
- **`components/revenue-chart.tsx`** shows the `ChartContainer` + recharts pattern — the
|
|
38
|
+
chart colors come from your theme tokens (`--color-chart-*`).
|
|
39
|
+
- **`components/orders-table.tsx`** shows the `DataTable` pattern — column defs via
|
|
40
|
+
`@tanstack/react-table`, sortable headers via `DataTableColumnHeader`.
|
|
41
|
+
|
|
42
|
+
## Add more components
|
|
43
|
+
|
|
44
|
+
This app includes a `cronus-ui.json`, so you can pull any component from the registry:
|
|
45
|
+
|
|
46
|
+
```sh
|
|
47
|
+
npx cronus-ui add dialog tabs dropdown-menu
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Scripts
|
|
51
|
+
|
|
52
|
+
| Script | Description |
|
|
53
|
+
| --- | --- |
|
|
54
|
+
| `dev` | Start the dev server. |
|
|
55
|
+
| `build` | Production build. |
|
|
56
|
+
| `start` | Serve the production build. |
|
|
57
|
+
|
|
58
|
+
## Learn more
|
|
59
|
+
|
|
60
|
+
- [Next.js documentation](https://nextjs.org/docs)
|
|
61
|
+
- [Tailwind CSS v4](https://tailwindcss.com)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
@import "@cronus-ui/tokens/styles.css";
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* REQUIRED on Tailwind v4.
|
|
6
|
+
*
|
|
7
|
+
* Tailwind v4 does not scan node_modules by default, and @cronus-ui/ui ships its
|
|
8
|
+
* Tailwind class strings baked into pre-compiled JS under dist. Without the
|
|
9
|
+
* @source below, none of the component utility classes (bg-primary, rounded-lg,
|
|
10
|
+
* bg-surface-raised, …) would be emitted and the components would render
|
|
11
|
+
* unstyled.
|
|
12
|
+
*/
|
|
13
|
+
@source "../node_modules/@cronus-ui/ui/dist/**/*.js";
|
|
14
|
+
|
|
15
|
+
/* Scan this app's own source too. */
|
|
16
|
+
@source "./**/*.{ts,tsx}";
|