@homepages/template-kit 0.9.0-dev-20260719224319 → 0.9.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 (129) hide show
  1. package/CHANGELOG.md +29 -59
  2. package/README.md +14 -4
  3. package/dist/contracts/fill-treatments.d.ts +23 -0
  4. package/dist/index.d.ts +2 -2
  5. package/dist/index.js +2 -2
  6. package/dist/islands/discover.js +1 -1
  7. package/dist/islands/esbuild-plugin.js +1 -1
  8. package/dist/islands/wrap.d.ts +9 -1
  9. package/dist/media/resolve-fixture-media.d.ts +22 -0
  10. package/dist/package.js +1 -1
  11. package/dist/schema/resolve-section-ref.d.ts +4 -0
  12. package/dist/schema/rows.d.ts +9 -0
  13. package/dist/schema/section-nav.d.ts +5 -1
  14. package/dist/schema/slot-types.d.ts +1 -1
  15. package/dist/ssr.d.ts +7 -2
  16. package/dist/ssr.js +6 -2
  17. package/guide/INDEX.md +5 -0
  18. package/guide/check.md +1 -0
  19. package/guide/dev.md +1 -0
  20. package/guide/llms.txt +11 -3
  21. package/guide/upgrading.md +12 -8
  22. package/package.json +4 -16
  23. package/dist/canvas-core/chrome-css.d.ts +0 -24
  24. package/dist/canvas-core/chrome-css.js +0 -403
  25. package/dist/canvas-core/chrome.d.ts +0 -49
  26. package/dist/canvas-core/chrome.js +0 -147
  27. package/dist/canvas-core/frame.d.ts +0 -20
  28. package/dist/canvas-core/frame.js +0 -23
  29. package/dist/canvas-core/hidden-tabs.d.ts +0 -16
  30. package/dist/canvas-core/hidden-tabs.js +0 -84
  31. package/dist/canvas-core/resolve-target.js +0 -55
  32. package/dist/canvas-core/scroll.d.ts +0 -10
  33. package/dist/canvas-core/scroll.js +0 -15
  34. package/dist/canvas-core/section-dom.d.ts +0 -6
  35. package/dist/canvas-core/section-dom.js +0 -43
  36. package/dist/canvas-core/srcdoc.d.ts +0 -8
  37. package/dist/canvas-core/srcdoc.js +0 -77
  38. package/dist/canvas-core/types.d.ts +0 -76
  39. package/dist/canvas-core.d.ts +0 -9
  40. package/dist/canvas-core.js +0 -9
  41. package/dist/cli/check/config.js +0 -41
  42. package/dist/cli/check/css.js +0 -131
  43. package/dist/cli/check/diagnostics.js +0 -37
  44. package/dist/cli/check/index.js +0 -94
  45. package/dist/cli/check/loader.js +0 -149
  46. package/dist/cli/check/paths.js +0 -15
  47. package/dist/cli/check/relativize.js +0 -18
  48. package/dist/cli/check/render-invariants.js +0 -24
  49. package/dist/cli/check/resolve-tool.js +0 -38
  50. package/dist/cli/check/section-assets.js +0 -58
  51. package/dist/cli/check/stages/deps.js +0 -337
  52. package/dist/cli/check/stages/lint.js +0 -46
  53. package/dist/cli/check/stages/render.js +0 -98
  54. package/dist/cli/check/stages/size.js +0 -274
  55. package/dist/cli/check/stages/tree.js +0 -206
  56. package/dist/cli/check/stages/typecheck.js +0 -46
  57. package/dist/cli/check/stages/validate/catalog-exhaustiveness.js +0 -18
  58. package/dist/cli/check/stages/validate/facts.js +0 -19
  59. package/dist/cli/check/stages/validate/formatter-contract.js +0 -14
  60. package/dist/cli/check/stages/validate/group-contract.js +0 -21
  61. package/dist/cli/check/stages/validate/index.js +0 -66
  62. package/dist/cli/check/stages/validate/list-scalar-contract.js +0 -22
  63. package/dist/cli/check/stages/validate/nav-contract.js +0 -93
  64. package/dist/cli/check/stages/validate/nested-slot-contract.js +0 -12
  65. package/dist/cli/check/stages/validate/object-list-contract.js +0 -31
  66. package/dist/cli/check/stages/validate/orchestrator.js +0 -241
  67. package/dist/cli/check/stages/validate/produced-by-contract.js +0 -18
  68. package/dist/cli/check/stages/validate/row-contract.js +0 -31
  69. package/dist/cli/check/stages/validate/select-contract.js +0 -13
  70. package/dist/cli/check/stages/validate/source-contract.js +0 -21
  71. package/dist/cli/check/stages/validate/variant-contract.js +0 -15
  72. package/dist/cli/check/workspace.js +0 -99
  73. package/dist/cli/dev/build-css.js +0 -97
  74. package/dist/cli/dev/catalog.js +0 -35
  75. package/dist/cli/dev/compose-template.js +0 -39
  76. package/dist/cli/dev/content-override.js +0 -58
  77. package/dist/cli/dev/diagnostics.js +0 -68
  78. package/dist/cli/dev/discover.js +0 -35
  79. package/dist/cli/dev/fill-state.js +0 -77
  80. package/dist/cli/dev/index.js +0 -42
  81. package/dist/cli/dev/inspect.js +0 -38
  82. package/dist/cli/dev/island-bootstrap.js +0 -38
  83. package/dist/cli/dev/island-canvas-bootstrap.js +0 -204
  84. package/dist/cli/dev/island-map.js +0 -35
  85. package/dist/cli/dev/island-transform.js +0 -31
  86. package/dist/cli/dev/manifest-instances.js +0 -31
  87. package/dist/cli/dev/render-section.js +0 -24
  88. package/dist/cli/dev/screenshot-target.js +0 -24
  89. package/dist/cli/dev/section-page.js +0 -91
  90. package/dist/cli/dev/server.js +0 -511
  91. package/dist/cli/dev/slot-schema.js +0 -12
  92. package/dist/cli/dev/structure-summary.js +0 -119
  93. package/dist/cli/dev/tailwind.js +0 -32
  94. package/dist/cli/dev/tsconfig-paths-plugin.js +0 -90
  95. package/dist/cli/dev/vite-server.js +0 -39
  96. package/dist/cli/dev/workspace.js +0 -63
  97. package/dist/cli/link/index.js +0 -74
  98. package/dist/cli/link/overlay.js +0 -59
  99. package/dist/cli/link/watch.js +0 -25
  100. package/dist/cli/new/emit.js +0 -48
  101. package/dist/cli/new/index.js +0 -69
  102. package/dist/cli/new/scaffold/section/_Renderer.tsx +0 -20
  103. package/dist/cli/new/scaffold/section/_fill-spec.ts +0 -18
  104. package/dist/cli/new/scaffold/section/_fixtures.ts +0 -13
  105. package/dist/cli/new/scaffold/section/_schema.ts +0 -24
  106. package/dist/cli/new/scaffold/template/_manifest.json +0 -9
  107. package/dist/cli/new/scaffold/template/sections/hero/ExpandableText.tsx +0 -45
  108. package/dist/cli/new/scaffold/template/sections/hero/Renderer.tsx +0 -40
  109. package/dist/cli/new/scaffold/template/sections/hero/components/Feature.tsx +0 -14
  110. package/dist/cli/new/scaffold/template/sections/hero/fill-spec.ts +0 -29
  111. package/dist/cli/new/scaffold/template/sections/hero/fixtures.ts +0 -41
  112. package/dist/cli/new/scaffold/template/sections/hero/schema.ts +0 -60
  113. package/dist/cli/new/scaffold/template/theme.css +0 -24
  114. package/dist/cli/new/scaffold/template/theme.ts +0 -27
  115. package/dist/cli/new/scaffold-assets.js +0 -20
  116. package/dist/cli/pack/collect.js +0 -44
  117. package/dist/cli/pack/guards.js +0 -57
  118. package/dist/cli/pack/index.js +0 -66
  119. package/dist/cli/pack/manifest.js +0 -15
  120. package/dist/cli/pack/zip.js +0 -86
  121. package/dist/cli/theme/generate.js +0 -43
  122. package/dist/cli/theme/index.js +0 -33
  123. package/dist/cli/theme/load-theme.js +0 -69
  124. package/dist/cli.d.ts +0 -1
  125. package/dist/cli.js +0 -138
  126. package/dist/dev-client/assets/index-CJa9G1Na.css +0 -1
  127. package/dist/dev-client/assets/index-C_1atZs4.js +0 -450
  128. package/dist/dev-client/index.html +0 -13
  129. package/dist/tokens.css +0 -21
