@homepages/template-kit 0.1.0 → 0.3.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 (111) hide show
  1. package/CHANGELOG.md +220 -0
  2. package/README.md +70 -16
  3. package/dist/base.css +36 -48
  4. package/dist/cli/check/config.js +37 -0
  5. package/dist/cli/check/css.js +117 -0
  6. package/dist/cli/check/diagnostics.js +32 -0
  7. package/dist/cli/check/index.js +87 -0
  8. package/dist/cli/check/loader.js +142 -0
  9. package/dist/cli/check/paths.js +15 -0
  10. package/dist/cli/check/relativize.js +18 -0
  11. package/dist/cli/check/render-invariants.js +24 -0
  12. package/dist/cli/check/resolve-tool.js +38 -0
  13. package/dist/cli/check/stages/deps.js +337 -0
  14. package/dist/cli/check/stages/lint.js +46 -0
  15. package/dist/cli/check/stages/render.js +101 -0
  16. package/dist/cli/check/stages/size.js +158 -0
  17. package/dist/cli/check/stages/tree.js +207 -0
  18. package/dist/cli/check/stages/typecheck.js +46 -0
  19. package/dist/cli/check/stages/validate/catalog-exhaustiveness.js +18 -0
  20. package/dist/cli/check/stages/validate/facts.js +18 -0
  21. package/dist/cli/check/stages/validate/formatter-contract.js +14 -0
  22. package/dist/cli/check/stages/validate/group-contract.js +21 -0
  23. package/dist/cli/check/stages/validate/index.js +66 -0
  24. package/dist/cli/check/stages/validate/list-scalar-contract.js +22 -0
  25. package/dist/cli/check/stages/validate/nav-contract.js +93 -0
  26. package/dist/cli/check/stages/validate/nested-slot-contract.js +12 -0
  27. package/dist/cli/check/stages/validate/object-list-contract.js +31 -0
  28. package/dist/cli/check/stages/validate/orchestrator.js +241 -0
  29. package/dist/cli/check/stages/validate/produced-by-contract.js +18 -0
  30. package/dist/cli/check/stages/validate/row-contract.js +31 -0
  31. package/dist/cli/check/stages/validate/select-contract.js +13 -0
  32. package/dist/cli/check/stages/validate/source-contract.js +21 -0
  33. package/dist/cli/check/stages/validate/variant-contract.js +15 -0
  34. package/dist/cli/check/workspace.js +86 -0
  35. package/dist/cli/theme/generate.js +43 -0
  36. package/dist/cli/theme/index.js +33 -0
  37. package/dist/cli/theme/load-theme.js +69 -0
  38. package/dist/cli.js +22 -4
  39. package/dist/contracts/fill-treatments.js +47 -0
  40. package/dist/design-system/theme.d.ts +30 -299
  41. package/dist/design-system/theme.js +112 -83
  42. package/dist/eslint/rules/image-bare-needs-reason.js +6 -1
  43. package/dist/eslint/rules/no-client-directive-in-contract.js +6 -1
  44. package/dist/eslint/rules/no-client-runtime-in-server.js +5 -1
  45. package/dist/eslint/rules/no-css-import-from-render-path.js +8 -2
  46. package/dist/eslint/rules/no-hex.js +84 -7
  47. package/dist/eslint/rules/no-inline-style.js +5 -1
  48. package/dist/eslint/rules/no-nondeterminism.js +5 -1
  49. package/dist/eslint/rules/no-raw-element.js +6 -1
  50. package/dist/eslint/rules/props-from-schema.js +6 -1
  51. package/dist/eslint/rules/serializable-island-props.js +5 -1
  52. package/dist/eslint/rules/slot-marker-literal.js +6 -1
  53. package/dist/eslint.js +1 -1
  54. package/dist/index.d.ts +2 -2
  55. package/dist/index.js +2 -2
  56. package/dist/package.js +1 -1
  57. package/dist/primitives/Image.js +1 -1
  58. package/dist/rules/registry.js +16 -0
  59. package/dist/schema/fill-spec.d.ts +80 -590
  60. package/dist/schema/fixture-schema.d.ts +5 -81
  61. package/dist/schema/manifest.d.ts +39 -475
  62. package/dist/schema/resolve-section-ref.js +10 -0
  63. package/dist/schema/rows.js +10 -0
  64. package/dist/schema/section-nav.js +9 -0
  65. package/dist/schema/section-schema.d.ts +51 -437
  66. package/dist/schema/slot-types.d.ts +12 -16
  67. package/dist/styles.css +31 -88
  68. package/docs/INDEX.md +5 -3
  69. package/docs/check.md +121 -0
  70. package/docs/eslint.md +25 -50
  71. package/docs/llms.txt +26 -11
  72. package/docs/primitives.md +5 -0
  73. package/docs/rules/INDEX.md +65 -0
  74. package/docs/rules/audit-severity.md +91 -0
  75. package/docs/rules/bundle-binary-asset.md +102 -0
  76. package/docs/rules/bundle-incomplete.md +117 -0
  77. package/docs/rules/css-reason.md +94 -0
  78. package/docs/rules/determinism-drift.md +112 -0
  79. package/docs/rules/fixtures-invalid.md +96 -0
  80. package/docs/rules/image-bare-needs-reason.md +117 -0
  81. package/docs/rules/license-denied.md +114 -0
  82. package/docs/rules/lockfile-missing.md +68 -0
  83. package/docs/rules/lockfile-stale.md +88 -0
  84. package/docs/rules/manifest-invalid.md +99 -0
  85. package/docs/rules/missing-slot-marker.md +140 -0
  86. package/docs/rules/no-bare-css-import.md +128 -0
  87. package/docs/rules/no-client-directive-in-contract.md +111 -0
  88. package/docs/rules/no-client-runtime-in-server.md +143 -0
  89. package/docs/rules/no-css-import-from-render-path.md +118 -0
  90. package/docs/rules/no-hex.md +163 -0
  91. package/docs/rules/no-inline-style.md +121 -0
  92. package/docs/rules/no-nondeterminism.md +100 -0
  93. package/docs/rules/no-raw-element.md +100 -0
  94. package/docs/rules/no-templates.md +87 -0
  95. package/docs/rules/parse-error.md +76 -0
  96. package/docs/rules/props-from-schema.md +119 -0
  97. package/docs/rules/render-invariant.md +165 -0
  98. package/docs/rules/schema-invalid.md +96 -0
  99. package/docs/rules/serializable-island-props.md +146 -0
  100. package/docs/rules/server-vs-client.md +105 -0
  101. package/docs/rules/sidebar-order.md +109 -0
  102. package/docs/rules/single-react.md +97 -0
  103. package/docs/rules/size-renderer-bundle.md +109 -0
  104. package/docs/rules/size-section-css.md +95 -0
  105. package/docs/rules/slot-group-marker.md +127 -0
  106. package/docs/rules/slot-marker-literal.md +148 -0
  107. package/docs/rules/typecheck.md +103 -0
  108. package/docs/rules/unlayered-fence.md +113 -0
  109. package/docs/theme-and-css.md +193 -82
  110. package/package.json +4 -9
  111. package/tsconfig.json +1 -1
