@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.
- package/CHANGELOG.md +220 -0
- package/README.md +70 -16
- package/dist/base.css +36 -48
- package/dist/cli/check/config.js +37 -0
- package/dist/cli/check/css.js +117 -0
- package/dist/cli/check/diagnostics.js +32 -0
- package/dist/cli/check/index.js +87 -0
- package/dist/cli/check/loader.js +142 -0
- package/dist/cli/check/paths.js +15 -0
- package/dist/cli/check/relativize.js +18 -0
- package/dist/cli/check/render-invariants.js +24 -0
- package/dist/cli/check/resolve-tool.js +38 -0
- package/dist/cli/check/stages/deps.js +337 -0
- package/dist/cli/check/stages/lint.js +46 -0
- package/dist/cli/check/stages/render.js +101 -0
- package/dist/cli/check/stages/size.js +158 -0
- package/dist/cli/check/stages/tree.js +207 -0
- package/dist/cli/check/stages/typecheck.js +46 -0
- package/dist/cli/check/stages/validate/catalog-exhaustiveness.js +18 -0
- package/dist/cli/check/stages/validate/facts.js +18 -0
- package/dist/cli/check/stages/validate/formatter-contract.js +14 -0
- package/dist/cli/check/stages/validate/group-contract.js +21 -0
- package/dist/cli/check/stages/validate/index.js +66 -0
- package/dist/cli/check/stages/validate/list-scalar-contract.js +22 -0
- package/dist/cli/check/stages/validate/nav-contract.js +93 -0
- package/dist/cli/check/stages/validate/nested-slot-contract.js +12 -0
- package/dist/cli/check/stages/validate/object-list-contract.js +31 -0
- package/dist/cli/check/stages/validate/orchestrator.js +241 -0
- package/dist/cli/check/stages/validate/produced-by-contract.js +18 -0
- package/dist/cli/check/stages/validate/row-contract.js +31 -0
- package/dist/cli/check/stages/validate/select-contract.js +13 -0
- package/dist/cli/check/stages/validate/source-contract.js +21 -0
- package/dist/cli/check/stages/validate/variant-contract.js +15 -0
- package/dist/cli/check/workspace.js +86 -0
- package/dist/cli/theme/generate.js +43 -0
- package/dist/cli/theme/index.js +33 -0
- package/dist/cli/theme/load-theme.js +69 -0
- package/dist/cli.js +22 -4
- package/dist/contracts/fill-treatments.js +47 -0
- package/dist/design-system/theme.d.ts +30 -299
- package/dist/design-system/theme.js +112 -83
- package/dist/eslint/rules/image-bare-needs-reason.js +6 -1
- package/dist/eslint/rules/no-client-directive-in-contract.js +6 -1
- package/dist/eslint/rules/no-client-runtime-in-server.js +5 -1
- package/dist/eslint/rules/no-css-import-from-render-path.js +8 -2
- package/dist/eslint/rules/no-hex.js +84 -7
- package/dist/eslint/rules/no-inline-style.js +5 -1
- package/dist/eslint/rules/no-nondeterminism.js +5 -1
- package/dist/eslint/rules/no-raw-element.js +6 -1
- package/dist/eslint/rules/props-from-schema.js +6 -1
- package/dist/eslint/rules/serializable-island-props.js +5 -1
- package/dist/eslint/rules/slot-marker-literal.js +6 -1
- package/dist/eslint.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/package.js +1 -1
- package/dist/primitives/Image.js +1 -1
- package/dist/rules/registry.js +16 -0
- package/dist/schema/fill-spec.d.ts +80 -590
- package/dist/schema/fixture-schema.d.ts +5 -81
- package/dist/schema/manifest.d.ts +39 -475
- package/dist/schema/resolve-section-ref.js +10 -0
- package/dist/schema/rows.js +10 -0
- package/dist/schema/section-nav.js +9 -0
- package/dist/schema/section-schema.d.ts +51 -437
- package/dist/schema/slot-types.d.ts +12 -16
- package/dist/styles.css +31 -88
- package/docs/INDEX.md +5 -3
- package/docs/check.md +121 -0
- package/docs/eslint.md +25 -50
- package/docs/llms.txt +26 -11
- package/docs/primitives.md +5 -0
- package/docs/rules/INDEX.md +65 -0
- package/docs/rules/audit-severity.md +91 -0
- package/docs/rules/bundle-binary-asset.md +102 -0
- package/docs/rules/bundle-incomplete.md +117 -0
- package/docs/rules/css-reason.md +94 -0
- package/docs/rules/determinism-drift.md +112 -0
- package/docs/rules/fixtures-invalid.md +96 -0
- package/docs/rules/image-bare-needs-reason.md +117 -0
- package/docs/rules/license-denied.md +114 -0
- package/docs/rules/lockfile-missing.md +68 -0
- package/docs/rules/lockfile-stale.md +88 -0
- package/docs/rules/manifest-invalid.md +99 -0
- package/docs/rules/missing-slot-marker.md +140 -0
- package/docs/rules/no-bare-css-import.md +128 -0
- package/docs/rules/no-client-directive-in-contract.md +111 -0
- package/docs/rules/no-client-runtime-in-server.md +143 -0
- package/docs/rules/no-css-import-from-render-path.md +118 -0
- package/docs/rules/no-hex.md +163 -0
- package/docs/rules/no-inline-style.md +121 -0
- package/docs/rules/no-nondeterminism.md +100 -0
- package/docs/rules/no-raw-element.md +100 -0
- package/docs/rules/no-templates.md +87 -0
- package/docs/rules/parse-error.md +76 -0
- package/docs/rules/props-from-schema.md +119 -0
- package/docs/rules/render-invariant.md +165 -0
- package/docs/rules/schema-invalid.md +96 -0
- package/docs/rules/serializable-island-props.md +146 -0
- package/docs/rules/server-vs-client.md +105 -0
- package/docs/rules/sidebar-order.md +109 -0
- package/docs/rules/single-react.md +97 -0
- package/docs/rules/size-renderer-bundle.md +109 -0
- package/docs/rules/size-section-css.md +95 -0
- package/docs/rules/slot-group-marker.md +127 -0
- package/docs/rules/slot-marker-literal.md +148 -0
- package/docs/rules/typecheck.md +103 -0
- package/docs/rules/unlayered-fence.md +113 -0
- package/docs/theme-and-css.md +193 -82
- package/package.json +4 -9
- 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
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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:
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
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
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const
|
|
91
|
-
|
|
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 {
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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
|
|
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: {
|
|
141
|
+
docs: {
|
|
142
|
+
description: "Ban hard-coded colour literals in section code",
|
|
143
|
+
url: docsPathForRule("no-hex")
|
|
144
|
+
},
|
|
71
145
|
schema: [],
|
|
72
|
-
messages: {
|
|
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
|
|
77
|
-
|
|
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: "
|
|
81
|
-
data: { value:
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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: {
|
|
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": "
|
|
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 {
|
|
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,
|
|
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 {
|
|
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,
|
|
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
package/dist/primitives/Image.js
CHANGED
|
@@ -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
|
|
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 };
|