@@ -1,90 +0,0 @@
1
- import { resolveFromWorkspace } from "../check/resolve-tool.js";
2
- import { isAbsolute, join, resolve } from "node:path";
3
- import { existsSync, statSync } from "node:fs";
4
-
5
- //#region src/cli/dev/tsconfig-paths-plugin.ts
6
- const RESOLVE_EXTENSIONS = [
7
- "",
8
- ".ts",
9
- ".tsx",
10
- ".js",
11
- ".jsx",
12
- ".mjs",
13
- ".cjs",
14
- ".json"
15
- ];
16
- function toPattern(key, targets, baseUrl) {
17
- const star = key.indexOf("*");
18
- const hasWildcard = star !== -1;
19
- return {
20
- prefix: hasWildcard ? key.slice(0, star) : key,
21
- suffix: hasWildcard ? key.slice(star + 1) : "",
22
- hasWildcard,
23
- targets: targets.map((target) => isAbsolute(target) ? target : join(baseUrl, target))
24
- };
25
- }
26
- /** The wildcard capture for a match, `""` for a wildcard-free exact match, or `undefined` when `specifier` doesn't match. */
27
- function matchPattern(pattern, specifier) {
28
- if (!pattern.hasWildcard) return specifier === pattern.prefix ? "" : void 0;
29
- if (!specifier.startsWith(pattern.prefix) || !specifier.endsWith(pattern.suffix)) return void 0;
30
- return specifier.slice(pattern.prefix.length, specifier.length - pattern.suffix.length);
31
- }
32
- function isFile(path) {
33
- try {
34
- return existsSync(path) && statSync(path).isFile();
35
- } catch {
36
- return false;
37
- }
38
- }
39
- /** `candidate` resolved as tsc would: itself, itself with a resolvable extension, or a directory index. */
40
- function resolveCandidate(candidate) {
41
- for (const ext of RESOLVE_EXTENSIONS) {
42
- const withExt = candidate + ext;
43
- if (isFile(withExt)) return withExt;
44
- }
45
- for (const ext of RESOLVE_EXTENSIONS.slice(1)) {
46
- const indexFile = join(candidate, `index${ext}`);
47
- if (isFile(indexFile)) return indexFile;
48
- }
49
- }
50
- /**
51
- * Builds a Vite `resolveId` plugin from `root`'s tsconfig `paths`, or returns
52
- * `null` when the workspace has none configured, or its tsconfig/`typescript`
53
- * can't be read at all — `check` already owns reporting a broken tsconfig
54
- * cleanly; `dev` degrades to its pre-existing behavior (no `paths` support)
55
- * rather than introduce a new startup failure mode over it.
56
- */
57
- async function createTsconfigPathsPlugin(root) {
58
- try {
59
- const ts = await resolveFromWorkspace(root, "typescript");
60
- const configPath = join(root, "tsconfig.json");
61
- const configFile = ts.readConfigFile(configPath, (file) => ts.sys.readFile(file));
62
- if (configFile.error) return null;
63
- const parsed = ts.parseJsonConfigFileContent(configFile.config, ts.sys, root);
64
- const rawPaths = parsed.options.paths;
65
- if (!rawPaths || Object.keys(rawPaths).length === 0) return null;
66
- const baseUrl = parsed.options.baseUrl ? resolve(root, parsed.options.baseUrl) : root;
67
- const patterns = Object.entries(rawPaths).map(([key, targets]) => toPattern(key, targets, baseUrl)).sort((a, b) => b.prefix.length - a.prefix.length);
68
- return {
69
- name: "template-kit:tsconfig-paths",
70
- enforce: "pre",
71
- resolveId(source) {
72
- if (source.startsWith(".") || isAbsolute(source)) return null;
73
- for (const pattern of patterns) {
74
- const matched = matchPattern(pattern, source);
75
- if (matched === void 0) continue;
76
- for (const target of pattern.targets) {
77
- const resolved = resolveCandidate(pattern.hasWildcard ? target.replace("*", () => matched) : target);
78
- if (resolved) return resolved;
79
- }
80
- }
81
- return null;
82
- }
83
- };
84
- } catch {
85
- return null;
86
- }
87
- }
88
-
89
- //#endregion
90
- export { createTsconfigPathsPlugin };
@@ -1,39 +0,0 @@
1
- import { resolveFromWorkspace } from "../check/resolve-tool.js";
2
- import { islandTransformPlugin } from "./island-transform.js";
3
- import { createTsconfigPathsPlugin } from "./tsconfig-paths-plugin.js";
4
-
5
- //#region src/cli/dev/vite-server.ts
6
- async function createDevViteServer(root, opts = {}) {
7
- const vite = await resolveFromWorkspace(root, "vite");
8
- const reactPlugin = await resolveFromWorkspace(root, "@vitejs/plugin-react");
9
- const tsconfigPathsPlugin = await createTsconfigPathsPlugin(root);
10
- return vite.createServer({
11
- root,
12
- appType: "custom",
13
- configFile: false,
14
- plugins: [
15
- reactPlugin.default(),
16
- islandTransformPlugin({ root }),
17
- ...tsconfigPathsPlugin ? [tsconfigPathsPlugin] : []
18
- ],
19
- server: {
20
- middlewareMode: true,
21
- fs: { allow: [root] },
22
- ...opts.hmrPort !== void 0 ? { hmr: { port: opts.hmrPort } } : {}
23
- },
24
- ssr: { external: [
25
- "react",
26
- "react-dom",
27
- "react-dom/server",
28
- "react-dom/client"
29
- ] },
30
- optimizeDeps: { include: [
31
- "react",
32
- "react-dom",
33
- "react-dom/client"
34
- ] }
35
- });
36
- }
37
-
38
- //#endregion
39
- export { createDevViteServer };
@@ -1,63 +0,0 @@
1
- import { readFile, readdir } from "node:fs/promises";
2
- import { basename, dirname, join, relative, resolve, sep } from "node:path";
3
- import { existsSync } from "node:fs";
4
-
5
- //#region src/cli/dev/workspace.ts
6
- /** Walk up from `cwd` to the nearest ancestor that contains a `templates/` dir. */
7
- function findWorkspaceRoot(cwd) {
8
- let dir = resolve(cwd);
9
- for (;;) {
10
- if (existsSync(join(dir, "templates"))) return dir;
11
- const parent = dirname(dir);
12
- if (parent === dir) return null;
13
- dir = parent;
14
- }
15
- }
16
- /**
17
- * Resolve the workspace at (or above) `cwd`: its root, every template under
18
- * `templates/`, and the template inferred from the cwd. Throws if no workspace
19
- * is found.
20
- */
21
- async function resolveWorkspace(cwd) {
22
- const root = findWorkspaceRoot(cwd);
23
- if (!root) throw new Error(`no templates/ directory found in ${cwd} or any parent — run this from a workspace`);
24
- const templatesDir = join(root, "templates");
25
- const entries = await readdir(templatesDir, { withFileTypes: true });
26
- const templates = [];
27
- for (const entry of entries) {
28
- if (!entry.isDirectory()) continue;
29
- const dir = join(templatesDir, entry.name);
30
- const manifestPath = join(dir, "manifest.json");
31
- if (!existsSync(manifestPath)) continue;
32
- templates.push({
33
- key: await readTemplateKey(manifestPath, entry.name),
34
- dir,
35
- manifestPath,
36
- themePath: existsSync(join(dir, "theme.ts")) ? join(dir, "theme.ts") : null
37
- });
38
- }
39
- templates.sort((a, b) => a.key.localeCompare(b.key));
40
- return {
41
- root,
42
- templatesDir,
43
- templates,
44
- cwdTemplate: inferTemplateFromCwd(templatesDir, cwd, templates)
45
- };
46
- }
47
- async function readTemplateKey(manifestPath, fallback) {
48
- try {
49
- const raw = JSON.parse(await readFile(manifestPath, "utf8"));
50
- return typeof raw.key === "string" && raw.key.length > 0 ? raw.key : fallback;
51
- } catch {
52
- return fallback;
53
- }
54
- }
55
- function inferTemplateFromCwd(templatesDir, cwd, templates) {
56
- const rel = relative(templatesDir, resolve(cwd));
57
- if (rel === "" || rel.startsWith("..")) return null;
58
- const folder = rel.split(sep)[0];
59
- return templates.find((t) => basename(t.dir) === folder)?.key ?? null;
60
- }
61
-
62
- //#endregion
63
- export { findWorkspaceRoot, resolveWorkspace };
@@ -1,74 +0,0 @@
1
- import { KIT_PKG_SUBPATH, findKitRoot, overlayKit, readLinkMarker } from "./overlay.js";
2
- import { findWorkspaceRoot } from "../check/workspace.js";
3
- import { parseArgs, promisify } from "node:util";
4
- import { readFile } from "node:fs/promises";
5
- import { join } from "node:path";
6
- import { execFile } from "node:child_process";
7
-
8
- //#region src/cli/link/index.ts
9
- const execFileAsync = promisify(execFile);
10
- const defaultBuild = async (kitRoot) => {
11
- await execFileAsync("npm", ["run", "build"], { cwd: kitRoot });
12
- };
13
- const defaultNpmCi = async (workspaceRoot) => {
14
- await execFileAsync("npm", ["ci"], { cwd: workspaceRoot });
15
- };
16
- async function resolveWorkspace(argv, cwd) {
17
- const { positionals } = parseArgs({
18
- args: argv,
19
- allowPositionals: true
20
- });
21
- return positionals[0] !== void 0 ? positionals[0] : findWorkspaceRoot(cwd);
22
- }
23
- async function installedKitVersion(workspaceRoot) {
24
- try {
25
- const raw = await readFile(join(workspaceRoot, KIT_PKG_SUBPATH, "package.json"), "utf8");
26
- return JSON.parse(raw).version ?? "unknown";
27
- } catch {
28
- return "not installed";
29
- }
30
- }
31
- async function runLinkOnce(argv, cwd, build = defaultBuild, now = (/* @__PURE__ */ new Date()).toISOString()) {
32
- const workspaceRoot = await resolveWorkspace(argv, cwd);
33
- const kitRoot = await findKitRoot(cwd);
34
- return {
35
- code: 0,
36
- output: `template-kit link\n overlaid kit ${(await overlayKit(kitRoot, workspaceRoot, build, now)).kitVersion} from ${kitRoot}\n into ${workspaceRoot}`,
37
- watch: {
38
- kitSrcDir: join(kitRoot, "src"),
39
- kitRoot,
40
- workspaceRoot
41
- }
42
- };
43
- }
44
- async function runStatus(argv, cwd) {
45
- const workspaceRoot = await resolveWorkspace(argv, cwd);
46
- const marker = await readLinkMarker(workspaceRoot);
47
- if (marker !== void 0) return {
48
- code: 0,
49
- output: `template-kit status\n overlay — linked from ${marker.linkedFrom} (kit ${marker.kitVersion})`
50
- };
51
- return {
52
- code: 0,
53
- output: `template-kit status\n registry — kit ${await installedKitVersion(workspaceRoot)}`
54
- };
55
- }
56
- async function runUnlink(argv, cwd, npmCi = defaultNpmCi) {
57
- const workspaceRoot = await resolveWorkspace(argv, cwd);
58
- if (await readLinkMarker(workspaceRoot) === void 0) return {
59
- code: 0,
60
- output: "template-kit unlink\n not linked — nothing to do."
61
- };
62
- await npmCi(workspaceRoot);
63
- return {
64
- code: 0,
65
- output: "template-kit unlink\n restored the registry install (npm ci)."
66
- };
67
- }
68
- /** One rebuild+recopy for the watch loop — a real tsdown build, then the overlay copy. */
69
- async function rebuildOverlay(kitRoot, workspaceRoot) {
70
- await overlayKit(kitRoot, workspaceRoot, defaultBuild, (/* @__PURE__ */ new Date()).toISOString());
71
- }
72
-
73
- //#endregion
74
- export { rebuildOverlay, runLinkOnce, runStatus, runUnlink };
@@ -1,59 +0,0 @@
1
- import { KIT_VERSION } from "../../index.js";
2
- import { cp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
3
- import { dirname, join, resolve } from "node:path";
4
-
5
- //#region src/cli/link/overlay.ts
6
- const KIT_PKG_SUBPATH = join("node_modules", "@homepages", "template-kit");
7
- const LINK_MARKER_NAME = ".template-kit-link.json";
8
- const PUBLISHABLE = [
9
- "dist",
10
- "guide",
11
- "package.json",
12
- "tsconfig.json",
13
- "tsconfig.base.json",
14
- "CHANGELOG.md"
15
- ];
16
- async function readLinkMarker(workspaceRoot) {
17
- try {
18
- const raw = await readFile(join(workspaceRoot, KIT_PKG_SUBPATH, LINK_MARKER_NAME), "utf8");
19
- return JSON.parse(raw);
20
- } catch {
21
- return;
22
- }
23
- }
24
- const isPackageManifest = (value) => typeof value === "object" && value !== null;
25
- async function findKitRoot(from) {
26
- let dir = resolve(from);
27
- for (;;) {
28
- try {
29
- const raw = await readFile(join(dir, "package.json"), "utf8");
30
- const parsed = JSON.parse(raw);
31
- if (isPackageManifest(parsed) && parsed.name === "@homepages/template-kit") return dir;
32
- } catch {}
33
- const parent = dirname(dir);
34
- if (parent === dir) throw new Error("not inside the template-kit repo — run `template-kit link` from a kit checkout.");
35
- dir = parent;
36
- }
37
- }
38
- async function overlayKit(kitRoot, workspaceRoot, build, now) {
39
- await build(kitRoot);
40
- const dest = join(workspaceRoot, KIT_PKG_SUBPATH);
41
- await rm(dest, {
42
- recursive: true,
43
- force: true
44
- });
45
- await mkdir(dest, { recursive: true });
46
- for (const item of PUBLISHABLE) await cp(join(kitRoot, item), join(dest, item), { recursive: true }).catch((cause) => {
47
- if (cause.code !== "ENOENT") throw cause;
48
- });
49
- const marker = {
50
- linkedFrom: kitRoot,
51
- kitVersion: KIT_VERSION,
52
- linkedAt: now
53
- };
54
- await writeFile(join(dest, LINK_MARKER_NAME), `${JSON.stringify(marker, null, 2)}\n`);
55
- return marker;
56
- }
57
-
58
- //#endregion
59
- export { KIT_PKG_SUBPATH, LINK_MARKER_NAME, PUBLISHABLE, findKitRoot, overlayKit, readLinkMarker };
@@ -1,25 +0,0 @@
1
- import { watch } from "node:fs";
2
-
3
- //#region src/cli/link/watch.ts
4
- function watchAndRebuild(kitSrcDir, onChange, onError, debounceMs = 150) {
5
- let timer;
6
- const watcher = watch(kitSrcDir, { recursive: true }, () => {
7
- if (timer !== void 0) clearTimeout(timer);
8
- timer = setTimeout(() => {
9
- timer = void 0;
10
- onChange();
11
- }, debounceMs);
12
- });
13
- const stop = () => {
14
- if (timer !== void 0) clearTimeout(timer);
15
- watcher.close();
16
- };
17
- watcher.on("error", (err) => {
18
- stop();
19
- onError(err);
20
- });
21
- return stop;
22
- }
23
-
24
- //#endregion
25
- export { watchAndRebuild };
@@ -1,48 +0,0 @@
1
- import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
2
- import { join, relative } from "node:path";
3
-
4
- //#region src/cli/new/emit.ts
5
- const RENAME = {
6
- "_gitignore": ".gitignore",
7
- "_package.json": "package.json",
8
- "_manifest.json": "manifest.json",
9
- "_Renderer.tsx": "Renderer.tsx",
10
- "_schema.ts": "schema.ts",
11
- "_fill-spec.ts": "fill-spec.ts",
12
- "_fixtures.ts": "fixtures.ts"
13
- };
14
- function substitute(body, tokens) {
15
- return body.replace(/\{\{(\w+)\}\}/g, (_m, key) => {
16
- const value = tokens[key];
17
- if (value === void 0) throw new Error(`scaffold: no value for token {{${key}}}`);
18
- return value;
19
- });
20
- }
21
- async function walk(dir) {
22
- const out = [];
23
- for (const entry of await readdir(dir, { withFileTypes: true })) {
24
- const full = join(dir, entry.name);
25
- if (entry.isDirectory()) out.push(...await walk(full));
26
- else out.push(full);
27
- }
28
- return out;
29
- }
30
- /** Emit `srcDir` into `destDir`; returns dest-relative paths written. */
31
- async function emitTree(srcDir, destDir, tokens) {
32
- const files = await walk(srcDir);
33
- const written = [];
34
- for (const file of files) {
35
- const parts = relative(srcDir, file).split("/");
36
- const base = parts[parts.length - 1];
37
- parts[parts.length - 1] = RENAME[base] ?? base;
38
- const destRel = parts.join("/");
39
- const destPath = join(destDir, destRel);
40
- await mkdir(join(destPath, ".."), { recursive: true });
41
- await writeFile(destPath, substitute(await readFile(file, "utf8"), tokens), "utf8");
42
- written.push(destRel);
43
- }
44
- return written;
45
- }
46
-
47
- //#endregion
48
- export { emitTree };
@@ -1,69 +0,0 @@
1
- import { findWorkspaceRoot } from "../check/workspace.js";
2
- import { emitTree } from "./emit.js";
3
- import { sectionAssetsDir, templateAssetsDir } from "./scaffold-assets.js";
4
- import { join } from "node:path";
5
- import { existsSync } from "node:fs";
6
-
7
- //#region src/cli/new/index.ts
8
- const KEY_RE = /^[a-z][a-z0-9-]*$/;
9
- function pascal(key) {
10
- return key.split("-").map((p) => p ? p[0].toUpperCase() + p.slice(1) : p).join("");
11
- }
12
- function titleCase(key) {
13
- return key.split("-").map((p) => p ? p[0].toUpperCase() + p.slice(1) : p).join(" ");
14
- }
15
- async function runNew(argv, cwd) {
16
- const name = argv[0];
17
- if (!name || !KEY_RE.test(name)) return {
18
- code: 1,
19
- output: `new: expected a kebab-case template name (${KEY_RE.source}). Usage: template-kit new <template>`
20
- };
21
- const dest = join(await findWorkspaceRoot(cwd), "templates", name);
22
- if (existsSync(dest)) return {
23
- code: 1,
24
- output: `new: templates/${name}/ already exists. Refusing to overwrite.`
25
- };
26
- await emitTree(templateAssetsDir(), dest, {
27
- TEMPLATE_KEY: name,
28
- TEMPLATE_NAME: titleCase(name)
29
- });
30
- return {
31
- code: 0,
32
- output: `Created templates/${name}/ (starter template)\n Next: npm run check -- ${name} then npm run dev -- ${name}`
33
- };
34
- }
35
- async function runNewSection(argv, cwd) {
36
- const arg = argv[0] ?? "";
37
- const slash = arg.indexOf("/");
38
- if (slash < 0) return {
39
- code: 1,
40
- output: `new-section: expected <template>/<section>. e.g. template-kit new-section starter/gallery`
41
- };
42
- const template = arg.slice(0, slash);
43
- const section = arg.slice(slash + 1);
44
- if (!KEY_RE.test(template) || !KEY_RE.test(section)) return {
45
- code: 1,
46
- output: `new-section: template and section must be kebab-case (${KEY_RE.source}).`
47
- };
48
- const templateDir = join(await findWorkspaceRoot(cwd), "templates", template);
49
- if (!existsSync(templateDir)) return {
50
- code: 1,
51
- output: `new-section: templates/${template}/ does not exist. Create it with: template-kit new ${template}`
52
- };
53
- const dest = join(templateDir, "sections", section);
54
- if (existsSync(dest)) return {
55
- code: 1,
56
- output: `new-section: ${dest} already exists. Refusing to overwrite.`
57
- };
58
- await emitTree(sectionAssetsDir(), dest, {
59
- SECTION_PASCAL: pascal(section),
60
- SECTION_DISPLAY: titleCase(section)
61
- });
62
- return {
63
- code: 0,
64
- output: `Created templates/${template}/sections/${section}/ (Renderer.tsx, schema.ts, fill-spec.ts, fixtures.ts)\n Reference it as "${section}" in templates/${template}/manifest.json, then npm run check -- ${template}`
65
- };
66
- }
67
-
68
- //#endregion
69
- export { runNew, runNewSection };
@@ -1,20 +0,0 @@
1
- import type { ReactNode } from "react";
2
-
3
- import { Section, Slot } from "@homepages/template-kit";
4
-
5
- import type { Props } from "./schema";
6
-
7
- // {{SECTION_DISPLAY}} — a pure, server-rendered function of its props. Wrap
8
- // every editable slot in <Slot id="…"> so the editor can resolve clicks. For
9
- // interactivity, add a "use client" island file beside this one.
10
- export default function {{SECTION_PASCAL}}Renderer({ slots, nav }: Props): ReactNode {
11
- return (
12
- <Section id={nav.selfAnchor} className="bg-background text-ink">
13
- <div className="mx-auto max-w-3xl px-6 py-12">
14
- <Slot id="headline" textLeaf>
15
- <h2 className="text-2xl font-semibold">{slots.headline}</h2>
16
- </Slot>
17
- </div>
18
- </Section>
19
- );
20
- }
@@ -1,18 +0,0 @@
1
- import type { FillSpecShape } from "@homepages/template-kit";
2
-
3
- // Decision ids must match the `produced_by` references in schema.ts.
4
- export const fillSpec = {
5
- reads: [],
6
- decisions: [
7
- {
8
- id: "headline_text",
9
- type: "text-block",
10
- produces: ["headline"],
11
- length_cap: 80,
12
- structure: "single_sentence",
13
- perspective: "third",
14
- voice_prompt: "Write a short, confident heading for this section.",
15
- content_prompt: "Anchor the section's purpose in one sentence.",
16
- },
17
- ],
18
- } as const satisfies FillSpecShape;
@@ -1,13 +0,0 @@
1
- import type { FixtureModule } from "@homepages/template-kit";
2
-
3
- // Base content only. Nullability coverage is derived from schema.ts. Add a
4
- // `states` entry for a designed content-length or cardinality branch.
5
- const fixtures: FixtureModule = {
6
- base: {
7
- slots: {
8
- headline: "A typical headline that reads well in a short line",
9
- },
10
- },
11
- };
12
-
13
- export default fixtures;
@@ -1,24 +0,0 @@
1
- import type { SectionProps, SectionSchemaShape } from "@homepages/template-kit";
2
-
3
- // Replace the slots/options with the real shape this section needs. Every
4
- // editable slot declares either a `source` (baked from the property graph via
5
- // `direct(...)`/`derived(...)`) or a `produced_by` naming a decision in
6
- // fill-spec.ts.
7
- export const schema = {
8
- meta: {
9
- displayName: "{{SECTION_DISPLAY}}",
10
- },
11
- slots: {
12
- headline: {
13
- type: "text",
14
- size: "medium",
15
- editable_by_user: true,
16
- required: true,
17
- produced_by: "headline_text",
18
- display_name: "Headline",
19
- },
20
- },
21
- } as const satisfies SectionSchemaShape;
22
-
23
- // Renderer prop type — derived from `schema`; do not hand-write it.
24
- export type Props = SectionProps<typeof schema>;
@@ -1,9 +0,0 @@
1
- {
2
- "key": "{{TEMPLATE_KEY}}",
3
- "name": "{{TEMPLATE_NAME}}",
4
- "deliverable_type": "single_property_website",
5
- "sections": [
6
- { "section": "hero", "instance_id": "hero-1", "options": {}, "locked": [], "required": true, "reorderable": false }
7
- ],
8
- "reconcile": []
9
- }
@@ -1,45 +0,0 @@
1
- "use client";
2
-
3
- import { type ReactNode, useState } from "react";
4
-
5
- import type { IslandEditorOptions } from "@homepages/template-kit";
6
-
7
- // A "use client" island: interactive, hydrated in the browser as its own React
8
- // root. State, effects, and event handlers are fine here (they are NOT in a
9
- // Renderer). Its props cross the server→browser boundary as JSON, so they must
10
- // be strictly serializable — here, two strings and a number.
11
- export default function ExpandableText({
12
- text,
13
- collapsedChars,
14
- moreLabel,
15
- }: {
16
- text: string;
17
- collapsedChars: number;
18
- moreLabel: string;
19
- }): ReactNode {
20
- const [open, setOpen] = useState(false);
21
- const isLong = text.length > collapsedChars;
22
- const shown = open || !isLong ? text : `${text.slice(0, collapsedChars).trimEnd()}…`;
23
- return (
24
- <p className="text-base leading-relaxed text-ink/80">
25
- {shown}
26
- {isLong ? (
27
- <button
28
- type="button"
29
- data-testid="expand-toggle"
30
- className="ml-2 font-medium text-accent underline"
31
- onClick={() => setOpen((v) => !v)}
32
- >
33
- {open ? "Show less" : moreLabel}
34
- </button>
35
- ) : null}
36
- </p>
37
- );
38
- }
39
-
40
- // Optional: how this island behaves inside the editor canvas. Omit `editor`
41
- // entirely and the island renders as static markup in the editor (right for
42
- // most). Here we hydrate it live so the toggle works while editing.
43
- export const editor: IslandEditorOptions = {
44
- live: true,
45
- };
@@ -1,40 +0,0 @@
1
- import type { ReactNode } from "react";
2
-
3
- import { Image, Section, Slot } from "@homepages/template-kit";
4
-
5
- import { Feature } from "./components/Feature";
6
- import ExpandableText from "./ExpandableText";
7
- import type { Props } from "./schema";
8
-
9
- // Hero — a pure server-rendered function of its props: no fetches, no globals,
10
- // no state. Every editable slot's outer DOM is wrapped in <Slot id="…"> (or a
11
- // primitive with slotId="…") so the editor can map a click back to the slot.
12
- // The one interactive part is the <ExpandableText> island.
13
- export default function HeroRenderer({ slots, options, nav }: Props): ReactNode {
14
- const align = options.align === "left" ? "items-start text-left" : "items-center text-center";
15
- return (
16
- <Section id={nav.selfAnchor} className="bg-background text-ink">
17
- <Image
18
- slotId="hero_image"
19
- src={slots.hero_image.url}
20
- responsive={slots.hero_image.responsive}
21
- alt={slots.hero_image.alt}
22
- loading="eager"
23
- className="w-full"
24
- />
25
- <div className={`mx-auto flex max-w-3xl flex-col gap-4 px-6 py-12 ${align}`}>
26
- <Slot id="headline" textLeaf>
27
- <h1 className="text-3xl font-semibold">{slots.headline}</h1>
28
- </Slot>
29
- <Slot id="blurb">
30
- {/* The island receives JSON-serializable props only. */}
31
- <ExpandableText text={slots.blurb} collapsedChars={140} moreLabel="Read more" />
32
- </Slot>
33
- <div className="mt-2 grid w-full grid-cols-2 gap-3">
34
- <Feature label="Status" value="For sale" />
35
- <Feature label="Type" value="Single-family" />
36
- </div>
37
- </div>
38
- </Section>
39
- );
40
- }
@@ -1,14 +0,0 @@
1
- import type { ReactNode } from "react";
2
-
3
- // A nested server component. Proof that a section folder is free-form: only the
4
- // four contract files (Renderer/schema/fill-spec/fixtures) are reserved — a
5
- // components/ dir (or hooks/, utils/, any nesting) is yours to organize. This
6
- // renders as static HTML and ships zero JavaScript.
7
- export function Feature({ label, value }: { label: string; value: string }): ReactNode {
8
- return (
9
- <div className="rounded-lg border border-ink/10 p-4">
10
- <div className="text-sm text-ink/60">{label}</div>
11
- <div className="text-lg font-semibold text-ink">{value}</div>
12
- </div>
13
- );
14
- }