@gallop.software/canon 2.11.0 → 2.13.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/index.d.ts
CHANGED
|
@@ -30,6 +30,7 @@ declare const plugin: {
|
|
|
30
30
|
};
|
|
31
31
|
'no-native-intersection-observer': import("eslint").Rule.RuleModule;
|
|
32
32
|
'no-component-in-blocks': import("eslint").Rule.RuleModule;
|
|
33
|
+
'prefer-list-components': import("eslint").Rule.RuleModule;
|
|
33
34
|
};
|
|
34
35
|
/**
|
|
35
36
|
* Recommended rule configurations - spread into your ESLint config
|
|
@@ -48,6 +49,7 @@ declare const plugin: {
|
|
|
48
49
|
readonly 'gallop/no-data-imports': "warn";
|
|
49
50
|
readonly 'gallop/no-native-intersection-observer': "warn";
|
|
50
51
|
readonly 'gallop/no-component-in-blocks': "warn";
|
|
52
|
+
readonly 'gallop/prefer-list-components': "warn";
|
|
51
53
|
};
|
|
52
54
|
};
|
|
53
55
|
export default plugin;
|
package/dist/eslint/index.js
CHANGED
|
@@ -10,6 +10,7 @@ import noCrossZoneImports from './rules/no-cross-zone-imports.js';
|
|
|
10
10
|
import noDataImports from './rules/no-data-imports.js';
|
|
11
11
|
import noNativeIntersectionObserver from './rules/no-native-intersection-observer.js';
|
|
12
12
|
import noComponentInBlocks from './rules/no-component-in-blocks.js';
|
|
13
|
+
import preferListComponents from './rules/prefer-list-components.js';
|
|
13
14
|
/**
|
|
14
15
|
* All Canon ESLint rules with recommended severity levels
|
|
15
16
|
*/
|
|
@@ -26,11 +27,12 @@ const recommended = {
|
|
|
26
27
|
'gallop/no-data-imports': 'warn',
|
|
27
28
|
'gallop/no-native-intersection-observer': 'warn',
|
|
28
29
|
'gallop/no-component-in-blocks': 'warn',
|
|
30
|
+
'gallop/prefer-list-components': 'warn',
|
|
29
31
|
};
|
|
30
32
|
const plugin = {
|
|
31
33
|
meta: {
|
|
32
34
|
name: 'eslint-plugin-gallop',
|
|
33
|
-
version: '2.
|
|
35
|
+
version: '2.12.0',
|
|
34
36
|
},
|
|
35
37
|
rules: {
|
|
36
38
|
'no-client-blocks': noClientBlocks,
|
|
@@ -45,6 +47,7 @@ const plugin = {
|
|
|
45
47
|
'no-data-imports': noDataImports,
|
|
46
48
|
'no-native-intersection-observer': noNativeIntersectionObserver,
|
|
47
49
|
'no-component-in-blocks': noComponentInBlocks,
|
|
50
|
+
'prefer-list-components': preferListComponents,
|
|
48
51
|
},
|
|
49
52
|
/**
|
|
50
53
|
* Recommended rule configurations - spread into your ESLint config
|
|
@@ -53,4 +56,4 @@ const plugin = {
|
|
|
53
56
|
recommended,
|
|
54
57
|
};
|
|
55
58
|
export default plugin;
|
|
56
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
59
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvZXNsaW50L2luZGV4LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sY0FBYyxNQUFNLDZCQUE2QixDQUFBO0FBQ3hELE9BQU8sb0JBQW9CLE1BQU0sb0NBQW9DLENBQUE7QUFDckUsT0FBTyxvQkFBb0IsTUFBTSxtQ0FBbUMsQ0FBQTtBQUNwRSxPQUFPLDBCQUEwQixNQUFNLHlDQUF5QyxDQUFBO0FBQ2hGLE9BQU8sc0JBQXNCLE1BQU0scUNBQXFDLENBQUE7QUFDeEUsT0FBTyxzQkFBc0IsTUFBTSxxQ0FBcUMsQ0FBQTtBQUN4RSxPQUFPLGNBQWMsTUFBTSw2QkFBNkIsQ0FBQTtBQUN4RCxPQUFPLGlCQUFpQixNQUFNLGdDQUFnQyxDQUFBO0FBQzlELE9BQU8sa0JBQWtCLE1BQU0sa0NBQWtDLENBQUE7QUFDakUsT0FBTyxhQUFhLE1BQU0sNEJBQTRCLENBQUE7QUFDdEQsT0FBTyw0QkFBNEIsTUFBTSw0Q0FBNEMsQ0FBQTtBQUNyRixPQUFPLG1CQUFtQixNQUFNLG1DQUFtQyxDQUFBO0FBQ25FLE9BQU8sb0JBQW9CLE1BQU0sbUNBQW1DLENBQUE7QUFFcEU7O0dBRUc7QUFDSCxNQUFNLFdBQVcsR0FBRztJQUNsQix5QkFBeUIsRUFBRSxNQUFNO0lBQ2pDLGdDQUFnQyxFQUFFLE1BQU07SUFDeEMsK0JBQStCLEVBQUUsTUFBTTtJQUN2QyxxQ0FBcUMsRUFBRSxNQUFNO0lBQzdDLGlDQUFpQyxFQUFFLE1BQU07SUFDekMsaUNBQWlDLEVBQUUsTUFBTTtJQUN6Qyx5QkFBeUIsRUFBRSxNQUFNO0lBQ2pDLDRCQUE0QixFQUFFLE1BQU07SUFDcEMsOEJBQThCLEVBQUUsTUFBTTtJQUN0Qyx3QkFBd0IsRUFBRSxNQUFNO0lBQ2hDLHdDQUF3QyxFQUFFLE1BQU07SUFDaEQsK0JBQStCLEVBQUUsTUFBTTtJQUN2QywrQkFBK0IsRUFBRSxNQUFNO0NBQy9CLENBQUE7QUFFVixNQUFNLE1BQU0sR0FBRztJQUNiLElBQUksRUFBRTtRQUNKLElBQUksRUFBRSxzQkFBc0I7UUFDNUIsT0FBTyxFQUFFLFFBQVE7S0FDbEI7SUFDRCxLQUFLLEVBQUU7UUFDTCxrQkFBa0IsRUFBRSxjQUFjO1FBQ2xDLHlCQUF5QixFQUFFLG9CQUFvQjtRQUMvQyx3QkFBd0IsRUFBRSxvQkFBb0I7UUFDOUMsOEJBQThCLEVBQUUsMEJBQTBCO1FBQzFELDBCQUEwQixFQUFFLHNCQUFzQjtRQUNsRCwwQkFBMEIsRUFBRSxzQkFBc0I7UUFDbEQsa0JBQWtCLEVBQUUsY0FBYztRQUNsQyxxQkFBcUIsRUFBRSxpQkFBaUI7UUFDeEMsdUJBQXVCLEVBQUUsa0JBQWtCO1FBQzNDLGlCQUFpQixFQUFFLGFBQWE7UUFDaEMsaUNBQWlDLEVBQUUsNEJBQTRCO1FBQy9ELHdCQUF3QixFQUFFLG1CQUFtQjtRQUM3Qyx3QkFBd0IsRUFBRSxvQkFBb0I7S0FDL0M7SUFDRDs7O09BR0c7SUFDSCxXQUFXO0NBQ1osQ0FBQTtBQUVELGVBQWUsTUFBTSxDQUFBIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IG5vQ2xpZW50QmxvY2tzIGZyb20gJy4vcnVsZXMvbm8tY2xpZW50LWJsb2Nrcy5qcydcbmltcG9ydCBub0NvbnRhaW5lckluU2VjdGlvbiBmcm9tICcuL3J1bGVzL25vLWNvbnRhaW5lci1pbi1zZWN0aW9uLmpzJ1xuaW1wb3J0IHByZWZlckNvbXBvbmVudFByb3BzIGZyb20gJy4vcnVsZXMvcHJlZmVyLWNvbXBvbmVudC1wcm9wcy5qcydcbmltcG9ydCBwcmVmZXJUeXBvZ3JhcGh5Q29tcG9uZW50cyBmcm9tICcuL3J1bGVzL3ByZWZlci10eXBvZ3JhcGh5LWNvbXBvbmVudHMuanMnXG5pbXBvcnQgcHJlZmVyTGF5b3V0Q29tcG9uZW50cyBmcm9tICcuL3J1bGVzL3ByZWZlci1sYXlvdXQtY29tcG9uZW50cy5qcydcbmltcG9ydCBiYWNrZ3JvdW5kSW1hZ2VSb3VuZGVkIGZyb20gJy4vcnVsZXMvYmFja2dyb3VuZC1pbWFnZS1yb3VuZGVkLmpzJ1xuaW1wb3J0IG5vSW5saW5lU3R5bGVzIGZyb20gJy4vcnVsZXMvbm8taW5saW5lLXN0eWxlcy5qcydcbmltcG9ydCBub0FyYml0cmFyeUNvbG9ycyBmcm9tICcuL3J1bGVzL25vLWFyYml0cmFyeS1jb2xvcnMuanMnXG5pbXBvcnQgbm9Dcm9zc1pvbmVJbXBvcnRzIGZyb20gJy4vcnVsZXMvbm8tY3Jvc3Mtem9uZS1pbXBvcnRzLmpzJ1xuaW1wb3J0IG5vRGF0YUltcG9ydHMgZnJvbSAnLi9ydWxlcy9uby1kYXRhLWltcG9ydHMuanMnXG5pbXBvcnQgbm9OYXRpdmVJbnRlcnNlY3Rpb25PYnNlcnZlciBmcm9tICcuL3J1bGVzL25vLW5hdGl2ZS1pbnRlcnNlY3Rpb24tb2JzZXJ2ZXIuanMnXG5pbXBvcnQgbm9Db21wb25lbnRJbkJsb2NrcyBmcm9tICcuL3J1bGVzL25vLWNvbXBvbmVudC1pbi1ibG9ja3MuanMnXG5pbXBvcnQgcHJlZmVyTGlzdENvbXBvbmVudHMgZnJvbSAnLi9ydWxlcy9wcmVmZXItbGlzdC1jb21wb25lbnRzLmpzJ1xuXG4vKipcbiAqIEFsbCBDYW5vbiBFU0xpbnQgcnVsZXMgd2l0aCByZWNvbW1lbmRlZCBzZXZlcml0eSBsZXZlbHNcbiAqL1xuY29uc3QgcmVjb21tZW5kZWQgPSB7XG4gICdnYWxsb3Avbm8tY2xpZW50LWJsb2Nrcyc6ICd3YXJuJyxcbiAgJ2dhbGxvcC9uby1jb250YWluZXItaW4tc2VjdGlvbic6ICd3YXJuJyxcbiAgJ2dhbGxvcC9wcmVmZXItY29tcG9uZW50LXByb3BzJzogJ3dhcm4nLFxuICAnZ2FsbG9wL3ByZWZlci10eXBvZ3JhcGh5LWNvbXBvbmVudHMnOiAnd2FybicsXG4gICdnYWxsb3AvcHJlZmVyLWxheW91dC1jb21wb25lbnRzJzogJ3dhcm4nLFxuICAnZ2FsbG9wL2JhY2tncm91bmQtaW1hZ2Utcm91bmRlZCc6ICd3YXJuJyxcbiAgJ2dhbGxvcC9uby1pbmxpbmUtc3R5bGVzJzogJ3dhcm4nLFxuICAnZ2FsbG9wL25vLWFyYml0cmFyeS1jb2xvcnMnOiAnd2FybicsXG4gICdnYWxsb3Avbm8tY3Jvc3Mtem9uZS1pbXBvcnRzJzogJ3dhcm4nLFxuICAnZ2FsbG9wL25vLWRhdGEtaW1wb3J0cyc6ICd3YXJuJyxcbiAgJ2dhbGxvcC9uby1uYXRpdmUtaW50ZXJzZWN0aW9uLW9ic2VydmVyJzogJ3dhcm4nLFxuICAnZ2FsbG9wL25vLWNvbXBvbmVudC1pbi1ibG9ja3MnOiAnd2FybicsXG4gICdnYWxsb3AvcHJlZmVyLWxpc3QtY29tcG9uZW50cyc6ICd3YXJuJyxcbn0gYXMgY29uc3RcblxuY29uc3QgcGx1Z2luID0ge1xuICBtZXRhOiB7XG4gICAgbmFtZTogJ2VzbGludC1wbHVnaW4tZ2FsbG9wJyxcbiAgICB2ZXJzaW9uOiAnMi4xMi4wJyxcbiAgfSxcbiAgcnVsZXM6IHtcbiAgICAnbm8tY2xpZW50LWJsb2Nrcyc6IG5vQ2xpZW50QmxvY2tzLFxuICAgICduby1jb250YWluZXItaW4tc2VjdGlvbic6IG5vQ29udGFpbmVySW5TZWN0aW9uLFxuICAgICdwcmVmZXItY29tcG9uZW50LXByb3BzJzogcHJlZmVyQ29tcG9uZW50UHJvcHMsXG4gICAgJ3ByZWZlci10eXBvZ3JhcGh5LWNvbXBvbmVudHMnOiBwcmVmZXJUeXBvZ3JhcGh5Q29tcG9uZW50cyxcbiAgICAncHJlZmVyLWxheW91dC1jb21wb25lbnRzJzogcHJlZmVyTGF5b3V0Q29tcG9uZW50cyxcbiAgICAnYmFja2dyb3VuZC1pbWFnZS1yb3VuZGVkJzogYmFja2dyb3VuZEltYWdlUm91bmRlZCxcbiAgICAnbm8taW5saW5lLXN0eWxlcyc6IG5vSW5saW5lU3R5bGVzLFxuICAgICduby1hcmJpdHJhcnktY29sb3JzJzogbm9BcmJpdHJhcnlDb2xvcnMsXG4gICAgJ25vLWNyb3NzLXpvbmUtaW1wb3J0cyc6IG5vQ3Jvc3Nab25lSW1wb3J0cyxcbiAgICAnbm8tZGF0YS1pbXBvcnRzJzogbm9EYXRhSW1wb3J0cyxcbiAgICAnbm8tbmF0aXZlLWludGVyc2VjdGlvbi1vYnNlcnZlcic6IG5vTmF0aXZlSW50ZXJzZWN0aW9uT2JzZXJ2ZXIsXG4gICAgJ25vLWNvbXBvbmVudC1pbi1ibG9ja3MnOiBub0NvbXBvbmVudEluQmxvY2tzLFxuICAgICdwcmVmZXItbGlzdC1jb21wb25lbnRzJzogcHJlZmVyTGlzdENvbXBvbmVudHMsXG4gIH0sXG4gIC8qKlxuICAgKiBSZWNvbW1lbmRlZCBydWxlIGNvbmZpZ3VyYXRpb25zIC0gc3ByZWFkIGludG8geW91ciBFU0xpbnQgY29uZmlnXG4gICAqIEBleGFtcGxlIHJ1bGVzOiB7IC4uLmdhbGxvcC5yZWNvbW1lbmRlZCB9XG4gICAqL1xuICByZWNvbW1lbmRlZCxcbn1cblxuZXhwb3J0IGRlZmF1bHQgcGx1Z2luXG4iXX0=
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { getCanonUrl, getCanonPattern } from '../utils/canon.js';
|
|
2
|
+
const RULE_NAME = 'prefer-list-components';
|
|
3
|
+
const pattern = getCanonPattern(RULE_NAME);
|
|
4
|
+
const rule = {
|
|
5
|
+
meta: {
|
|
6
|
+
type: 'suggestion',
|
|
7
|
+
docs: {
|
|
8
|
+
description: pattern?.summary || 'Use List/Li, not raw ul/li tags',
|
|
9
|
+
recommended: true,
|
|
10
|
+
url: getCanonUrl(RULE_NAME),
|
|
11
|
+
},
|
|
12
|
+
messages: {
|
|
13
|
+
useList: `[Canon ${pattern?.id || '026'}] Use the List component instead of <ul>. Import: import { List } from "@/components"`,
|
|
14
|
+
useLi: `[Canon ${pattern?.id || '026'}] Use the Li component instead of <li>. Import: import { Li } from "@/components"`,
|
|
15
|
+
},
|
|
16
|
+
schema: [],
|
|
17
|
+
},
|
|
18
|
+
create(context) {
|
|
19
|
+
const filename = context.filename || context.getFilename();
|
|
20
|
+
// Only apply to block files
|
|
21
|
+
if (!filename.includes('/blocks/')) {
|
|
22
|
+
return {};
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
JSXOpeningElement(node) {
|
|
26
|
+
const elementName = node.name?.name;
|
|
27
|
+
// Check <ul> tags
|
|
28
|
+
if (elementName === 'ul') {
|
|
29
|
+
context.report({
|
|
30
|
+
node,
|
|
31
|
+
messageId: 'useList',
|
|
32
|
+
});
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
// Check <li> tags
|
|
36
|
+
if (elementName === 'li') {
|
|
37
|
+
context.report({
|
|
38
|
+
node,
|
|
39
|
+
messageId: 'useLi',
|
|
40
|
+
});
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
export default rule;
|
|
48
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicHJlZmVyLWxpc3QtY29tcG9uZW50cy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3NyYy9lc2xpbnQvcnVsZXMvcHJlZmVyLWxpc3QtY29tcG9uZW50cy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFDQSxPQUFPLEVBQUUsV0FBVyxFQUFFLGVBQWUsRUFBRSxNQUFNLG1CQUFtQixDQUFBO0FBRWhFLE1BQU0sU0FBUyxHQUFHLHdCQUF3QixDQUFBO0FBQzFDLE1BQU0sT0FBTyxHQUFHLGVBQWUsQ0FBQyxTQUFTLENBQUMsQ0FBQTtBQUUxQyxNQUFNLElBQUksR0FBb0I7SUFDNUIsSUFBSSxFQUFFO1FBQ0osSUFBSSxFQUFFLFlBQVk7UUFDbEIsSUFBSSxFQUFFO1lBQ0osV0FBVyxFQUFFLE9BQU8sRUFBRSxPQUFPLElBQUksaUNBQWlDO1lBQ2xFLFdBQVcsRUFBRSxJQUFJO1lBQ2pCLEdBQUcsRUFBRSxXQUFXLENBQUMsU0FBUyxDQUFDO1NBQzVCO1FBQ0QsUUFBUSxFQUFFO1lBQ1IsT0FBTyxFQUFFLFVBQVUsT0FBTyxFQUFFLEVBQUUsSUFBSSxLQUFLLHVGQUF1RjtZQUM5SCxLQUFLLEVBQUUsVUFBVSxPQUFPLEVBQUUsRUFBRSxJQUFJLEtBQUssbUZBQW1GO1NBQ3pIO1FBQ0QsTUFBTSxFQUFFLEVBQUU7S0FDWDtJQUVELE1BQU0sQ0FBQyxPQUFPO1FBQ1osTUFBTSxRQUFRLEdBQUcsT0FBTyxDQUFDLFFBQVEsSUFBSSxPQUFPLENBQUMsV0FBVyxFQUFFLENBQUE7UUFFMUQsNEJBQTRCO1FBQzVCLElBQUksQ0FBQyxRQUFRLENBQUMsUUFBUSxDQUFDLFVBQVUsQ0FBQyxFQUFFLENBQUM7WUFDbkMsT0FBTyxFQUFFLENBQUE7UUFDWCxDQUFDO1FBRUQsT0FBTztZQUNMLGlCQUFpQixDQUFDLElBQVM7Z0JBQ3pCLE1BQU0sV0FBVyxHQUFHLElBQUksQ0FBQyxJQUFJLEVBQUUsSUFBSSxDQUFBO2dCQUVuQyxrQkFBa0I7Z0JBQ2xCLElBQUksV0FBVyxLQUFLLElBQUksRUFBRSxDQUFDO29CQUN6QixPQUFPLENBQUMsTUFBTSxDQUFDO3dCQUNiLElBQUk7d0JBQ0osU0FBUyxFQUFFLFNBQVM7cUJBQ3JCLENBQUMsQ0FBQTtvQkFDRixPQUFNO2dCQUNSLENBQUM7Z0JBRUQsa0JBQWtCO2dCQUNsQixJQUFJLFdBQVcsS0FBSyxJQUFJLEVBQUUsQ0FBQztvQkFDekIsT0FBTyxDQUFDLE1BQU0sQ0FBQzt3QkFDYixJQUFJO3dCQUNKLFNBQVMsRUFBRSxPQUFPO3FCQUNuQixDQUFDLENBQUE7b0JBQ0YsT0FBTTtnQkFDUixDQUFDO1lBQ0gsQ0FBQztTQUNGLENBQUE7SUFDSCxDQUFDO0NBQ0YsQ0FBQTtBQUVELGVBQWUsSUFBSSxDQUFBIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IHR5cGUgeyBSdWxlIH0gZnJvbSAnZXNsaW50J1xuaW1wb3J0IHsgZ2V0Q2Fub25VcmwsIGdldENhbm9uUGF0dGVybiB9IGZyb20gJy4uL3V0aWxzL2Nhbm9uLmpzJ1xuXG5jb25zdCBSVUxFX05BTUUgPSAncHJlZmVyLWxpc3QtY29tcG9uZW50cydcbmNvbnN0IHBhdHRlcm4gPSBnZXRDYW5vblBhdHRlcm4oUlVMRV9OQU1FKVxuXG5jb25zdCBydWxlOiBSdWxlLlJ1bGVNb2R1bGUgPSB7XG4gIG1ldGE6IHtcbiAgICB0eXBlOiAnc3VnZ2VzdGlvbicsXG4gICAgZG9jczoge1xuICAgICAgZGVzY3JpcHRpb246IHBhdHRlcm4/LnN1bW1hcnkgfHwgJ1VzZSBMaXN0L0xpLCBub3QgcmF3IHVsL2xpIHRhZ3MnLFxuICAgICAgcmVjb21tZW5kZWQ6IHRydWUsXG4gICAgICB1cmw6IGdldENhbm9uVXJsKFJVTEVfTkFNRSksXG4gICAgfSxcbiAgICBtZXNzYWdlczoge1xuICAgICAgdXNlTGlzdDogYFtDYW5vbiAke3BhdHRlcm4/LmlkIHx8ICcwMjYnfV0gVXNlIHRoZSBMaXN0IGNvbXBvbmVudCBpbnN0ZWFkIG9mIDx1bD4uIEltcG9ydDogaW1wb3J0IHsgTGlzdCB9IGZyb20gXCJAL2NvbXBvbmVudHNcImAsXG4gICAgICB1c2VMaTogYFtDYW5vbiAke3BhdHRlcm4/LmlkIHx8ICcwMjYnfV0gVXNlIHRoZSBMaSBjb21wb25lbnQgaW5zdGVhZCBvZiA8bGk+LiBJbXBvcnQ6IGltcG9ydCB7IExpIH0gZnJvbSBcIkAvY29tcG9uZW50c1wiYCxcbiAgICB9LFxuICAgIHNjaGVtYTogW10sXG4gIH0sXG5cbiAgY3JlYXRlKGNvbnRleHQpIHtcbiAgICBjb25zdCBmaWxlbmFtZSA9IGNvbnRleHQuZmlsZW5hbWUgfHwgY29udGV4dC5nZXRGaWxlbmFtZSgpXG5cbiAgICAvLyBPbmx5IGFwcGx5IHRvIGJsb2NrIGZpbGVzXG4gICAgaWYgKCFmaWxlbmFtZS5pbmNsdWRlcygnL2Jsb2Nrcy8nKSkge1xuICAgICAgcmV0dXJuIHt9XG4gICAgfVxuXG4gICAgcmV0dXJuIHtcbiAgICAgIEpTWE9wZW5pbmdFbGVtZW50KG5vZGU6IGFueSkge1xuICAgICAgICBjb25zdCBlbGVtZW50TmFtZSA9IG5vZGUubmFtZT8ubmFtZVxuXG4gICAgICAgIC8vIENoZWNrIDx1bD4gdGFnc1xuICAgICAgICBpZiAoZWxlbWVudE5hbWUgPT09ICd1bCcpIHtcbiAgICAgICAgICBjb250ZXh0LnJlcG9ydCh7XG4gICAgICAgICAgICBub2RlLFxuICAgICAgICAgICAgbWVzc2FnZUlkOiAndXNlTGlzdCcsXG4gICAgICAgICAgfSlcbiAgICAgICAgICByZXR1cm5cbiAgICAgICAgfVxuXG4gICAgICAgIC8vIENoZWNrIDxsaT4gdGFnc1xuICAgICAgICBpZiAoZWxlbWVudE5hbWUgPT09ICdsaScpIHtcbiAgICAgICAgICBjb250ZXh0LnJlcG9ydCh7XG4gICAgICAgICAgICBub2RlLFxuICAgICAgICAgICAgbWVzc2FnZUlkOiAndXNlTGknLFxuICAgICAgICAgIH0pXG4gICAgICAgICAgcmV0dXJuXG4gICAgICAgIH1cbiAgICAgIH0sXG4gICAgfVxuICB9LFxufVxuXG5leHBvcnQgZGVmYXVsdCBydWxlXG4iXX0=
|
package/package.json
CHANGED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# Pattern 027: Luxon for Dates
|
|
2
|
+
|
|
3
|
+
**Canon Version:** 1.0
|
|
4
|
+
**Status:** Stable
|
|
5
|
+
**Category:** Components
|
|
6
|
+
**Enforcement:** Documentation
|
|
7
|
+
|
|
8
|
+
## Decision
|
|
9
|
+
|
|
10
|
+
Use Luxon's `DateTime` for all date and time operations. Do not use the native JavaScript `Date` object.
|
|
11
|
+
|
|
12
|
+
## Rationale
|
|
13
|
+
|
|
14
|
+
1. **Timezone consistency** — Native `Date` operates in the user's local timezone, causing inconsistencies when users in different timezones interact with the same dates. A user in California viewing a New York restaurant's reservation system would see incorrect "today" calculations.
|
|
15
|
+
2. **Explicit timezone handling** — Luxon's `.setZone()` method allows explicit timezone specification, ensuring dates are always interpreted in the intended timezone (e.g., the business's location).
|
|
16
|
+
3. **Immutable API** — Luxon DateTime objects are immutable, preventing accidental mutation bugs common with native Date.
|
|
17
|
+
4. **Better API** — Luxon provides clearer methods for formatting, parsing, and date arithmetic.
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
### Import
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
import { DateTime } from 'luxon'
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Get Current Time in Specific Timezone
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
// Get "now" in New York, regardless of user's location
|
|
31
|
+
const now = DateTime.now().setZone('America/New_York')
|
|
32
|
+
|
|
33
|
+
// Get start of "today" in business timezone
|
|
34
|
+
const today = DateTime.now().setZone('America/Chicago').startOf('day')
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Compare Dates in Timezone
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
// Check if a date is in the past (relative to business timezone)
|
|
41
|
+
function isDatePast(year: number, month: number, day: number, timezone: string) {
|
|
42
|
+
const date = DateTime.fromObject(
|
|
43
|
+
{ year, month: month + 1, day },
|
|
44
|
+
{ zone: timezone }
|
|
45
|
+
).startOf('day')
|
|
46
|
+
const today = DateTime.now().setZone(timezone).startOf('day')
|
|
47
|
+
return date < today
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Format Dates
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
// Format for display
|
|
55
|
+
const formatted = DateTime.now()
|
|
56
|
+
.setZone('America/New_York')
|
|
57
|
+
.toFormat('MMMM d, yyyy') // "January 19, 2026"
|
|
58
|
+
|
|
59
|
+
// Format for form submission (timezone-agnostic string)
|
|
60
|
+
const formValue = DateTime.fromObject({ year: 2026, month: 1, day: 19 })
|
|
61
|
+
.toFormat('yyyy-MM-dd') // "2026-01-19"
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Parse User Input
|
|
65
|
+
|
|
66
|
+
```tsx
|
|
67
|
+
// Parse a date string and interpret in business timezone
|
|
68
|
+
const date = DateTime.fromFormat('2026-01-19', 'yyyy-MM-dd', {
|
|
69
|
+
zone: 'America/New_York'
|
|
70
|
+
})
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Examples
|
|
74
|
+
|
|
75
|
+
### Good
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
import { DateTime } from 'luxon'
|
|
79
|
+
|
|
80
|
+
// Business timezone from config/props
|
|
81
|
+
const BUSINESS_TIMEZONE = 'America/Chicago'
|
|
82
|
+
|
|
83
|
+
// Get "today" in business timezone
|
|
84
|
+
const today = DateTime.now().setZone(BUSINESS_TIMEZONE).startOf('day')
|
|
85
|
+
|
|
86
|
+
// Check if user-selected date is valid
|
|
87
|
+
const selectedDate = DateTime.fromObject(
|
|
88
|
+
{ year: 2026, month: 1, day: 19 },
|
|
89
|
+
{ zone: BUSINESS_TIMEZONE }
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
if (selectedDate < today) {
|
|
93
|
+
console.log('Cannot select a date in the past')
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Bad
|
|
98
|
+
|
|
99
|
+
```tsx
|
|
100
|
+
// Native Date uses user's local timezone
|
|
101
|
+
const today = new Date()
|
|
102
|
+
today.setHours(0, 0, 0, 0) // Still in user's timezone!
|
|
103
|
+
|
|
104
|
+
// This will be wrong for users in different timezones
|
|
105
|
+
const selectedDate = new Date(2026, 0, 19)
|
|
106
|
+
if (selectedDate < today) {
|
|
107
|
+
console.log('This comparison is timezone-dependent!')
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Common Timezone Identifiers
|
|
112
|
+
|
|
113
|
+
| City | IANA Timezone |
|
|
114
|
+
|------|---------------|
|
|
115
|
+
| New York | `America/New_York` |
|
|
116
|
+
| Chicago / Dallas / Houston | `America/Chicago` |
|
|
117
|
+
| Denver | `America/Denver` |
|
|
118
|
+
| Los Angeles / San Francisco | `America/Los_Angeles` |
|
|
119
|
+
| London | `Europe/London` |
|
|
120
|
+
| Paris | `Europe/Paris` |
|
|
121
|
+
| Tokyo | `Asia/Tokyo` |
|
|
122
|
+
|
|
123
|
+
## Enforcement
|
|
124
|
+
|
|
125
|
+
- **Method:** Code review / documentation
|
|
126
|
+
- **Check:** Native `new Date()` should not be used for date comparisons or timezone-sensitive operations
|
|
127
|
+
|
|
128
|
+
## References
|
|
129
|
+
|
|
130
|
+
- Luxon Documentation: https://moment.github.io/luxon/
|
|
131
|
+
- IANA Timezone Database: https://www.iana.org/time-zones
|
|
132
|
+
- Form components use `timezone` prop for business location
|
package/schema.json
CHANGED
|
@@ -290,6 +290,26 @@
|
|
|
290
290
|
"enforcement": "eslint",
|
|
291
291
|
"rule": "gallop/no-component-in-blocks",
|
|
292
292
|
"summary": "Component functions must be in components folder, not blocks"
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
"id": "026",
|
|
296
|
+
"title": "List Components",
|
|
297
|
+
"file": "patterns/026-list-components.md",
|
|
298
|
+
"category": "components",
|
|
299
|
+
"status": "stable",
|
|
300
|
+
"enforcement": "eslint",
|
|
301
|
+
"rule": "gallop/prefer-list-components",
|
|
302
|
+
"summary": "Use List/Li, not raw ul/li tags"
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
"id": "027",
|
|
306
|
+
"title": "Luxon for Dates",
|
|
307
|
+
"file": "patterns/027-luxon-dates.md",
|
|
308
|
+
"category": "components",
|
|
309
|
+
"status": "stable",
|
|
310
|
+
"enforcement": "documentation",
|
|
311
|
+
"rule": null,
|
|
312
|
+
"summary": "Use Luxon DateTime, not native JavaScript Date"
|
|
293
313
|
}
|
|
294
314
|
],
|
|
295
315
|
"guarantees": [
|