@homepages/template-kit 0.0.1 → 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.
Files changed (90) hide show
  1. package/CHANGELOG.md +182 -0
  2. package/README.md +67 -11
  3. package/dist/base.css +85 -0
  4. package/dist/browser.d.ts +26 -0
  5. package/dist/browser.js +55 -0
  6. package/dist/cli.js +1 -1
  7. package/dist/contracts/image-descriptor.d.ts +17 -0
  8. package/dist/contracts/markers.d.ts +20 -0
  9. package/dist/contracts/markers.js +21 -0
  10. package/dist/contracts/slot-catalog.d.ts +237 -0
  11. package/dist/contracts/slot-catalog.js +247 -0
  12. package/dist/contracts/transforms.d.ts +108 -0
  13. package/dist/contracts/transforms.js +117 -0
  14. package/dist/design-system/breakpoints.d.ts +5 -0
  15. package/dist/design-system/breakpoints.js +14 -0
  16. package/dist/design-system/theme.d.ts +307 -0
  17. package/dist/design-system/theme.js +95 -0
  18. package/dist/eslint/is-client-file.js +79 -0
  19. package/dist/eslint/rules/image-bare-needs-reason.js +38 -0
  20. package/dist/eslint/rules/no-client-directive-in-contract.js +26 -0
  21. package/dist/eslint/rules/no-client-runtime-in-server.js +99 -0
  22. package/dist/eslint/rules/no-css-import-from-render-path.js +23 -0
  23. package/dist/eslint/rules/no-hex.js +111 -0
  24. package/dist/eslint/rules/no-inline-style.js +23 -0
  25. package/dist/eslint/rules/no-nondeterminism.js +44 -0
  26. package/dist/eslint/rules/no-raw-element.js +32 -0
  27. package/dist/eslint/rules/props-from-schema.js +32 -0
  28. package/dist/eslint/rules/serializable-island-props.js +108 -0
  29. package/dist/eslint/rules/slot-marker-literal.js +33 -0
  30. package/dist/eslint.d.ts +4 -8
  31. package/dist/eslint.js +75 -11
  32. package/dist/fixtures/sample-property.d.ts +67 -0
  33. package/dist/fixtures/sample-property.js +707 -0
  34. package/dist/fixtures.d.ts +2 -0
  35. package/dist/fixtures.js +3 -0
  36. package/dist/index.d.ts +28 -12
  37. package/dist/index.js +34 -2
  38. package/dist/island-runtime.d.ts +30 -0
  39. package/dist/island-runtime.js +73 -0
  40. package/dist/islands/contract.d.ts +56 -0
  41. package/dist/islands/contract.js +71 -0
  42. package/dist/islands/marker.d.ts +16 -0
  43. package/dist/islands/marker.js +18 -0
  44. package/dist/package.js +5 -0
  45. package/dist/primitives/Image.d.ts +49 -0
  46. package/dist/primitives/Image.js +95 -0
  47. package/dist/primitives/Section.d.ts +22 -0
  48. package/dist/primitives/Section.js +26 -0
  49. package/dist/primitives/Slot.d.ts +50 -0
  50. package/dist/primitives/Slot.js +39 -0
  51. package/dist/primitives/SlotGroup.d.ts +38 -0
  52. package/dist/primitives/SlotGroup.js +24 -0
  53. package/dist/primitives/SlotItem.d.ts +43 -0
  54. package/dist/primitives/SlotItem.js +38 -0
  55. package/dist/primitives/index.d.ts +7 -0
  56. package/dist/primitives/picture-sources.d.ts +22 -0
  57. package/dist/primitives/picture-sources.js +52 -0
  58. package/dist/schema/fill-spec.d.ts +692 -0
  59. package/dist/schema/fill-spec.js +98 -0
  60. package/dist/schema/fixture-schema.d.ts +143 -0
  61. package/dist/schema/fixture-schema.js +113 -0
  62. package/dist/schema/index.d.ts +16 -0
  63. package/dist/schema/manifest.d.ts +527 -0
  64. package/dist/schema/manifest.js +73 -0
  65. package/dist/schema/property-facts.d.ts +44 -0
  66. package/dist/schema/resolve-variants.d.ts +11 -0
  67. package/dist/schema/resolve-variants.js +15 -0
  68. package/dist/schema/runtime-values.d.ts +45 -0
  69. package/dist/schema/section-nav.d.ts +7 -0
  70. package/dist/schema/section-props.d.ts +69 -0
  71. package/dist/schema/section-schema.d.ts +533 -0
  72. package/dist/schema/section-schema.js +72 -0
  73. package/dist/schema/slot-types.d.ts +117 -0
  74. package/dist/schema/slot-types.js +181 -0
  75. package/dist/schema/source.d.ts +13 -0
  76. package/dist/schema/source.js +12 -0
  77. package/dist/schema/synthesize-nullable.d.ts +18 -0
  78. package/dist/schema/synthesize-nullable.js +47 -0
  79. package/dist/styles.css +131 -9
  80. package/docs/INDEX.md +6 -4
  81. package/docs/eslint.md +90 -0
  82. package/docs/islands.md +150 -0
  83. package/docs/llms.txt +30 -3
  84. package/docs/primitives.md +214 -0
  85. package/docs/schema-system.md +240 -0
  86. package/docs/theme-and-css.md +179 -0
  87. package/package.json +30 -6
  88. package/tsconfig.base.json +16 -0
  89. package/tsconfig.json +2 -13
  90. package/dist/src-dZr5N30y.js +0 -35
