create-bw-app 0.3.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.
Files changed (41) hide show
  1. package/README.md +34 -0
  2. package/bin/create-bw-app.mjs +9 -0
  3. package/package.json +27 -0
  4. package/src/cli.mjs +78 -0
  5. package/src/constants.mjs +114 -0
  6. package/src/generator.mjs +821 -0
  7. package/template/base/app/bootstrap/page.tsx +75 -0
  8. package/template/base/app/globals.css +545 -0
  9. package/template/base/app/layout.tsx +16 -0
  10. package/template/base/app/page.tsx +144 -0
  11. package/template/base/app/playground/auth/auth-playground.tsx +112 -0
  12. package/template/base/app/playground/auth/page.tsx +5 -0
  13. package/template/base/app/playground/layout.tsx +41 -0
  14. package/template/base/app/preview/app-shell/page.tsx +11 -0
  15. package/template/base/app/preview/app-shell-preview.tsx +185 -0
  16. package/template/base/config/bootstrap.ts +125 -0
  17. package/template/base/config/brand.ts +21 -0
  18. package/template/base/config/client.ts +18 -0
  19. package/template/base/config/env.ts +64 -0
  20. package/template/base/config/modules.ts +62 -0
  21. package/template/base/next-env.d.ts +6 -0
  22. package/template/base/public/brand/logo-dark.svg +7 -0
  23. package/template/base/public/brand/logo-light.svg +7 -0
  24. package/template/base/public/brand/logo-mark.svg +5 -0
  25. package/template/base/tsconfig.json +36 -0
  26. package/template/modules/admin/app/api/admin/users/roles/route.ts +6 -0
  27. package/template/modules/admin/app/api/admin/users/route.ts +6 -0
  28. package/template/modules/admin/app/playground/admin/page.tsx +102 -0
  29. package/template/modules/crm/app/playground/crm/page.tsx +103 -0
  30. package/template/modules/projects/app/playground/projects/page.tsx +93 -0
  31. package/template/site/base/app/globals.css +68 -0
  32. package/template/site/base/app/layout.tsx +17 -0
  33. package/template/site/base/app/page.tsx +165 -0
  34. package/template/site/base/components/ui/badge.tsx +28 -0
  35. package/template/site/base/components/ui/button.tsx +52 -0
  36. package/template/site/base/components/ui/card.tsx +28 -0
  37. package/template/site/base/components.json +17 -0
  38. package/template/site/base/lib/utils.ts +6 -0
  39. package/template/site/base/next-env.d.ts +6 -0
  40. package/template/site/base/postcss.config.mjs +7 -0
  41. package/template/site/base/tsconfig.json +23 -0
