Skip to content

Commit fbdcfa4

Browse files
committed
feat: add docs-style homepage section
1 parent 241dbd8 commit fbdcfa4

6 files changed

Lines changed: 374 additions & 4 deletions

File tree

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,14 @@ pnpm cli add preset next-saas --target ./my-app
4545
pnpm cli diff api-keys --target ./my-app
4646
```
4747

48+
The CLI can also install generated registry block JSON:
49+
50+
```bash
51+
pnpm cli add https://stackfoundry.dev/r/api-keys.json --target ./my-app
52+
pnpm cli add https://stackfoundry.dev/r/vendor-examples.json --target ./my-app
53+
pnpm cli diff https://stackfoundry.dev/r/api-keys.json --target ./my-app
54+
```
55+
4856
Use any shadcn-compatible registry client when you want direct source-block installation:
4957

5058
```bash

apps/cli/src/cli.mjs

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@ Usage:
2828
stackfoundry add <registry-item-url-or-file> [--target <dir>] [--dry-run] [--force]
2929
stackfoundry diff <module> [--target <dir>]
3030
stackfoundry diff <registry-item-url-or-file> [--target <dir>]
31+
32+
Examples:
33+
stackfoundry add api-keys --target ./app
34+
stackfoundry add preset vendor-examples --target ./app
35+
stackfoundry add https://stackfoundry.dev/r/api-keys.json --target ./app
36+
stackfoundry add public/r/vendor-examples.json --target ./app
3137
`);
3238
}
3339

@@ -443,6 +449,62 @@ async function addPreset(name, flags) {
443449
}
444450
}
445451

452+
async function addRegistryItem(specifier, flags, visited = new Set()) {
453+
const item = await readRegistryItem(specifier);
454+
const itemId = isHttpUrl(specifier) ? specifier : path.resolve(specifier);
455+
456+
if (visited.has(itemId)) return;
457+
visited.add(itemId);
458+
459+
if (item.type !== "registry:block") {
460+
throw new Error(`${item.name ?? specifier}: expected registry:block`);
461+
}
462+
463+
for (const dependency of item.registryDependencies ?? []) {
464+
await addRegistryItem(dependency, flags, visited);
465+
}
466+
467+
const installed = await loadInstallManifest(flags.target);
468+
const installedFiles = {};
469+
console.log(`${flags.dryRun ? "would install" : "installing"} registry item ${item.name}`);
470+
471+
for (const file of item.files ?? []) {
472+
if (!file.content) throw new Error(`${item.name}: ${file.path} is missing embedded content`);
473+
474+
const relative = file.target ?? file.path;
475+
const sourceHash = createHash("sha256").update(file.content).digest("hex");
476+
await writeFileWithSafety({
477+
sourceHash,
478+
content: file.content,
479+
relative,
480+
target: flags.target,
481+
flags,
482+
});
483+
installedFiles[relative] = sourceHash;
484+
}
485+
486+
const envVars = Object.keys(item.envVars ?? {});
487+
if (envVars.length > 0) {
488+
const relative = `.env.stackfoundry.${item.name}.example`;
489+
const content = envVars.map((key) => `${key}=`).join("\n") + "\n";
490+
const sourceHash = createHash("sha256").update(content).digest("hex");
491+
await writeFileWithSafety({ sourceHash, content, relative, target: flags.target, flags });
492+
installedFiles[relative] = sourceHash;
493+
}
494+
495+
if (!flags.dryRun) {
496+
installed.modules[item.name] = {
497+
installedAt: new Date().toISOString(),
498+
files: installedFiles,
499+
dependencies: item.dependencies ?? [],
500+
devDependencies: item.devDependencies ?? [],
501+
env: envVars,
502+
source: specifier,
503+
};
504+
await saveInstallManifest(flags.target, installed);
505+
}
506+
}
507+
446508
async function diffModule(name, flags) {
447509
const { dir } = await getModule(name);
448510
const filesDir = path.join(dir, "files");
@@ -468,6 +530,37 @@ async function diffModule(name, flags) {
468530
process.exitCode = changes > 0 ? 1 : 0;
469531
}
470532

533+
async function diffRegistryItem(specifier, flags, visited = new Set()) {
534+
const item = await readRegistryItem(specifier);
535+
const itemId = isHttpUrl(specifier) ? specifier : path.resolve(specifier);
536+
if (visited.has(itemId)) return;
537+
visited.add(itemId);
538+
539+
let changes = 0;
540+
for (const dependency of item.registryDependencies ?? []) {
541+
await diffRegistryItem(dependency, flags, visited);
542+
}
543+
544+
for (const file of item.files ?? []) {
545+
const relative = file.target ?? file.path;
546+
const dest = path.join(flags.target, relative);
547+
if (!existsSync(dest)) {
548+
console.log(`missing ${relative}`);
549+
changes += 1;
550+
continue;
551+
}
552+
const sourceHash = createHash("sha256").update(file.content ?? "").digest("hex");
553+
if ((await hashFile(dest)) !== sourceHash) {
554+
console.log(`changed ${relative}`);
555+
changes += 1;
556+
} else {
557+
console.log(`same ${relative}`);
558+
}
559+
}
560+
561+
process.exitCode = changes > 0 ? 1 : process.exitCode;
562+
}
563+
471564
async function buildRegistry() {
472565
const outputDir = path.join(repoRoot, "public", "r");
473566
await mkdir(outputDir, { recursive: true });
@@ -605,7 +698,9 @@ async function main() {
605698

606699
if (command === "add" && presetName) return addPreset(presetName, flags);
607700
if (!moduleName) throw new Error(`${command} requires a module name`);
701+
if (command === "add" && isRegistryItemSpecifier(moduleName)) return addRegistryItem(moduleName, flags);
608702
if (command === "add") return addModule(moduleName, flags);
703+
if (command === "diff" && isRegistryItemSpecifier(moduleName)) return diffRegistryItem(moduleName, flags);
609704
if (command === "diff") return diffModule(moduleName, flags);
610705

611706
throw new Error(`Unknown command: ${command}`);

apps/web/app/globals.css

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -932,6 +932,154 @@ tbody tr:last-child td {
932932
padding: 20px 22px;
933933
}
934934

935+
.docs-preview {
936+
align-items: start;
937+
display: grid;
938+
gap: 0;
939+
grid-template-columns: 280px 1fr;
940+
}
941+
942+
.docs-sidebar {
943+
border-right: 1px solid var(--border);
944+
padding: 28px 24px 28px 0;
945+
position: sticky;
946+
top: 72px;
947+
}
948+
949+
.docs-side-section {
950+
display: grid;
951+
gap: 3px;
952+
margin-bottom: 26px;
953+
}
954+
955+
.docs-side-section h4 {
956+
color: var(--muted);
957+
font-family: var(--font-mono);
958+
font-size: 11px;
959+
font-weight: 510;
960+
letter-spacing: 0.1em;
961+
margin: 0 0 8px;
962+
text-transform: uppercase;
963+
}
964+
965+
.docs-side-section a {
966+
align-items: center;
967+
border-radius: 6px;
968+
color: var(--muted-2);
969+
display: flex;
970+
font-size: 13.5px;
971+
gap: 8px;
972+
padding: 6px 8px;
973+
}
974+
975+
.docs-side-section a:hover,
976+
.docs-side-section a.active {
977+
background: var(--surface);
978+
color: var(--fg);
979+
}
980+
981+
.docs-side-section a.active {
982+
color: var(--accent);
983+
}
984+
985+
.docs-side-section a span {
986+
background: currentColor;
987+
border-radius: 50%;
988+
height: 3px;
989+
width: 3px;
990+
}
991+
992+
.docs-side-section a em {
993+
color: var(--muted);
994+
font-family: var(--font-mono);
995+
font-size: 11px;
996+
font-style: normal;
997+
margin-left: auto;
998+
}
999+
1000+
.docs-content {
1001+
padding: 28px 0 28px 48px;
1002+
}
1003+
1004+
.crumbs {
1005+
align-items: center;
1006+
color: var(--muted);
1007+
display: flex;
1008+
font-family: var(--font-mono);
1009+
font-size: 12px;
1010+
gap: 9px;
1011+
margin-bottom: 22px;
1012+
}
1013+
1014+
.crumbs strong {
1015+
color: var(--fg);
1016+
font-weight: 400;
1017+
}
1018+
1019+
.docs-content h2 {
1020+
font-family: var(--font-display);
1021+
font-size: clamp(40px, 6vw, 72px);
1022+
font-weight: 600;
1023+
letter-spacing: -0.04em;
1024+
line-height: 0.96;
1025+
margin: 0 0 16px;
1026+
}
1027+
1028+
.doc-lede {
1029+
color: var(--muted-2);
1030+
font-size: 18px;
1031+
line-height: 1.55;
1032+
margin: 0 0 18px;
1033+
max-width: 66ch;
1034+
}
1035+
1036+
.doc-meta {
1037+
color: var(--muted);
1038+
display: flex;
1039+
flex-wrap: wrap;
1040+
font-family: var(--font-mono);
1041+
font-size: 11px;
1042+
gap: 10px;
1043+
margin-bottom: 42px;
1044+
}
1045+
1046+
.doc-meta span {
1047+
border: 1px solid var(--border);
1048+
border-radius: 4px;
1049+
padding: 4px 7px;
1050+
}
1051+
1052+
.docs-content h3 {
1053+
border-top: 1px solid var(--border);
1054+
font-size: 22px;
1055+
letter-spacing: -0.02em;
1056+
margin: 32px 0 10px;
1057+
padding-top: 24px;
1058+
}
1059+
1060+
.docs-content p {
1061+
color: var(--muted-2);
1062+
line-height: 1.6;
1063+
margin: 0 0 18px;
1064+
max-width: 72ch;
1065+
}
1066+
1067+
.docs-code {
1068+
background: var(--surface);
1069+
border: 1px solid var(--border);
1070+
border-radius: 10px;
1071+
overflow: hidden;
1072+
}
1073+
1074+
.docs-code pre {
1075+
color: var(--fg);
1076+
font-size: 13px;
1077+
line-height: 1.7;
1078+
margin: 0;
1079+
overflow-x: auto;
1080+
padding: 18px 20px;
1081+
}
1082+
9351083
.sponsor-strip {
9361084
align-items: center;
9371085
border-bottom: 1px solid var(--border);
@@ -1003,10 +1151,21 @@ tbody tr:last-child td {
10031151
.how-grid,
10041152
.showcase-grid,
10051153
.manifest-block,
1154+
.docs-preview,
10061155
.sponsor-strip {
10071156
grid-template-columns: 1fr;
10081157
}
10091158

1159+
.docs-sidebar {
1160+
border-bottom: 1px solid var(--border);
1161+
border-right: 0;
1162+
position: static;
1163+
}
1164+
1165+
.docs-content {
1166+
padding: 32px 0 0;
1167+
}
1168+
10101169
.module-head {
10111170
padding-right: 0;
10121171
}

0 commit comments

Comments
 (0)