@@ -0,0 +1,111 @@
1
+ //#region src/eslint/rules/no-hex.ts
2
+ const COLOR_FN = /(?:oklch|oklab|rgba?|hsla?|color-mix)\s*\([^)]*\)/;
3
+ const HEX = /#(?:[0-9a-fA-F]{8}|[0-9a-fA-F]{6}|[0-9a-fA-F]{4}|[0-9a-fA-F]{3})\b/g;
4
+ const COLOR_UTILITY_PREFIXES = [
5
+ "bg",
6
+ "text",
7
+ "border",
8
+ "ring",
9
+ "outline",
10
+ "shadow",
11
+ "fill",
12
+ "stroke",
13
+ "decoration",
14
+ "divide",
15
+ "accent",
16
+ "caret",
17
+ "placeholder",
18
+ "from",
19
+ "via",
20
+ "to"
21
+ ];
22
+ const COLOR_PROPERTY_NAMES = [
23
+ "color",
24
+ "background",
25
+ "background-color",
26
+ "border-color",
27
+ "outline-color",
28
+ "box-shadow",
29
+ "text-shadow",
30
+ "fill",
31
+ "stroke",
32
+ "caret-color",
33
+ "accent-color",
34
+ "text-decoration-color"
35
+ ];
36
+ function colorBracketRanges(text) {
37
+ const ranges = [];
38
+ const re = /\[[^[\]]*\]/g;
39
+ let m;
40
+ while (m = re.exec(text)) {
41
+ const start = m.index;
42
+ const end = m.index + m[0].length;
43
+ const inner = m[0].slice(1, -1);
44
+ if (COLOR_PROPERTY_NAMES.some((prop) => inner.startsWith(`${prop}:`))) {
45
+ ranges.push([start, end]);
46
+ continue;
47
+ }
48
+ if (inner.startsWith("--") && inner.includes(":")) {
49
+ ranges.push([start, end]);
50
+ continue;
51
+ }
52
+ const before = text.slice(0, start).match(/\S+$/)?.[0] ?? "";
53
+ const lastColon = before.lastIndexOf(":");
54
+ const utility = lastColon === -1 ? before : before.slice(lastColon + 1);
55
+ if (COLOR_UTILITY_PREFIXES.some((prefix) => utility === prefix || utility.startsWith(`${prefix}-`))) ranges.push([start, end]);
56
+ }
57
+ return ranges;
58
+ }
59
+ function isWithinStyleAttribute(node) {
60
+ let cur = node;
61
+ while (cur) {
62
+ if (cur.type === "JSXAttribute" && cur.name?.type === "JSXIdentifier" && cur.name.name === "style") return true;
63
+ cur = cur.parent;
64
+ }
65
+ return false;
66
+ }
67
+ const noHex = {
68
+ meta: {
69
+ type: "problem",
70
+ docs: { description: "Ban hard-coded colour literals in section code" },
71
+ schema: [],
72
+ messages: { hardCodedColor: "`{{value}}` is a hard-coded colour — a section carries no palette of its own, or it breaks the first time another template reuses it. Fix: use a semantic token utility (e.g. `bg-surface-alt`, `text-ink`); if the value is genuinely brand decoration with no token, add it to the template's `theme.ts` palette and use the `--tr-color-<name>` utility it compiles to." }
73
+ },
74
+ create(context) {
75
+ function check(node, text) {
76
+ const fn = COLOR_FN.exec(text);
77
+ if (fn) {
78
+ context.report({
79
+ node,
80
+ messageId: "hardCodedColor",
81
+ data: { value: fn[0] }
82
+ });
83
+ return;
84
+ }
85
+ const hexMatches = [...text.matchAll(HEX)];
86
+ if (hexMatches.length === 0) return;
87
+ const inStyle = isWithinStyleAttribute(node);
88
+ const colorRanges = colorBracketRanges(text);
89
+ for (const hex of hexMatches) {
90
+ if (!colorRanges.some(([start, end]) => hex.index >= start && hex.index < end) && !inStyle) continue;
91
+ context.report({
92
+ node,
93
+ messageId: "hardCodedColor",
94
+ data: { value: hex[0] }
95
+ });
96
+ return;
97
+ }
98
+ }
99
+ return {
100
+ Literal(node) {
101
+ if (typeof node.value === "string") check(node, node.value);
102
+ },
103
+ TemplateElement(node) {
104
+ check(node, node.value.raw);
105
+ }
106
+ };
107
+ }
108
+ };
109
+
110
+ //#endregion
111
+ export { noHex };
@@ -0,0 +1,23 @@
1
+ import { hasClientDirective } from "../is-client-file.js";
2
+
3
+ //#region src/eslint/rules/no-inline-style.ts
4
+ const noInlineStyle = {
5
+ meta: {
6
+ type: "problem",
7
+ docs: { description: "Ban the inline style prop in server-rendered section JSX" },
8
+ schema: [],
9
+ messages: { inlineStyle: "The inline `style=` prop is banned in server-rendered section JSX — it bypasses the cascade and Tailwind's responsive prefixes. Fix: use Tailwind utility classes; for a value that only exists at runtime, move the markup into a `\"use client\"` island, where `style` is allowed." }
10
+ },
11
+ create(context) {
12
+ if (hasClientDirective(context.sourceCode.getText())) return {};
13
+ return { JSXAttribute(node) {
14
+ if (node.name.type === "JSXIdentifier" && node.name.name === "style") context.report({
15
+ node,
16
+ messageId: "inlineStyle"
17
+ });
18
+ } };
19
+ }
20
+ };
21
+
22
+ //#endregion
23
+ export { noInlineStyle };
@@ -0,0 +1,44 @@
1
+ import { hasClientDirective } from "../is-client-file.js";
2
+
3
+ //#region src/eslint/rules/no-nondeterminism.ts
4
+ const BANNED_MEMBERS = {
5
+ Date: /* @__PURE__ */ new Set(["now"]),
6
+ Math: /* @__PURE__ */ new Set(["random"]),
7
+ crypto: /* @__PURE__ */ new Set(["randomUUID", "getRandomValues"]),
8
+ performance: /* @__PURE__ */ new Set(["now"])
9
+ };
10
+ const noNondeterminism = {
11
+ meta: {
12
+ type: "problem",
13
+ docs: { description: "Ban clock and randomness reads in server-rendered section code" },
14
+ schema: [],
15
+ messages: { nondeterministic: "`{{what}}` is nondeterministic and is banned in server-rendered code — the publisher and the editor must render byte-identical HTML. Fix: compute this at fill time and pass it in as a slot value, or move the code into a `\"use client\"` island, where it runs in the browser." }
16
+ },
17
+ create(context) {
18
+ if (hasClientDirective(context.sourceCode.getText())) return {};
19
+ const sourceCode = context.sourceCode;
20
+ return {
21
+ MemberExpression(node) {
22
+ if (node.computed || node.object.type !== "Identifier" || node.property.type !== "Identifier") return;
23
+ if (!BANNED_MEMBERS[node.object.name]?.has(node.property.name)) return;
24
+ const ref = sourceCode.getScope(node).references.find((r) => r.identifier === node.object);
25
+ if (ref?.resolved && ref.resolved.defs.length > 0) return;
26
+ context.report({
27
+ node,
28
+ messageId: "nondeterministic",
29
+ data: { what: `${node.object.name}.${node.property.name}()` }
30
+ });
31
+ },
32
+ NewExpression(node) {
33
+ if (node.callee.type === "Identifier" && node.callee.name === "Date" && node.arguments.length === 0) context.report({
34
+ node,
35
+ messageId: "nondeterministic",
36
+ data: { what: "new Date()" }
37
+ });
38
+ }
39
+ };
40
+ }
41
+ };
42
+
43
+ //#endregion
44
+ export { noNondeterminism };
@@ -0,0 +1,32 @@
1
+ //#region src/eslint/rules/no-raw-element.ts
2
+ const FORBIDDEN = {
3
+ section: "<Section>",
4
+ img: "<Image>"
5
+ };
6
+ const noRawElement = {
7
+ meta: {
8
+ type: "problem",
9
+ docs: { description: "Ban raw <section> and <img> in section JSX" },
10
+ schema: [],
11
+ messages: { rawElement: "Raw `<{{tag}}>` is banned in section JSX — {{use}} carries the contract it skips. Fix: `import { {{name}} } from \"@homepages/template-kit\";` and use {{use}}." }
12
+ },
13
+ create(context) {
14
+ return { JSXOpeningElement(node) {
15
+ if (node.name.type !== "JSXIdentifier" || !node.name.name) return;
16
+ const use = FORBIDDEN[node.name.name];
17
+ if (!use) return;
18
+ context.report({
19
+ node,
20
+ messageId: "rawElement",
21
+ data: {
22
+ tag: node.name.name,
23
+ use,
24
+ name: use.slice(1, -1)
25
+ }
26
+ });
27
+ } };
28
+ }
29
+ };
30
+
31
+ //#endregion
32
+ export { noRawElement };
@@ -0,0 +1,32 @@
1
+ //#region src/eslint/rules/props-from-schema.ts
2
+ const propsFromSchema = {
3
+ meta: {
4
+ type: "problem",
5
+ docs: { description: "A Renderer must import its Props type from ./schema" },
6
+ schema: [],
7
+ messages: { handWritten: "`{{name}}` is a hand-written props type in a Renderer — it bypasses the schema→Props inference and will drift. Fix: add `export type Props = SectionProps<typeof schema>;` to schema.ts and `import type { Props } from \"./schema.js\";` here." }
8
+ },
9
+ create(context) {
10
+ return {
11
+ "ExportNamedDeclaration > TSTypeAliasDeclaration"(node) {
12
+ if (!node.id.name.endsWith("Props")) return;
13
+ context.report({
14
+ node,
15
+ messageId: "handWritten",
16
+ data: { name: node.id.name }
17
+ });
18
+ },
19
+ "ExportNamedDeclaration > TSInterfaceDeclaration"(node) {
20
+ if (!node.id.name.endsWith("Props")) return;
21
+ context.report({
22
+ node,
23
+ messageId: "handWritten",
24
+ data: { name: node.id.name }
25
+ });
26
+ }
27
+ };
28
+ }
29
+ };
30
+
31
+ //#endregion
32
+ export { propsFromSchema };
@@ -0,0 +1,108 @@
1
+ import { isClientFile, resolveImportedFile } from "../is-client-file.js";
2
+
3
+ //#region src/eslint/rules/serializable-island-props.ts
4
+ function inspect(node, path, out) {
5
+ switch (node.type) {
6
+ case "ArrowFunctionExpression":
7
+ case "FunctionExpression":
8
+ out.push({
9
+ node,
10
+ what: "a function",
11
+ path,
12
+ messageId: "notSerializableBadType"
13
+ });
14
+ return;
15
+ case "NewExpression":
16
+ if (node.callee?.type === "Identifier" && node.callee.name && node.callee.name !== "Object" && node.callee.name !== "Array") out.push({
17
+ node,
18
+ what: `a ${node.callee.name}`,
19
+ path,
20
+ messageId: "notSerializableClassInstance"
21
+ });
22
+ return;
23
+ case "ObjectExpression":
24
+ for (const prop of node.properties ?? []) {
25
+ if (prop.type !== "Property" || prop.computed) continue;
26
+ if (prop.kind !== "init") continue;
27
+ const key = prop.key.type === "Identifier" ? prop.key.name : prop.key.type === "Literal" ? String(prop.key.value) : null;
28
+ if (key === null) continue;
29
+ inspect(prop.value, `${path}.${key}`, out);
30
+ }
31
+ return;
32
+ case "ArrayExpression":
33
+ (node.elements ?? []).forEach((el, i) => {
34
+ if (el && el.type !== "SpreadElement") inspect(el, `${path}[${i}]`, out);
35
+ });
36
+ return;
37
+ case "Literal":
38
+ if (typeof node.bigint === "string") out.push({
39
+ node,
40
+ what: "a bigint",
41
+ path,
42
+ messageId: "notSerializableBadType"
43
+ });
44
+ return;
45
+ case "Identifier":
46
+ if (node.name === "NaN" || node.name === "Infinity") out.push({
47
+ node,
48
+ what: node.name,
49
+ path,
50
+ messageId: "notSerializableNanInfinity"
51
+ });
52
+ return;
53
+ default: return;
54
+ }
55
+ }
56
+ const serializableIslandProps = {
57
+ meta: {
58
+ type: "problem",
59
+ docs: { description: "Island props must be JSON-serializable" },
60
+ schema: [],
61
+ messages: {
62
+ notSerializableBadType: "{{path}} is {{what}} — island props must be JSON-serializable (string, number, boolean, null, plain object, array). Fix: pass data, not behavior — an island owns its own handlers.",
63
+ notSerializableClassInstance: "{{path}} is {{what}} — island props must be JSON-serializable (plain objects and arrays only). Fix: pass data, not behavior — a Date/Map/Set/RegExp crosses as its plain-JSON equivalent (e.g. an ISO string) that you reconstruct inside the island.",
64
+ notSerializableNanInfinity: "{{path}} is {{what}} — island props must be JSON-serializable, and JSON has no NaN or Infinity. Fix: pass a finite number, or encode the sentinel as a string prop the island parses."
65
+ }
66
+ },
67
+ create(context) {
68
+ const filename = context.filename;
69
+ const islandNames = /* @__PURE__ */ new Map();
70
+ function isIsland(name) {
71
+ return islandNames.get(name) ?? false;
72
+ }
73
+ return {
74
+ ImportDeclaration(node) {
75
+ const source = node.source.value;
76
+ if (typeof source !== "string") return;
77
+ const resolved = resolveImportedFile(source, filename);
78
+ if (resolved === null) return;
79
+ const island = isClientFile(resolved);
80
+ for (const spec of node.specifiers) islandNames.set(spec.local.name, island);
81
+ },
82
+ JSXOpeningElement(node) {
83
+ if (node.name.type !== "JSXIdentifier" || !node.name.name) return;
84
+ if (!isIsland(node.name.name)) return;
85
+ for (const attr of node.attributes) {
86
+ if (attr.type !== "JSXAttribute") continue;
87
+ const nameNode = attr.name;
88
+ if (nameNode.type !== "JSXIdentifier" || !nameNode.name) continue;
89
+ const value = attr.value;
90
+ if (!value || value.type !== "JSXExpressionContainer" || !value.expression) continue;
91
+ const bad = [];
92
+ inspect(value.expression, `props.${nameNode.name}`, bad);
93
+ for (const b of bad) context.report({
94
+ node: b.node,
95
+ messageId: b.messageId,
96
+ data: {
97
+ path: b.path,
98
+ what: b.what
99
+ }
100
+ });
101
+ }
102
+ }
103
+ };
104
+ }
105
+ };
106
+
107
+ //#endregion
108
+ export { serializableIslandProps };
@@ -0,0 +1,33 @@
1
+ //#region src/eslint/rules/slot-marker-literal.ts
2
+ const KEY_ATTRS = /* @__PURE__ */ new Set([
3
+ "data-slot-id",
4
+ "data-slot-group",
5
+ "slotId"
6
+ ]);
7
+ const KEY_COMPONENTS = /* @__PURE__ */ new Set(["Slot", "SlotGroup"]);
8
+ const slotMarkerLiteral = {
9
+ meta: {
10
+ type: "problem",
11
+ docs: { description: "Slot markers must carry a string-literal key" },
12
+ schema: [],
13
+ messages: { markerNotLiteral: "`{{attr}}` must be a string literal, not an expression — the slot-marker completeness check reads this key statically, and cannot resolve a computed one (the slot would silently lose editor selection). Fix: write the key inline, e.g. `{{attr}}=\"headline\"`." }
14
+ },
15
+ create(context) {
16
+ return { JSXAttribute(node) {
17
+ if (node.name.type !== "JSXIdentifier" || !node.name.name) return;
18
+ const attr = node.name.name;
19
+ const isKeyAttr = KEY_ATTRS.has(attr);
20
+ const isComponentId = attr === "id" && node.parent?.name?.type === "JSXIdentifier" && KEY_COMPONENTS.has(node.parent.name.name ?? "");
21
+ if (!isKeyAttr && !isComponentId) return;
22
+ if (node.value?.type === "Literal") return;
23
+ context.report({
24
+ node,
25
+ messageId: "markerNotLiteral",
26
+ data: { attr }
27
+ });
28
+ } };
29
+ }
30
+ };
31
+
32
+ //#endregion
33
+ export { slotMarkerLiteral };
package/dist/eslint.d.ts CHANGED
@@ -1,10 +1,6 @@
1
- import { Linter } from "eslint";
2
- //#region src/eslint.d.ts
3
- /**
4
- * The HomePages template-authoring ESLint preset.
5
- * Rules arrive with the surface they govern (schema, primitives, determinism bans).
6
- * Shipped now, empty, so a workspace's flat config can reference it from 0.0.x.
7
- */
1
+ import { ESLint, Linter } from "eslint";
2
+ //#region src/eslint/index.d.ts
3
+ declare const plugin: ESLint.Plugin;
8
4
  declare const templateKitConfig: Linter.Config[];