@@ -0,0 +1,144 @@
1
+ import Link from "next/link";
2
+ import { getStarterClientConfig } from "../config/client";
3
+
4
+ export default function HomePage() {
5
+ const config = getStarterClientConfig();
6
+
7
+ return (
8
+ <main className="shell starter-home">
9
+ <div className="frame">
10
+ <section className="starter-hero">
11
+ <div className="starter-hero-copy">
12
+ <span className="eyebrow">{config.brand.companyName}</span>
13
+ <h1 className="title">{config.brand.productName}</h1>
14
+ <p className="lead">{config.brand.tagline}</p>
15
+ <div className="actions">
16
+ <Link href="/preview/app-shell" className="action">Preview App Shell</Link>
17
+ <Link href="/bootstrap" className="action secondary">Open Bootstrap Checklist</Link>
18
+ <Link href="/playground/auth" className="action secondary">Open Module Playgrounds</Link>
19
+ </div>
20
+ </div>
21
+
22
+ <div className="starter-hero-card panel">
23
+ <div className="panel-inner">
24
+ <p className={`status ${config.envReadiness.allReady ? "ok" : "warn"}`}>
25
+ {config.envReadiness.allReady ? "Starter ready" : "Setup in progress"}
26
+ </p>
27
+ <div className="starter-stat-grid">
28
+ <div>
29
+ <span className="stat-number">{config.enabledModules.length}</span>
30
+ <p className="muted">enabled modules</p>
31
+ </div>
32
+ <div>
33
+ <span className="stat-number">{config.envStatus.filter((item) => item.present).length}</span>
34
+ <p className="muted">configured env keys</p>
35
+ </div>
36
+ </div>
37
+ <div className="starter-hero-note">
38
+ <strong>What this template gives you</strong>
39
+ <p className="muted">
40
+ One place to brand, provision, preview, and validate a new client before the first deployment.
41
+ </p>
42
+ </div>
43
+ </div>
44
+ </div>
45
+ </section>
46
+
47
+ <section className="grid starter-signal-grid">
48
+ <article className="panel preview-glass-card">
49
+ <div className="panel-inner">
50
+ <p className="eyebrow">Brand</p>
51
+ <h2>{config.brand.companyName}</h2>
52
+ <p className="muted">{config.brand.contactEmail} · {config.brand.supportEmail}</p>
53
+ <p className="package-name">{config.brand.slug}</p>
54
+ </div>
55
+ </article>
56
+
57
+ <article className="panel preview-glass-card">
58
+ <div className="panel-inner">
59
+ <p className="eyebrow">Infrastructure</p>
60
+ <h2>{config.envReadiness.allReady ? "Ready to validate" : "Needs provisioning"}</h2>
61
+ <p className="muted">
62
+ {config.envReadiness.allReady
63
+ ? "Core env and service wiring are present."
64
+ : `${config.envReadiness.missing.length} required environment key(s) are still missing.`}
65
+ </p>
66
+ </div>
67
+ </article>
68
+ </section>
69
+
70
+ <section className="panel" style={{ marginTop: 18 }}>
71
+ <div className="panel-inner">
72
+ <h2>Enabled platform modules</h2>
73
+ <div className="grid">
74
+ {config.enabledModules.map((moduleConfig) => (
75
+ <article key={moduleConfig.key} className="panel preview-glass-card" style={{ background: "rgba(255,255,255,0.72)" }}>
76
+ <div className="panel-inner">
77
+ <p className={`status ${moduleConfig.enabled ? "ok" : "warn"}`}>{moduleConfig.placement}</p>
78
+ <h3>{moduleConfig.label}</h3>
79
+ <p className="muted">{moduleConfig.description}</p>
80
+ <p className="package-name">{moduleConfig.packageName}</p>
81
+ {moduleConfig.playgroundHref ? (
82
+ <Link href={moduleConfig.playgroundHref} className="inline-link">
83
+ Open module playground
84
+ </Link>
85
+ ) : null}
86
+ </div>
87
+ </article>
88
+ ))}
89
+ </div>
90
+ </div>
91
+ </section>
92
+
93
+ <section className="grid" style={{ marginTop: 18 }}>
94
+ <article className="panel preview-glass-card">
95
+ <div className="panel-inner">
96
+ <h2>App-shell surfaces</h2>
97
+ <ul className="list">
98
+ {config.shellPreview.primaryNav.map((item) => (
99
+ <li key={item.href}>{item.label} · {item.href}</li>
100
+ ))}
101
+ {config.shellPreview.moduleGroups.map((group) => (
102
+ <li key={group.key}>
103
+ {group.label} · {group.children.length} item(s)
104
+ </li>
105
+ ))}
106
+ {config.shellPreview.adminNavItem ? <li>{config.shellPreview.adminNavItem.label} · {config.shellPreview.adminNavItem.href}</li> : null}
107
+ </ul>
108
+ </div>
109
+ </article>
110
+
111
+ <article className="panel preview-glass-card">
112
+ <div className="panel-inner">
113
+ <h2>Starter controls</h2>
114
+ <ul className="list">
115
+ <li>`config/brand.ts` for client identity and contact details.</li>
116
+ <li>`config/modules.ts` for enabled platform modules.</li>
117
+ <li>`config/env.ts` for infra requirements and readiness checks.</li>
118
+ <li>`.env.local` from `.env.example` for per-client secrets and flags.</li>
119
+ </ul>
120
+ </div>
121
+ </article>
122
+ </section>
123
+
124
+ <section className="panel" style={{ marginTop: 18 }}>
125
+ <div className="panel-inner">
126
+ <h2>Infrastructure checklist</h2>
127
+ <div className="grid">
128
+ {config.envStatus.map((item) => (
129
+ <article key={item.key} className="panel preview-glass-card" style={{ background: "rgba(255,255,255,0.72)" }}>
130
+ <div className="panel-inner">
131
+ <p className={`status ${item.present ? "ok" : "warn"}`}>{item.present ? "Configured" : "Missing"}</p>
132
+ <h3>{item.key}</h3>
133
+ <p className="muted">{item.description}</p>
134
+ <p className="package-name">{item.scope} · {item.requiredFor.join(", ")}</p>
135
+ </div>
136
+ </article>
137
+ ))}
138
+ </div>
139
+ </div>
140
+ </section>
141
+ </div>
142
+ </main>
143
+ );
144
+ }
@@ -0,0 +1,112 @@
1
+ "use client";
2
+
3
+ import { useMemo, useState } from "react";
4
+ import {
5
+ AUTH_RESEND_COOLDOWN_SECONDS,
6
+ buildResetPasswordRedirectUrl,
7
+ buildSignupCallbackUrl,
8
+ useCooldownTimer,
9
+ validateEmail,
10
+ validatePassword,
11
+ } from "@brightweblabs/core-auth/client";
12
+
13
+ export function AuthPlayground() {
14
+ const [email, setEmail] = useState("hello@brightweblabs.pt");
15
+ const [password, setPassword] = useState("Brightweb2026");
16
+ const passwordResult = useMemo(() => validatePassword(password), [password]);
17
+ const emailIsValid = useMemo(() => validateEmail(email), [email]);
18
+ const cooldown = useCooldownTimer(AUTH_RESEND_COOLDOWN_SECONDS);
19
+ const authUrls = useMemo(() => {
20
+ try {
21
+ return {
22
+ signupCallbackUrl: buildSignupCallbackUrl(),
23
+ resetPasswordUrl: buildResetPasswordRedirectUrl(),
24
+ error: null,
25
+ };
26
+ } catch (error) {
27
+ return {
28
+ signupCallbackUrl: null,
29
+ resetPasswordUrl: null,
30
+ error: error instanceof Error ? error.message : "Unknown error",
31
+ };
32
+ }
33
+ }, []);
34
+
35
+ return (
36
+ <>
37
+ <article className="panel">
38
+ <div className="panel-inner">
39
+ <p className="eyebrow">Core Auth</p>
40
+ <h1>Auth package playground</h1>
41
+ <p className="muted">
42
+ This page exercises the external `@brightweblabs/core-auth` client package directly.
43
+ </p>
44
+ </div>
45
+ </article>
46
+
47
+ <article className="panel">
48
+ <div className="panel-inner form-grid">
49
+ <div className="field">
50
+ <label htmlFor="email">Email validator</label>
51
+ <input id="email" value={email} onChange={(event) => setEmail(event.target.value)} />
52
+ </div>
53
+
54
+ <p className={`status ${emailIsValid ? "ok" : "error"}`}>
55
+ {emailIsValid ? "Valid email" : "Invalid email"}
56
+ </p>
57
+
58
+ <div className="field">
59
+ <label htmlFor="password">Password validator</label>
60
+ <input id="password" value={password} onChange={(event) => setPassword(event.target.value)} />
61
+ </div>
62
+
63
+ <div className="result-box">
64
+ <strong>{passwordResult.isValid ? "Password accepted" : "Password rejected"}</strong>
65
+ <ul>
66
+ {passwordResult.errors.length === 0 ? <li>No validation errors.</li> : null}
67
+ {passwordResult.errors.map((error) => <li key={error}>{error}</li>)}
68
+ </ul>
69
+ </div>
70
+ </div>
71
+ </article>
72
+
73
+ <article className="panel">
74
+ <div className="panel-inner stack">
75
+ <div>
76
+ <h2>Derived URLs</h2>
77
+ <p className="muted">These values come from the auth package and depend on `NEXT_PUBLIC_APP_URL`.</p>
78
+ </div>
79
+ <div className="code-box">
80
+ {authUrls.error ? (
81
+ <div>
82
+ <strong>Configuration required:</strong> {authUrls.error}
83
+ </div>
84
+ ) : (
85
+ <>
86
+ <div><strong>Signup callback:</strong> {authUrls.signupCallbackUrl}</div>
87
+ <div><strong>Reset password:</strong> {authUrls.resetPasswordUrl}</div>
88
+ </>
89
+ )}
90
+ </div>
91
+ </div>
92
+ </article>
93
+
94
+ <article className="panel">
95
+ <div className="panel-inner stack">
96
+ <div>
97
+ <h2>Cooldown hook</h2>
98
+ <p className="muted">Useful for resend-email and rate-limited auth actions.</p>
99
+ </div>
100
+ <div className="actions">
101
+ <button className="action" onClick={() => cooldown.start()} disabled={cooldown.isCoolingDown}>
102
+ {cooldown.isCoolingDown ? `Cooling down: ${cooldown.remaining}s` : "Start cooldown"}
103
+ </button>
104
+ <button className="action secondary" onClick={() => cooldown.reset()}>
105
+ Reset
106
+ </button>
107
+ </div>
108
+ </div>
109
+ </article>
110
+ </>
111
+ );
112
+ }
@@ -0,0 +1,5 @@
1
+ import { AuthPlayground } from "./auth-playground";
2
+
3
+ export default function AuthPlaygroundPage() {
4
+ return <AuthPlayground />;
5
+ }
@@ -0,0 +1,41 @@
1
+ import type { ReactNode } from "react";
2
+ import Link from "next/link";
3
+ import { getEnabledStarterModules } from "../../config/modules";
4
+
5
+ const links = getEnabledStarterModules()
6
+ .filter((moduleConfig) => moduleConfig.playgroundHref)
7
+ .map((moduleConfig) => ({
8
+ href: moduleConfig.playgroundHref!,
9
+ label: moduleConfig.label,
10
+ }));
11
+
12
+ export default function PlaygroundLayout({ children }: { children: ReactNode }) {
13
+ return (
14
+ <main className="shell">
15
+ <div className="frame playground-layout">
16
+ <aside className="panel sidebar">
17
+ <div className="panel-inner stack">
18
+ <div>
19
+ <span className="eyebrow">Sandbox</span>
20
+ <h2 style={{ marginBottom: 8 }}>Module Playground</h2>
21
+ <p className="muted">
22
+ Use these routes to inspect package behavior while editing the shared repo.
23
+ </p>
24
+ </div>
25
+ <nav className="nav-list">
26
+ {links.map((link) => (
27
+ <Link key={link.href} href={link.href} className="nav-item">
28
+ {link.label}
29
+ </Link>
30
+ ))}
31
+ <Link href="/" className="nav-item">
32
+ Back to overview
33
+ </Link>
34
+ </nav>
35
+ </div>
36
+ </aside>
37
+ <section className="stack">{children}</section>
38
+ </div>
39
+ </main>
40
+ );
41
+ }
@@ -0,0 +1,11 @@
1
+ import { AppShellPreview } from "../app-shell-preview";
2
+
3
+ export default function AppShellPreviewPage() {
4
+ return (
5
+ <main className="shell shell-preview-page">
6
+ <div className="frame">
7
+ <AppShellPreview />
8
+ </div>
9
+ </main>
10
+ );
11
+ }
@@ -0,0 +1,185 @@
1
+ "use client";
2
+
3
+ import { useMemo, useState } from "react";
4
+ import {
5
+ AccountMenu,
6
+ AppHeader,
7
+ DesktopSidebar,
8
+ MobileNav,
9
+ computeInitials,
10
+ getShellNavGroup,
11
+ type NavGroupConfig,
12
+ type ResolvedClientAppShellConfig,
13
+ } from "@brightweblabs/app-shell";
14
+ import { LayoutTemplate, Sparkles, Users } from "lucide-react";
15
+ import { starterBrandConfig } from "../../config/brand";
16
+ import { getStarterShellConfig } from "../../config/shell";
17
+
18
+ const mockUser = {
19
+ email: "admin@starter-client.test",
20
+ user_metadata: {
21
+ first_name: "Starter",
22
+ last_name: "Admin",
23
+ },
24
+ };
25
+
26
+ const mockSurfaceCards = [
27
+ {
28
+ title: "Module activation",
29
+ description: "Nav, admin access, and tools placement are resolved from the starter client config.",
30
+ icon: LayoutTemplate,
31
+ },
32
+ {
33
+ title: "Shared updates",
34
+ description: "The shell primitives come from `@brightweblabs/app-shell`, so new clients keep the same integration contract.",
35
+ icon: Sparkles,
36
+ },
37
+ {
38
+ title: "User governance",
39
+ description: "Admin placement stays visible because the preview assumes an admin/staff viewer.",
40
+ icon: Users,
41
+ },
42
+ ];
43
+
44
+ export function AppShellPreview() {
45
+ const { shellPreview: config } = getStarterShellConfig() as { shellPreview: ResolvedClientAppShellConfig };
46
+ const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
47
+ const [toolsExpanded, setToolsExpanded] = useState(true);
48
+ const [crmExpanded, setCrmExpanded] = useState(true);
49
+ const [activeHref, setActiveHref] = useState(config.primaryNav[0]?.href ?? "/dashboard");
50
+
51
+ const crmNavGroup = useMemo(
52
+ () =>
53
+ getShellNavGroup(config, "crm")
54
+ ?? ({
55
+ label: "CRM",
56
+ icon: Users,
57
+ children: [{ href: "/crm", label: "CRM", icon: Users }],
58
+ } satisfies NavGroupConfig),
59
+ [config],
60
+ );
61
+ const collapsedToolsHref = config.toolsSection.items[0]?.href ?? "/";
62
+ const displayName = "Starter Admin";
63
+ const userInitials = computeInitials(displayName);
64
+ const isNavItemActive = (href: string) => activeHref === href;
65
+ const isToolLinkActive = (href: string) => activeHref === href;
66
+ const isCrmChildActive = (href: string) => activeHref === href;
67
+ const isCrmGroupActive = crmNavGroup.children.some((item) => activeHref === item.href);
68
+ const isToolActive = config.toolsSection.items.some((item) => activeHref === item.href);
69
+
70
+ return (
71
+ <div className="app-preview-shell">
72
+ <DesktopSidebar
73
+ className="app-preview-sidebar"
74
+ brand={config.brand}
75
+ collapsedToolsHref={collapsedToolsHref}
76
+ isSidebarCollapsed={isSidebarCollapsed}
77
+ isToolActive={isToolActive}
78
+ toolsExpanded={toolsExpanded}
79
+ visiblePrimaryNav={config.primaryNav}
80
+ adminNavItem={config.adminNavItem}
81
+ visibleToolNav={config.toolsSection.items}
82
+ crmNavGroup={crmNavGroup}
83
+ crmGroupExpanded={crmExpanded}
84
+ isCrmGroupActive={isCrmGroupActive}
85
+ isNavItemActive={isNavItemActive}
86
+ isToolLinkActive={isToolLinkActive}
87
+ isCrmChildActive={isCrmChildActive}
88
+ onToggleSidebar={() => setIsSidebarCollapsed((current) => !current)}
89
+ onToggleTools={() => setToolsExpanded((current) => !current)}
90
+ onToggleCrmGroup={() => setCrmExpanded((current) => !current)}
91
+ />
92
+
93
+ <div className="app-preview-main">
94
+ <AppHeader className="app-preview-header">
95
+ <div className="app-preview-header-copy">
96
+ <span className="eyebrow">{starterBrandConfig.companyName}</span>
97
+ <h1>Logged-in shell preview</h1>
98
+ <p className="muted">This is the packaged shell running with the starter client registration.</p>
99
+ </div>
100
+ <div className="app-preview-header-actions">
101
+ <div className="status ok">Admin preview</div>
102
+ <AccountMenu
103
+ displayName={displayName}
104
+ isStaff
105
+ onSignOut={async () => {}}
106
+ user={mockUser}
107
+ userInitials={userInitials}
108
+ />
109
+ </div>
110
+ </AppHeader>
111
+
112
+ <div className="app-preview-mobile-nav">
113
+ <MobileNav
114
+ className="panel"
115
+ toolsExpanded={toolsExpanded}
116
+ visiblePrimaryNav={config.primaryNav}
117
+ visibleToolNav={config.toolsSection.items}
118
+ isNavItemActive={isNavItemActive}
119
+ isToolLinkActive={isToolLinkActive}
120
+ onToggleTools={() => setToolsExpanded((current) => !current)}
121
+ />
122
+ </div>
123
+
124
+ <section className="app-preview-content">
125
+ <div className="app-preview-stage">
126
+ <div className="grid">
127
+ {mockSurfaceCards.map((card) => (
128
+ <article key={card.title} className="panel preview-glass-card">
129
+ <div className="panel-inner">
130
+ <div className="preview-card-icon">
131
+ <card.icon className="size-4" />
132
+ </div>
133
+ <h2>{card.title}</h2>
134
+ <p className="muted">{card.description}</p>
135
+ </div>
136
+ </article>
137
+ ))}
138
+ </div>
139
+
140
+ <article className="panel preview-stage-panel">
141
+ <div className="panel-inner">
142
+ <div className="preview-stage-head">
143
+ <div>
144
+ <span className="eyebrow">Active path</span>
145
+ <h2>{activeHref}</h2>
146
+ </div>
147
+ <div className="actions">
148
+ {[...config.primaryNav, ...crmNavGroup.children, ...(config.adminNavItem ? [config.adminNavItem] : []), ...config.toolsSection.items].map((item) => (
149
+ <button
150
+ key={item.href}
151
+ type="button"
152
+ className={`nav-chip ${activeHref === item.href ? "active" : ""}`}
153
+ onClick={() => setActiveHref(item.href)}
154
+ >
155
+ {item.label}
156
+ </button>
157
+ ))}
158
+ </div>
159
+ </div>
160
+
161
+ <div className="preview-surface-grid">
162
+ <div className="preview-surface-card">
163
+ <p className="preview-label">Primary navigation</p>
164
+ <strong>{config.primaryNav.length} item(s)</strong>
165
+ <span>{config.primaryNav.map((item) => item.label).join(" · ")}</span>
166
+ </div>
167
+ <div className="preview-surface-card">
168
+ <p className="preview-label">CRM group</p>
169
+ <strong>{crmNavGroup.children.length} item(s)</strong>
170
+ <span>{crmNavGroup.children.map((item) => item.label).join(" · ")}</span>
171
+ </div>
172
+ <div className="preview-surface-card">
173
+ <p className="preview-label">Tools section</p>
174
+ <strong>{config.toolsSection.items.length} item(s)</strong>
175
+ <span>{config.toolsSection.items.map((item) => item.label).join(" · ") || "No tools enabled"}</span>
176
+ </div>
177
+ </div>
178
+ </div>
179
+ </article>
180
+ </div>
181
+ </section>
182
+ </div>
183
+ </div>
184
+ );
185
+ }
@@ -0,0 +1,125 @@
1
+ import { getStarterClientConfig } from "./client";
2
+
3
+ export type StarterChecklistItem = {
4
+ label: string;
5
+ done: boolean;
6
+ detail?: string;
7
+ };
8
+
9
+ export type StarterChecklistSection = {
10
+ key: string;
11
+ title: string;
12
+ items: StarterChecklistItem[];
13
+ };
14
+
15
+ function hasModule(moduleKeys: string[], key: string) {
16
+ return moduleKeys.includes(key);
17
+ }
18
+
19
+ export function getStarterBootstrapChecklist() {
20
+ const config = getStarterClientConfig();
21
+ const moduleKeys = config.enabledModules.map((moduleConfig) => moduleConfig.key);
22
+
23
+ const identitySection: StarterChecklistSection = {
24
+ key: "identity",
25
+ title: "Client identity",
26
+ items: [
27
+ {
28
+ label: "Define company and product name",
29
+ done: config.brand.companyName !== "Starter Client" && config.brand.productName !== "Operations Platform",
30
+ detail: `${config.brand.companyName} / ${config.brand.productName}`,
31
+ },
32
+ {
33
+ label: "Set client slug",
34
+ done: config.brand.slug !== "starter-client",
35
+ detail: config.brand.slug,
36
+ },
37
+ {
38
+ label: "Set client contact and support inboxes",
39
+ done: config.brand.contactEmail !== "hello@example.com" && config.brand.supportEmail !== "support@example.com",
40
+ detail: `${config.brand.contactEmail} · ${config.brand.supportEmail}`,
41
+ },
42
+ ],
43
+ };
44
+
45
+ const envSection: StarterChecklistSection = {
46
+ key: "environment",
47
+ title: "Environment and infrastructure",
48
+ items: config.envStatus.map((item) => ({
49
+ label: `Configure ${item.key}`,
50
+ done: item.present,
51
+ detail: `${item.scope} · ${item.requiredFor.join(", ")}`,
52
+ })),
53
+ };
54
+
55
+ const dataSection: StarterChecklistSection = {
56
+ key: "services",
57
+ title: "Client services",
58
+ items: [
59
+ {
60
+ label: "Provision dedicated Supabase project",
61
+ done: config.envStatus.filter((item) => item.key.includes("SUPABASE")).every((item) => item.present),
62
+ detail: "Database, auth, storage, and RPC functions should be client-specific.",
63
+ },
64
+ {
65
+ label: "Configure Resend for the client sender domain",
66
+ done: Boolean(config.envStatus.find((item) => item.key === "RESEND_API_KEY")?.present),
67
+ detail: "Use a sender/domain owned by this client instance.",
68
+ },
69
+ {
70
+ label: "Create per-client environment variables",
71
+ done: config.envReadiness.allReady,
72
+ detail: "Copy `.env.example` into `.env.local` and fill the real values.",
73
+ },
74
+ ],
75
+ };
76
+
77
+ const modulesSection: StarterChecklistSection = {
78
+ key: "modules",
79
+ title: "Enabled modules",
80
+ items: config.enabledModules.map((moduleConfig) => ({
81
+ label: `Validate ${moduleConfig.label}`,
82
+ done: Boolean(moduleConfig.playgroundHref),
83
+ detail: moduleConfig.playgroundHref
84
+ ? `Preview at ${moduleConfig.playgroundHref}`
85
+ : `Package ${moduleConfig.packageName}`,
86
+ })),
87
+ };
88
+
89
+ const rolloutSection: StarterChecklistSection = {
90
+ key: "rollout",
91
+ title: "Launch readiness",
92
+ items: [
93
+ {
94
+ label: "Preview Auth flows",
95
+ done: true,
96
+ detail: "/playground/auth",
97
+ },
98
+ {
99
+ label: "Preview CRM module",
100
+ done: hasModule(moduleKeys, "crm"),
101
+ detail: hasModule(moduleKeys, "crm") ? "/playground/crm" : "CRM not enabled",
102
+ },
103
+ {
104
+ label: "Preview Projects module",
105
+ done: hasModule(moduleKeys, "projects"),
106
+ detail: hasModule(moduleKeys, "projects") ? "/playground/projects" : "Projects not enabled",
107
+ },
108
+ {
109
+ label: "Preview Admin module",
110
+ done: hasModule(moduleKeys, "admin"),
111
+ detail: hasModule(moduleKeys, "admin") ? "/playground/admin" : "Admin not enabled",
112
+ },
113
+ {
114
+ label: "Deploy only after env readiness is green",
115
+ done: config.envReadiness.allReady,
116
+ detail: config.envReadiness.allReady ? "Ready for deploy validation." : `${config.envReadiness.missing.length} env key(s) still missing.`,
117
+ },
118
+ ],
119
+ };
120
+
121
+ return {
122
+ client: config,
123
+ sections: [identitySection, envSection, dataSection, modulesSection, rolloutSection],
124
+ };
125
+ }
@@ -0,0 +1,21 @@
1
+ export type StarterBrandConfig = {
2
+ companyName: string;
3
+ productName: string;
4
+ slug: string;
5
+ tagline: string;
6
+ contactEmail: string;
7
+ supportEmail: string;
8
+ primaryHex: string;
9
+ };
10
+
11
+ export const starterBrandConfig: StarterBrandConfig = {
12
+ companyName: process.env.NEXT_PUBLIC_CLIENT_COMPANY_NAME?.trim() || "Starter Client",
13
+ productName: process.env.NEXT_PUBLIC_CLIENT_PRODUCT_NAME?.trim() || "Operations Platform",
14
+ slug: process.env.NEXT_PUBLIC_CLIENT_SLUG?.trim() || "starter-client",
15
+ tagline:
16
+ process.env.NEXT_PUBLIC_CLIENT_TAGLINE?.trim()
17
+ || "A configurable Brightweb starter app for shipping new client instances without rebuilding the platform.",
18
+ contactEmail: process.env.NEXT_PUBLIC_CLIENT_CONTACT_EMAIL?.trim() || "hello@example.com",
19
+ supportEmail: process.env.NEXT_PUBLIC_CLIENT_SUPPORT_EMAIL?.trim() || "support@example.com",
20
+ primaryHex: process.env.NEXT_PUBLIC_CLIENT_PRIMARY_HEX?.trim() || "#1f7a45",
21
+ };
@@ -0,0 +1,18 @@
1
+ import { starterBrandConfig } from "./brand";
2
+ import { getStarterEnvStatus, isStarterEnvReady } from "./env";
3
+ import { getStarterShellConfig } from "./shell";
4
+
5
+ export function getStarterClientConfig() {
6
+ const shell = getStarterShellConfig();
7
+ const enabledModules = shell.enabledModules;
8
+ const moduleKeys = enabledModules.map((moduleConfig) => moduleConfig.key);
9
+
10
+ return {
11
+ brand: starterBrandConfig,
12
+ enabledModules,
13
+ envStatus: getStarterEnvStatus(),
14
+ envReadiness: isStarterEnvReady(moduleKeys),
15
+ shellPreview: shell.shellPreview,
16
+ toolbarRoutes: shell.toolbarRoutes,
17
+ };
18
+ }