@homepages/template-kit 0.0.0 → 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 +71 -14
  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 +37 -13
  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-CrdHXijV.js +0 -23
@@ -0,0 +1,14 @@
1
+ //#region src/design-system/breakpoints.ts
2
+ const VERIFICATION_WIDTHS = [
3
+ 350,
4
+ 390,
5
+ 425,
6
+ 512,
7
+ 768,
8
+ 1024,
9
+ 1440,
10
+ 1920
11
+ ];
12
+
13
+ //#endregion
14
+ export { VERIFICATION_WIDTHS };
@@ -0,0 +1,307 @@
1
+ import { z } from "zod";
2
+ //#region src/design-system/theme.d.ts
3
+ declare const COLOR_ROLE_VARS: {
4
+ readonly ink: "--tr-color-ink";
5
+ readonly inkSoft: "--tr-color-ink-soft";
6
+ readonly inkMute: "--tr-color-ink-mute";
7
+ readonly background: "--tr-color-background";
8
+ readonly surface: "--tr-color-surface";
9
+ readonly surfaceAlt: "--tr-color-surface-alt";
10
+ readonly border: "--tr-color-border";
11
+ readonly borderStrong: "--tr-color-border-strong";
12
+ readonly primary: "--tr-color-primary";
13
+ readonly primaryFg: "--tr-color-primary-fg";
14
+ readonly primaryStrong: "--tr-color-primary-darker";
15
+ readonly accent: "--tr-color-accent";
16
+ readonly accentFg: "--tr-color-accent-contrast";
17
+ readonly accentStrong: "--tr-color-accent-strong";
18
+ readonly overlay: "--tr-color-overlay";
19
+ readonly overlayStrong: "--tr-color-overlay-strong";
20
+ readonly overlayLight: "--tr-color-overlay-light";
21
+ readonly darkBg: "--tr-color-footer-bg";
22
+ readonly darkFg: "--tr-color-footer-fg";
23
+ readonly darkFgMute: "--tr-color-footer-fg-mute";
24
+ readonly darkBorder: "--tr-color-footer-divider";
25
+ };
26
+ type ColorRole = keyof typeof COLOR_ROLE_VARS;
27
+ declare const FONT_ROLE_VARS: {
28
+ readonly body: "--tr-font-sans";
29
+ readonly heading: "--tr-font-serif";
30
+ readonly numeric: "--tr-font-num";
31
+ readonly label: "--tr-font-label";
32
+ };
33
+ declare const TYPE_STEP_VARS: {
34
+ readonly xs: "--tr-text-xs";
35
+ readonly sm: "--tr-text-sm";
36
+ readonly base: "--tr-text-base";
37
+ readonly lg: "--tr-text-lg";
38
+ readonly xl: "--tr-text-xl";
39
+ readonly "2xl": "--tr-text-2xl";
40
+ readonly "3xl": "--tr-text-3xl";
41
+ readonly "4xl": "--tr-text-4xl";
42
+ readonly "5xl": "--tr-text-5xl";
43
+ readonly display: "--tr-text-display";
44
+ };
45
+ declare const RADIUS_VARS: {
46
+ readonly sm: "--tr-radius-sm";
47
+ readonly md: "--tr-radius-md";
48
+ readonly lg: "--tr-radius-lg";
49
+ readonly full: "--tr-radius-full";
50
+ };
51
+ declare const SHADOW_VARS: {
52
+ readonly sm: "--tr-shadow-sm";
53
+ readonly md: "--tr-shadow-md";
54
+ readonly lg: "--tr-shadow-lg";
55
+ };
56
+ declare const LAYOUT_VARS: {
57
+ readonly containerMax: "--tr-container-max";
58
+ readonly containerPad: "--tr-container-pad";
59
+ readonly headerHeight: "--tr-header-height";
60
+ };
61
+ declare const TokenThemeSchema: z.ZodObject<{
62
+ colors: z.ZodObject<Record<"ink" | "inkSoft" | "inkMute" | "background" | "surface" | "surfaceAlt" | "border" | "borderStrong" | "primary" | "primaryFg" | "primaryStrong" | "accent" | "accentFg" | "accentStrong" | "overlay" | "overlayStrong" | "overlayLight" | "darkBg" | "darkFg" | "darkFgMute" | "darkBorder", z.ZodString>, "strip", z.ZodTypeAny, {
63
+ ink: string;
64
+ inkSoft: string;
65
+ inkMute: string;
66
+ background: string;
67
+ surface: string;
68
+ surfaceAlt: string;
69
+ border: string;
70
+ borderStrong: string;
71
+ primary: string;
72
+ primaryFg: string;
73
+ primaryStrong: string;
74
+ accent: string;
75
+ accentFg: string;
76
+ accentStrong: string;
77
+ overlay: string;
78
+ overlayStrong: string;
79
+ overlayLight: string;
80
+ darkBg: string;
81
+ darkFg: string;
82
+ darkFgMute: string;
83
+ darkBorder: string;
84
+ }, {
85
+ ink: string;
86
+ inkSoft: string;
87
+ inkMute: string;
88
+ background: string;
89
+ surface: string;
90
+ surfaceAlt: string;
91
+ border: string;
92
+ borderStrong: string;
93
+ primary: string;
94
+ primaryFg: string;
95
+ primaryStrong: string;
96
+ accent: string;
97
+ accentFg: string;
98
+ accentStrong: string;
99
+ overlay: string;
100
+ overlayStrong: string;
101
+ overlayLight: string;
102
+ darkBg: string;
103
+ darkFg: string;
104
+ darkFgMute: string;
105
+ darkBorder: string;
106
+ }>;
107
+ /** Optional open brand palette → `--tr-color-<name>` (the escape hatch). */
108
+ palette: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
109
+ fonts: z.ZodObject<Record<"body" | "heading" | "numeric" | "label", z.ZodString> & {
110
+ /** Font-loading block (@import / @font-face) emitted before :root. */
111
+ fontFaces: z.ZodOptional<z.ZodString>;
112
+ }, "strip", z.ZodTypeAny, {
113
+ body: string;
114
+ heading: string;
115
+ numeric: string;
116
+ label: string;
117
+ fontFaces?: string | undefined;
118
+ }, {
119
+ body: string;
120
+ heading: string;
121
+ numeric: string;
122
+ label: string;
123
+ fontFaces?: string | undefined;
124
+ }>;
125
+ type: z.ZodObject<Record<"xs" | "sm" | "base" | "lg" | "xl" | "2xl" | "3xl" | "4xl" | "5xl" | "display", z.ZodString>, "strip", z.ZodTypeAny, {
126
+ xs: string;
127
+ sm: string;
128
+ base: string;
129
+ lg: string;
130
+ xl: string;
131
+ "2xl": string;
132
+ "3xl": string;
133
+ "4xl": string;
134
+ "5xl": string;
135
+ display: string;
136
+ }, {
137
+ xs: string;
138
+ sm: string;
139
+ base: string;
140
+ lg: string;
141
+ xl: string;
142
+ "2xl": string;
143
+ "3xl": string;
144
+ "4xl": string;
145
+ "5xl": string;
146
+ display: string;
147
+ }>;
148
+ radii: z.ZodObject<Record<"sm" | "lg" | "md" | "full", z.ZodString>, "strip", z.ZodTypeAny, {
149
+ sm: string;
150
+ lg: string;
151
+ md: string;
152
+ full: string;
153
+ }, {
154
+ sm: string;
155
+ lg: string;
156
+ md: string;
157
+ full: string;
158
+ }>;
159
+ shadows: z.ZodObject<Record<"sm" | "lg" | "md", z.ZodString>, "strip", z.ZodTypeAny, {
160
+ sm: string;
161
+ lg: string;
162
+ md: string;
163
+ }, {
164
+ sm: string;
165
+ lg: string;
166
+ md: string;
167
+ }>;
168
+ layout: z.ZodObject<Record<"containerMax" | "containerPad" | "headerHeight", z.ZodString>, "strip", z.ZodTypeAny, {
169
+ containerMax: string;
170
+ containerPad: string;
171
+ headerHeight: string;
172
+ }, {
173
+ containerMax: string;
174
+ containerPad: string;
175
+ headerHeight: string;
176
+ }>;
177
+ }, "strict", z.ZodTypeAny, {
178
+ type: {
179
+ xs: string;
180
+ sm: string;
181
+ base: string;
182
+ lg: string;
183
+ xl: string;
184
+ "2xl": string;
185
+ "3xl": string;
186
+ "4xl": string;
187
+ "5xl": string;
188
+ display: string;
189
+ };
190
+ colors: {
191
+ ink: string;
192
+ inkSoft: string;
193
+ inkMute: string;
194
+ background: string;
195
+ surface: string;
196
+ surfaceAlt: string;
197
+ border: string;
198
+ borderStrong: string;
199
+ primary: string;
200
+ primaryFg: string;
201
+ primaryStrong: string;
202
+ accent: string;
203
+ accentFg: string;
204
+ accentStrong: string;
205
+ overlay: string;
206
+ overlayStrong: string;
207
+ overlayLight: string;
208
+ darkBg: string;
209
+ darkFg: string;
210
+ darkFgMute: string;
211
+ darkBorder: string;
212
+ };
213
+ fonts: {
214
+ body: string;
215
+ heading: string;
216
+ numeric: string;
217
+ label: string;
218
+ fontFaces?: string | undefined;
219
+ };
220
+ radii: {
221
+ sm: string;
222
+ lg: string;
223
+ md: string;
224
+ full: string;
225
+ };
226
+ shadows: {
227
+ sm: string;
228
+ lg: string;
229
+ md: string;
230
+ };
231
+ layout: {
232
+ containerMax: string;
233
+ containerPad: string;
234
+ headerHeight: string;
235
+ };
236
+ palette?: Record<string, string> | undefined;
237
+ }, {
238
+ type: {
239
+ xs: string;
240
+ sm: string;
241
+ base: string;
242
+ lg: string;
243
+ xl: string;
244
+ "2xl": string;
245
+ "3xl": string;
246
+ "4xl": string;
247
+ "5xl": string;
248
+ display: string;
249
+ };
250
+ colors: {
251
+ ink: string;
252
+ inkSoft: string;
253
+ inkMute: string;
254
+ background: string;
255
+ surface: string;
256
+ surfaceAlt: string;
257
+ border: string;
258
+ borderStrong: string;
259
+ primary: string;
260
+ primaryFg: string;
261
+ primaryStrong: string;
262
+ accent: string;
263
+ accentFg: string;
264
+ accentStrong: string;
265
+ overlay: string;
266
+ overlayStrong: string;
267
+ overlayLight: string;
268
+ darkBg: string;
269
+ darkFg: string;
270
+ darkFgMute: string;
271
+ darkBorder: string;
272
+ };
273
+ fonts: {
274
+ body: string;
275
+ heading: string;
276
+ numeric: string;
277
+ label: string;
278
+ fontFaces?: string | undefined;
279
+ };
280
+ radii: {
281
+ sm: string;
282
+ lg: string;
283
+ md: string;
284
+ full: string;
285
+ };
286
+ shadows: {
287
+ sm: string;
288
+ lg: string;
289
+ md: string;
290
+ };
291
+ layout: {
292
+ containerMax: string;
293
+ containerPad: string;
294
+ headerHeight: string;
295
+ };
296
+ palette?: Record<string, string> | undefined;
297
+ }>;
298
+ type TokenTheme = z.infer<typeof TokenThemeSchema>;
299
+ /**
300
+ * Compile a `TokenTheme` into the CSS a template's bundle ships: an optional
301
+ * font-loading block followed by a single `:root { --tr-*: … }` declaration.
302
+ * Deterministic (stable key order; palette keys sorted) so the same theme
303
+ * always content-hashes identically.
304
+ */
305
+ declare function compileThemeToCss(theme: TokenTheme): string;
306
+ //#endregion
307
+ export { COLOR_ROLE_VARS, ColorRole, FONT_ROLE_VARS, LAYOUT_VARS, RADIUS_VARS, SHADOW_VARS, TYPE_STEP_VARS, TokenTheme, TokenThemeSchema, compileThemeToCss };
@@ -0,0 +1,95 @@
1
+ import { z } from "zod";
2
+
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"
26
+ };
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))
74
+ }).strict();
75
+ /**
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.
80
+ */
81
+ function compileThemeToCss(theme) {
82
+ 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`;
92
+ }
93
+
94
+ //#endregion
95
+ export { COLOR_ROLE_VARS, FONT_ROLE_VARS, LAYOUT_VARS, RADIUS_VARS, SHADOW_VARS, TYPE_STEP_VARS, TokenThemeSchema, compileThemeToCss };
@@ -0,0 +1,79 @@
1
+ import { readFileSync, statSync } from "node:fs";
2
+ import { dirname, resolve } from "node:path";
3
+
4
+ //#region src/eslint/is-client-file.ts
5
+ function hasClientDirective(text) {
6
+ let i = 0;
7
+ while (i < text.length) {
8
+ const rest = text.slice(i);
9
+ const ws = /^\s+/.exec(rest);
10
+ if (ws) {
11
+ i += ws[0].length;
12
+ continue;
13
+ }
14
+ const line = /^\/\/[^\n]*/.exec(rest);
15
+ if (line) {
16
+ i += line[0].length;
17
+ continue;
18
+ }
19
+ const block = /^\/\*[\s\S]*?\*\//.exec(rest);
20
+ if (block) {
21
+ i += block[0].length;
22
+ continue;
23
+ }
24
+ const directive = /^(["'])([^"']*)\1\s*;?/.exec(rest);
25
+ if (!directive) return false;
26
+ if (directive[2] === "use client") return true;
27
+ i += directive[0].length;
28
+ }
29
+ return false;
30
+ }
31
+ const cache = /* @__PURE__ */ new Map();
32
+ function isClientFile(filename) {
33
+ const absolute = resolve(filename);
34
+ let mtimeMs;
35
+ try {
36
+ mtimeMs = statSync(absolute).mtimeMs;
37
+ } catch {
38
+ return false;
39
+ }
40
+ const key = `${absolute}:${mtimeMs}`;
41
+ const hit = cache.get(key);
42
+ if (hit !== void 0) return hit;
43
+ let answer = false;
44
+ try {
45
+ answer = hasClientDirective(readFileSync(absolute, "utf8"));
46
+ } catch {
47
+ answer = false;
48
+ }
49
+ cache.set(key, answer);
50
+ return answer;
51
+ }
52
+ const SOURCE_EXTS = [
53
+ ".tsx",
54
+ ".ts",
55
+ ".jsx",
56
+ ".js"
57
+ ];
58
+ function isFile(path) {
59
+ try {
60
+ return statSync(path).isFile();
61
+ } catch {
62
+ return false;
63
+ }
64
+ }
65
+ function resolveImportedFile(specifier, fromFile) {
66
+ if (!specifier.startsWith(".")) return null;
67
+ const base = resolve(dirname(fromFile), specifier);
68
+ const candidates = [];
69
+ const jsExt = /\.(js|jsx)$/.exec(base);
70
+ if (jsExt) {
71
+ const stem = base.slice(0, -jsExt[0].length);
72
+ candidates.push(...SOURCE_EXTS.filter((ext) => ext !== ".js").map((ext) => stem + ext));
73
+ }
74
+ candidates.push(base, ...SOURCE_EXTS.map((ext) => base + ext));
75
+ return candidates.find(isFile) ?? null;
76
+ }
77
+
78
+ //#endregion
79
+ export { hasClientDirective, isClientFile, resolveImportedFile };
@@ -0,0 +1,38 @@
1
+ //#region src/eslint/rules/image-bare-needs-reason.ts
2
+ const imageBareNeedsReason = {
3
+ meta: {
4
+ type: "problem",
5
+ docs: { description: "<Image bare> requires a `// bare:` reason comment" },
6
+ schema: [],
7
+ 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
+ },
9
+ create(context) {
10
+ const comments = context.sourceCode.getAllComments();
11
+ const LOOKBACK_LINES = 5;
12
+ let previousBareElementEnd = -1;
13
+ return { JSXOpeningElement(node) {
14
+ if (node.name.type !== "JSXIdentifier" || node.name.name !== "Image") return;
15
+ if (!node.attributes.some((a) => a.type === "JSXAttribute" && a.name?.name === "bare")) return;
16
+ const startLine = node.loc?.start.line;
17
+ const watermark = previousBareElementEnd;
18
+ const hasReason = startLine != null && comments.some((c) => {
19
+ if (!/\bbare:/.test(c.value)) return false;
20
+ const endLine = c.loc?.end.line;
21
+ if (endLine == null) return false;
22
+ const linesAbove = startLine - endLine;
23
+ if (linesAbove <= 0 || linesAbove > LOOKBACK_LINES) return false;
24
+ const commentStart = c.range?.[0];
25
+ return commentStart != null && commentStart > watermark;
26
+ });
27
+ if (node.range) previousBareElementEnd = node.range[1];
28
+ if (hasReason) return;
29
+ context.report({
30
+ node,
31
+ messageId: "bareNeedsReason"
32
+ });
33
+ } };
34
+ }
35
+ };
36
+
37
+ //#endregion
38
+ export { imageBareNeedsReason };
@@ -0,0 +1,26 @@
1
+ //#region src/eslint/rules/no-client-directive-in-contract.ts
2
+ const noClientDirectiveInContract = {
3
+ meta: {
4
+ type: "problem",
5
+ docs: { description: "Ban the \"use client\" directive in a section's four contract files" },
6
+ schema: [],
7
+ 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
+ },
9
+ create(context) {
10
+ return { Program(node) {
11
+ for (const stmt of node.body) {
12
+ if (stmt.type !== "ExpressionStatement" || stmt.expression.type !== "Literal" || typeof stmt.expression.value !== "string") return;
13
+ if (stmt.expression.value === "use client") {
14
+ context.report({
15
+ node: stmt,
16
+ messageId: "clientContractFile"
17
+ });
18
+ return;
19
+ }
20
+ }
21
+ } };
22
+ }
23
+ };
24
+
25
+ //#endregion
26
+ export { noClientDirectiveInContract };
@@ -0,0 +1,99 @@
1
+ import { hasClientDirective, isClientFile, resolveImportedFile } from "../is-client-file.js";
2
+
3
+ //#region src/eslint/rules/no-client-runtime-in-server.ts
4
+ const BANNED_HOOKS = /* @__PURE__ */ new Set([
5
+ "useEffect",
6
+ "useLayoutEffect",
7
+ "useState",
8
+ "useReducer",
9
+ "useRef",
10
+ "useSyncExternalStore",
11
+ "useImperativeHandle"
12
+ ]);
13
+ const BANNED_GLOBALS = /* @__PURE__ */ new Set([
14
+ "fetch",
15
+ "XMLHttpRequest",
16
+ "window",
17
+ "document",
18
+ "localStorage",
19
+ "sessionStorage",
20
+ "navigator"
21
+ ]);
22
+ const noClientRuntimeInServer = {
23
+ meta: {
24
+ type: "problem",
25
+ docs: { description: "Ban effects, state, handlers, network, and browser globals in server-rendered section code" },
26
+ schema: [],
27
+ messages: {
28
+ 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.",
29
+ clientHandler: "`{{what}}` is an event handler, which cannot cross the server→browser boundary — it is dropped from the published HTML. Fix: move the interactive markup into a `\"use client\"` component in the section folder and render it from the Renderer."
30
+ }
31
+ },
32
+ create(context) {
33
+ if (hasClientDirective(context.sourceCode.getText())) return {};
34
+ const sourceCode = context.sourceCode;
35
+ const filename = context.filename;
36
+ const islandNames = /* @__PURE__ */ new Map();
37
+ function isIsland(name) {
38
+ return islandNames.get(name) ?? false;
39
+ }
40
+ return {
41
+ ImportDeclaration(node) {
42
+ const source = node.source.value;
43
+ if (typeof source !== "string") return;
44
+ const resolved = resolveImportedFile(source, filename);
45
+ if (resolved === null) return;
46
+ const island = isClientFile(resolved);
47
+ for (const spec of node.specifiers) islandNames.set(spec.local.name, island);
48
+ },
49
+ CallExpression(node) {
50
+ if (node.callee.type === "MemberExpression" && !node.callee.computed && node.callee.property.type === "Identifier" && BANNED_HOOKS.has(node.callee.property.name)) {
51
+ context.report({
52
+ node,
53
+ messageId: "clientRuntime",
54
+ data: { what: node.callee.property.name }
55
+ });
56
+ return;
57
+ }
58
+ if (node.callee.type !== "Identifier") return;
59
+ const name = node.callee.name;
60
+ if (!BANNED_HOOKS.has(name)) return;
61
+ const ref = sourceCode.getScope(node).references.find((r) => r.identifier === node.callee);
62
+ if (ref?.resolved && ref.resolved.defs.some((d) => d.type !== "ImportBinding")) return;
63
+ context.report({
64
+ node,
65
+ messageId: "clientRuntime",
66
+ data: { what: name }
67
+ });
68
+ },
69
+ Identifier(node) {
70
+ if (!BANNED_GLOBALS.has(node.name)) return;
71
+ const ref = sourceCode.getScope(node).references.find((r) => r.identifier === node);
72
+ if (!ref) return;
73
+ if (ref.resolved && ref.resolved.defs.length > 0) return;
74
+ context.report({
75
+ node,
76
+ messageId: "clientRuntime",
77
+ data: { what: node.name }
78
+ });
79
+ },
80
+ JSXAttribute(node) {
81
+ const attrName = node.name.name;
82
+ if (typeof attrName !== "string" || !/^on[A-Z]/.test(attrName)) return;
83
+ if (node.value === null || node.value.type !== "JSXExpressionContainer") return;
84
+ const expr = node.value.expression;
85
+ if (expr && (expr.type === "Literal" || expr.type === "JSXElement")) return;
86
+ const parent = node.parent;
87
+ if (parent?.type === "JSXOpeningElement" && parent.name?.type === "JSXIdentifier" && typeof parent.name.name === "string" && isIsland(parent.name.name)) return;
88
+ context.report({
89
+ node,
90
+ messageId: "clientHandler",
91
+ data: { what: attrName }
92
+ });
93
+ }
94
+ };
95
+ }
96
+ };
97
+
98
+ //#endregion
99
+ export { noClientRuntimeInServer };
@@ -0,0 +1,23 @@
1
+ //#region src/eslint/rules/no-css-import-from-render-path.ts
2
+ const noCssImportFromRenderPath = {
3
+ meta: {
4
+ type: "problem",
5
+ docs: { description: "Ban CSS imports from a section's render path" },
6
+ 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." }
8
+ },
9
+ create(context) {
10
+ return { ImportDeclaration(node) {
11
+ const source = node.source.value;
12
+ if (typeof source !== "string" || !source.endsWith(".css")) return;
13
+ context.report({
14
+ node,
15
+ messageId: "cssFromRenderPath",
16
+ data: { source }
17
+ });
18
+ } };
19
+ }
20
+ };
21
+
22
+ //#endregion
23
+ export { noCssImportFromRenderPath };