@gallop.software/canon 2.0.1 → 2.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.
- package/dist/eslint/configs/recommended.d.ts +1 -0
- package/dist/eslint/configs/recommended.js +2 -1
- package/dist/eslint/configs/speedwell.d.ts +1 -0
- package/dist/eslint/configs/speedwell.js +3 -1
- package/dist/eslint/index.d.ts +3 -0
- package/dist/eslint/index.js +3 -1
- package/dist/eslint/rules/prefer-layout-components.d.ts +3 -0
- package/dist/eslint/rules/prefer-layout-components.js +113 -0
- package/package.json +2 -2
- package/patterns/018-layout-components.md +85 -0
- package/schema.json +11 -1
|
@@ -9,7 +9,8 @@ const recommendedConfig = {
|
|
|
9
9
|
'gallop/no-client-blocks': 'warn',
|
|
10
10
|
'gallop/no-container-in-section': 'warn',
|
|
11
11
|
'gallop/prefer-component-props': 'warn',
|
|
12
|
+
'gallop/prefer-layout-components': 'warn',
|
|
12
13
|
},
|
|
13
14
|
};
|
|
14
15
|
export default recommendedConfig;
|
|
15
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
16
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicmVjb21tZW5kZWQuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMvZXNsaW50L2NvbmZpZ3MvcmVjb21tZW5kZWQudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7OztHQUdHO0FBQ0gsTUFBTSxpQkFBaUIsR0FBRztJQUN4QixPQUFPLEVBQUUsQ0FBQyxRQUFRLENBQUM7SUFDbkIsS0FBSyxFQUFFO1FBQ0wsMENBQTBDO1FBQzFDLHlCQUF5QixFQUFFLE1BQU07UUFDakMsZ0NBQWdDLEVBQUUsTUFBTTtRQUN4QywrQkFBK0IsRUFBRSxNQUFNO1FBQ3ZDLGlDQUFpQyxFQUFFLE1BQU07S0FDMUM7Q0FDRixDQUFBO0FBRUQsZUFBZSxpQkFBaUIsQ0FBQSIsInNvdXJjZXNDb250ZW50IjpbIi8qKlxuICogUmVjb21tZW5kZWQgY29uZmlndXJhdGlvblxuICogQSBzZW5zaWJsZSBkZWZhdWx0IGZvciBhbnkgR2FsbG9wLWJhc2VkIHRlbXBsYXRlXG4gKi9cbmNvbnN0IHJlY29tbWVuZGVkQ29uZmlnID0ge1xuICBwbHVnaW5zOiBbJ2dhbGxvcCddLFxuICBydWxlczoge1xuICAgIC8vIENvcmUgcnVsZXMgdGhhdCBhcHBseSB0byBtb3N0IHRlbXBsYXRlc1xuICAgICdnYWxsb3Avbm8tY2xpZW50LWJsb2Nrcyc6ICd3YXJuJyxcbiAgICAnZ2FsbG9wL25vLWNvbnRhaW5lci1pbi1zZWN0aW9uJzogJ3dhcm4nLFxuICAgICdnYWxsb3AvcHJlZmVyLWNvbXBvbmVudC1wcm9wcyc6ICd3YXJuJyxcbiAgICAnZ2FsbG9wL3ByZWZlci1sYXlvdXQtY29tcG9uZW50cyc6ICd3YXJuJyxcbiAgfSxcbn1cblxuZXhwb3J0IGRlZmF1bHQgcmVjb21tZW5kZWRDb25maWdcbiJdfQ==
|
|
@@ -11,7 +11,9 @@ const speedwellConfig = {
|
|
|
11
11
|
'gallop/no-container-in-section': 'warn',
|
|
12
12
|
// Use component props instead of className for style values
|
|
13
13
|
'gallop/prefer-component-props': 'warn',
|
|
14
|
+
// Use Grid/Columns instead of raw div with grid classes
|
|
15
|
+
'gallop/prefer-layout-components': 'warn',
|
|
14
16
|
},
|
|
15
17
|
};
|
|
16
18
|
export default speedwellConfig;
|
|
17
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
19
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic3BlZWR3ZWxsLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vc3JjL2VzbGludC9jb25maWdzL3NwZWVkd2VsbC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTs7O0dBR0c7QUFDSCxNQUFNLGVBQWUsR0FBRztJQUN0QixPQUFPLEVBQUUsQ0FBQyxRQUFRLENBQUM7SUFDbkIsS0FBSyxFQUFFO1FBQ0wsMEVBQTBFO1FBQzFFLHlCQUF5QixFQUFFLE1BQU07UUFFakMsdUNBQXVDO1FBQ3ZDLGdDQUFnQyxFQUFFLE1BQU07UUFFeEMsNERBQTREO1FBQzVELCtCQUErQixFQUFFLE1BQU07UUFFdkMsd0RBQXdEO1FBQ3hELGlDQUFpQyxFQUFFLE1BQU07S0FDMUM7Q0FDRixDQUFBO0FBRUQsZUFBZSxlQUFlLENBQUEiLCJzb3VyY2VzQ29udGVudCI6WyIvKipcbiAqIFNwZWVkd2VsbCB0ZW1wbGF0ZSBjb25maWd1cmF0aW9uXG4gKiBFbmFibGVzIGFsbCBHYWxsb3AgcnVsZXMgcmVsZXZhbnQgdG8gdGhlIFNwZWVkd2VsbCBhcmNoaXRlY3R1cmVcbiAqL1xuY29uc3Qgc3BlZWR3ZWxsQ29uZmlnID0ge1xuICBwbHVnaW5zOiBbJ2dhbGxvcCddLFxuICBydWxlczoge1xuICAgIC8vIEJsb2NrcyBzaG91bGQgYmUgc2VydmVyIGNvbXBvbmVudHMgLSBleHRyYWN0IGNsaWVudCBsb2dpYyB0byBjb21wb25lbnRzXG4gICAgJ2dhbGxvcC9uby1jbGllbnQtYmxvY2tzJzogJ3dhcm4nLFxuXG4gICAgLy8gU2VjdGlvbiBhbHJlYWR5IHByb3ZpZGVzIGNvbnRhaW5tZW50XG4gICAgJ2dhbGxvcC9uby1jb250YWluZXItaW4tc2VjdGlvbic6ICd3YXJuJyxcblxuICAgIC8vIFVzZSBjb21wb25lbnQgcHJvcHMgaW5zdGVhZCBvZiBjbGFzc05hbWUgZm9yIHN0eWxlIHZhbHVlc1xuICAgICdnYWxsb3AvcHJlZmVyLWNvbXBvbmVudC1wcm9wcyc6ICd3YXJuJyxcblxuICAgIC8vIFVzZSBHcmlkL0NvbHVtbnMgaW5zdGVhZCBvZiByYXcgZGl2IHdpdGggZ3JpZCBjbGFzc2VzXG4gICAgJ2dhbGxvcC9wcmVmZXItbGF5b3V0LWNvbXBvbmVudHMnOiAnd2FybicsXG4gIH0sXG59XG5cbmV4cG9ydCBkZWZhdWx0IHNwZWVkd2VsbENvbmZpZ1xuIl19
|
package/dist/eslint/index.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ declare const plugin: {
|
|
|
14
14
|
name: string;
|
|
15
15
|
};
|
|
16
16
|
'prefer-typography-components': import("eslint").Rule.RuleModule;
|
|
17
|
+
'prefer-layout-components': import("eslint").Rule.RuleModule;
|
|
17
18
|
};
|
|
18
19
|
configs: {
|
|
19
20
|
speedwell: {
|
|
@@ -22,6 +23,7 @@ declare const plugin: {
|
|
|
22
23
|
'gallop/no-client-blocks': string;
|
|
23
24
|
'gallop/no-container-in-section': string;
|
|
24
25
|
'gallop/prefer-component-props': string;
|
|
26
|
+
'gallop/prefer-layout-components': string;
|
|
25
27
|
};
|
|
26
28
|
};
|
|
27
29
|
recommended: {
|
|
@@ -30,6 +32,7 @@ declare const plugin: {
|
|
|
30
32
|
'gallop/no-client-blocks': string;
|
|
31
33
|
'gallop/no-container-in-section': string;
|
|
32
34
|
'gallop/prefer-component-props': string;
|
|
35
|
+
'gallop/prefer-layout-components': string;
|
|
33
36
|
};
|
|
34
37
|
};
|
|
35
38
|
};
|
package/dist/eslint/index.js
CHANGED
|
@@ -2,6 +2,7 @@ import noClientBlocks from './rules/no-client-blocks.js';
|
|
|
2
2
|
import noContainerInSection from './rules/no-container-in-section.js';
|
|
3
3
|
import preferComponentProps from './rules/prefer-component-props.js';
|
|
4
4
|
import preferTypographyComponents from './rules/prefer-typography-components.js';
|
|
5
|
+
import preferLayoutComponents from './rules/prefer-layout-components.js';
|
|
5
6
|
import speedwellConfig from './configs/speedwell.js';
|
|
6
7
|
import recommendedConfig from './configs/recommended.js';
|
|
7
8
|
const plugin = {
|
|
@@ -14,6 +15,7 @@ const plugin = {
|
|
|
14
15
|
'no-container-in-section': noContainerInSection,
|
|
15
16
|
'prefer-component-props': preferComponentProps,
|
|
16
17
|
'prefer-typography-components': preferTypographyComponents,
|
|
18
|
+
'prefer-layout-components': preferLayoutComponents,
|
|
17
19
|
},
|
|
18
20
|
configs: {
|
|
19
21
|
speedwell: speedwellConfig,
|
|
@@ -21,4 +23,4 @@ const plugin = {
|
|
|
21
23
|
},
|
|
22
24
|
};
|
|
23
25
|
export default plugin;
|
|
24
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
26
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvZXNsaW50L2luZGV4LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sY0FBYyxNQUFNLDZCQUE2QixDQUFBO0FBQ3hELE9BQU8sb0JBQW9CLE1BQU0sb0NBQW9DLENBQUE7QUFDckUsT0FBTyxvQkFBb0IsTUFBTSxtQ0FBbUMsQ0FBQTtBQUNwRSxPQUFPLDBCQUEwQixNQUFNLHlDQUF5QyxDQUFBO0FBQ2hGLE9BQU8sc0JBQXNCLE1BQU0scUNBQXFDLENBQUE7QUFDeEUsT0FBTyxlQUFlLE1BQU0sd0JBQXdCLENBQUE7QUFDcEQsT0FBTyxpQkFBaUIsTUFBTSwwQkFBMEIsQ0FBQTtBQUV4RCxNQUFNLE1BQU0sR0FBRztJQUNiLElBQUksRUFBRTtRQUNKLElBQUksRUFBRSxzQkFBc0I7UUFDNUIsT0FBTyxFQUFFLE9BQU87S0FDakI7SUFDRCxLQUFLLEVBQUU7UUFDTCxrQkFBa0IsRUFBRSxjQUFjO1FBQ2xDLHlCQUF5QixFQUFFLG9CQUFvQjtRQUMvQyx3QkFBd0IsRUFBRSxvQkFBb0I7UUFDOUMsOEJBQThCLEVBQUUsMEJBQTBCO1FBQzFELDBCQUEwQixFQUFFLHNCQUFzQjtLQUNuRDtJQUNELE9BQU8sRUFBRTtRQUNQLFNBQVMsRUFBRSxlQUFlO1FBQzFCLFdBQVcsRUFBRSxpQkFBaUI7S0FDL0I7Q0FDRixDQUFBO0FBRUQsZUFBZSxNQUFNLENBQUEiLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgbm9DbGllbnRCbG9ja3MgZnJvbSAnLi9ydWxlcy9uby1jbGllbnQtYmxvY2tzLmpzJ1xuaW1wb3J0IG5vQ29udGFpbmVySW5TZWN0aW9uIGZyb20gJy4vcnVsZXMvbm8tY29udGFpbmVyLWluLXNlY3Rpb24uanMnXG5pbXBvcnQgcHJlZmVyQ29tcG9uZW50UHJvcHMgZnJvbSAnLi9ydWxlcy9wcmVmZXItY29tcG9uZW50LXByb3BzLmpzJ1xuaW1wb3J0IHByZWZlclR5cG9ncmFwaHlDb21wb25lbnRzIGZyb20gJy4vcnVsZXMvcHJlZmVyLXR5cG9ncmFwaHktY29tcG9uZW50cy5qcydcbmltcG9ydCBwcmVmZXJMYXlvdXRDb21wb25lbnRzIGZyb20gJy4vcnVsZXMvcHJlZmVyLWxheW91dC1jb21wb25lbnRzLmpzJ1xuaW1wb3J0IHNwZWVkd2VsbENvbmZpZyBmcm9tICcuL2NvbmZpZ3Mvc3BlZWR3ZWxsLmpzJ1xuaW1wb3J0IHJlY29tbWVuZGVkQ29uZmlnIGZyb20gJy4vY29uZmlncy9yZWNvbW1lbmRlZC5qcydcblxuY29uc3QgcGx1Z2luID0ge1xuICBtZXRhOiB7XG4gICAgbmFtZTogJ2VzbGludC1wbHVnaW4tZ2FsbG9wJyxcbiAgICB2ZXJzaW9uOiAnMS4wLjEnLFxuICB9LFxuICBydWxlczoge1xuICAgICduby1jbGllbnQtYmxvY2tzJzogbm9DbGllbnRCbG9ja3MsXG4gICAgJ25vLWNvbnRhaW5lci1pbi1zZWN0aW9uJzogbm9Db250YWluZXJJblNlY3Rpb24sXG4gICAgJ3ByZWZlci1jb21wb25lbnQtcHJvcHMnOiBwcmVmZXJDb21wb25lbnRQcm9wcyxcbiAgICAncHJlZmVyLXR5cG9ncmFwaHktY29tcG9uZW50cyc6IHByZWZlclR5cG9ncmFwaHlDb21wb25lbnRzLFxuICAgICdwcmVmZXItbGF5b3V0LWNvbXBvbmVudHMnOiBwcmVmZXJMYXlvdXRDb21wb25lbnRzLFxuICB9LFxuICBjb25maWdzOiB7XG4gICAgc3BlZWR3ZWxsOiBzcGVlZHdlbGxDb25maWcsXG4gICAgcmVjb21tZW5kZWQ6IHJlY29tbWVuZGVkQ29uZmlnLFxuICB9LFxufVxuXG5leHBvcnQgZGVmYXVsdCBwbHVnaW5cbiJdfQ==
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { getCanonUrl, getCanonPattern } from '../utils/canon.js';
|
|
2
|
+
const RULE_NAME = 'prefer-layout-components';
|
|
3
|
+
const pattern = getCanonPattern(RULE_NAME);
|
|
4
|
+
const rule = {
|
|
5
|
+
meta: {
|
|
6
|
+
type: 'suggestion',
|
|
7
|
+
docs: {
|
|
8
|
+
description: pattern?.summary || 'Use Grid/Columns, not raw div with grid',
|
|
9
|
+
recommended: true,
|
|
10
|
+
url: getCanonUrl(RULE_NAME),
|
|
11
|
+
},
|
|
12
|
+
messages: {
|
|
13
|
+
useLayoutComponent: `[Canon ${pattern?.id || '018'}] Use the Grid or Columns component instead of <div className="grid ...">. Import: import { Grid, Columns, Column } from "@/components"`,
|
|
14
|
+
},
|
|
15
|
+
schema: [],
|
|
16
|
+
},
|
|
17
|
+
create(context) {
|
|
18
|
+
const filename = context.filename || context.getFilename();
|
|
19
|
+
// Only apply to block files
|
|
20
|
+
if (!filename.includes('/blocks/')) {
|
|
21
|
+
return {};
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
JSXOpeningElement(node) {
|
|
25
|
+
const elementName = node.name?.name;
|
|
26
|
+
// Only check div elements
|
|
27
|
+
if (elementName !== 'div') {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
// Check if className contains 'grid'
|
|
31
|
+
const classNameAttr = node.attributes?.find((attr) => attr.type === 'JSXAttribute' &&
|
|
32
|
+
attr.name?.name === 'className');
|
|
33
|
+
if (!classNameAttr) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
// Handle string literal className
|
|
37
|
+
if (classNameAttr.value?.type === 'Literal') {
|
|
38
|
+
const classValue = classNameAttr.value.value;
|
|
39
|
+
if (typeof classValue === 'string' && hasGridClass(classValue)) {
|
|
40
|
+
context.report({
|
|
41
|
+
node,
|
|
42
|
+
messageId: 'useLayoutComponent',
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
// Handle template literal className
|
|
48
|
+
if (classNameAttr.value?.type === 'JSXExpressionContainer') {
|
|
49
|
+
const expr = classNameAttr.value.expression;
|
|
50
|
+
// Direct template literal: className={`grid ...`}
|
|
51
|
+
if (expr.type === 'TemplateLiteral') {
|
|
52
|
+
const quasis = expr.quasis || [];
|
|
53
|
+
for (const quasi of quasis) {
|
|
54
|
+
if (quasi.value?.raw && hasGridClass(quasi.value.raw)) {
|
|
55
|
+
context.report({
|
|
56
|
+
node,
|
|
57
|
+
messageId: 'useLayoutComponent',
|
|
58
|
+
});
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// clsx call: className={clsx('grid', ...)}
|
|
64
|
+
if (expr.type === 'CallExpression') {
|
|
65
|
+
const args = expr.arguments || [];
|
|
66
|
+
for (const arg of args) {
|
|
67
|
+
if (arg.type === 'Literal' && typeof arg.value === 'string') {
|
|
68
|
+
if (hasGridClass(arg.value)) {
|
|
69
|
+
context.report({
|
|
70
|
+
node,
|
|
71
|
+
messageId: 'useLayoutComponent',
|
|
72
|
+
});
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// Check template literals in clsx args
|
|
77
|
+
if (arg.type === 'TemplateLiteral') {
|
|
78
|
+
const quasis = arg.quasis || [];
|
|
79
|
+
for (const quasi of quasis) {
|
|
80
|
+
if (quasi.value?.raw && hasGridClass(quasi.value.raw)) {
|
|
81
|
+
context.report({
|
|
82
|
+
node,
|
|
83
|
+
messageId: 'useLayoutComponent',
|
|
84
|
+
});
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* Check if a className string contains grid classes
|
|
98
|
+
* Matches: 'grid', 'grid-cols-', etc.
|
|
99
|
+
* Does NOT match: 'grid-area', component names with 'grid' in them
|
|
100
|
+
*/
|
|
101
|
+
function hasGridClass(classString) {
|
|
102
|
+
// Split by whitespace and check each class
|
|
103
|
+
const classes = classString.split(/\s+/);
|
|
104
|
+
for (const cls of classes) {
|
|
105
|
+
// Match standalone 'grid' or 'grid-cols-*' patterns
|
|
106
|
+
if (cls === 'grid' || cls.startsWith('grid-cols-')) {
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
export default rule;
|
|
113
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicHJlZmVyLWxheW91dC1jb21wb25lbnRzLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vc3JjL2VzbGludC9ydWxlcy9wcmVmZXItbGF5b3V0LWNvbXBvbmVudHMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQ0EsT0FBTyxFQUFFLFdBQVcsRUFBRSxlQUFlLEVBQUUsTUFBTSxtQkFBbUIsQ0FBQTtBQUVoRSxNQUFNLFNBQVMsR0FBRywwQkFBMEIsQ0FBQTtBQUM1QyxNQUFNLE9BQU8sR0FBRyxlQUFlLENBQUMsU0FBUyxDQUFDLENBQUE7QUFFMUMsTUFBTSxJQUFJLEdBQW9CO0lBQzVCLElBQUksRUFBRTtRQUNKLElBQUksRUFBRSxZQUFZO1FBQ2xCLElBQUksRUFBRTtZQUNKLFdBQVcsRUFBRSxPQUFPLEVBQUUsT0FBTyxJQUFJLHlDQUF5QztZQUMxRSxXQUFXLEVBQUUsSUFBSTtZQUNqQixHQUFHLEVBQUUsV0FBVyxDQUFDLFNBQVMsQ0FBQztTQUM1QjtRQUNELFFBQVEsRUFBRTtZQUNSLGtCQUFrQixFQUFFLFVBQVUsT0FBTyxFQUFFLEVBQUUsSUFBSSxLQUFLLHlJQUF5STtTQUM1TDtRQUNELE1BQU0sRUFBRSxFQUFFO0tBQ1g7SUFFRCxNQUFNLENBQUMsT0FBTztRQUNaLE1BQU0sUUFBUSxHQUFHLE9BQU8sQ0FBQyxRQUFRLElBQUksT0FBTyxDQUFDLFdBQVcsRUFBRSxDQUFBO1FBRTFELDRCQUE0QjtRQUM1QixJQUFJLENBQUMsUUFBUSxDQUFDLFFBQVEsQ0FBQyxVQUFVLENBQUMsRUFBRSxDQUFDO1lBQ25DLE9BQU8sRUFBRSxDQUFBO1FBQ1gsQ0FBQztRQUVELE9BQU87WUFDTCxpQkFBaUIsQ0FBQyxJQUFTO2dCQUN6QixNQUFNLFdBQVcsR0FBRyxJQUFJLENBQUMsSUFBSSxFQUFFLElBQUksQ0FBQTtnQkFFbkMsMEJBQTBCO2dCQUMxQixJQUFJLFdBQVcsS0FBSyxLQUFLLEVBQUUsQ0FBQztvQkFDMUIsT0FBTTtn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QkFDSCxDQUFDO29CQUNILENBQUM7Z0JBQ0gsQ0FBQztZQUNILENBQUM7U0FDRixDQUFBO0lBQ0gsQ0FBQztDQUNGLENBQUE7QUFFRDs7OztHQUlHO0FBQ0gsU0FBUyxZQUFZLENBQUMsV0FBbUI7SUFDdkMsMkNBQTJDO0lBQzNDLE1BQU0sT0FBTyxHQUFHLFdBQVcsQ0FBQyxLQUFLLENBQUMsS0FBSyxDQUFDLENBQUE7SUFFeEMsS0FBSyxNQUFNLEdBQUcsSUFBSSxPQUFPLEVBQUUsQ0FBQztRQUMxQixvREFBb0Q7UUFDcEQsSUFBSSxHQUFHLEtBQUssTUFBTSxJQUFJLEdBQUcsQ0FBQyxVQUFVLENBQUMsWUFBWSxDQUFDLEVBQUUsQ0FBQztZQUNuRCxPQUFPLElBQUksQ0FBQTtRQUNiLENBQUM7SUFDSCxDQUFDO0lBRUQsT0FBTyxLQUFLLENBQUE7QUFDZCxDQUFDO0FBRUQsZUFBZSxJQUFJLENBQUEiLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgdHlwZSB7IFJ1bGUgfSBmcm9tICdlc2xpbnQnXG5pbXBvcnQgeyBnZXRDYW5vblVybCwgZ2V0Q2Fub25QYXR0ZXJuIH0gZnJvbSAnLi4vdXRpbHMvY2Fub24uanMnXG5cbmNvbnN0IFJVTEVfTkFNRSA9ICdwcmVmZXItbGF5b3V0LWNvbXBvbmVudHMnXG5jb25zdCBwYXR0ZXJuID0gZ2V0Q2Fub25QYXR0ZXJuKFJVTEVfTkFNRSlcblxuY29uc3QgcnVsZTogUnVsZS5SdWxlTW9kdWxlID0ge1xuICBtZXRhOiB7XG4gICAgdHlwZTogJ3N1Z2dlc3Rpb24nLFxuICAgIGRvY3M6IHtcbiAgICAgIGRlc2NyaXB0aW9uOiBwYXR0ZXJuPy5zdW1tYXJ5IHx8ICdVc2UgR3JpZC9Db2x1bW5zLCBub3QgcmF3IGRpdiB3aXRoIGdyaWQnLFxuICAgICAgcmVjb21tZW5kZWQ6IHRydWUsXG4gICAgICB1cmw6IGdldENhbm9uVXJsKFJVTEVfTkFNRSksXG4gICAgfSxcbiAgICBtZXNzYWdlczoge1xuICAgICAgdXNlTGF5b3V0Q29tcG9uZW50OiBgW0Nhbm9uICR7cGF0dGVybj8uaWQgfHwgJzAxOCd9XSBVc2UgdGhlIEdyaWQgb3IgQ29sdW1ucyBjb21wb25lbnQgaW5zdGVhZCBvZiA8ZGl2IGNsYXNzTmFtZT1cImdyaWQgLi4uXCI+LiBJbXBvcnQ6IGltcG9ydCB7IEdyaWQsIENvbHVtbnMsIENvbHVtbiB9IGZyb20gXCJAL2NvbXBvbmVudHNcImAsXG4gICAgfSxcbiAgICBzY2hlbWE6IFtdLFxuICB9LFxuXG4gIGNyZWF0ZShjb250ZXh0KSB7XG4gICAgY29uc3QgZmlsZW5hbWUgPSBjb250ZXh0LmZpbGVuYW1lIHx8IGNvbnRleHQuZ2V0RmlsZW5hbWUoKVxuXG4gICAgLy8gT25seSBhcHBseSB0byBibG9jayBmaWxlc1xuICAgIGlmICghZmlsZW5hbWUuaW5jbHVkZXMoJy9ibG9ja3MvJykpIHtcbiAgICAgIHJldHVybiB7fVxuICAgIH1cblxuICAgIHJldHVybiB7XG4gICAgICBKU1hPcGVuaW5nRWxlbWVudChub2RlOiBhbnkpIHtcbiAgICAgICAgY29uc3QgZWxlbWVudE5hbWUgPSBub2RlLm5hbWU/Lm5hbWVcblxuICAgICAgICAvLyBPbmx5IGNoZWNrIGRpdiBlbGVtZW50c1xuICAgICAgICBpZiAoZWxlbWVudE5hbWUgIT09ICdkaXYnKSB7XG4gICAgICAgICAgcmV0dXJuXG4gICAgICAgIH1cblxuICAgICAgICAvLyBDaGVjayBpZiBjbGFzc05hbWUgY29udGFpbnMgJ2dyaWQnXG4gICAgICAgIGNvbnN0IGNsYXNzTmFtZUF0dHIgPSBub2RlLmF0dHJpYnV0ZXM/LmZpbmQoXG4gICAgICAgICAgKGF0dHI6IGFueSkgPT5cbiAgICAgICAgICAgIGF0dHIudHlwZSA9PT0gJ0pTWEF0dHJpYnV0ZScgJiZcbiAgICAgICAgICAgIGF0dHIubmFtZT8ubmFtZSA9PT0gJ2NsYXNzTmFtZSdcbiAgICAgICAgKVxuXG4gICAgICAgIGlmICghY2xhc3NOYW1lQXR0cikge1xuICAgICAgICAgIHJldHVyblxuICAgICAgICB9XG5cbiAgICAgICAgLy8gSGFuZGxlIHN0cmluZyBsaXRlcmFsIGNsYXNzTmFtZVxuICAgICAgICBpZiAoY2xhc3NOYW1lQXR0ci52YWx1ZT8udHlwZSA9PT0gJ0xpdGVyYWwnKSB7XG4gICAgICAgICAgY29uc3QgY2xhc3NWYWx1ZSA9IGNsYXNzTmFtZUF0dHIudmFsdWUudmFsdWVcbiAgICAgICAgICBpZiAodHlwZW9mIGNsYXNzVmFsdWUgPT09ICdzdHJpbmcnICYmIGhhc0dyaWRDbGFzcyhjbGFzc1ZhbHVlKSkge1xuICAgICAgICAgICAgY29udGV4dC5yZXBvcnQoe1xuICAgICAgICAgICAgICBub2RlLFxuICAgICAgICAgICAgICBtZXNzYWdlSWQ6ICd1c2VMYXlvdXRDb21wb25lbnQnLFxuICAgICAgICAgICAgfSlcbiAgICAgICAgICB9XG4gICAgICAgICAgcmV0dXJuXG4gICAgICAgIH1cblxuICAgICAgICAvLyBIYW5kbGUgdGVtcGxhdGUgbGl0ZXJhbCBjbGFzc05hbWVcbiAgICAgICAgaWYgKGNsYXNzTmFtZUF0dHIudmFsdWU/LnR5cGUgPT09ICdKU1hFeHByZXNzaW9uQ29udGFpbmVyJykge1xuICAgICAgICAgIGNvbnN0IGV4cHIgPSBjbGFzc05hbWVBdHRyLnZhbHVlLmV4cHJlc3Npb25cblxuICAgICAgICAgIC8vIERpcmVjdCB0ZW1wbGF0ZSBsaXRlcmFsOiBjbGFzc05hbWU9e2BncmlkIC4uLmB9XG4gICAgICAgICAgaWYgKGV4cHIudHlwZSA9PT0gJ1RlbXBsYXRlTGl0ZXJhbCcpIHtcbiAgICAgICAgICAgIGNvbnN0IHF1YXNpcyA9IGV4cHIucXVhc2lzIHx8IFtdXG4gICAgICAgICAgICBmb3IgKGNvbnN0IHF1YXNpIG9mIHF1YXNpcykge1xuICAgICAgICAgICAgICBpZiAocXVhc2kudmFsdWU/LnJhdyAmJiBoYXNHcmlkQ2xhc3MocXVhc2kudmFsdWUucmF3KSkge1xuICAgICAgICAgICAgICAgIGNvbnRleHQucmVwb3J0KHtcbiAgICAgICAgICAgICAgICAgIG5vZGUsXG4gICAgICAgICAgICAgICAgICBtZXNzYWdlSWQ6ICd1c2VMYXlvdXRDb21wb25lbnQnLFxuICAgICAgICAgICAgICAgIH0pXG4gICAgICAgICAgICAgICAgcmV0dXJuXG4gICAgICAgICAgICAgIH1cbiAgICAgICAgICAgIH1cbiAgICAgICAgICB9XG5cbiAgICAgICAgICAvLyBjbHN4IGNhbGw6IGNsYXNzTmFtZT17Y2xzeCgnZ3JpZCcsIC4uLil9XG4gICAgICAgICAgaWYgKGV4cHIudHlwZSA9PT0gJ0NhbGxFeHByZXNzaW9uJykge1xuICAgICAgICAgICAgY29uc3QgYXJncyA9IGV4cHIuYXJndW1lbnRzIHx8IFtdXG4gICAgICAgICAgICBmb3IgKGNvbnN0IGFyZyBvZiBhcmdzKSB7XG4gICAgICAgICAgICAgIGlmIChhcmcudHlwZSA9PT0gJ0xpdGVyYWwnICYmIHR5cGVvZiBhcmcudmFsdWUgPT09ICdzdHJpbmcnKSB7XG4gICAgICAgICAgICAgICAgaWYgKGhhc0dyaWRDbGFzcyhhcmcudmFsdWUpKSB7XG4gICAgICAgICAgICAgICAgICBjb250ZXh0LnJlcG9ydCh7XG4gICAgICAgICAgICAgICAgICAgIG5vZGUsXG4gICAgICAgICAgICAgICAgICAgIG1lc3NhZ2VJZDogJ3VzZUxheW91dENvbXBvbmVudCcsXG4gICAgICAgICAgICAgICAgICB9KVxuICAgICAgICAgICAgICAgICAgcmV0dXJuXG4gICAgICAgICAgICAgICAgfVxuICAgICAgICAgICAgICB9XG4gICAgICAgICAgICAgIC8vIENoZWNrIHRlbXBsYXRlIGxpdGVyYWxzIGluIGNsc3ggYXJnc1xuICAgICAgICAgICAgICBpZiAoYXJnLnR5cGUgPT09ICdUZW1wbGF0ZUxpdGVyYWwnKSB7XG4gICAgICAgICAgICAgICAgY29uc3QgcXVhc2lzID0gYXJnLnF1YXNpcyB8fCBbXVxuICAgICAgICAgICAgICAgIGZvciAoY29uc3QgcXVhc2kgb2YgcXVhc2lzKSB7XG4gICAgICAgICAgICAgICAgICBpZiAocXVhc2kudmFsdWU/LnJhdyAmJiBoYXNHcmlkQ2xhc3MocXVhc2kudmFsdWUucmF3KSkge1xuICAgICAgICAgICAgICAgICAgICBjb250ZXh0LnJlcG9ydCh7XG4gICAgICAgICAgICAgICAgICAgICAgbm9kZSxcbiAgICAgICAgICAgICAgICAgICAgICBtZXNzYWdlSWQ6ICd1c2VMYXlvdXRDb21wb25lbnQnLFxuICAgICAgICAgICAgICAgICAgICB9KVxuICAgICAgICAgICAgICAgICAgICByZXR1cm5cbiAgICAgICAgICAgICAgICAgIH1cbiAgICAgICAgICAgICAgICB9XG4gICAgICAgICAgICAgIH1cbiAgICAgICAgICAgIH1cbiAgICAgICAgICB9XG4gICAgICAgIH1cbiAgICAgIH0sXG4gICAgfVxuICB9LFxufVxuXG4vKipcbiAqIENoZWNrIGlmIGEgY2xhc3NOYW1lIHN0cmluZyBjb250YWlucyBncmlkIGNsYXNzZXNcbiAqIE1hdGNoZXM6ICdncmlkJywgJ2dyaWQtY29scy0nLCBldGMuXG4gKiBEb2VzIE5PVCBtYXRjaDogJ2dyaWQtYXJlYScsIGNvbXBvbmVudCBuYW1lcyB3aXRoICdncmlkJyBpbiB0aGVtXG4gKi9cbmZ1bmN0aW9uIGhhc0dyaWRDbGFzcyhjbGFzc1N0cmluZzogc3RyaW5nKTogYm9vbGVhbiB7XG4gIC8vIFNwbGl0IGJ5IHdoaXRlc3BhY2UgYW5kIGNoZWNrIGVhY2ggY2xhc3NcbiAgY29uc3QgY2xhc3NlcyA9IGNsYXNzU3RyaW5nLnNwbGl0KC9cXHMrLylcbiAgXG4gIGZvciAoY29uc3QgY2xzIG9mIGNsYXNzZXMpIHtcbiAgICAvLyBNYXRjaCBzdGFuZGFsb25lICdncmlkJyBvciAnZ3JpZC1jb2xzLSonIHBhdHRlcm5zXG4gICAgaWYgKGNscyA9PT0gJ2dyaWQnIHx8IGNscy5zdGFydHNXaXRoKCdncmlkLWNvbHMtJykpIHtcbiAgICAgIHJldHVybiB0cnVlXG4gICAgfVxuICB9XG4gIFxuICByZXR1cm4gZmFsc2Vcbn1cblxuZXhwb3J0IGRlZmF1bHQgcnVsZVxuIl19
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gallop.software/canon",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Gallop Canon - Architecture patterns, ESLint plugin, and CLI for template governance",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"license": "MIT",
|
|
50
50
|
"repository": {
|
|
51
51
|
"type": "git",
|
|
52
|
-
"url": "https://github.com/gallop-software/
|
|
52
|
+
"url": "https://github.com/gallop-software/canon",
|
|
53
53
|
"directory": "canon"
|
|
54
54
|
},
|
|
55
55
|
"peerDependencies": {
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# 018: Layout Components
|
|
2
|
+
|
|
3
|
+
**Category:** Layout
|
|
4
|
+
**Status:** Stable
|
|
5
|
+
**Enforcement:** ESLint (`gallop/prefer-layout-components`)
|
|
6
|
+
|
|
7
|
+
## Summary
|
|
8
|
+
|
|
9
|
+
Use the `Grid` or `Columns` component instead of raw `<div>` elements with grid classes.
|
|
10
|
+
|
|
11
|
+
## Rationale
|
|
12
|
+
|
|
13
|
+
Raw `<div>` elements with grid classes bypass the design system's layout abstractions. The `Grid` and `Columns` components provide:
|
|
14
|
+
|
|
15
|
+
- **Consistent defaults** for gaps, columns, and alignment
|
|
16
|
+
- **Semantic intent** - code is more readable when intent is clear
|
|
17
|
+
- **Centralized updates** - layout defaults can be updated in one place
|
|
18
|
+
- **Reduced errors** - no need to remember all required grid classes
|
|
19
|
+
|
|
20
|
+
## Bad
|
|
21
|
+
|
|
22
|
+
```tsx
|
|
23
|
+
<div className="grid grid-cols-3 gap-6">
|
|
24
|
+
<Card>...</Card>
|
|
25
|
+
<Card>...</Card>
|
|
26
|
+
<Card>...</Card>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 items-center">
|
|
30
|
+
<div>Left content</div>
|
|
31
|
+
<div>Right content</div>
|
|
32
|
+
</div>
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Good
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
import { Grid, Columns, Column } from '@/components'
|
|
39
|
+
|
|
40
|
+
<Grid cols="grid-cols-3" gap="gap-6">
|
|
41
|
+
<Card>...</Card>
|
|
42
|
+
<Card>...</Card>
|
|
43
|
+
<Card>...</Card>
|
|
44
|
+
</Grid>
|
|
45
|
+
|
|
46
|
+
<Columns>
|
|
47
|
+
<Column>Left content</Column>
|
|
48
|
+
<Column>Right content</Column>
|
|
49
|
+
</Columns>
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Component Reference
|
|
53
|
+
|
|
54
|
+
### Grid
|
|
55
|
+
|
|
56
|
+
For multi-item grid layouts (3+ columns, card grids, galleries):
|
|
57
|
+
|
|
58
|
+
```tsx
|
|
59
|
+
<Grid
|
|
60
|
+
cols="grid-cols-1 lg:grid-cols-3" // optional, defaults to 1 → 3
|
|
61
|
+
gap="gap-6" // optional, has sensible default
|
|
62
|
+
>
|
|
63
|
+
{children}
|
|
64
|
+
</Grid>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Columns
|
|
68
|
+
|
|
69
|
+
For two-column layouts with optional reversal:
|
|
70
|
+
|
|
71
|
+
```tsx
|
|
72
|
+
<Columns
|
|
73
|
+
cols="grid-cols-1 lg:grid-cols-2" // optional
|
|
74
|
+
gap="gap-8" // optional
|
|
75
|
+
align="items-center" // optional
|
|
76
|
+
reverseColumns={true} // swap order on lg+
|
|
77
|
+
>
|
|
78
|
+
<Column>Left</Column>
|
|
79
|
+
<Column>Right</Column>
|
|
80
|
+
</Columns>
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Exceptions
|
|
84
|
+
|
|
85
|
+
The rule only applies to files in `/blocks/`. Component files may use raw divs with grid when building the layout primitives themselves.
|
package/schema.json
CHANGED
|
@@ -210,6 +210,16 @@
|
|
|
210
210
|
"enforcement": "documentation",
|
|
211
211
|
"rule": null,
|
|
212
212
|
"summary": "PageMetadata structure, structured data"
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
"id": "018",
|
|
216
|
+
"title": "Layout Components",
|
|
217
|
+
"file": "patterns/018-layout-components.md",
|
|
218
|
+
"category": "layout",
|
|
219
|
+
"status": "stable",
|
|
220
|
+
"enforcement": "eslint",
|
|
221
|
+
"rule": "gallop/prefer-layout-components",
|
|
222
|
+
"summary": "Use Grid/Columns, not raw div with grid"
|
|
213
223
|
}
|
|
214
224
|
],
|
|
215
225
|
"guarantees": [
|
|
@@ -239,7 +249,7 @@
|
|
|
239
249
|
"name": "Design System Compliance",
|
|
240
250
|
"since": "1.0.0",
|
|
241
251
|
"status": "stable",
|
|
242
|
-
"patterns": ["003", "004", "009", "010", "011"]
|
|
252
|
+
"patterns": ["003", "004", "009", "010", "011", "018"]
|
|
243
253
|
}
|
|
244
254
|
]
|
|
245
255
|
}
|