@nebutra/landing-builder 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts DELETED
@@ -1,291 +0,0 @@
1
- import { mkdir } from "node:fs/promises";
2
- import { dirname, join } from "node:path";
3
- import { requireCapabilityTenant } from "@nebutra/capability-kit";
4
- import { appendCapabilityDebug, readCapabilityDebug } from "@nebutra/capability-kit/debug";
5
- import { ContentStore } from "@nebutra/content-store";
6
- import { CapabilityError } from "@nebutra/errors";
7
- import { EventLog } from "@nebutra/event-log";
8
- import { assetId, type BrandContext } from "@nebutra/generation-context";
9
-
10
- export type LandingMode = "one_pager" | "marketing_site" | "webapp_mvp" | "site_edit";
11
-
12
- export interface OnePagerInput {
13
- readonly tenantId?: string;
14
- readonly brand: BrandContext;
15
- readonly productDesc: string;
16
- readonly ctaText: string;
17
- readonly mode?: LandingMode;
18
- }
19
-
20
- export interface SiteFile {
21
- readonly path: string;
22
- readonly mime: string;
23
- readonly bytes: number;
24
- }
25
-
26
- export interface SitePreview {
27
- readonly kind: "static-content-store";
28
- readonly entrypoint: string;
29
- }
30
-
31
- export interface DeployHandoff {
32
- readonly status: "handoff";
33
- readonly targets: readonly string[];
34
- readonly manifestPath: string;
35
- readonly suggestion: string;
36
- }
37
-
38
- export interface SitePackage {
39
- readonly tenantId: string;
40
- readonly play: "one_pager";
41
- readonly brandId: string;
42
- readonly mode: "one_pager";
43
- readonly files: readonly SiteFile[];
44
- readonly preview: SitePreview;
45
- readonly deploy: DeployHandoff;
46
- readonly eventId: string;
47
- }
48
-
49
- export interface TailwindTheme {
50
- readonly cssVariables: string;
51
- readonly primary: string;
52
- readonly background: string;
53
- readonly accent: string;
54
- }
55
-
56
- export interface LandingBuilderDoctorReport {
57
- readonly capability: "landing-builder";
58
- readonly ok: boolean;
59
- readonly checkedAt: string;
60
- readonly plays: readonly string[];
61
- readonly modes: readonly LandingMode[];
62
- readonly deployAdapters: readonly { readonly provider: string; readonly ok: boolean }[];
63
- readonly suggestion?: string;
64
- }
65
-
66
- export interface LandingBuilderOptions {
67
- readonly tenantId?: string;
68
- readonly root?: string;
69
- readonly debugRoot?: string;
70
- readonly contentStore?: ContentStore;
71
- readonly eventLog?: EventLog;
72
- }
73
-
74
- const PLAY_NAME = "one_pager" as const;
75
-
76
- function colorByRole(brand: BrandContext, role: string, fallback: string): string {
77
- return brand.palette.find((color) => color.role === role)?.hex ?? fallback;
78
- }
79
-
80
- function escapeHtml(value: string): string {
81
- return value
82
- .replaceAll("&", "&")
83
- .replaceAll("<", "&lt;")
84
- .replaceAll(">", "&gt;")
85
- .replaceAll('"', "&quot;");
86
- }
87
-
88
- export function renderTailwindTheme(brand: BrandContext): TailwindTheme {
89
- const primary = colorByRole(brand, "primary", "#111827");
90
- const background = colorByRole(brand, "background", "#FFFFFF");
91
- const accent = colorByRole(brand, "accent", primary);
92
- return {
93
- primary,
94
- background,
95
- accent,
96
- cssVariables: [
97
- ":root {",
98
- ` --brand-primary: ${primary};`,
99
- ` --brand-background: ${background};`,
100
- ` --brand-accent: ${accent};`,
101
- ` --brand-heading: ${brand.typography.heading};`,
102
- ` --brand-body: ${brand.typography.body};`,
103
- "}",
104
- "",
105
- ].join("\n"),
106
- };
107
- }
108
-
109
- export function renderOnePagerHtml(input: OnePagerInput): string {
110
- const { brand } = input;
111
- const theme = renderTailwindTheme(brand);
112
- const keywords = brand.visualStyle.keywords.join(" / ");
113
- return [
114
- "<!doctype html>",
115
- '<html lang="en">',
116
- "<head>",
117
- ' <meta charset="utf-8" />',
118
- ' <meta name="viewport" content="width=device-width, initial-scale=1" />',
119
- ` <title>${escapeHtml(brand.name)}</title>`,
120
- ' <link rel="stylesheet" href="./theme.css" />',
121
- "</head>",
122
- `<body data-brand-id="${escapeHtml(brand.brandId)}">`,
123
- ' <main class="page">',
124
- ' <section class="hero">',
125
- ` <p class="eyebrow">${escapeHtml(keywords)}</p>`,
126
- ` <h1>${escapeHtml(brand.name)}</h1>`,
127
- ` <p class="lead">${escapeHtml(input.productDesc)}</p>`,
128
- ` <a class="cta" href="#waitlist">${escapeHtml(input.ctaText)}</a>`,
129
- " </section>",
130
- ' <section class="proof">',
131
- " <h2>Why it matters</h2>",
132
- ` <p>${escapeHtml(brand.toneKeywords.join(", "))} execution, grounded in one shared brand context.</p>`,
133
- " </section>",
134
- ' <section id="waitlist" class="waitlist">',
135
- " <h2>Join the waitlist</h2>",
136
- " <form>",
137
- ' <input aria-label="Email" placeholder="you@example.com" />',
138
- ' <button type="button">Join</button>',
139
- " </form>",
140
- " </section>",
141
- " </main>",
142
- "</body>",
143
- "</html>",
144
- `<!-- ${theme.primary} ${theme.accent} -->`,
145
- "",
146
- ].join("\n");
147
- }
148
-
149
- function renderThemeCss(brand: BrandContext): string {
150
- const theme = renderTailwindTheme(brand);
151
- return [
152
- theme.cssVariables,
153
- "* { box-sizing: border-box; }",
154
- "body { margin: 0; background: var(--brand-background); color: color-mix(in oklab, var(--brand-primary) 80%, black); font-family: var(--brand-body), system-ui, sans-serif; }",
155
- ".page { min-height: 100vh; }",
156
- ".hero { min-height: 82vh; display: grid; align-content: center; gap: 24px; padding: clamp(32px, 8vw, 96px); }",
157
- ".eyebrow { color: var(--brand-accent); font-family: var(--brand-heading), monospace; text-transform: uppercase; letter-spacing: 0; }",
158
- "h1 { max-width: 11ch; margin: 0; font-family: var(--brand-heading), system-ui; font-size: clamp(56px, 12vw, 132px); line-height: .92; letter-spacing: 0; }",
159
- ".lead { max-width: 760px; font-size: clamp(20px, 3vw, 34px); line-height: 1.2; }",
160
- ".cta, button { width: fit-content; border: 0; border-radius: 8px; background: var(--brand-primary); color: var(--brand-background); padding: 14px 20px; font: inherit; text-decoration: none; }",
161
- ".proof, .waitlist { padding: 48px clamp(32px, 8vw, 96px); border-top: 1px solid color-mix(in oklab, var(--brand-primary) 20%, transparent); }",
162
- "input { min-width: min(360px, 100%); border: 1px solid color-mix(in oklab, var(--brand-primary) 35%, transparent); border-radius: 8px; padding: 14px 16px; font: inherit; }",
163
- "form { display: flex; flex-wrap: wrap; gap: 12px; }",
164
- "",
165
- ].join("\n");
166
- }
167
-
168
- function deployManifest(input: OnePagerInput): string {
169
- return `${JSON.stringify(
170
- {
171
- mode: "one_pager",
172
- brandId: input.brand.brandId,
173
- entrypoint: "company/landing/index.html",
174
- targets: ["managed-hosting", "edge-pages", "static-hosting", "git-pages"],
175
- requiresExplicitDeployConsent: true,
176
- },
177
- null,
178
- 2,
179
- )}\n`;
180
- }
181
-
182
- export class LandingBuilder {
183
- readonly #tenantId: string | undefined;
184
- readonly #debugRoot: string;
185
- readonly #contentStore: ContentStore;
186
- readonly #eventLog: EventLog;
187
-
188
- private constructor(
189
- options: LandingBuilderOptions & { contentStore: ContentStore; eventLog: EventLog },
190
- ) {
191
- this.#tenantId = options.tenantId;
192
- this.#debugRoot = options.debugRoot ?? process.cwd();
193
- this.#contentStore = options.contentStore;
194
- this.#eventLog = options.eventLog;
195
- }
196
-
197
- static async open(
198
- root = ".nebutra/landing-builder",
199
- options: Omit<LandingBuilderOptions, "root" | "contentStore" | "eventLog"> = {},
200
- ): Promise<LandingBuilder> {
201
- const tenantId = options.tenantId ?? "local";
202
- await mkdir(root, { recursive: true });
203
- const contentStore = await ContentStore.open(join(root, "content"), { tenantId });
204
- const eventLog = await EventLog.open(join(root, "event-log"), { tenantId });
205
- return new LandingBuilder({ ...options, tenantId, root, contentStore, eventLog });
206
- }
207
-
208
- async runOnePager(input: OnePagerInput): Promise<SitePackage> {
209
- const tenantId = requireCapabilityTenant({
210
- explicit: input.tenantId ?? input.brand.tenantId,
211
- fallback: this.#tenantId,
212
- onMissing: () =>
213
- new CapabilityError("landing-builder", "Landing Builder requires tenant context", {
214
- suggestion:
215
- "Pass tenantId on the input, BrandContext, or construct LandingBuilder with tenantId.",
216
- statusCode: 400,
217
- }),
218
- });
219
- const html = renderOnePagerHtml(input);
220
- const css = renderThemeCss(input.brand);
221
- const manifest = deployManifest(input);
222
- const files = [
223
- { path: "company/landing/index.html", mime: "text/html", content: html },
224
- { path: "company/landing/theme.css", mime: "text/css", content: css },
225
- { path: "company/landing/deploy.json", mime: "application/json", content: manifest },
226
- ] as const;
227
- for (const file of files) await this.#contentStore.write(file.path, file.content);
228
- const eventId = await this.#eventLog.commit({
229
- traceId: assetId("landing_builder", input.brand.brandId),
230
- kind: "content_write",
231
- affected: files.map((file) => file.path),
232
- parent: null,
233
- snapshot: Object.fromEntries(files.map((file) => [file.path, file.content])),
234
- });
235
- const result: SitePackage = {
236
- tenantId,
237
- play: PLAY_NAME,
238
- brandId: input.brand.brandId,
239
- mode: "one_pager",
240
- files: files.map((file) => ({
241
- path: file.path,
242
- mime: file.mime,
243
- bytes: file.content.length,
244
- })),
245
- preview: { kind: "static-content-store", entrypoint: "company/landing/index.html" },
246
- deploy: {
247
- status: "handoff",
248
- targets: ["managed-hosting", "edge-pages", "static-hosting", "git-pages"],
249
- manifestPath: "company/landing/deploy.json",
250
- suggestion: "Connect a deploy integration and call the explicit deploy Play after review.",
251
- },
252
- eventId,
253
- };
254
- await this.#debug({ type: "run", tenantId, brandId: input.brand.brandId, eventId });
255
- return result;
256
- }
257
-
258
- async doctor(): Promise<LandingBuilderDoctorReport> {
259
- return {
260
- capability: "landing-builder",
261
- ok: true,
262
- checkedAt: new Date().toISOString(),
263
- plays: [PLAY_NAME],
264
- modes: ["one_pager", "marketing_site", "webapp_mvp", "site_edit"],
265
- deployAdapters: [
266
- { provider: "static-content-store", ok: true },
267
- { provider: "integration-vault", ok: false },
268
- ],
269
- suggestion:
270
- "Static generation is ready; real deploy requires integration-vault deploy adapters.",
271
- };
272
- }
273
-
274
- async close(): Promise<void> {
275
- await this.#contentStore.close();
276
- }
277
-
278
- async #debug(entry: Record<string, unknown>): Promise<void> {
279
- await mkdir(dirname(join(this.#debugRoot, ".nebutra", "debug", "landing-builder.jsonl")), {
280
- recursive: true,
281
- });
282
- await appendCapabilityDebug("landing-builder", entry, { root: this.#debugRoot });
283
- }
284
- }
285
-
286
- export async function readLandingBuilderDebug(
287
- root = process.cwd(),
288
- limit = 20,
289
- ): Promise<unknown[]> {
290
- return readCapabilityDebug("landing-builder", { root, limit });
291
- }
package/tsconfig.json DELETED
@@ -1,9 +0,0 @@
1
- {
2
- "extends": "../../../tsconfig.base.json",
3
- "compilerOptions": {
4
- "outDir": "dist",
5
- "rootDir": ".",
6
- "types": ["node", "vitest"]
7
- },
8
- "include": ["src/**/*.ts", "examples/**/*.ts"]
9
- }
package/tsup.config.ts DELETED
@@ -1,11 +0,0 @@
1
- import { defineConfig } from "tsup";
2
-
3
- export default defineConfig({
4
- entry: ["src/index.ts", "src/cli.ts"],
5
- format: ["esm"],
6
- sourcemap: true,
7
- dts: true,
8
- clean: true,
9
- target: "es2022",
10
- external: [/^@nebutra\//],
11
- });