9
5
  //#endregion
10
- export { templateKitConfig as default, templateKitConfig };
6
+ export { templateKitConfig as default, templateKitConfig, plugin };
package/dist/eslint.js CHANGED
@@ -1,13 +1,77 @@
1
- //#region src/eslint.ts
2
- /**
3
- * The HomePages template-authoring ESLint preset.
4
- * Rules arrive with the surface they govern (schema, primitives, determinism bans).
5
- * Shipped now, empty, so a workspace's flat config can reference it from 0.0.x.
6
- */
7
- const templateKitConfig = [{
8
- name: "@homepages/template-kit/base",
9
- rules: {}
10
- }];
1
+ import { imageBareNeedsReason } from "./eslint/rules/image-bare-needs-reason.js";
2
+ import { noClientDirectiveInContract } from "./eslint/rules/no-client-directive-in-contract.js";
3
+ import { noClientRuntimeInServer } from "./eslint/rules/no-client-runtime-in-server.js";
4
+ import { noCssImportFromRenderPath } from "./eslint/rules/no-css-import-from-render-path.js";
5
+ import { noHex } from "./eslint/rules/no-hex.js";
6
+ import { noInlineStyle } from "./eslint/rules/no-inline-style.js";
7
+ import { noNondeterminism } from "./eslint/rules/no-nondeterminism.js";
8
+ import { noRawElement } from "./eslint/rules/no-raw-element.js";
9
+ import { propsFromSchema } from "./eslint/rules/props-from-schema.js";
10
+ import { serializableIslandProps } from "./eslint/rules/serializable-island-props.js";
11
+ import { slotMarkerLiteral } from "./eslint/rules/slot-marker-literal.js";
12
+ import tsParser from "@typescript-eslint/parser";
13
+
14
+ //#region src/eslint/index.ts
15
+ const rules = {
16
+ "no-client-directive-in-contract": noClientDirectiveInContract,
17
+ "no-nondeterminism": noNondeterminism,
18
+ "no-client-runtime-in-server": noClientRuntimeInServer,
19
+ "serializable-island-props": serializableIslandProps,
20
+ "props-from-schema": propsFromSchema,
21
+ "no-inline-style": noInlineStyle,
22
+ "no-raw-element": noRawElement,
23
+ "image-bare-needs-reason": imageBareNeedsReason,
24
+ "slot-marker-literal": slotMarkerLiteral,
25
+ "no-hex": noHex,
26
+ "no-css-import-from-render-path": noCssImportFromRenderPath
27
+ };
28
+ const plugin = {
29
+ meta: { name: "@homepages/template-kit" },
30
+ rules
31
+ };
32
+ const CONTRACT_FILES = [
33
+ "**/Renderer.tsx",
34
+ "**/schema.ts",
35
+ "**/fill-spec.ts",
36
+ "**/fixtures.ts"
37
+ ];
38
+ const templateKitConfig = [
39
+ {
40
+ name: "template-kit/authoring",
41
+ files: ["**/*.ts", "**/*.tsx"],
42
+ ignores: ["**/*.config.{ts,mts,cts,tsx}"],
43
+ languageOptions: {
44
+ parser: tsParser,
45
+ ecmaVersion: "latest",
46
+ sourceType: "module",
47
+ parserOptions: { ecmaFeatures: { jsx: true } }
48
+ },
49
+ plugins: { "template-kit": plugin },
50
+ rules: {
51
+ "template-kit/no-nondeterminism": "error",
52
+ "template-kit/no-client-runtime-in-server": "error",
53
+ "template-kit/serializable-island-props": "error",
54
+ "template-kit/no-inline-style": "error",
55
+ "template-kit/no-raw-element": "error",
56
+ "template-kit/image-bare-needs-reason": "error",
57
+ "template-kit/slot-marker-literal": "error",
58
+ "template-kit/no-hex": "error",
59
+ "template-kit/no-css-import-from-render-path": "error"
60
+ }
61
+ },
62
+ {
63
+ name: "template-kit/contract-files",
64
+ files: CONTRACT_FILES,
65
+ plugins: { "template-kit": plugin },
66
+ rules: { "template-kit/no-client-directive-in-contract": "error" }
67
+ },
68
+ {
69
+ name: "template-kit/renderer",
70
+ files: ["**/Renderer.tsx"],
71
+ plugins: { "template-kit": plugin },
72
+ rules: { "template-kit/props-from-schema": "error" }
73
+ }
74
+ ];
11
75
 
