create-app-ui 1.0.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 +117 -0
- package/boilerplate/README.md +18 -0
- package/boilerplate/react-base/.env.example +1 -0
- package/boilerplate/react-base/README.md +3 -0
- package/boilerplate/react-base/components.json +19 -0
- package/boilerplate/react-base/eslint.config.js +32 -0
- package/boilerplate/react-base/index.html +12 -0
- package/boilerplate/react-base/package.json +71 -0
- package/boilerplate/react-base/postcss.config.js +6 -0
- package/boilerplate/react-base/prettier.config.js +6 -0
- package/boilerplate/react-base/src/api/axios.ts +20 -0
- package/boilerplate/react-base/src/app/store.ts +13 -0
- package/boilerplate/react-base/src/components/data-table.tsx +919 -0
- package/boilerplate/react-base/src/components/ui/accordion.tsx +44 -0
- package/boilerplate/react-base/src/components/ui/alert-dialog.tsx +105 -0
- package/boilerplate/react-base/src/components/ui/alert.tsx +40 -0
- package/boilerplate/react-base/src/components/ui/avatar.tsx +30 -0
- package/boilerplate/react-base/src/components/ui/badge.tsx +27 -0
- package/boilerplate/react-base/src/components/ui/bar-chart.tsx +76 -0
- package/boilerplate/react-base/src/components/ui/breadcrumb.tsx +87 -0
- package/boilerplate/react-base/src/components/ui/button.tsx +34 -0
- package/boilerplate/react-base/src/components/ui/calendar.tsx +63 -0
- package/boilerplate/react-base/src/components/ui/card.tsx +36 -0
- package/boilerplate/react-base/src/components/ui/chart.tsx +280 -0
- package/boilerplate/react-base/src/components/ui/checkbox.tsx +51 -0
- package/boilerplate/react-base/src/components/ui/context-menu.tsx +173 -0
- package/boilerplate/react-base/src/components/ui/date-picker.tsx +42 -0
- package/boilerplate/react-base/src/components/ui/dialog.tsx +87 -0
- package/boilerplate/react-base/src/components/ui/drawer.tsx +81 -0
- package/boilerplate/react-base/src/components/ui/dropdown-menu.tsx +81 -0
- package/boilerplate/react-base/src/components/ui/dropdown-types.ts +28 -0
- package/boilerplate/react-base/src/components/ui/field.tsx +194 -0
- package/boilerplate/react-base/src/components/ui/hover-card.tsx +26 -0
- package/boilerplate/react-base/src/components/ui/input-group.tsx +98 -0
- package/boilerplate/react-base/src/components/ui/input-otp.tsx +63 -0
- package/boilerplate/react-base/src/components/ui/input.tsx +12 -0
- package/boilerplate/react-base/src/components/ui/item.tsx +152 -0
- package/boilerplate/react-base/src/components/ui/kbd.tsx +13 -0
- package/boilerplate/react-base/src/components/ui/label.tsx +14 -0
- package/boilerplate/react-base/src/components/ui/line-chart.tsx +65 -0
- package/boilerplate/react-base/src/components/ui/menubar.tsx +217 -0
- package/boilerplate/react-base/src/components/ui/multi-select-dropdown.tsx +200 -0
- package/boilerplate/react-base/src/components/ui/navigation-menu.tsx +120 -0
- package/boilerplate/react-base/src/components/ui/pie-chart.tsx +87 -0
- package/boilerplate/react-base/src/components/ui/popover.tsx +29 -0
- package/boilerplate/react-base/src/components/ui/progress.tsx +19 -0
- package/boilerplate/react-base/src/components/ui/radio-group.tsx +36 -0
- package/boilerplate/react-base/src/components/ui/scroll-area.tsx +38 -0
- package/boilerplate/react-base/src/components/ui/searchable-dropdown.tsx +118 -0
- package/boilerplate/react-base/src/components/ui/select.tsx +140 -0
- package/boilerplate/react-base/src/components/ui/separator.tsx +20 -0
- package/boilerplate/react-base/src/components/ui/sheet.tsx +70 -0
- package/boilerplate/react-base/src/components/ui/sidebar.tsx +470 -0
- package/boilerplate/react-base/src/components/ui/skeleton.tsx +11 -0
- package/boilerplate/react-base/src/components/ui/slider.tsx +23 -0
- package/boilerplate/react-base/src/components/ui/sonner.tsx +21 -0
- package/boilerplate/react-base/src/components/ui/sparkline.tsx +38 -0
- package/boilerplate/react-base/src/components/ui/spinner.tsx +10 -0
- package/boilerplate/react-base/src/components/ui/switch.tsx +16 -0
- package/boilerplate/react-base/src/components/ui/table.tsx +80 -0
- package/boilerplate/react-base/src/components/ui/tabs.tsx +32 -0
- package/boilerplate/react-base/src/components/ui/textarea.tsx +12 -0
- package/boilerplate/react-base/src/components/ui/toggle-group.tsx +49 -0
- package/boilerplate/react-base/src/components/ui/toggle.tsx +33 -0
- package/boilerplate/react-base/src/components/ui/tooltip.tsx +23 -0
- package/boilerplate/react-base/src/components/ui/typography.tsx +76 -0
- package/boilerplate/react-base/src/config/constants.ts +3 -0
- package/boilerplate/react-base/src/config/theme.ts +432 -0
- package/boilerplate/react-base/src/config/user.ts +52 -0
- package/boilerplate/react-base/src/context/theme-provider.tsx +12 -0
- package/boilerplate/react-base/src/features/auth/authSlice.ts +19 -0
- package/boilerplate/react-base/src/hooks/index.ts +1 -0
- package/boilerplate/react-base/src/hooks/use-mobile.ts +17 -0
- package/boilerplate/react-base/src/lib/utils.ts +6 -0
- package/boilerplate/react-base/src/routes/index.tsx +7 -0
- package/boilerplate/react-base/src/styles/globals.css +15 -0
- package/boilerplate/react-base/src/vite-env.d.ts +31 -0
- package/boilerplate/react-base/tailwind.config.ts +75 -0
- package/boilerplate/react-base/tsconfig.app.json +20 -0
- package/boilerplate/react-base/tsconfig.json +7 -0
- package/boilerplate/react-base/tsconfig.node.json +16 -0
- package/boilerplate/react-base/vite.config.ts +12 -0
- package/dist/bin/index.js +8 -0
- package/dist/src/cli-args.js +52 -0
- package/dist/src/generator.js +85 -0
- package/dist/src/installer.js +7 -0
- package/dist/src/paths.js +61 -0
- package/dist/src/prompts.js +79 -0
- package/dist/src/replace-placeholders.js +22 -0
- package/dist/src/utils.js +16 -0
- package/package.json +63 -0
- package/templates/admin-portal/README.md +26 -0
- package/templates/admin-portal/src/App.tsx +85 -0
- package/templates/admin-portal/src/assets/auth-hero.jpg +0 -0
- package/templates/admin-portal/src/assets/brand-logo.png +0 -0
- package/templates/admin-portal/src/components/app-breadcrumb.tsx +41 -0
- package/templates/admin-portal/src/components/app-header.tsx +20 -0
- package/templates/admin-portal/src/components/app-sidebar.tsx +78 -0
- package/templates/admin-portal/src/components/auth-layout.tsx +66 -0
- package/templates/admin-portal/src/components/dashboard-metric-card.tsx +105 -0
- package/templates/admin-portal/src/components/data-table.tsx +919 -0
- package/templates/admin-portal/src/components/layout-shell.tsx +23 -0
- package/templates/admin-portal/src/components/notifications-sheet.tsx +91 -0
- package/templates/admin-portal/src/components/sidebar-nav.tsx +164 -0
- package/templates/admin-portal/src/components/user-avatar.tsx +26 -0
- package/templates/admin-portal/src/components/user-menu.tsx +163 -0
- package/templates/admin-portal/src/config/branding.ts +17 -0
- package/templates/admin-portal/src/config/chart-data.ts +44 -0
- package/templates/admin-portal/src/config/navigation.ts +42 -0
- package/templates/admin-portal/src/context/auth-context.tsx +32 -0
- package/templates/admin-portal/src/lib/breadcrumbs.ts +58 -0
- package/templates/admin-portal/src/main.tsx +18 -0
- package/templates/admin-portal/src/pages/components/demo-columns.tsx +170 -0
- package/templates/admin-portal/src/pages/components.tsx +1368 -0
- package/templates/admin-portal/src/pages/dashboard.tsx +143 -0
- package/templates/admin-portal/src/pages/login.tsx +81 -0
- package/templates/admin-portal/src/pages/settings/notifications.tsx +31 -0
- package/templates/admin-portal/src/pages/settings/profile.tsx +26 -0
- package/templates/admin-portal/src/pages/signup.tsx +81 -0
- package/templates/admin-portal/src/pages/users.tsx +12 -0
- package/templates/admin-portal/tsconfig.json +10 -0
- package/templates/blank/README.md +15 -0
- package/templates/blank/src/App.tsx +5 -0
- package/templates/blank/src/main.tsx +15 -0
- package/templates/blank/src/pages/home.tsx +20 -0
- package/templates/blank/tsconfig.json +10 -0
- package/templates/tsconfig.overlay.base.json +7 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export const CURRENT_USER = {
|
|
2
|
+
name: "Admin User",
|
|
3
|
+
email: "admin@company.com",
|
|
4
|
+
initials: "AU",
|
|
5
|
+
avatarUrl: "https://ui-avatars.com/api/?name=Admin+User&background=0D8ABC&color=fff&size=128",
|
|
6
|
+
} as const;
|
|
7
|
+
|
|
8
|
+
export type NotificationItem = {
|
|
9
|
+
id: string;
|
|
10
|
+
title: string;
|
|
11
|
+
description: string;
|
|
12
|
+
time: string;
|
|
13
|
+
read: boolean;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const MOCK_NOTIFICATIONS: NotificationItem[] = [
|
|
17
|
+
{
|
|
18
|
+
id: "1",
|
|
19
|
+
title: "New user registered",
|
|
20
|
+
description: "Mia Chen joined the workspace as Viewer.",
|
|
21
|
+
time: "2 min ago",
|
|
22
|
+
read: false,
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: "2",
|
|
26
|
+
title: "Deployment succeeded",
|
|
27
|
+
description: "Production release v2.4.1 finished without errors.",
|
|
28
|
+
time: "1 hour ago",
|
|
29
|
+
read: false,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: "3",
|
|
33
|
+
title: "Storage threshold",
|
|
34
|
+
description: "Workspace storage is at 82% capacity.",
|
|
35
|
+
time: "3 hours ago",
|
|
36
|
+
read: false,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: "4",
|
|
40
|
+
title: "Weekly report ready",
|
|
41
|
+
description: "Your analytics summary for last week is available.",
|
|
42
|
+
time: "Yesterday",
|
|
43
|
+
read: true,
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: "5",
|
|
47
|
+
title: "Password updated",
|
|
48
|
+
description: "Your account password was changed successfully.",
|
|
49
|
+
time: "2 days ago",
|
|
50
|
+
read: true,
|
|
51
|
+
},
|
|
52
|
+
];
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createContext, useContext, type ReactNode } from "react";
|
|
2
|
+
import { THEME, type ThemePreset } from "@/config/theme";
|
|
3
|
+
|
|
4
|
+
const ThemeContext = createContext<ThemePreset>(THEME);
|
|
5
|
+
|
|
6
|
+
export function ThemeProvider({ children }: { children: ReactNode }) {
|
|
7
|
+
return <ThemeContext.Provider value={THEME}>{children}</ThemeContext.Provider>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function useTheme(): ThemePreset {
|
|
11
|
+
return useContext(ThemeContext);
|
|
12
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth feature module — replace with Redux slice, Zustand store, or context as needed.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export type AuthUser = {
|
|
6
|
+
id: string;
|
|
7
|
+
email: string;
|
|
8
|
+
name: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type AuthState = {
|
|
12
|
+
user: AuthUser | null;
|
|
13
|
+
isAuthenticated: boolean;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const initialAuthState: AuthState = {
|
|
17
|
+
user: null,
|
|
18
|
+
isAuthenticated: false,
|
|
19
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useIsMobile } from "./use-mobile";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
const MOBILE_BREAKPOINT = 768;
|
|
4
|
+
|
|
5
|
+
export function useIsMobile() {
|
|
6
|
+
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined);
|
|
7
|
+
|
|
8
|
+
React.useEffect(() => {
|
|
9
|
+
const media = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
|
|
10
|
+
const onChange = () => setIsMobile(media.matches);
|
|
11
|
+
onChange();
|
|
12
|
+
media.addEventListener("change", onChange);
|
|
13
|
+
return () => media.removeEventListener("change", onChange);
|
|
14
|
+
}, []);
|
|
15
|
+
|
|
16
|
+
return !!isMobile;
|
|
17
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
@tailwind base;
|
|
2
|
+
@tailwind components;
|
|
3
|
+
@tailwind utilities;
|
|
4
|
+
|
|
5
|
+
/* Design tokens are injected from src/config/theme.ts — do not add :root variables here. */
|
|
6
|
+
|
|
7
|
+
html,
|
|
8
|
+
body,
|
|
9
|
+
#root {
|
|
10
|
+
height: 100%;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
body {
|
|
14
|
+
@apply bg-background text-foreground antialiased;
|
|
15
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
2
|
+
|
|
3
|
+
declare module "*.svg" {
|
|
4
|
+
const src: string;
|
|
5
|
+
export default src;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
declare module "*.png" {
|
|
9
|
+
const src: string;
|
|
10
|
+
export default src;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
declare module "*.jpg" {
|
|
14
|
+
const src: string;
|
|
15
|
+
export default src;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
declare module "*.jpeg" {
|
|
19
|
+
const src: string;
|
|
20
|
+
export default src;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
declare module "*.webp" {
|
|
24
|
+
const src: string;
|
|
25
|
+
export default src;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
declare module "*.gif" {
|
|
29
|
+
const src: string;
|
|
30
|
+
export default src;
|
|
31
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { Config } from "tailwindcss";
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
darkMode: ["class"],
|
|
5
|
+
content: ["./index.html", "./src/**/*.{ts,tsx}"],
|
|
6
|
+
theme: {
|
|
7
|
+
extend: {
|
|
8
|
+
colors: {
|
|
9
|
+
border: "hsl(var(--border))",
|
|
10
|
+
input: "hsl(var(--input))",
|
|
11
|
+
ring: "hsl(var(--ring))",
|
|
12
|
+
background: "hsl(var(--background))",
|
|
13
|
+
foreground: "hsl(var(--foreground))",
|
|
14
|
+
primary: { DEFAULT: "hsl(var(--primary))", foreground: "hsl(var(--primary-foreground))" },
|
|
15
|
+
secondary: { DEFAULT: "hsl(var(--secondary))", foreground: "hsl(var(--secondary-foreground))" },
|
|
16
|
+
muted: { DEFAULT: "hsl(var(--muted))", foreground: "hsl(var(--muted-foreground))" },
|
|
17
|
+
accent: { DEFAULT: "hsl(var(--accent))", foreground: "hsl(var(--accent-foreground))" },
|
|
18
|
+
card: { DEFAULT: "hsl(var(--card))", foreground: "hsl(var(--card-foreground))" },
|
|
19
|
+
popover: { DEFAULT: "hsl(var(--popover))", foreground: "hsl(var(--popover-foreground))" },
|
|
20
|
+
destructive: {
|
|
21
|
+
DEFAULT: "hsl(var(--destructive))",
|
|
22
|
+
foreground: "hsl(var(--destructive-foreground))",
|
|
23
|
+
},
|
|
24
|
+
sidebar: {
|
|
25
|
+
DEFAULT: "hsl(var(--sidebar-background))",
|
|
26
|
+
foreground: "hsl(var(--sidebar-foreground))",
|
|
27
|
+
primary: "hsl(var(--sidebar-primary))",
|
|
28
|
+
"primary-foreground": "hsl(var(--sidebar-primary-foreground))",
|
|
29
|
+
accent: "hsl(var(--sidebar-accent))",
|
|
30
|
+
"accent-foreground": "hsl(var(--sidebar-accent-foreground))",
|
|
31
|
+
border: "hsl(var(--sidebar-border))",
|
|
32
|
+
ring: "hsl(var(--sidebar-ring))",
|
|
33
|
+
},
|
|
34
|
+
chart: {
|
|
35
|
+
1: "hsl(var(--chart-1))",
|
|
36
|
+
2: "hsl(var(--chart-2))",
|
|
37
|
+
3: "hsl(var(--chart-3))",
|
|
38
|
+
4: "hsl(var(--chart-4))",
|
|
39
|
+
5: "hsl(var(--chart-5))",
|
|
40
|
+
},
|
|
41
|
+
brand: {
|
|
42
|
+
DEFAULT: "hsl(var(--brand))",
|
|
43
|
+
foreground: "hsl(var(--brand-foreground))",
|
|
44
|
+
},
|
|
45
|
+
"auth-page": "hsl(var(--auth-page))",
|
|
46
|
+
"auth-hero": "hsl(var(--auth-hero))",
|
|
47
|
+
},
|
|
48
|
+
borderRadius: {
|
|
49
|
+
lg: "var(--radius)",
|
|
50
|
+
md: "calc(var(--radius) - 2px)",
|
|
51
|
+
sm: "calc(var(--radius) - 4px)",
|
|
52
|
+
},
|
|
53
|
+
keyframes: {
|
|
54
|
+
"accordion-down": {
|
|
55
|
+
from: { height: "0" },
|
|
56
|
+
to: { height: "var(--radix-accordion-content-height)" },
|
|
57
|
+
},
|
|
58
|
+
"accordion-up": {
|
|
59
|
+
from: { height: "var(--radix-accordion-content-height)" },
|
|
60
|
+
to: { height: "0" },
|
|
61
|
+
},
|
|
62
|
+
"caret-blink": {
|
|
63
|
+
"0%,70%,100%": { opacity: "1" },
|
|
64
|
+
"20%,50%": { opacity: "0" },
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
animation: {
|
|
68
|
+
"accordion-down": "accordion-down 0.2s ease-out",
|
|
69
|
+
"accordion-up": "accordion-up 0.2s ease-out",
|
|
70
|
+
"caret-blink": "caret-blink 1.25s ease-out infinite",
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
plugins: [],
|
|
75
|
+
} satisfies Config;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"skipLibCheck": true,
|
|
7
|
+
"moduleResolution": "Bundler",
|
|
8
|
+
"allowImportingTsExtensions": false,
|
|
9
|
+
"resolveJsonModule": true,
|
|
10
|
+
"isolatedModules": true,
|
|
11
|
+
"noEmit": true,
|
|
12
|
+
"jsx": "react-jsx",
|
|
13
|
+
"strict": true,
|
|
14
|
+
"baseUrl": ".",
|
|
15
|
+
"paths": {
|
|
16
|
+
"@/*": ["./src/*"]
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"include": ["src"]
|
|
20
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"lib": ["ES2023"],
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"skipLibCheck": true,
|
|
7
|
+
"moduleResolution": "Bundler",
|
|
8
|
+
"allowImportingTsExtensions": false,
|
|
9
|
+
"resolveJsonModule": true,
|
|
10
|
+
"isolatedModules": true,
|
|
11
|
+
"noEmit": true,
|
|
12
|
+
"strict": true,
|
|
13
|
+
"types": ["node"]
|
|
14
|
+
},
|
|
15
|
+
"include": ["vite.config.ts", "tailwind.config.ts"]
|
|
16
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import { runCli } from "../src/generator.js";
|
|
4
|
+
runCli(process.argv.slice(2)).catch((error) => {
|
|
5
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
6
|
+
console.error(chalk.red(`Fatal error: ${message}`));
|
|
7
|
+
process.exit(1);
|
|
8
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
const TEMPLATE_NAMES = ["admin-portal", "blank"];
|
|
2
|
+
export function parseArgv(argv) {
|
|
3
|
+
const positional = [];
|
|
4
|
+
let nonInteractive = false;
|
|
5
|
+
let template;
|
|
6
|
+
let installDependencies;
|
|
7
|
+
let initializeGit;
|
|
8
|
+
let companyName;
|
|
9
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
10
|
+
const arg = argv[i];
|
|
11
|
+
if (arg === "--yes" || arg === "-y") {
|
|
12
|
+
nonInteractive = true;
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
if (arg === "--template" || arg === "-t") {
|
|
16
|
+
const value = argv[++i];
|
|
17
|
+
if (value && TEMPLATE_NAMES.includes(value)) {
|
|
18
|
+
template = value;
|
|
19
|
+
nonInteractive = true;
|
|
20
|
+
}
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (arg === "--no-install") {
|
|
24
|
+
installDependencies = false;
|
|
25
|
+
nonInteractive = true;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (arg === "--git") {
|
|
29
|
+
initializeGit = true;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (arg === "--no-git") {
|
|
33
|
+
initializeGit = false;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (arg === "--company" || arg === "-c") {
|
|
37
|
+
companyName = argv[++i]?.trim();
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (!arg.startsWith("-")) {
|
|
41
|
+
positional.push(arg);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
positional,
|
|
46
|
+
nonInteractive,
|
|
47
|
+
template,
|
|
48
|
+
installDependencies,
|
|
49
|
+
initializeGit,
|
|
50
|
+
companyName,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import fs from "fs-extra";
|
|
4
|
+
import ora from "ora";
|
|
5
|
+
import { parseArgv } from "./cli-args.js";
|
|
6
|
+
import { askOverwriteConfirmation, askQuestions } from "./prompts.js";
|
|
7
|
+
import { initializeGit, installDependencies } from "./installer.js";
|
|
8
|
+
import { replacePlaceholders } from "./replace-placeholders.js";
|
|
9
|
+
import { resolveBoilerplateDir, resolveTemplateDir } from "./paths.js";
|
|
10
|
+
import { isValidAppName, resolveTargetDir, toTitleCase } from "./utils.js";
|
|
11
|
+
export async function runCli(args) {
|
|
12
|
+
const parsed = parseArgv(args);
|
|
13
|
+
const appNameArg = parsed.positional[0]?.trim();
|
|
14
|
+
if (appNameArg && !isValidAppName(appNameArg)) {
|
|
15
|
+
throw new Error("Invalid app name. Use a valid npm package name.");
|
|
16
|
+
}
|
|
17
|
+
const answers = await askQuestions(appNameArg, parsed);
|
|
18
|
+
const cwd = process.cwd();
|
|
19
|
+
const targetDir = resolveTargetDir(cwd, answers.appName);
|
|
20
|
+
if (await fs.pathExists(targetDir)) {
|
|
21
|
+
const shouldOverwrite = await askOverwriteConfirmation(answers.appName);
|
|
22
|
+
if (!shouldOverwrite) {
|
|
23
|
+
throw new Error("Aborted to avoid overwriting existing directory.");
|
|
24
|
+
}
|
|
25
|
+
await fs.emptyDir(targetDir);
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
await fs.ensureDir(targetDir);
|
|
29
|
+
}
|
|
30
|
+
const boilerplateDir = resolveBoilerplateDir();
|
|
31
|
+
const templateDir = resolveTemplateDir(answers.template);
|
|
32
|
+
if (!(await fs.pathExists(boilerplateDir))) {
|
|
33
|
+
throw new Error(`Boilerplate not found: ${boilerplateDir}`);
|
|
34
|
+
}
|
|
35
|
+
if (!(await fs.pathExists(templateDir))) {
|
|
36
|
+
throw new Error(`Template not found: ${templateDir}`);
|
|
37
|
+
}
|
|
38
|
+
const baseSpinner = ora("Copying shared boilerplate...").start();
|
|
39
|
+
await fs.copy(boilerplateDir, targetDir, {
|
|
40
|
+
filter: (src) => !src.includes(`${path.sep}node_modules${path.sep}`),
|
|
41
|
+
});
|
|
42
|
+
baseSpinner.succeed("Boilerplate copied.");
|
|
43
|
+
const templateSpinner = ora(`Applying "${answers.template}" template...`).start();
|
|
44
|
+
await fs.copy(templateDir, targetDir, {
|
|
45
|
+
overwrite: true,
|
|
46
|
+
filter: (src) => {
|
|
47
|
+
if (src.includes(`${path.sep}node_modules${path.sep}`)) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
// IDE-only overlay config; generated apps keep boilerplate tsconfig.* only.
|
|
51
|
+
const rel = path.relative(templateDir, src).replace(/\\/g, "/");
|
|
52
|
+
if (rel === "tsconfig.json" || rel.endsWith("/tsconfig.json")) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
return true;
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
templateSpinner.succeed("Template applied.");
|
|
59
|
+
const title = toTitleCase(answers.appName.split("/").pop() ?? answers.appName);
|
|
60
|
+
await replacePlaceholders(targetDir, {
|
|
61
|
+
"__APP_NAME__": answers.appName,
|
|
62
|
+
"__APP_TITLE__": title,
|
|
63
|
+
"__COMPANY_NAME__": answers.companyName,
|
|
64
|
+
});
|
|
65
|
+
const packageJsonPath = path.join(targetDir, "package.json");
|
|
66
|
+
const packageJson = await fs.readJson(packageJsonPath);
|
|
67
|
+
packageJson.name = answers.appName;
|
|
68
|
+
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
|
|
69
|
+
if (answers.installDependencies) {
|
|
70
|
+
const installSpinner = ora("Installing dependencies...").start();
|
|
71
|
+
await installDependencies(targetDir);
|
|
72
|
+
installSpinner.succeed("Dependencies installed.");
|
|
73
|
+
}
|
|
74
|
+
if (answers.initializeGit) {
|
|
75
|
+
const gitSpinner = ora("Initializing git repository...").start();
|
|
76
|
+
await initializeGit(targetDir);
|
|
77
|
+
gitSpinner.succeed("Git repository initialized.");
|
|
78
|
+
}
|
|
79
|
+
console.log(chalk.green("\nSuccess! Your app is ready.\n"));
|
|
80
|
+
console.log(`Next steps:\n cd ${answers.appName}`);
|
|
81
|
+
if (!answers.installDependencies) {
|
|
82
|
+
console.log(" npm install");
|
|
83
|
+
}
|
|
84
|
+
console.log(" npm run dev\n");
|
|
85
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { execa } from "execa";
|
|
2
|
+
export async function installDependencies(targetDir) {
|
|
3
|
+
await execa("npm", ["install"], { cwd: targetDir, stdio: "inherit" });
|
|
4
|
+
}
|
|
5
|
+
export async function initializeGit(targetDir) {
|
|
6
|
+
await execa("git", ["init"], { cwd: targetDir, stdio: "inherit" });
|
|
7
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import fs from "fs-extra";
|
|
4
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
5
|
+
const __dirname = path.dirname(__filename);
|
|
6
|
+
function findCreateAppUiPackageRoot() {
|
|
7
|
+
let dir = __dirname;
|
|
8
|
+
for (let depth = 0; depth < 10; depth += 1) {
|
|
9
|
+
const packageJsonPath = path.join(dir, "package.json");
|
|
10
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
11
|
+
try {
|
|
12
|
+
const pkg = fs.readJsonSync(packageJsonPath);
|
|
13
|
+
if (pkg.name === "create-app-ui") {
|
|
14
|
+
return dir;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
// ignore invalid package.json
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const parent = path.dirname(dir);
|
|
22
|
+
if (parent === dir) {
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
dir = parent;
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
/** Resolves the directory that contains `boilerplate/react-base` and `templates/`. */
|
|
30
|
+
export function resolvePlatformRoot() {
|
|
31
|
+
const packageRoot = findCreateAppUiPackageRoot();
|
|
32
|
+
const searchRoots = packageRoot
|
|
33
|
+
? [packageRoot, path.dirname(packageRoot), path.dirname(path.dirname(packageRoot))]
|
|
34
|
+
: [__dirname];
|
|
35
|
+
const seen = new Set();
|
|
36
|
+
for (const start of searchRoots) {
|
|
37
|
+
let dir = start;
|
|
38
|
+
for (let depth = 0; depth < 10; depth += 1) {
|
|
39
|
+
if (seen.has(dir)) {
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
seen.add(dir);
|
|
43
|
+
const boilerplate = path.join(dir, "boilerplate", "react-base");
|
|
44
|
+
if (fs.existsSync(boilerplate)) {
|
|
45
|
+
return dir;
|
|
46
|
+
}
|
|
47
|
+
const parent = path.dirname(dir);
|
|
48
|
+
if (parent === dir) {
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
dir = parent;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
throw new Error("Could not locate boilerplate/react-base. Reinstall create-app-ui or run from the ui-platform monorepo.");
|
|
55
|
+
}
|
|
56
|
+
export function resolveBoilerplateDir() {
|
|
57
|
+
return path.resolve(resolvePlatformRoot(), "boilerplate", "react-base");
|
|
58
|
+
}
|
|
59
|
+
export function resolveTemplateDir(templateName) {
|
|
60
|
+
return path.resolve(resolvePlatformRoot(), "templates", templateName);
|
|
61
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import prompts from "prompts";
|
|
2
|
+
import { isValidAppName } from "./utils.js";
|
|
3
|
+
const DEFAULT_COMPANY_NAME = "Omobio";
|
|
4
|
+
export async function askQuestions(initialAppName, parsed) {
|
|
5
|
+
if (parsed?.nonInteractive) {
|
|
6
|
+
const appName = (initialAppName ?? parsed.positional[0] ?? "").trim();
|
|
7
|
+
if (!isValidAppName(appName)) {
|
|
8
|
+
throw new Error("Non-interactive mode requires a valid app name: create-app-ui <name> --template <admin-portal|blank> -y");
|
|
9
|
+
}
|
|
10
|
+
if (!parsed.template) {
|
|
11
|
+
throw new Error("Non-interactive mode requires --template admin-portal|blank");
|
|
12
|
+
}
|
|
13
|
+
return {
|
|
14
|
+
appName,
|
|
15
|
+
template: parsed.template,
|
|
16
|
+
installDependencies: parsed.installDependencies ?? false,
|
|
17
|
+
initializeGit: parsed.initializeGit ?? false,
|
|
18
|
+
overwrite: false,
|
|
19
|
+
companyName: parsed.companyName?.trim() || DEFAULT_COMPANY_NAME,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
const responses = await prompts([
|
|
23
|
+
{
|
|
24
|
+
type: initialAppName ? null : "text",
|
|
25
|
+
name: "appName",
|
|
26
|
+
message: "Application name (npm package, lowercase, e.g. my-app)",
|
|
27
|
+
validate: (value) => {
|
|
28
|
+
const trimmed = value.trim();
|
|
29
|
+
if (!trimmed) {
|
|
30
|
+
return "Name is required.";
|
|
31
|
+
}
|
|
32
|
+
if (isValidAppName(trimmed)) {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
return "Use lowercase letters, numbers, and hyphens only (e.g. my-app, acme-portal).";
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
type: "select",
|
|
40
|
+
name: "template",
|
|
41
|
+
message: "Select a template",
|
|
42
|
+
choices: [
|
|
43
|
+
{ title: "admin-portal", value: "admin-portal" },
|
|
44
|
+
{ title: "blank", value: "blank" },
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
type: "confirm",
|
|
49
|
+
name: "installDependencies",
|
|
50
|
+
message: "Install dependencies now?",
|
|
51
|
+
initial: true,
|
|
52
|
+
},
|
|
53
|
+
], {
|
|
54
|
+
onCancel: () => {
|
|
55
|
+
throw new Error("Operation cancelled by user.");
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
const appName = (initialAppName ?? responses.appName ?? "").trim();
|
|
59
|
+
if (!isValidAppName(appName)) {
|
|
60
|
+
throw new Error("Invalid application name.");
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
appName,
|
|
64
|
+
template: responses.template,
|
|
65
|
+
installDependencies: Boolean(responses.installDependencies),
|
|
66
|
+
initializeGit: parsed?.initializeGit ?? false,
|
|
67
|
+
overwrite: false,
|
|
68
|
+
companyName: parsed?.companyName?.trim() || DEFAULT_COMPANY_NAME,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
export async function askOverwriteConfirmation(appName) {
|
|
72
|
+
const response = await prompts({
|
|
73
|
+
type: "confirm",
|
|
74
|
+
name: "overwrite",
|
|
75
|
+
message: `Directory "${appName}" already exists. Overwrite?`,
|
|
76
|
+
initial: false,
|
|
77
|
+
});
|
|
78
|
+
return Boolean(response.overwrite);
|
|
79
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import fs from "fs-extra";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
const BINARY_EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".ico", ".woff", ".woff2"]);
|
|
4
|
+
export async function replacePlaceholders(rootDir, replacements) {
|
|
5
|
+
const entries = await fs.readdir(rootDir);
|
|
6
|
+
for (const entry of entries) {
|
|
7
|
+
const absolute = path.join(rootDir, entry);
|
|
8
|
+
const stats = await fs.stat(absolute);
|
|
9
|
+
if (stats.isDirectory()) {
|
|
10
|
+
await replacePlaceholders(absolute, replacements);
|
|
11
|
+
continue;
|
|
12
|
+
}
|
|
13
|
+
if (BINARY_EXTENSIONS.has(path.extname(absolute))) {
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
const content = await fs.readFile(absolute, "utf8");
|
|
17
|
+
const next = Object.entries(replacements).reduce((acc, [key, value]) => acc.replaceAll(key, value), content);
|
|
18
|
+
if (next !== content) {
|
|
19
|
+
await fs.writeFile(absolute, next, "utf8");
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
const APP_NAME_REGEX = /^(?:@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
|
|
3
|
+
export function isValidAppName(name) {
|
|
4
|
+
return APP_NAME_REGEX.test(name);
|
|
5
|
+
}
|
|
6
|
+
export function toTitleCase(input) {
|
|
7
|
+
return input
|
|
8
|
+
.replace(/[-_]/g, " ")
|
|
9
|
+
.split(" ")
|
|
10
|
+
.filter(Boolean)
|
|
11
|
+
.map((part) => part[0].toUpperCase() + part.slice(1))
|
|
12
|
+
.join(" ");
|
|
13
|
+
}
|
|
14
|
+
export function resolveTargetDir(cwd, appName) {
|
|
15
|
+
return path.resolve(cwd, appName);
|
|
16
|
+
}
|