pagelathe 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/dist/bin.js +10 -0
- package/dist/chunk-LRG7WLGI.js +1020 -0
- package/dist/index.js +9 -0
- package/dist/registry/app/astro.config.mjs +7 -0
- package/dist/registry/app/package.json +29 -0
- package/dist/registry/app/public/favicon.svg +4 -0
- package/dist/registry/app/scripts/vendor-sections.mjs +50 -0
- package/dist/registry/app/src/components/SectionRenderer.astro +23 -0
- package/dist/registry/app/src/components/sections/.gitkeep +0 -0
- package/dist/registry/app/src/content/_schema.ts +27 -0
- package/dist/registry/app/src/content/landing/index.yaml +135 -0
- package/dist/registry/app/src/content.config.ts +10 -0
- package/dist/registry/app/src/layouts/Base.astro +24 -0
- package/dist/registry/app/src/pages/index.astro +13 -0
- package/dist/registry/app/src/styles/global.css +20 -0
- package/dist/registry/app/tsconfig.json +1 -0
- package/dist/registry/sections/codeDemo/schema.ts +46 -0
- package/dist/registry/sections/codeDemo/section.astro +55 -0
- package/dist/registry/sections/features/schema.ts +44 -0
- package/dist/registry/sections/features/section.astro +25 -0
- package/dist/registry/sections/finalCta/schema.ts +30 -0
- package/dist/registry/sections/finalCta/section.astro +18 -0
- package/dist/registry/sections/footer/schema.ts +44 -0
- package/dist/registry/sections/footer/section.astro +29 -0
- package/dist/registry/sections/header/schema.ts +35 -0
- package/dist/registry/sections/header/section.astro +28 -0
- package/dist/registry/sections/hero/schema.ts +50 -0
- package/dist/registry/sections/hero/section.astro +56 -0
- package/dist/registry/sections/package.json +23 -0
- package/dist/registry/sections/pricing/schema.ts +55 -0
- package/dist/registry/sections/pricing/section.astro +29 -0
- package/dist/registry/sections/src/a11y.ts +34 -0
- package/dist/registry/sections/src/env.d.ts +7 -0
- package/dist/registry/sections/src/manifest.ts +41 -0
- package/dist/registry/sections/src/page.ts +48 -0
- package/dist/registry/sections/src/registry.ts +62 -0
- package/dist/registry/sections/src/render-harness.ts +11 -0
- package/dist/registry/sections/test/chrome.test.ts +29 -0
- package/dist/registry/sections/test/codeDemo.test.ts +16 -0
- package/dist/registry/sections/test/features.test.ts +18 -0
- package/dist/registry/sections/test/finalCta.test.ts +15 -0
- package/dist/registry/sections/test/hero.test.ts +30 -0
- package/dist/registry/sections/test/page.test.ts +58 -0
- package/dist/registry/sections/test/pricing.test.ts +22 -0
- package/dist/registry/sections/test/registry-complete.test.ts +38 -0
- package/dist/registry/sections/test/registry.test.ts +12 -0
- package/dist/registry/sections/test/validate.test.ts +29 -0
- package/dist/registry/sections/tsconfig.json +8 -0
- package/dist/registry/sections/vitest.config.ts +7 -0
- package/package.json +56 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pagelathe/app-engine",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "astro dev",
|
|
8
|
+
"prebuild": "node ./scripts/vendor-sections.mjs",
|
|
9
|
+
"build": "astro build",
|
|
10
|
+
"preview": "astro preview",
|
|
11
|
+
"clean:sections": "node ./scripts/vendor-sections.mjs --clean"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"astro": "6.4.8",
|
|
15
|
+
"@tailwindcss/vite": "4.3.1",
|
|
16
|
+
"tailwindcss": "4.3.1",
|
|
17
|
+
"shiki": "^1.29.2",
|
|
18
|
+
"zod": "^3.24.1"
|
|
19
|
+
},
|
|
20
|
+
"comment": "Build-toolchain versions are pinned exactly and vite is overridden to a tested release so a freshly-installed scaffold reproduces a working Astro + Tailwind v4 build (avoids a @tailwindcss/vite <-> vite/rolldown binding mismatch on newer floating versions).",
|
|
21
|
+
"overrides": {
|
|
22
|
+
"vite": "7.3.5"
|
|
23
|
+
},
|
|
24
|
+
"pnpm": {
|
|
25
|
+
"overrides": {
|
|
26
|
+
"vite": "7.3.5"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* vendor-sections.mjs
|
|
4
|
+
*
|
|
5
|
+
* Copies registry/sections/<type>/section.astro into
|
|
6
|
+
* registry/app/src/components/sections/<type>.astro (flattened).
|
|
7
|
+
*
|
|
8
|
+
* Run with --clean to remove vendored copies (keeping .gitkeep).
|
|
9
|
+
*
|
|
10
|
+
* NOTE: The vendored .astro files contain `import type { propsSchema } from "./schema.js"`.
|
|
11
|
+
* That import is type-only and is erased at build by esbuild under verbatimModuleSyntax —
|
|
12
|
+
* the missing ./schema.js is never resolved at build time.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { copyFileSync, existsSync, rmSync } from "node:fs";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
import { join, dirname } from "node:path";
|
|
18
|
+
|
|
19
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const appRoot = join(__dirname, "..");
|
|
21
|
+
const sectionsRoot = join(appRoot, "..", "sections");
|
|
22
|
+
const destDir = join(appRoot, "src", "components", "sections");
|
|
23
|
+
|
|
24
|
+
// The 7 R-set section types (mirrors sectionModules order in registry.ts)
|
|
25
|
+
const SECTION_TYPES = ["hero", "header", "footer", "features", "codeDemo", "pricing", "finalCta"];
|
|
26
|
+
|
|
27
|
+
const isClean = process.argv.includes("--clean");
|
|
28
|
+
|
|
29
|
+
if (isClean) {
|
|
30
|
+
for (const type of SECTION_TYPES) {
|
|
31
|
+
const dest = join(destDir, `${type}.astro`);
|
|
32
|
+
if (existsSync(dest)) {
|
|
33
|
+
rmSync(dest);
|
|
34
|
+
console.log(`[vendor-sections] removed ${type}.astro`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
console.log("[vendor-sections] clean done");
|
|
38
|
+
} else {
|
|
39
|
+
for (const type of SECTION_TYPES) {
|
|
40
|
+
const src = join(sectionsRoot, type, "section.astro");
|
|
41
|
+
const dest = join(destDir, `${type}.astro`);
|
|
42
|
+
if (!existsSync(src)) {
|
|
43
|
+
console.error(`[vendor-sections] ERROR: source not found: ${src}`);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
copyFileSync(src, dest);
|
|
47
|
+
console.log(`[vendor-sections] copied ${type}.astro`);
|
|
48
|
+
}
|
|
49
|
+
console.log("[vendor-sections] done");
|
|
50
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
type: string;
|
|
4
|
+
props: Record<string, unknown>;
|
|
5
|
+
}
|
|
6
|
+
const { type, props } = Astro.props;
|
|
7
|
+
|
|
8
|
+
const modules = import.meta.glob("./sections/*.astro", { eager: true }) as Record<
|
|
9
|
+
string,
|
|
10
|
+
{ default: unknown }
|
|
11
|
+
>;
|
|
12
|
+
const entry = modules[`./sections/${type}.astro`];
|
|
13
|
+
const Component = entry?.default as
|
|
14
|
+
| ((props: Record<string, unknown>) => unknown)
|
|
15
|
+
| undefined;
|
|
16
|
+
if (!Component) {
|
|
17
|
+
console.warn(
|
|
18
|
+
`[pagelathe] unknown section type "${type}" — no component at ./sections/${type}.astro`,
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
{Component ? <Component {...props} /> : null}
|
|
File without changes
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Envelope schema for the landing document. The pagelathe CLI validates every
|
|
5
|
+
* section's props against its strict per-section schema at generation time;
|
|
6
|
+
* this app-side schema validates document STRUCTURE so the project builds with
|
|
7
|
+
* only public dependencies (no private @pagelathe/* package required).
|
|
8
|
+
*/
|
|
9
|
+
export const pageDocumentSchema = z.object({
|
|
10
|
+
meta: z.object({
|
|
11
|
+
title: z.string().min(1),
|
|
12
|
+
description: z.string().min(1),
|
|
13
|
+
locales: z.array(z.string()).min(1).default(["en"]),
|
|
14
|
+
primaryGoal: z.string().default("signup"),
|
|
15
|
+
}),
|
|
16
|
+
theme: z.object({ tokens: z.record(z.string()).default({}) }).default({ tokens: {} }),
|
|
17
|
+
archetype: z.string().default("general"),
|
|
18
|
+
sections: z
|
|
19
|
+
.array(
|
|
20
|
+
z.object({
|
|
21
|
+
type: z.string().min(1),
|
|
22
|
+
id: z.string().min(1),
|
|
23
|
+
props: z.record(z.unknown()),
|
|
24
|
+
}),
|
|
25
|
+
)
|
|
26
|
+
.min(1),
|
|
27
|
+
});
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
meta:
|
|
2
|
+
title: "Branchy — Postgres branching for teams"
|
|
3
|
+
description: "Spin up an isolated database branch per pull request in one command."
|
|
4
|
+
locales: ["en"]
|
|
5
|
+
primaryGoal: signup
|
|
6
|
+
theme:
|
|
7
|
+
tokens:
|
|
8
|
+
colorPrimary: "#3A6463"
|
|
9
|
+
radius: "0.5rem"
|
|
10
|
+
font: "Inter"
|
|
11
|
+
archetype: sdk-infra
|
|
12
|
+
sections:
|
|
13
|
+
- type: header
|
|
14
|
+
id: header-1
|
|
15
|
+
props:
|
|
16
|
+
brand: "Branchy"
|
|
17
|
+
links:
|
|
18
|
+
- label: "Docs"
|
|
19
|
+
href: "/docs"
|
|
20
|
+
- label: "Pricing"
|
|
21
|
+
href: "#pricing"
|
|
22
|
+
github:
|
|
23
|
+
href: "https://github.com"
|
|
24
|
+
stars: "4.2k"
|
|
25
|
+
cta:
|
|
26
|
+
label: "Start free"
|
|
27
|
+
href: "#get-started"
|
|
28
|
+
|
|
29
|
+
- type: hero
|
|
30
|
+
id: hero-1
|
|
31
|
+
props:
|
|
32
|
+
variant: code-snippet
|
|
33
|
+
eyebrow: "Open source"
|
|
34
|
+
headline: "Postgres branching for teams"
|
|
35
|
+
subhead: "Spin up an isolated database branch per pull request in one command."
|
|
36
|
+
ctas:
|
|
37
|
+
- label: "Start free"
|
|
38
|
+
href: "#get-started"
|
|
39
|
+
style: primary
|
|
40
|
+
- label: "View on GitHub"
|
|
41
|
+
href: "https://github.com"
|
|
42
|
+
style: secondary
|
|
43
|
+
code:
|
|
44
|
+
lang: bash
|
|
45
|
+
source: "npx branchy create --from main\n# → branch ready in 1.2s"
|
|
46
|
+
|
|
47
|
+
- type: features
|
|
48
|
+
id: features-1
|
|
49
|
+
props:
|
|
50
|
+
heading: "Built for teams that ship"
|
|
51
|
+
subhead: "Isolated environments, instant reset, zero config."
|
|
52
|
+
items:
|
|
53
|
+
- title: "Branch per PR"
|
|
54
|
+
body: "Every pull request gets an isolated database."
|
|
55
|
+
emphasis: true
|
|
56
|
+
- title: "1-second reset"
|
|
57
|
+
body: "Roll back to a clean state instantly."
|
|
58
|
+
emphasis: false
|
|
59
|
+
- title: "Self-host or cloud"
|
|
60
|
+
body: "Run it your way; data stays yours."
|
|
61
|
+
emphasis: false
|
|
62
|
+
|
|
63
|
+
- type: codeDemo
|
|
64
|
+
id: codeDemo-1
|
|
65
|
+
props:
|
|
66
|
+
heading: "Drop it into your stack"
|
|
67
|
+
subhead: "Same API across languages."
|
|
68
|
+
snippets:
|
|
69
|
+
- lang: python
|
|
70
|
+
label: "Python"
|
|
71
|
+
source: "from branchy import Client\nc = Client()\nc.create(\"feat-x\")"
|
|
72
|
+
- lang: javascript
|
|
73
|
+
label: "Node"
|
|
74
|
+
source: "import { Client } from 'branchy'\nawait new Client().create('feat-x')"
|
|
75
|
+
- lang: go
|
|
76
|
+
label: "Go"
|
|
77
|
+
source: "client := branchy.New()\nclient.Create(\"feat-x\")"
|
|
78
|
+
|
|
79
|
+
- type: pricing
|
|
80
|
+
id: pricing-1
|
|
81
|
+
props:
|
|
82
|
+
heading: "Honest pricing"
|
|
83
|
+
subhead: "Start free. No credit card."
|
|
84
|
+
tiers:
|
|
85
|
+
- name: "Open source"
|
|
86
|
+
price: "$0"
|
|
87
|
+
description: "Self-host, unlimited branches."
|
|
88
|
+
features:
|
|
89
|
+
- "Self-hosted"
|
|
90
|
+
- "Community support"
|
|
91
|
+
- "Apache-2.0"
|
|
92
|
+
cta:
|
|
93
|
+
label: "Get started"
|
|
94
|
+
href: "#get-started"
|
|
95
|
+
featured: false
|
|
96
|
+
- name: "Team"
|
|
97
|
+
price: "$29"
|
|
98
|
+
period: "/mo"
|
|
99
|
+
description: "Managed cloud for small teams."
|
|
100
|
+
features:
|
|
101
|
+
- "10 projects"
|
|
102
|
+
- "Daily backups"
|
|
103
|
+
- "Email support"
|
|
104
|
+
cta:
|
|
105
|
+
label: "Start trial"
|
|
106
|
+
href: "#trial"
|
|
107
|
+
featured: true
|
|
108
|
+
|
|
109
|
+
- type: finalCta
|
|
110
|
+
id: finalCta-1
|
|
111
|
+
props:
|
|
112
|
+
headline: "Ship your first branch today"
|
|
113
|
+
subhead: "Free and open source. Self-host in minutes."
|
|
114
|
+
cta:
|
|
115
|
+
label: "Start free"
|
|
116
|
+
href: "#get-started"
|
|
117
|
+
microcopy: "No credit card · Apache-2.0"
|
|
118
|
+
|
|
119
|
+
- type: footer
|
|
120
|
+
id: footer-1
|
|
121
|
+
props:
|
|
122
|
+
brand: "Branchy"
|
|
123
|
+
tagline: "Database branching for teams."
|
|
124
|
+
columns:
|
|
125
|
+
- heading: "Product"
|
|
126
|
+
links:
|
|
127
|
+
- label: "Docs"
|
|
128
|
+
href: "/docs"
|
|
129
|
+
- label: "Pricing"
|
|
130
|
+
href: "#pricing"
|
|
131
|
+
- heading: "Community"
|
|
132
|
+
links:
|
|
133
|
+
- label: "GitHub"
|
|
134
|
+
href: "https://github.com"
|
|
135
|
+
copyright: "© 2026 Branchy. Apache-2.0."
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { defineCollection } from "astro:content";
|
|
2
|
+
import { glob } from "astro/loaders";
|
|
3
|
+
import { pageDocumentSchema } from "./content/_schema.js";
|
|
4
|
+
|
|
5
|
+
const landing = defineCollection({
|
|
6
|
+
loader: glob({ pattern: "**/*.yaml", base: "./src/content/landing" }),
|
|
7
|
+
schema: pageDocumentSchema,
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
export const collections = { landing };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
import "../styles/global.css";
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
title: string;
|
|
6
|
+
description: string;
|
|
7
|
+
locale?: string;
|
|
8
|
+
}
|
|
9
|
+
const { title, description, locale = "en" } = Astro.props;
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
<!doctype html>
|
|
13
|
+
<html lang={locale}>
|
|
14
|
+
<head>
|
|
15
|
+
<meta charset="utf-8" />
|
|
16
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
17
|
+
<title>{title}</title>
|
|
18
|
+
<meta name="description" content={description} />
|
|
19
|
+
<link rel="icon" href="/favicon.svg" />
|
|
20
|
+
</head>
|
|
21
|
+
<body class="min-h-screen antialiased">
|
|
22
|
+
<slot />
|
|
23
|
+
</body>
|
|
24
|
+
</html>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { getEntry } from "astro:content";
|
|
3
|
+
import Base from "../layouts/Base.astro";
|
|
4
|
+
import SectionRenderer from "../components/SectionRenderer.astro";
|
|
5
|
+
|
|
6
|
+
const page = await getEntry("landing", "index");
|
|
7
|
+
if (!page) throw new Error("Missing src/content/landing/index.yaml");
|
|
8
|
+
const { meta, sections } = page.data;
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
<Base title={meta.title} description={meta.description} locale={meta.locales[0]}>
|
|
12
|
+
{sections.map((s) => <SectionRenderer type={s.type} props={s.props} />)}
|
|
13
|
+
</Base>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
|
|
3
|
+
@theme {
|
|
4
|
+
--color-primary: #3a6463;
|
|
5
|
+
--radius: 0.5rem;
|
|
6
|
+
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
:root {
|
|
10
|
+
color-scheme: dark;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
html {
|
|
14
|
+
font-family: var(--font-sans);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
body {
|
|
18
|
+
background-color: #0a0a0a;
|
|
19
|
+
color: #fafafa;
|
|
20
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "extends": "astro/tsconfigs/strict", "include": [".astro", "src"], "exclude": ["dist"] }
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { SectionManifest } from "../src/manifest.js";
|
|
3
|
+
|
|
4
|
+
export const snippetSchema = z.object({
|
|
5
|
+
lang: z.string().min(1),
|
|
6
|
+
label: z.string().min(1),
|
|
7
|
+
source: z.string().min(1),
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
export const propsSchema = z.object({
|
|
11
|
+
heading: z.string().min(1),
|
|
12
|
+
subhead: z.string().optional(),
|
|
13
|
+
snippets: z.array(snippetSchema).min(1).max(6),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export const entrySchema = z.object({
|
|
17
|
+
type: z.literal("codeDemo"),
|
|
18
|
+
id: z.string().min(1),
|
|
19
|
+
props: propsSchema,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export const manifest: SectionManifest = {
|
|
23
|
+
type: "codeDemo",
|
|
24
|
+
title: "Code / Integration demo",
|
|
25
|
+
description: "Multi-language code tabs with shiki highlighting and copy-to-clipboard.",
|
|
26
|
+
required: true,
|
|
27
|
+
archetypes: ["sdk-infra", "technical-app"],
|
|
28
|
+
componentFile: "section.astro",
|
|
29
|
+
defaultProps: {
|
|
30
|
+
heading: "Drop it into your stack",
|
|
31
|
+
subhead: "Same API across languages.",
|
|
32
|
+
snippets: [
|
|
33
|
+
{
|
|
34
|
+
lang: "python",
|
|
35
|
+
label: "Python",
|
|
36
|
+
source: 'from branchy import Client\nc = Client()\nc.create("feat-x")',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
lang: "javascript",
|
|
40
|
+
label: "Node",
|
|
41
|
+
source: "import { Client } from 'branchy'\nawait new Client().create('feat-x')",
|
|
42
|
+
},
|
|
43
|
+
{ lang: "go", label: "Go", source: 'client := branchy.New()\nclient.Create("feat-x")' },
|
|
44
|
+
],
|
|
45
|
+
} satisfies z.input<typeof propsSchema>,
|
|
46
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { codeToHtml } from "shiki";
|
|
3
|
+
import type { z } from "zod";
|
|
4
|
+
import type { propsSchema } from "./schema.js";
|
|
5
|
+
|
|
6
|
+
type Props = z.infer<typeof propsSchema>;
|
|
7
|
+
const { heading, subhead, snippets } = Astro.props as Props;
|
|
8
|
+
|
|
9
|
+
const rendered = await Promise.all(
|
|
10
|
+
snippets.map(async (s) => ({
|
|
11
|
+
label: s.label,
|
|
12
|
+
html: await codeToHtml(s.source, { lang: s.lang, theme: "github-dark" }),
|
|
13
|
+
})),
|
|
14
|
+
);
|
|
15
|
+
const groupId = `code-${Math.abs(heading.length * 31 + snippets.length)}`;
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
<section class="mx-auto max-w-5xl px-6 py-24" data-section="codeDemo">
|
|
19
|
+
<div class="max-w-2xl">
|
|
20
|
+
<h2 class="text-3xl font-bold tracking-tight text-white">{heading}</h2>
|
|
21
|
+
{subhead && <p class="mt-3 text-white/60">{subhead}</p>}
|
|
22
|
+
</div>
|
|
23
|
+
<div class="mt-10 overflow-hidden rounded-[var(--radius)] border border-white/10 bg-black/40" data-codetabs={groupId}>
|
|
24
|
+
<div class="flex gap-1 border-b border-white/10 px-2 pt-2" role="tablist" aria-label="Code language">
|
|
25
|
+
{rendered.map((r, i) => (
|
|
26
|
+
<button
|
|
27
|
+
type="button"
|
|
28
|
+
role="tab"
|
|
29
|
+
aria-selected={i === 0 ? "true" : "false"}
|
|
30
|
+
data-tab={i}
|
|
31
|
+
class="rounded-t-md px-3 py-1.5 text-sm text-white/60 aria-selected:bg-white/5 aria-selected:text-white"
|
|
32
|
+
>
|
|
33
|
+
{r.label}
|
|
34
|
+
</button>
|
|
35
|
+
))}
|
|
36
|
+
</div>
|
|
37
|
+
{rendered.map((r, i) => (
|
|
38
|
+
<div data-panel={i} hidden={i !== 0} class="overflow-x-auto p-4 text-sm [&_pre]:!bg-transparent" set:html={r.html} />
|
|
39
|
+
))}
|
|
40
|
+
</div>
|
|
41
|
+
</section>
|
|
42
|
+
|
|
43
|
+
<script>
|
|
44
|
+
for (const group of document.querySelectorAll("[data-codetabs]")) {
|
|
45
|
+
const tabs = group.querySelectorAll("[data-tab]");
|
|
46
|
+
const panels = group.querySelectorAll("[data-panel]");
|
|
47
|
+
for (const tab of tabs) {
|
|
48
|
+
tab.addEventListener("click", () => {
|
|
49
|
+
const idx = tab.getAttribute("data-tab");
|
|
50
|
+
for (const t of tabs) t.setAttribute("aria-selected", t === tab ? "true" : "false");
|
|
51
|
+
for (const p of panels) (p as HTMLElement).hidden = p.getAttribute("data-panel") !== idx;
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
</script>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { SectionManifest } from "../src/manifest.js";
|
|
3
|
+
|
|
4
|
+
export const featureSchema = z.object({
|
|
5
|
+
title: z.string().min(1),
|
|
6
|
+
body: z.string().min(1),
|
|
7
|
+
/** Larger tile = more important feature (first item is the lead). */
|
|
8
|
+
emphasis: z.boolean().default(false),
|
|
9
|
+
icon: z.string().optional(),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export const propsSchema = z.object({
|
|
13
|
+
heading: z.string().min(1),
|
|
14
|
+
subhead: z.string().optional(),
|
|
15
|
+
items: z.array(featureSchema).min(2).max(8),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export const entrySchema = z.object({
|
|
19
|
+
type: z.literal("features"),
|
|
20
|
+
id: z.string().min(1),
|
|
21
|
+
props: propsSchema,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export const manifest: SectionManifest = {
|
|
25
|
+
type: "features",
|
|
26
|
+
title: "Features / Bento",
|
|
27
|
+
description: "Feature grid; the emphasized tile is the most important capability.",
|
|
28
|
+
required: true,
|
|
29
|
+
archetypes: ["sdk-infra", "technical-app", "general"],
|
|
30
|
+
componentFile: "section.astro",
|
|
31
|
+
defaultProps: {
|
|
32
|
+
heading: "Built for teams that ship",
|
|
33
|
+
subhead: "Isolated environments, instant reset, zero config.",
|
|
34
|
+
items: [
|
|
35
|
+
{
|
|
36
|
+
title: "Branch per PR",
|
|
37
|
+
body: "Every pull request gets an isolated database.",
|
|
38
|
+
emphasis: true,
|
|
39
|
+
},
|
|
40
|
+
{ title: "1-second reset", body: "Roll back to a clean state instantly." },
|
|
41
|
+
{ title: "Self-host or cloud", body: "Run it your way; data stays yours." },
|
|
42
|
+
],
|
|
43
|
+
} satisfies z.input<typeof propsSchema>,
|
|
44
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { z } from "zod";
|
|
3
|
+
import type { propsSchema } from "./schema.js";
|
|
4
|
+
|
|
5
|
+
type Props = z.infer<typeof propsSchema>;
|
|
6
|
+
const { heading, subhead, items } = Astro.props as Props;
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
<section class="mx-auto max-w-6xl px-6 py-24" data-section="features">
|
|
10
|
+
<div class="max-w-2xl">
|
|
11
|
+
<h2 class="text-3xl font-bold tracking-tight text-white">{heading}</h2>
|
|
12
|
+
{subhead && <p class="mt-3 text-white/60">{subhead}</p>}
|
|
13
|
+
</div>
|
|
14
|
+
<div class="mt-12 grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
|
|
15
|
+
{items.map((f) => (
|
|
16
|
+
<div
|
|
17
|
+
class={`rounded-[var(--radius)] border border-white/10 bg-white/[0.02] p-6 ${f.emphasis ? "sm:col-span-2 lg:row-span-2" : ""}`}
|
|
18
|
+
>
|
|
19
|
+
{f.icon && <div class="mb-3 text-2xl" aria-hidden="true">{f.icon}</div>}
|
|
20
|
+
<h3 class="text-lg font-semibold text-white">{f.title}</h3>
|
|
21
|
+
<p class="mt-2 text-sm text-white/60">{f.body}</p>
|
|
22
|
+
</div>
|
|
23
|
+
))}
|
|
24
|
+
</div>
|
|
25
|
+
</section>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { SectionManifest } from "../src/manifest.js";
|
|
3
|
+
|
|
4
|
+
export const propsSchema = z.object({
|
|
5
|
+
headline: z.string().min(1),
|
|
6
|
+
subhead: z.string().optional(),
|
|
7
|
+
cta: z.object({ label: z.string().min(1), href: z.string().min(1) }),
|
|
8
|
+
microcopy: z.string().optional(),
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
export const entrySchema = z.object({
|
|
12
|
+
type: z.literal("finalCta"),
|
|
13
|
+
id: z.string().min(1),
|
|
14
|
+
props: propsSchema,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export const manifest: SectionManifest = {
|
|
18
|
+
type: "finalCta",
|
|
19
|
+
title: "Final CTA",
|
|
20
|
+
description: "Closing call to action with action-specific label and honest microcopy.",
|
|
21
|
+
required: true,
|
|
22
|
+
archetypes: ["sdk-infra", "technical-app", "general"],
|
|
23
|
+
componentFile: "section.astro",
|
|
24
|
+
defaultProps: {
|
|
25
|
+
headline: "Ship your first branch today",
|
|
26
|
+
subhead: "Free and open source. Self-host in minutes.",
|
|
27
|
+
cta: { label: "Start free", href: "#get-started" },
|
|
28
|
+
microcopy: "No credit card · Apache-2.0",
|
|
29
|
+
} satisfies z.input<typeof propsSchema>,
|
|
30
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { z } from "zod";
|
|
3
|
+
import type { propsSchema } from "./schema.js";
|
|
4
|
+
|
|
5
|
+
type Props = z.infer<typeof propsSchema>;
|
|
6
|
+
const { headline, subhead, cta, microcopy } = Astro.props as Props;
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
<section class="mx-auto max-w-4xl px-6 py-28 text-center" data-section="finalCta">
|
|
10
|
+
<h2 class="text-3xl font-bold tracking-tight text-white sm:text-4xl">{headline}</h2>
|
|
11
|
+
{subhead && <p class="mt-4 text-lg text-white/60">{subhead}</p>}
|
|
12
|
+
<div class="mt-8">
|
|
13
|
+
<a href={cta.href} class="inline-flex rounded-[var(--radius)] bg-[var(--color-primary)] px-6 py-3 text-sm font-semibold text-white hover:opacity-90">
|
|
14
|
+
{cta.label}
|
|
15
|
+
</a>
|
|
16
|
+
</div>
|
|
17
|
+
{microcopy && <p class="mt-4 text-xs text-white/40">{microcopy}</p>}
|
|
18
|
+
</section>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { SectionManifest } from "../src/manifest.js";
|
|
3
|
+
|
|
4
|
+
export const footerColumnSchema = z.object({
|
|
5
|
+
heading: z.string().min(1),
|
|
6
|
+
links: z.array(z.object({ label: z.string().min(1), href: z.string().min(1) })).min(1),
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
export const propsSchema = z.object({
|
|
10
|
+
brand: z.string().min(1),
|
|
11
|
+
tagline: z.string().optional(),
|
|
12
|
+
columns: z.array(footerColumnSchema).max(4).default([]),
|
|
13
|
+
copyright: z.string().min(1),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export const entrySchema = z.object({
|
|
17
|
+
type: z.literal("footer"),
|
|
18
|
+
id: z.string().min(1),
|
|
19
|
+
props: propsSchema,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export const manifest: SectionManifest = {
|
|
23
|
+
type: "footer",
|
|
24
|
+
title: "Footer",
|
|
25
|
+
description: "Site footer with link columns and copyright.",
|
|
26
|
+
required: true,
|
|
27
|
+
archetypes: ["sdk-infra", "technical-app", "general"],
|
|
28
|
+
componentFile: "section.astro",
|
|
29
|
+
defaultProps: {
|
|
30
|
+
brand: "Branchy",
|
|
31
|
+
tagline: "Database branching for teams.",
|
|
32
|
+
columns: [
|
|
33
|
+
{
|
|
34
|
+
heading: "Product",
|
|
35
|
+
links: [
|
|
36
|
+
{ label: "Docs", href: "/docs" },
|
|
37
|
+
{ label: "Pricing", href: "#pricing" },
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
{ heading: "Community", links: [{ label: "GitHub", href: "https://github.com" }] },
|
|
41
|
+
],
|
|
42
|
+
copyright: "© 2026 Branchy. Apache-2.0.",
|
|
43
|
+
} satisfies z.input<typeof propsSchema>,
|
|
44
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { z } from "zod";
|
|
3
|
+
import type { propsSchema } from "./schema.js";
|
|
4
|
+
|
|
5
|
+
type Props = z.infer<typeof propsSchema>;
|
|
6
|
+
const { brand, tagline, columns, copyright } = Astro.props as Props;
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
<footer class="border-t border-white/10 bg-black/40" data-section="footer">
|
|
10
|
+
<div class="mx-auto grid max-w-6xl gap-8 px-6 py-14 sm:grid-cols-2 lg:grid-cols-4">
|
|
11
|
+
<div>
|
|
12
|
+
<p class="text-sm font-bold text-white">{brand}</p>
|
|
13
|
+
{tagline && <p class="mt-2 text-sm text-white/60">{tagline}</p>}
|
|
14
|
+
</div>
|
|
15
|
+
{columns.map((col) => (
|
|
16
|
+
<div>
|
|
17
|
+
<p class="text-xs font-semibold uppercase tracking-wide text-white/40">{col.heading}</p>
|
|
18
|
+
<ul class="mt-3 space-y-2">
|
|
19
|
+
{col.links.map((l) => (
|
|
20
|
+
<li><a href={l.href} class="text-sm text-white/70 hover:text-white">{l.label}</a></li>
|
|
21
|
+
))}
|
|
22
|
+
</ul>
|
|
23
|
+
</div>
|
|
24
|
+
))}
|
|
25
|
+
</div>
|
|
26
|
+
<div class="border-t border-white/10 px-6 py-6">
|
|
27
|
+
<p class="mx-auto max-w-6xl text-xs text-white/50">{copyright}</p>
|
|
28
|
+
</div>
|
|
29
|
+
</footer>
|