12
76
  //#endregion
13
- export { templateKitConfig as default, templateKitConfig };
77
+ export { templateKitConfig as default, templateKitConfig, plugin };
@@ -0,0 +1,67 @@
1
+ import { ResponsiveImage } from "../contracts/image-descriptor.js";
2
+ import { PropertyFacts } from "../schema/property-facts.js";
3
+ //#region src/fixtures/sample-property.d.ts
4
+ type PhotoFixture = {
5
+ id: string;
6
+ url: string;
7
+ alt: string;
8
+ tags: {
9
+ room?: string;
10
+ category?: string;
11
+ };
12
+ };
13
+ declare const allPhotos: PhotoFixture[];
14
+ declare function mkResponsive(url: string, width: number, height: number): ResponsiveImage;
15
+ type PoiFixture = {
16
+ id: string;
17
+ category: "restaurant" | "park" | "transit" | "shopping" | "coffee" | "fitness";
18
+ name: string;
19
+ distance: string;
20
+ lat: number;
21
+ lng: number;
22
+ url: string;
23
+ };
24
+ declare const samplePois: PoiFixture[];
25
+ type ContactFixture = {
26
+ library_item_id: string;
27
+ category: "contact";
28
+ type: "agent";
29
+ position: number;
30
+ fields: {
31
+ display_name: string;
32
+ title: string;
33
+ bio: string;
34
+ headshot_url: string;
35
+ website_url: string;
36
+ email: string;
37
+ phone: string;
38
+ address: string;
39
+ socials: Array<{
40
+ platform: "instagram" | "linkedin" | "x" | "facebook";
41
+ url: string;
42
+ }>;
43
+ };
44
+ };
45
+ declare const sampleContacts: ContactFixture[];
46
+ type AmenityItem = {
47
+ id: string;
48
+ label: string;
49
+ /** Catalog glyph key; narrowed by the consuming section (see amenities' safeIcon). */
50
+ icon: string;
51
+ description: string;
52
+ items: string[];
53
+ };
54
+ declare const sampleAmenities: AmenityItem[];
55
+ declare const sampleProperty: PropertyFacts;
56
+ declare const sampleHeroSlots: {
57
+ readonly headline: "A mansard victorian single-family home in the heart of Jamaica Plain";
58
+ readonly hero_image: {
59
+ readonly photo_id: string;
60
+ readonly url: string;
61
+ readonly alt: string;
62
+ };
63
+ readonly brand_logo: null;
64
+ readonly listing_status: "for_sale";
65
+ };
66
+ //#endregion
67
+ export { AmenityItem, ContactFixture, PhotoFixture, PoiFixture, allPhotos, mkResponsive, sampleAmenities, sampleContacts, sampleHeroSlots, samplePois, sampleProperty };