@@ -1,95 +1,124 @@
1
1
  import { z } from "zod";
2
2
 
3
3
  //#region src/design-system/theme.ts
4
- const COLOR_ROLE_VARS = {
5
- ink: "--tr-color-ink",
6
- inkSoft: "--tr-color-ink-soft",
7
- inkMute: "--tr-color-ink-mute",
8
- background: "--tr-color-background",
9
- surface: "--tr-color-surface",
10
- surfaceAlt: "--tr-color-surface-alt",
11
- border: "--tr-color-border",
12
- borderStrong: "--tr-color-border-strong",
13
- primary: "--tr-color-primary",
14
- primaryFg: "--tr-color-primary-fg",
15
- primaryStrong: "--tr-color-primary-darker",
16
- accent: "--tr-color-accent",
17
- accentFg: "--tr-color-accent-contrast",
18
- accentStrong: "--tr-color-accent-strong",
19
- overlay: "--tr-color-overlay",
20
- overlayStrong: "--tr-color-overlay-strong",
21
- overlayLight: "--tr-color-overlay-light",
22
- darkBg: "--tr-color-footer-bg",
23
- darkFg: "--tr-color-footer-fg",
24
- darkFgMute: "--tr-color-footer-fg-mute",
25
- darkBorder: "--tr-color-footer-divider"
4
+ const TokenNameSchema = z.string().regex(/^[a-z0-9]+(-[a-z0-9]+)*$/, "token names are kebab-case: [a-z0-9] groups separated by single hyphens");
5
+ const tokens = z.record(TokenNameSchema, z.string().min(1));
6
+ const NAMESPACES = {
7
+ colors: {
8
+ tw: "--color-",
9
+ tr: "--tr-color-"
10
+ },
11
+ fonts: {
12
+ tw: "--font-",
13
+ tr: "--tr-font-"
14
+ },
15
+ type: {
16
+ tw: "--text-",
17
+ tr: "--tr-text-"
18
+ },
19
+ radii: {
20
+ tw: "--radius-",
21
+ tr: "--tr-radius-"
22
+ },
23
+ shadows: {
24
+ tw: "--shadow-",
25
+ tr: "--tr-shadow-"
26
+ }
26
27
  };
27
- const FONT_ROLE_VARS = {
28
- body: "--tr-font-sans",
29
- heading: "--tr-font-serif",
30
- numeric: "--tr-font-num",
31
- label: "--tr-font-label"
32
- };
33
- const TYPE_STEP_VARS = {
34
- xs: "--tr-text-xs",
35
- sm: "--tr-text-sm",
36
- base: "--tr-text-base",
37
- lg: "--tr-text-lg",
38
- xl: "--tr-text-xl",
39
- "2xl": "--tr-text-2xl",
40
- "3xl": "--tr-text-3xl",
41
- "4xl": "--tr-text-4xl",
42
- "5xl": "--tr-text-5xl",
43
- display: "--tr-text-display"
44
- };
45
- const RADIUS_VARS = {
46
- sm: "--tr-radius-sm",
47
- md: "--tr-radius-md",
48
- lg: "--tr-radius-lg",
49
- full: "--tr-radius-full"
50
- };
51
- const SHADOW_VARS = {
52
- sm: "--tr-shadow-sm",
53
- md: "--tr-shadow-md",
54
- lg: "--tr-shadow-lg"
55
- };
56
- const LAYOUT_VARS = {
57
- containerMax: "--tr-container-max",
58
- containerPad: "--tr-container-pad",
59
- headerHeight: "--tr-header-height"
60
- };
61
- const colorRoles = Object.fromEntries(Object.keys(COLOR_ROLE_VARS).map((k) => [k, z.string().min(1)]));
62
- const scale = (keys) => z.object(Object.fromEntries(keys.map((k) => [k, z.string().min(1)])));
63
- const TokenThemeSchema = z.object({
64
- colors: z.object(colorRoles),
65
- /** Optional open brand palette → `--tr-color-<name>` (the escape hatch). */
66
- palette: z.record(z.string().min(1)).optional(),
67
- fonts: scale(Object.keys(FONT_ROLE_VARS)).extend({
68
- /** Font-loading block (@import / @font-face) emitted before :root. */
69
- fontFaces: z.string().optional() }),
70
- type: scale(Object.keys(TYPE_STEP_VARS)),
71
- radii: scale(Object.keys(RADIUS_VARS)),
72
- shadows: scale(Object.keys(SHADOW_VARS)),
73
- layout: scale(Object.keys(LAYOUT_VARS))
28
+ const NAMESPACED_GROUPS = Object.keys(NAMESPACES);
29
+ const DocumentDefaultsSchema = z.object({
30
+ font: TokenNameSchema,
31
+ color: TokenNameSchema,
32
+ background: TokenNameSchema,
33
+ fontSize: TokenNameSchema
74
34
  }).strict();
35
+ const TokenThemeSchema = z.object({
36
+ colors: tokens,
37
+ fonts: tokens,
38
+ type: tokens,
39
+ radii: tokens,
40
+ shadows: tokens,
41
+ /** Layout values → `--tr-<name>`. No Tailwind alias: not a utility namespace. */
42
+ layout: tokens,
43
+ /** Font-loading block (@import / @font-face), emitted before every other block. */
44
+ fontFaces: z.string().optional(),
45
+ document: DocumentDefaultsSchema
46
+ }).strict().superRefine((theme, ctx) => {
47
+ for (const name of Object.keys(theme.colors)) if (Object.hasOwn(theme.type, name)) ctx.addIssue({
48
+ code: z.ZodIssueCode.custom,
49
+ path: ["type", name],
50
+ message: `"${name}" is declared in both \`colors\` and \`type\`, and both compile to \`text-${name}\` — rename one.`
51
+ });
52
+ }).superRefine((theme, ctx) => {
53
+ const refs = [
54
+ [
55
+ "font",
56
+ "fonts",
57
+ theme.document.font
58
+ ],
59
+ [
60
+ "color",
61
+ "colors",
62
+ theme.document.color
63
+ ],
64
+ [
65
+ "background",
66
+ "colors",
67
+ theme.document.background
68
+ ],
69
+ [
70
+ "fontSize",
71
+ "type",
72
+ theme.document.fontSize
73
+ ]
74
+ ];
75
+ for (const [field, group, name] of refs) if (!Object.hasOwn(theme[group], name)) ctx.addIssue({
76
+ code: z.ZodIssueCode.custom,
77
+ path: ["document", field],
78
+ message: `document.${field}: "${name}" is not a declared \`${group}\` token`
79
+ });
80
+ });
75
81
  /**
76
- * Compile a `TokenTheme` into the CSS a template's bundle ships: an optional
77
- * font-loading block followed by a single `:root { --tr-*: … }` declaration.
78
- * Deterministic (stable key order; palette keys sorted) so the same theme
79
- * always content-hashes identically.
82
+ * Compile a `TokenTheme` into the CSS a template's bundle ships:
83
+ *
84
+ * 1. the optional font-loading block;
85
+ * 2. `@theme inline { --color-<name>: var(--tr-color-<name>); … }` — the Tailwind
86
+ * alias layer, which is what brings the token utilities into existence;
87
+ * 3. `:root { --tr-color-<name>: <value>; … }` — the values behind those aliases;
88
+ * 4. `body { … }` — the document defaults, as `var()` indirections so that a runtime
89
+ * `:root` override still cascades to the page.
90
+ *
91
+ * The output MUST be compiled INSIDE the Tailwind build graph: `@theme` is a Tailwind
92
+ * directive, and anywhere else it is inert — silently, yielding a stylesheet with no
93
+ * token utilities and no error to explain why.
94
+ *
95
+ * Deterministic: keys are sorted, so the same theme always content-hashes identically.
80
96
  */
81
97
  function compileThemeToCss(theme) {
98
+ const aliases = [];
82
99
  const decls = [];
83
- for (const role of Object.keys(COLOR_ROLE_VARS)) decls.push(` ${COLOR_ROLE_VARS[role]}: ${theme.colors[role]};`);
84
- if (theme.palette) for (const name of Object.keys(theme.palette).sort()) decls.push(` --tr-color-${name}: ${theme.palette[name]};`);
85
- for (const k of Object.keys(FONT_ROLE_VARS)) decls.push(` ${FONT_ROLE_VARS[k]}: ${theme.fonts[k]};`);
86
- for (const k of Object.keys(TYPE_STEP_VARS)) decls.push(` ${TYPE_STEP_VARS[k]}: ${theme.type[k]};`);
87
- for (const k of Object.keys(RADIUS_VARS)) decls.push(` ${RADIUS_VARS[k]}: ${theme.radii[k]};`);
88
- for (const k of Object.keys(SHADOW_VARS)) decls.push(` ${SHADOW_VARS[k]}: ${theme.shadows[k]};`);
89
- for (const k of Object.keys(LAYOUT_VARS)) decls.push(` ${LAYOUT_VARS[k]}: ${theme.layout[k]};`);
90
- const root = `:root {\n${decls.join("\n")}\n}`;
91
- return theme.fonts.fontFaces ? `${theme.fonts.fontFaces}\n\n${root}\n` : `${root}\n`;
100
+ for (const group of NAMESPACED_GROUPS) {
101
+ const { tw, tr } = NAMESPACES[group];
102
+ for (const name of Object.keys(theme[group]).sort()) {
103
+ aliases.push(` ${tw}${name}: var(${tr}${name});`);
104
+ decls.push(` ${tr}${name}: ${theme[group][name]};`);
105
+ }
106
+ }
107
+ for (const name of Object.keys(theme.layout).sort()) decls.push(` --tr-${name}: ${theme.layout[name]};`);
108
+ const { font, color, background, fontSize } = theme.document;
109
+ const body = `body {
110
+ font-family: var(--tr-font-${font});
111
+ color: var(--tr-color-${color});
112
+ background: var(--tr-color-${background});
113
+ font-size: var(--tr-text-${fontSize});
114
+ }`;
115
+ const blocks = [
116
+ `@theme inline {\n${aliases.join("\n")}\n}`,
117
+ `:root {\n${decls.join("\n")}\n}`,
118
+ body
119
+ ].join("\n\n");
120
+ return theme.fontFaces ? `${theme.fontFaces}\n\n${blocks}\n` : `${blocks}\n`;
92
121
  }
93
122
 
94
123
  //#endregion
95
- export { COLOR_ROLE_VARS, FONT_ROLE_VARS, LAYOUT_VARS, RADIUS_VARS, SHADOW_VARS, TYPE_STEP_VARS, TokenThemeSchema, compileThemeToCss };
124
+ export { TokenNameSchema, TokenThemeSchema, compileThemeToCss };
@@ -1,8 +1,13 @@
1
+ import { docsPathForRule } from "../../rules/registry.js";
2
+
1
3
  //#region src/eslint/rules/image-bare-needs-reason.ts
2
4
  const imageBareNeedsReason = {
3
5
  meta: {
4
6
  type: "problem",
5
- docs: { description: "<Image bare> requires a `// bare:` reason comment" },
7
+ docs: {
8
+ description: "<Image bare> requires a `// bare:` reason comment",
9
+ url: docsPathForRule("image-bare-needs-reason")
10
+ },
6
11
  schema: [],
7
12
  messages: { bareNeedsReason: "`<Image bare>` skips the framed wrapper's aspect-ratio box and fallback rect. Fix: document why this case warrants it with a `// bare: <reason>` comment immediately above the element." }
8
13
  },
@@ -1,8 +1,13 @@
1
+ import { docsPathForRule } from "../../rules/registry.js";
2
+
1
3
  //#region src/eslint/rules/no-client-directive-in-contract.ts
2
4
  const noClientDirectiveInContract = {
3
5
  meta: {
4
6
  type: "problem",
5
- docs: { description: "Ban the \"use client\" directive in a section's four contract files" },
7
+ docs: {
8
+ description: "Ban the \"use client\" directive in a section's four contract files",
9
+ url: docsPathForRule("no-client-directive-in-contract")
10
+ },
6
11
  schema: [],
7
12
  messages: { clientContractFile: "A section's contract files (Renderer.tsx, schema.ts, fill-spec.ts, fixtures.ts) are server-only and must not carry \"use client\" — the platform reads them outside a browser. Fix: put the interactive part in its own \"use client\" component in the section folder and render it from the Renderer." }
8
13
  },
@@ -1,3 +1,4 @@
1
+ import { docsPathForRule } from "../../rules/registry.js";
1
2
  import { hasClientDirective, isClientFile, resolveImportedFile } from "../is-client-file.js";
2
3
 
3
4
  //#region src/eslint/rules/no-client-runtime-in-server.ts
@@ -22,7 +23,10 @@ const BANNED_GLOBALS = /* @__PURE__ */ new Set([
22
23
  const noClientRuntimeInServer = {
23
24
  meta: {
24
25
  type: "problem",
25
- docs: { description: "Ban effects, state, handlers, network, and browser globals in server-rendered section code" },
26
+ docs: {
27
+ description: "Ban effects, state, handlers, network, and browser globals in server-rendered section code",
28
+ url: docsPathForRule("no-client-runtime-in-server")
29
+ },
26
30
  schema: [],
27
31
  messages: {
28
32
  clientRuntime: "`{{what}}` is browser-only and is banned in server-rendered code — a Renderer must be a pure function of its props. Fix: move this into a `\"use client\"` component in the section folder and render it from the Renderer; it will be hydrated as an island.",
@@ -1,15 +1,21 @@
1
+ import { docsPathForRule } from "../../rules/registry.js";
2
+
1
3
  //#region src/eslint/rules/no-css-import-from-render-path.ts
2
4
  const noCssImportFromRenderPath = {
3
5
  meta: {
4
6
  type: "problem",
5
- docs: { description: "Ban CSS imports from a section's render path" },
7
+ docs: {
8
+ description: "Ban local CSS imports from a section's render path",
9
+ url: docsPathForRule("no-css-import-from-render-path")
10
+ },
6
11
  schema: [],
7
- messages: { cssFromRenderPath: "`{{source}}` is a CSS import in the render path — the renderer is compiled for SSR, where this is dropped, so the styles would silently not ship. Fix: `@import` it from the section's root `styles.css`, which is the entry the CSS build actually reads." }
12
+ messages: { cssFromRenderPath: "`{{source}}` is a local stylesheet imported from the render path — the build already collects every `.css` file in the section folder by filename, so this ships the rules twice. Fix: delete the import; the file is picked up on its own. (A package's stylesheet belongs here, imported by its bare specifier — that is how it reaches the section's layer.)" }
8
13
  },
9
14
  create(context) {
10
15
  return { ImportDeclaration(node) {
11
16
  const source = node.source.value;
12
17
  if (typeof source !== "string" || !source.endsWith(".css")) return;
18
+ if (!source.startsWith(".") && !source.startsWith("/")) return;
13
19
  context.report({
14
20
  node,
15
21
  messageId: "cssFromRenderPath",
@@ -1,6 +1,77 @@
1
+ import { docsPathForRule } from "../../rules/registry.js";
2
+
1
3
  //#region src/eslint/rules/no-hex.ts
2
- const COLOR_FN = /(?:oklch|oklab|rgba?|hsla?|color-mix)\s*\([^)]*\)/;
3
4
  const HEX = /#(?:[0-9a-fA-F]{8}|[0-9a-fA-F]{6}|[0-9a-fA-F]{4}|[0-9a-fA-F]{3})\b/g;
5
+ const SEPARATOR = /[\s_]/;
6
+ const TRIM_SEPARATORS = /^[\s_]+|[\s_]+$/g;
7
+ const PERCENTAGE = /^[+-]?(?:\d+\.?\d*|\.\d+)%$/;
8
+ const VAR_REF = /^var\((.*)\)$/s;
9
+ const NESTED_COLOR_MIX = /^color-mix[\s_]*\((.*)\)$/s;
10
+ const CUSTOM_PROPERTY_NAME = /^--[A-Za-z0-9_-]+$/;
11
+ const PALETTE_FREE_KEYWORDS = /* @__PURE__ */ new Set(["transparent", "currentcolor"]);
12
+ function splitTopLevel(text, isSeparator) {
13
+ const parts = [];
14
+ let depth = 0;
15
+ let current = "";
16
+ for (const char of text) {
17
+ if (char === "(") depth += 1;
18
+ else if (char === ")") depth -= 1;
19
+ if (depth === 0 && isSeparator(char)) {
20
+ if (current !== "") parts.push(current);
21
+ current = "";
22
+ continue;
23
+ }
24
+ current += char;
25
+ }
26
+ if (current !== "") parts.push(current);
27
+ return parts;
28
+ }
29
+ function findColorFunctionCalls(text) {
30
+ const head = /(?:oklch|oklab|rgba?|hsla?|color-mix)[\s_]*\(/g;
31
+ const calls = [];
32
+ let match;
33
+ while (match = head.exec(text)) {
34
+ const open = match.index + match[0].length - 1;
35
+ let depth = 0;
36
+ let close = -1;
37
+ for (let i = open; i < text.length; i += 1) if (text[i] === "(") depth += 1;
38
+ else if (text[i] === ")") {
39
+ depth -= 1;
40
+ if (depth === 0) {
41
+ close = i;
42
+ break;
43
+ }
44
+ }
45
+ if (close === -1) continue;
46
+ calls.push({
47
+ raw: text.slice(match.index, close + 1),
48
+ name: match[0].replace(/[\s_]*\($/, "").toLowerCase(),
49
+ args: text.slice(open + 1, close)
50
+ });
51
+ }
52
+ return calls;
53
+ }
54
+ function isThemeDerivedColor(token) {
55
+ const value = token.replace(TRIM_SEPARATORS, "");
56
+ if (PALETTE_FREE_KEYWORDS.has(value.toLowerCase())) return true;
57
+ const varRef = VAR_REF.exec(value);
58
+ if (varRef) {
59
+ const [name, ...fallback] = splitTopLevel(varRef[1] ?? "", (char) => char === ",");
60
+ if (name === void 0 || !CUSTOM_PROPERTY_NAME.test(name.replace(TRIM_SEPARATORS, ""))) return false;
61
+ return fallback.length === 0 || isThemeDerivedColor(fallback.join(","));
62
+ }
63
+ const nested = NESTED_COLOR_MIX.exec(value);
64
+ return nested !== null && isThemeDerivedColorMix(nested[1] ?? "");
65
+ }
66
+ function isThemeDerivedColorMix(args) {
67
+ const [interpolation, ...colors] = splitTopLevel(args, (char) => char === ",");
68
+ if (interpolation === void 0 || !/^in[\s_]/.test(interpolation.replace(TRIM_SEPARATORS, ""))) return false;
69
+ if (colors.length === 0) return false;
70
+ return colors.every((color) => {
71
+ const [colorToken, ...rest] = splitTopLevel(color, (char) => SEPARATOR.test(char)).filter((token) => !PERCENTAGE.test(token));
72
+ return colorToken !== void 0 && rest.length === 0 && isThemeDerivedColor(colorToken);
73
+ });
74
+ }
4
75
  const COLOR_UTILITY_PREFIXES = [
5
76
  "bg",
6
77
  "text",
@@ -67,18 +138,24 @@ function isWithinStyleAttribute(node) {
67
138
  const noHex = {
68
139
  meta: {
69
140
  type: "problem",
70
- docs: { description: "Ban hard-coded colour literals in section code" },
141
+ docs: {
142
+ description: "Ban hard-coded colour literals in section code",
143
+ url: docsPathForRule("no-hex")
144
+ },
71
145
  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." }
146
+ messages: {
147
+ 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 one of your theme's colour utilities; if the value is genuinely brand decoration with no token yet, add it to `colors` in the template's `theme.ts` and use the `bg-<name>` / `text-<name>` utility it compiles to.",
148
+ hardCodedColorFunction: "`{{value}}` bakes a colour into a section — a section carries no palette of its own, or it breaks the first time another template reuses it. Fix: derive the colour from the theme instead. A `color-mix()` over theme vars is allowed (e.g. `color-mix(in srgb, var(--tr-color-primary) 80%, var(--tr-color-surface))`, or mix toward `transparent` for a translucent tint) — only a hard-coded argument makes it a violation."
149
+ }
73
150
  },
74
151
  create(context) {
75
152
  function check(node, text) {
76
- const fn = COLOR_FN.exec(text);
77
- if (fn) {
153
+ for (const call of findColorFunctionCalls(text)) {
154
+ if (call.name === "color-mix" && isThemeDerivedColorMix(call.args)) continue;
78
155
  context.report({
79
156
  node,
80
- messageId: "hardCodedColor",
81
- data: { value: fn[0] }
157
+ messageId: "hardCodedColorFunction",
158
+ data: { value: call.raw }
82
159
  });
83
160
  return;
84
161
  }
@@ -1,10 +1,14 @@
1
+ import { docsPathForRule } from "../../rules/registry.js";
1
2
  import { hasClientDirective } from "../is-client-file.js";
2
3
 
3
4
  //#region src/eslint/rules/no-inline-style.ts
4
5
  const noInlineStyle = {
5
6
  meta: {
6
7
  type: "problem",
7
- docs: { description: "Ban the inline style prop in server-rendered section JSX" },
8
+ docs: {
9
+ description: "Ban the inline style prop in server-rendered section JSX",
10
+ url: docsPathForRule("no-inline-style")
11
+ },
8
12
  schema: [],
9
13
  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
14
  },
@@ -1,3 +1,4 @@
1
+ import { docsPathForRule } from "../../rules/registry.js";
1
2
  import { hasClientDirective } from "../is-client-file.js";
2
3
 
3
4
  //#region src/eslint/rules/no-nondeterminism.ts
@@ -10,7 +11,10 @@ const BANNED_MEMBERS = {
10
11
  const noNondeterminism = {
11
12
  meta: {
12
13
  type: "problem",
13
- docs: { description: "Ban clock and randomness reads in server-rendered section code" },
14
+ docs: {
15
+ description: "Ban clock and randomness reads in server-rendered section code",
16
+ url: docsPathForRule("no-nondeterminism")
17
+ },
14
18
  schema: [],
15
19
  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
20
  },
@@ -1,3 +1,5 @@
1
+ import { docsPathForRule } from "../../rules/registry.js";
2
+
1
3
  //#region src/eslint/rules/no-raw-element.ts
2
4
  const FORBIDDEN = {
3
5
  section: "<Section>",
@@ -6,7 +8,10 @@ const FORBIDDEN = {
6
8
  const noRawElement = {
7
9
  meta: {
8
10
  type: "problem",
9
- docs: { description: "Ban raw <section> and <img> in section JSX" },
11
+ docs: {
12
+ description: "Ban raw <section> and <img> in section JSX",
13
+ url: docsPathForRule("no-raw-element")
14
+ },
10
15
  schema: [],
11
16
  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
17
  },
@@ -1,8 +1,13 @@
1
+ import { docsPathForRule } from "../../rules/registry.js";
2
+
1
3
  //#region src/eslint/rules/props-from-schema.ts
2
4
  const propsFromSchema = {
3
5
  meta: {
4
6
  type: "problem",
5
- docs: { description: "A Renderer must import its Props type from ./schema" },
7
+ docs: {
8
+ description: "A Renderer must import its Props type from ./schema",
9
+ url: docsPathForRule("props-from-schema")
10
+ },
6
11
  schema: [],
7
12
  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
13
  },
@@ -1,3 +1,4 @@
1
+ import { docsPathForRule } from "../../rules/registry.js";
1
2
  import { isClientFile, resolveImportedFile } from "../is-client-file.js";
2
3
 
3
4
  //#region src/eslint/rules/serializable-island-props.ts
@@ -56,7 +57,10 @@ function inspect(node, path, out) {
56
57
  const serializableIslandProps = {
57
58
  meta: {
58
59
  type: "problem",
59
- docs: { description: "Island props must be JSON-serializable" },
60
+ docs: {
61
+ description: "Island props must be JSON-serializable",
62
+ url: docsPathForRule("serializable-island-props")
63
+ },
60
64
  schema: [],
61
65
  messages: {
62
66
  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.",
@@ -1,3 +1,5 @@
1
+ import { docsPathForRule } from "../../rules/registry.js";
2
+
1
3
  //#region src/eslint/rules/slot-marker-literal.ts
2
4
  const KEY_ATTRS = /* @__PURE__ */ new Set([
3
5
  "data-slot-id",
@@ -8,7 +10,10 @@ const KEY_COMPONENTS = /* @__PURE__ */ new Set(["Slot", "SlotGroup"]);
8
10
  const slotMarkerLiteral = {
9
11
  meta: {
10
12
  type: "problem",
11
- docs: { description: "Slot markers must carry a string-literal key" },
13
+ docs: {
14
+ description: "Slot markers must carry a string-literal key",
15
+ url: docsPathForRule("slot-marker-literal")
16
+ },
12
17
  schema: [],
13
18
  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
19
  },
package/dist/eslint.js CHANGED
@@ -55,7 +55,7 @@ const templateKitConfig = [
55
55
  "template-kit/no-raw-element": "error",
56
56
  "template-kit/image-bare-needs-reason": "error",
57
57
  "template-kit/slot-marker-literal": "error",
58
- "template-kit/no-hex": "error",
58
+ "template-kit/no-hex": "warn",
59
59
  "template-kit/no-css-import-from-render-path": "error"
60
60
  }
61
61
  },
package/dist/index.d.ts CHANGED
@@ -24,7 +24,7 @@ import { SlotGroup } from "./primitives/SlotGroup.js";
24
24
  import { SlotItem } from "./primitives/SlotItem.js";
25
25
  import "./primitives/index.js";
26
26
  import { ATTR_SECTION_INSTANCE_ID, ATTR_SLOT_GROUP, ATTR_SLOT_ID, ATTR_SLOT_ITEM, ATTR_SLOT_TEXT_LEAF, ATTR_TR_ENHANCE } from "./contracts/markers.js";
27
- import { COLOR_ROLE_VARS, ColorRole, FONT_ROLE_VARS, LAYOUT_VARS, RADIUS_VARS, SHADOW_VARS, TYPE_STEP_VARS, TokenTheme, TokenThemeSchema, compileThemeToCss } from "./design-system/theme.js";
27
+ import { TokenNameSchema, TokenTheme, TokenThemeSchema, compileThemeToCss } from "./design-system/theme.js";
28
28
  //#region src/index.d.ts
29
29
  /**
30
30
  * The published version of this kit.
@@ -37,4 +37,4 @@ import { COLOR_ROLE_VARS, ColorRole, FONT_ROLE_VARS, LAYOUT_VARS, RADIUS_VARS, S
37
37
  */
38
38
  declare const KIT_VERSION: string;
39
39
  //#endregion
40
- export { ATTR_ISLAND_ID, ATTR_ISLAND_KEY, ATTR_ISLAND_PROPS, ATTR_SECTION_INSTANCE_ID, ATTR_SLOT_GROUP, ATTR_SLOT_ID, ATTR_SLOT_ITEM, ATTR_SLOT_TEXT_LEAF, ATTR_TR_ENHANCE, type AddressParts, COLLECTIONS, COLOR_ROLE_VARS, CONTENT_SLOT_TYPES, type CollectionField, type CollectionIdKey, type CollectionOwnership, type ColorRole, ContentSlot, type ContentSlotType, type Coordinates, DECISION_TYPES, DERIVED_INPUT_FIELDS, Decision, type DecisionType, type DerivedInputName, FONT_ROLE_VARS, FORMATTER_NAMES, FillSpec, type FillSpecShape, type FixtureModule, FixtureModuleShape, type FixtureOverride, type FloorplanAssignment, type FormatterName, type FrameConfig, ISLAND_ELEMENT, Image, type ImageCollectionValue, type ImageValue, IslandComponent, IslandEditorOptions, IslandMarker, type IslandMarkerProps, IslandModule, IslandProps, IslandPropsError, KIT_VERSION, LAYOUT_VARS, OptionDef, type OptionValue, type PicturePlan, type PictureSourceSpec, type PoiRow, type PropertyFacts, RADIUS_VARS, REFERRAL_FIELDS, REFERRAL_IMAGE_FIELDS, REF_SLOT_TYPES, type RefSlotType, type ReferralField, type ReferralImageField, type ResolvedFixture, type ResponsiveImage, type ResponsiveSource, SHADOW_VARS, SLOT_TYPES, SLOT_TYPE_DESCRIPTORS, SOURCE_FIELDS, Section, SectionMeta, type SectionMetaShape, type SectionNav, type SectionProps, SectionSchema, type SectionSchemaResolved, type SectionSchemaShape, ShieldMode, Slot, SlotDef, type Slot$1 as SlotDefinition, SlotGroup, SlotGroupDef, type SlotGroupDefShape, SlotItem, type SlotType, type SlotValue, type SourceFieldContainer, type SourceFieldDescriptor, type SourceFieldName, type SourceFieldValueType, TRANSFORMS, TRANSFORM_NAMES, TYPE_STEP_VARS, TemplateManifest, type TemplateManifestShape, type TokenTheme, TokenThemeSchema, type TransformDescriptor, type TransformFn, type TransformInput, type TransformName, type TransformUnit, type UnitRow, VERIFICATION_WIDTHS, type VariantSelectorFn, type VariantsMeta, type VerificationWidth, type WritebackTarget, buildFixtureSet, buildInvariantFixtures, collectionForSlot, compileThemeToCss, defaultForSlotType, derived, deserializeIslandProps, direct, islandId, makeResolveVariants, serializeIslandProps, slotDefTypes };
40
+ export { ATTR_ISLAND_ID, ATTR_ISLAND_KEY, ATTR_ISLAND_PROPS, ATTR_SECTION_INSTANCE_ID, ATTR_SLOT_GROUP, ATTR_SLOT_ID, ATTR_SLOT_ITEM, ATTR_SLOT_TEXT_LEAF, ATTR_TR_ENHANCE, type AddressParts, COLLECTIONS, CONTENT_SLOT_TYPES, type CollectionField, type CollectionIdKey, type CollectionOwnership, ContentSlot, type ContentSlotType, type Coordinates, DECISION_TYPES, DERIVED_INPUT_FIELDS, Decision, type DecisionType, type DerivedInputName, FORMATTER_NAMES, FillSpec, type FillSpecShape, type FixtureModule, FixtureModuleShape, type FixtureOverride, type FloorplanAssignment, type FormatterName, type FrameConfig, ISLAND_ELEMENT, Image, type ImageCollectionValue, type ImageValue, IslandComponent, IslandEditorOptions, IslandMarker, type IslandMarkerProps, IslandModule, IslandProps, IslandPropsError, KIT_VERSION, OptionDef, type OptionValue, type PicturePlan, type PictureSourceSpec, type PoiRow, type PropertyFacts, REFERRAL_FIELDS, REFERRAL_IMAGE_FIELDS, REF_SLOT_TYPES, type RefSlotType, type ReferralField, type ReferralImageField, type ResolvedFixture, type ResponsiveImage, type ResponsiveSource, SLOT_TYPES, SLOT_TYPE_DESCRIPTORS, SOURCE_FIELDS, Section, SectionMeta, type SectionMetaShape, type SectionNav, type SectionProps, SectionSchema, type SectionSchemaResolved, type SectionSchemaShape, ShieldMode, Slot, SlotDef, type Slot$1 as SlotDefinition, SlotGroup, SlotGroupDef, type SlotGroupDefShape, SlotItem, type SlotType, type SlotValue, type SourceFieldContainer, type SourceFieldDescriptor, type SourceFieldName, type SourceFieldValueType, TRANSFORMS, TRANSFORM_NAMES, TemplateManifest, type TemplateManifestShape, TokenNameSchema, type TokenTheme, TokenThemeSchema, type TransformDescriptor, type TransformFn, type TransformInput, type TransformName, type TransformUnit, type UnitRow, VERIFICATION_WIDTHS, type VariantSelectorFn, type VariantsMeta, type VerificationWidth, type WritebackTarget, buildFixtureSet, buildInvariantFixtures, collectionForSlot, compileThemeToCss, defaultForSlotType, derived, deserializeIslandProps, direct, islandId, makeResolveVariants, serializeIslandProps, slotDefTypes };
package/dist/index.js CHANGED
@@ -17,7 +17,7 @@ import { Section } from "./primitives/Section.js";
17
17
  import { Slot } from "./primitives/Slot.js";
18
18
  import { SlotGroup } from "./primitives/SlotGroup.js";
19
19
  import { SlotItem } from "./primitives/SlotItem.js";
20
- import { COLOR_ROLE_VARS, FONT_ROLE_VARS, LAYOUT_VARS, RADIUS_VARS, SHADOW_VARS, TYPE_STEP_VARS, TokenThemeSchema, compileThemeToCss } from "./design-system/theme.js";
20
+ import { TokenNameSchema, TokenThemeSchema, compileThemeToCss } from "./design-system/theme.js";
21
21
 
22
22
  //#region src/index.ts
23
23
  /**
@@ -32,4 +32,4 @@ import { COLOR_ROLE_VARS, FONT_ROLE_VARS, LAYOUT_VARS, RADIUS_VARS, SHADOW_VARS,
32
32
  const KIT_VERSION = version;
33
33
 
34
34
  //#endregion
35
- export { ATTR_ISLAND_ID, ATTR_ISLAND_KEY, ATTR_ISLAND_PROPS, ATTR_SECTION_INSTANCE_ID, ATTR_SLOT_GROUP, ATTR_SLOT_ID, ATTR_SLOT_ITEM, ATTR_SLOT_TEXT_LEAF, ATTR_TR_ENHANCE, COLLECTIONS, COLOR_ROLE_VARS, CONTENT_SLOT_TYPES, ContentSlot, DECISION_TYPES, DERIVED_INPUT_FIELDS, Decision, FONT_ROLE_VARS, FORMATTER_NAMES, FillSpec, FixtureModuleShape, ISLAND_ELEMENT, Image, IslandMarker, IslandPropsError, KIT_VERSION, LAYOUT_VARS, OptionDef, RADIUS_VARS, REFERRAL_FIELDS, REFERRAL_IMAGE_FIELDS, REF_SLOT_TYPES, SHADOW_VARS, SLOT_TYPES, SLOT_TYPE_DESCRIPTORS, SOURCE_FIELDS, Section, SectionMeta, SectionSchema, Slot, SlotDef, SlotGroup, SlotGroupDef, SlotItem, TRANSFORMS, TRANSFORM_NAMES, TYPE_STEP_VARS, TemplateManifest, TokenThemeSchema, VERIFICATION_WIDTHS, buildFixtureSet, buildInvariantFixtures, collectionForSlot, compileThemeToCss, defaultForSlotType, derived, deserializeIslandProps, direct, islandId, makeResolveVariants, serializeIslandProps, slotDefTypes };
35
+ export { ATTR_ISLAND_ID, ATTR_ISLAND_KEY, ATTR_ISLAND_PROPS, ATTR_SECTION_INSTANCE_ID, ATTR_SLOT_GROUP, ATTR_SLOT_ID, ATTR_SLOT_ITEM, ATTR_SLOT_TEXT_LEAF, ATTR_TR_ENHANCE, COLLECTIONS, CONTENT_SLOT_TYPES, ContentSlot, DECISION_TYPES, DERIVED_INPUT_FIELDS, Decision, FORMATTER_NAMES, FillSpec, FixtureModuleShape, ISLAND_ELEMENT, Image, IslandMarker, IslandPropsError, KIT_VERSION, OptionDef, REFERRAL_FIELDS, REFERRAL_IMAGE_FIELDS, REF_SLOT_TYPES, SLOT_TYPES, SLOT_TYPE_DESCRIPTORS, SOURCE_FIELDS, Section, SectionMeta, SectionSchema, Slot, SlotDef, SlotGroup, SlotGroupDef, SlotItem, TRANSFORMS, TRANSFORM_NAMES, TemplateManifest, TokenNameSchema, TokenThemeSchema, VERIFICATION_WIDTHS, buildFixtureSet, buildInvariantFixtures, collectionForSlot, compileThemeToCss, defaultForSlotType, derived, deserializeIslandProps, direct, islandId, makeResolveVariants, serializeIslandProps, slotDefTypes };
package/dist/package.js CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
- var version = "0.1.0";
2
+ var version = "0.3.0";
3
3
 
4
4
  //#endregion
5
5
  export { version };
@@ -64,7 +64,7 @@ function Image({ src, alt, aspectRatio, slotId, bare, srcMobile, mobileBreakpoin
64
64
  ...responsiveImgAttrs
65
65
  }));
66
66
  }
67
- const containerCls = ["tr-image-frame relative block w-full overflow-hidden bg-surface-alt", className].filter(Boolean).join(" ");
67
+ const containerCls = ["tr-image-frame relative block w-full overflow-hidden", className].filter(Boolean).join(" ");
68
68
  const containerStyle = aspectRatio ? { aspectRatio } : void 0;
69
69
  const slotAttrs = slotId ? { [ATTR_SLOT_ID]: slotId } : {};
70
70
  if (!src) return /* @__PURE__ */ jsx("div", {
@@ -0,0 +1,16 @@
1
+ //#region src/rules/registry.ts
2
+ /**
3
+ * `no-hex` → `docs/rules/no-hex.md`.
4
+ *
5
+ * Package-relative on purpose: the caller decides what to resolve it against. A
6
+ * printer prefixes the resolved package root so an author sees a path they can open
7
+ * with no network (`node_modules/@homepages/template-kit/docs/rules/no-hex.md`);
8
+ * ESLint's `meta.docs.url` uses it as-is. This module never touches the filesystem —
9
+ * the ESLint preset imports it.
10
+ */
11
+ function docsPathForRule(id) {
12
+ return `docs/rules/${id}.md`;
13
+ }
14
+
15
+ //#endregion
16
+ export { docsPathForRule };