@starci/eslint-canon-fe 9.2.0 → 9.4.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 (178) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +5 -1
  3. package/dist/brand-values.d.ts +28 -0
  4. package/dist/brand-values.js +172 -0
  5. package/dist/button-intent.d.ts +11 -0
  6. package/dist/button-intent.js +361 -0
  7. package/dist/callback-shape.d.ts +7 -0
  8. package/dist/callback-shape.js +396 -0
  9. package/dist/class-names.d.ts +19 -0
  10. package/dist/class-names.js +170 -0
  11. package/dist/client-boundary.d.ts +42 -0
  12. package/dist/client-boundary.js +404 -0
  13. package/dist/comments.d.ts +47 -0
  14. package/dist/comments.js +156 -0
  15. package/dist/control-height.d.ts +7 -0
  16. package/dist/control-height.js +83 -0
  17. package/dist/drawing.d.ts +31 -0
  18. package/dist/drawing.js +376 -0
  19. package/dist/env-owner.d.ts +22 -0
  20. package/dist/env-owner.js +291 -0
  21. package/dist/fe-stack.d.ts +10 -0
  22. package/dist/fe-stack.js +73 -0
  23. package/dist/file-layout.d.ts +71 -0
  24. package/dist/file-layout.js +465 -0
  25. package/dist/formatting.d.ts +25 -0
  26. package/dist/formatting.js +104 -0
  27. package/dist/function-budget.d.ts +9 -0
  28. package/dist/function-budget.js +48 -0
  29. package/dist/grammar-shim.d.ts +5 -0
  30. package/dist/grammar-shim.js +126 -0
  31. package/dist/hooks-folder.d.ts +34 -0
  32. package/dist/hooks-folder.js +320 -0
  33. package/dist/hygiene.d.ts +39 -0
  34. package/dist/hygiene.js +540 -0
  35. package/dist/index.d.ts +105 -0
  36. package/dist/index.js +310 -0
  37. package/{landmark.mjs → dist/landmark.d.ts} +4 -6
  38. package/dist/landmark.js +6 -0
  39. package/dist/lib/ast.d.ts +36 -0
  40. package/dist/lib/ast.js +94 -0
  41. package/dist/lib/bindings.d.ts +84 -0
  42. package/dist/lib/bindings.js +168 -0
  43. package/dist/lib/class-text.d.ts +3 -0
  44. package/dist/lib/class-text.js +30 -0
  45. package/dist/lib/config.d.ts +81 -0
  46. package/dist/lib/config.js +141 -0
  47. package/dist/lib/drawing-scope.d.ts +5 -0
  48. package/{lib/drawing-scope.mjs → dist/lib/drawing-scope.js} +6 -11
  49. package/dist/lib/eslint-boundary.d.ts +23 -0
  50. package/dist/lib/eslint-boundary.js +6 -0
  51. package/dist/lib/fe-stack.d.ts +49 -0
  52. package/dist/lib/fe-stack.js +98 -0
  53. package/dist/lib/hfs-view.d.ts +74 -0
  54. package/dist/lib/hfs-view.js +6 -0
  55. package/dist/lib/hfs.d.ts +34 -0
  56. package/dist/lib/hfs.js +72 -0
  57. package/dist/lib/next.d.ts +4 -0
  58. package/dist/lib/next.js +10 -0
  59. package/dist/lib/own-hook-origin.d.ts +17 -0
  60. package/dist/lib/own-hook-origin.js +52 -0
  61. package/dist/lib/package-root.d.ts +4 -0
  62. package/dist/lib/package-root.js +10 -0
  63. package/dist/lib/path.d.ts +2 -0
  64. package/{lib/path.mjs → dist/lib/path.js} +1 -1
  65. package/dist/lib/platform.d.ts +26 -0
  66. package/{lib/platform.mjs → dist/lib/platform.js} +38 -35
  67. package/dist/lib/program-for.d.ts +5 -0
  68. package/dist/lib/program-for.js +23 -0
  69. package/dist/lib/pure-owner-helper.d.ts +10 -0
  70. package/dist/lib/pure-owner-helper.js +156 -0
  71. package/dist/lib/react.d.ts +16 -0
  72. package/dist/lib/react.js +22 -0
  73. package/dist/lib/redux-hook-origin.d.ts +4 -0
  74. package/dist/lib/redux-hook-origin.js +50 -0
  75. package/dist/lib/rule.d.ts +28 -0
  76. package/dist/lib/rule.js +5 -0
  77. package/dist/lib/scope.d.ts +88 -0
  78. package/{lib/scope.mjs → dist/lib/scope.js} +83 -93
  79. package/dist/lib/typed-class-values.d.ts +13 -0
  80. package/dist/lib/typed-class-values.js +92 -0
  81. package/dist/lib/types.d.ts +44 -0
  82. package/dist/lib/types.js +64 -0
  83. package/dist/lib/why.d.ts +350 -0
  84. package/dist/lib/why.js +350 -0
  85. package/dist/lint-escape-hatch.d.ts +24 -0
  86. package/{lint-escape-hatch.mjs → dist/lint-escape-hatch.js} +38 -45
  87. package/dist/lists.d.ts +31 -0
  88. package/dist/lists.js +202 -0
  89. package/dist/loading.d.ts +34 -0
  90. package/dist/loading.js +157 -0
  91. package/dist/naming.d.ts +33 -0
  92. package/dist/naming.js +176 -0
  93. package/dist/native-controls.d.ts +24 -0
  94. package/dist/native-controls.js +105 -0
  95. package/dist/next-conventions.d.ts +48 -0
  96. package/dist/next-conventions.js +452 -0
  97. package/dist/project-graph.d.ts +5 -0
  98. package/{project-graph.mjs → dist/project-graph.js} +8 -12
  99. package/dist/props-and-slots.d.ts +19 -0
  100. package/dist/props-and-slots.js +320 -0
  101. package/dist/served-locale.d.ts +38 -0
  102. package/dist/served-locale.js +263 -0
  103. package/dist/shape-slot.d.ts +18 -0
  104. package/dist/shape-slot.js +324 -0
  105. package/dist/size-and-state-budget.d.ts +46 -0
  106. package/dist/size-and-state-budget.js +424 -0
  107. package/dist/size-growth.d.ts +24 -0
  108. package/dist/size-growth.js +64 -0
  109. package/dist/sonar-parity.d.ts +5 -0
  110. package/{sonar-parity.mjs → dist/sonar-parity.js} +11 -7
  111. package/dist/surface-rules.d.ts +18 -0
  112. package/dist/surface-rules.js +491 -0
  113. package/dist/swr-family.d.ts +15 -0
  114. package/dist/swr-family.js +419 -0
  115. package/dist/the-split.d.ts +27 -0
  116. package/dist/the-split.js +157 -0
  117. package/dist/tokens.d.ts +9 -0
  118. package/dist/tokens.js +35 -0
  119. package/dist/translation.d.ts +51 -0
  120. package/dist/translation.js +403 -0
  121. package/dist/transport.d.ts +73 -0
  122. package/dist/transport.js +885 -0
  123. package/dist/type-safety.d.ts +43 -0
  124. package/dist/type-safety.js +139 -0
  125. package/dist/ui-rulings.d.ts +11 -0
  126. package/dist/ui-rulings.js +121 -0
  127. package/package.json +31 -9
  128. package/runtime/knowledge/hfs/canon-pins.yaml +22 -14
  129. package/runtime/scripts/lib/package-at.mjs +5 -5
  130. package/brand-values.mjs +0 -172
  131. package/button-intent.mjs +0 -320
  132. package/callback-shape.mjs +0 -332
  133. package/class-names.mjs +0 -149
  134. package/client-boundary.mjs +0 -400
  135. package/comments.mjs +0 -159
  136. package/control-height.mjs +0 -77
  137. package/drawing.mjs +0 -315
  138. package/env-owner.mjs +0 -244
  139. package/fe-stack.mjs +0 -86
  140. package/file-layout.mjs +0 -472
  141. package/formatting.mjs +0 -110
  142. package/function-budget.mjs +0 -49
  143. package/grammar-shim.mjs +0 -123
  144. package/hooks-folder.mjs +0 -300
  145. package/hygiene.mjs +0 -513
  146. package/index.mjs +0 -317
  147. package/lib/ast.mjs +0 -97
  148. package/lib/bindings.mjs +0 -159
  149. package/lib/class-text.mjs +0 -29
  150. package/lib/config.mjs +0 -142
  151. package/lib/fe-stack.mjs +0 -77
  152. package/lib/hfs.mjs +0 -49
  153. package/lib/next.mjs +0 -6
  154. package/lib/own-hook-origin.mjs +0 -53
  155. package/lib/program-for.mjs +0 -22
  156. package/lib/pure-owner-helper.mjs +0 -148
  157. package/lib/react.mjs +0 -28
  158. package/lib/redux-hook-origin.mjs +0 -44
  159. package/lib/typed-class-values.mjs +0 -81
  160. package/lib/types.mjs +0 -63
  161. package/lib/why.mjs +0 -352
  162. package/lists.mjs +0 -193
  163. package/loading.mjs +0 -156
  164. package/naming.mjs +0 -185
  165. package/native-controls.mjs +0 -108
  166. package/next-conventions.mjs +0 -447
  167. package/props-and-slots.mjs +0 -276
  168. package/served-locale.mjs +0 -243
  169. package/size-and-state-budget.mjs +0 -376
  170. package/size-growth.mjs +0 -62
  171. package/surface-rules.mjs +0 -443
  172. package/swr-family.mjs +0 -326
  173. package/the-split.mjs +0 -155
  174. package/tokens.mjs +0 -39
  175. package/translation.mjs +0 -392
  176. package/transport.mjs +0 -830
  177. package/type-safety.mjs +0 -147
  178. package/ui-rulings.mjs +0 -134
package/surface-rules.mjs DELETED
@@ -1,443 +0,0 @@
1
- /** Grammar owns surface nesting and the single content edge of a panel list (ANATOMY-2, PADDING-0). */
2
- import { attributeValue, stringOf, walk } from "./lib/ast.mjs"
3
- import { functionOf, importOf, isFn, patternPath, unwrap, variableOf } from "./lib/bindings.mjs"
4
- import { typedClassStrings, typedPropertyStrings } from "./lib/typed-class-values.mjs"
5
- import { reactHookOf } from "./lib/react.mjs"
6
-
7
- const GRAMMAR_ENTRY = /^@starci\/grammar(?:\/(?:common|core|offset-pop))?$/
8
- const SURFACES = new Set([
9
- "SurfaceCard", "SurfaceListCard", "SurfaceAccordionCard", "Sidebar",
10
- "Dialog", "Drawer", "AlertDialog", "Popover",
11
- "WorkspaceShell", "TopBar", "NavigationFeatureNav",
12
- ])
13
- const FIELDS = new Set([
14
- "Input", "TextField", "Textarea", "TextArea", "SearchField", "NumberField", "DateField", "TimeField",
15
- "Select", "ComboBox", "DatePicker", "DateRangePicker", "Checkbox", "CheckboxGroup", "RadioGroup", "PressableField", "OtpInput",
16
- ])
17
- const CLASS_BUILDERS = new Set(["cn", "clsx", "classNames", "cva", "tv"])
18
-
19
- /** Resolve a JSX name to its Grammar export and optional compound parts, preserving lexical import identity. */
20
- const grammarName = (context, name) => {
21
- if (name.type === "JSXMemberExpression") {
22
- const owner = grammarName(context, name.object)
23
- return owner === null ? null : [...owner, name.property.name]
24
- }
25
- if (name.type !== "JSXIdentifier") return null
26
- const imported = importOf(context, name)
27
- if (!imported || !GRAMMAR_ENTRY.test(imported.source)) return null
28
- if (imported.imported !== null) return [imported.imported]
29
- const binding = variableOf(context, name)?.defs.find((definition) => definition.type === "ImportBinding")
30
- return binding?.node.type === "ImportNamespaceSpecifier" ? [] : null
31
- }
32
-
33
- /** Class and variant expressions expose their statically authored strings, including const aliases and cn branches. */
34
- const authoredStrings = (context, input, seen = new Set(), mode = "classes") => {
35
- const node = unwrap(input?.type === "JSXExpressionContainer" ? input.expression : input)
36
- if (!node || seen.has(node)) return []
37
- seen.add(node)
38
- const text = stringOf(node)
39
- if (text !== null) return [{ node, text }]
40
- if (node.type === "TemplateElement") return [{ node, text: node.value.cooked ?? node.value.raw }]
41
- if (node.type === "Identifier") {
42
- const values = bindingValues(context, node)
43
- return values.length ? values.flatMap((value) => authoredStrings(context, value, seen, mode)) : typedClassStrings(context, node, mode)
44
- }
45
- if (node.type === "MemberExpression") {
46
- const propertyName = node.computed ? stringOf(node.property) : node.property.name
47
- const values = propertyValues(context, node.object, propertyName)
48
- return values.length ? values.flatMap((value) => authoredStrings(context, value, seen, mode)) : typedClassStrings(context, node, mode)
49
- }
50
- const children = stringChildren(node, mode)
51
- return children.flatMap((child) => authoredStrings(context, child, seen, mode))
52
- }
53
-
54
- /** Only value-bearing positions contribute classes; object keys support cn's conditional class dictionary. */
55
- const stringChildren = (node, mode) => {
56
- if (node.type === "TemplateLiteral") return mode === "classes" ? [...node.quasis, ...node.expressions] : []
57
- if (node.type === "ArrayExpression") return node.elements
58
- if (node.type === "CallExpression") return mode === "classes" ? node.arguments : []
59
- if (node.type === "ConditionalExpression") return [node.consequent, node.alternate]
60
- if (node.type === "LogicalExpression") return node.operator === "&&" ? [node.right] : [node.left, node.right]
61
- if (node.type === "ObjectExpression") return node.properties.flatMap((property) => property.type === "Property" ? [property.key, property.value] : [property.argument])
62
- return []
63
- }
64
-
65
- /** A declared React component is a separate render tree; inline collection/render callbacks keep the surrounding context. */
66
- const componentBoundary = (context, node) => {
67
- if (!isFn(node)) return false
68
- if (node.parent?.type === "CallExpression" && ["memo", "forwardRef"].includes(reactHookOf(context, node.parent))) return true
69
- if (node.parent?.type === "JSXExpressionContainer" || node.parent?.type === "CallExpression" && node.parent.arguments.includes(node)) return false
70
- const name = node.id?.name ?? (node.parent?.type === "VariableDeclarator" ? node.parent.id.name : null)
71
- return name !== null && /^[A-Z]/.test(name)
72
- }
73
-
74
- /** The Grammar ancestors whose rendered content contains this node, excluding an overlay's trigger. */
75
- function* surfaceAncestors(context, node) {
76
- const triggers = new Set()
77
- for (let current = node.parent; current; current = current.parent) {
78
- if (componentBoundary(context, current)) return
79
- if (current.type === "JSXAttribute" && current.name.name === "trigger") triggers.add(current.parent)
80
- if (current.type === "JSXElement" && !triggers.has(current.openingElement)) yield current.openingElement
81
- }
82
- }
83
-
84
- const isSurface = (context, node, descendant) => {
85
- const name = grammarName(context, node.name)
86
- if (name?.length !== 1) return false
87
- if (name[0] === "SurfaceAccordionCard") {
88
- let frame = "painted"
89
- for (const entry of node.attributes) {
90
- const value = entry.type === "JSXSpreadAttribute" ? frameEffect(context, entry.argument, frame)
91
- : entry.name.name === "frame" ? unpaintedFrame(context, attributeValue(entry)) ? "unpainted" : "painted" : "absent"
92
- if (value !== "absent") frame = value
93
- }
94
- return frame !== "unpainted"
95
- }
96
- if (name[0] === "WorkspaceShell") {
97
- for (let current = descendant; current && current !== node.parent; current = current.parent) {
98
- if (current.type === "JSXAttribute" && current.parent === node && current.name.name === "header") return true
99
- }
100
- return false
101
- }
102
- return SURFACES.has(name[0])
103
- }
104
- const classTokens = (text) => text.split(/\s+/).map((token) => {
105
- let depth = 0
106
- let start = 0
107
- for (let index = 0; index < token.length; index++) {
108
- if (token[index] === "[") depth++
109
- else if (token[index] === "]") depth--
110
- else if (token[index] === ":" && depth === 0) start = index + 1
111
- }
112
- return token.slice(start).replace(/^!|!$/g, "")
113
- })
114
- /** Paint and typography belong to Grammar even when the app uses a token rather than a raw colour. */
115
- const paintProperty = (name) => {
116
- const property = String(name).replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`).replace(/^-(?:webkit|moz|ms)-/, "")
117
- if (/^text-(?:align|overflow|wrap)(?:-|$)/.test(property)) return false
118
- return /^(?:background|border|outline|font|text|fill|stroke|mask|color|accent-color|caret-color)(?:-|$)/.test(property)
119
- || /^(?:opacity|box-shadow|filter|backdrop-filter|mix-blend-mode|appearance|letter-spacing|line-height|clip-path)$/.test(property)
120
- }
121
-
122
- const rawSurface = (token) => {
123
- const arbitrary = /^\[([^:]+):/.exec(token)
124
- if (arbitrary) return paintProperty(arbitrary[1])
125
- if (/^text-(?:left|right|center|justify|start|end|wrap|nowrap|balance|pretty|ellipsis|clip)$/.test(token)) return false
126
- return /^-?(?:bg|from|via|to|shadow|inset-shadow|text-shadow|ring|inset-ring|outline|border|divide|rounded|text|font|leading|tracking|fill|stroke|mask|accent|caret|decoration|opacity|mix-blend|bg-blend|blur|brightness|contrast|drop-shadow|grayscale|hue-rotate|invert|saturate|sepia|backdrop|filter|appearance)(?:-|$)/.test(token)
127
- || /^(?:underline|overline|line-through|no-underline|italic|not-italic|antialiased|subpixel-antialiased)$/.test(token)
128
- }
129
- const horizontalPadding = (token) => /^(?:p|px|ps|pe|pl|pr)-.+$/.test(token) || /^\[padding-inline(?:-start|-end)?:.+\]$/.test(token)
130
-
131
- /** Computed keys select their evaluated static name, rather than the identifier's spelling. */
132
- const propertyName = (context, property) => {
133
- if (!property.computed) return property.key.name ?? property.key.value
134
- const values = authoredStrings(context, property.key, new Set(), "variant")
135
- return values.length === 1 ? values[0].text : null
136
- }
137
-
138
- /** A binding selects its own object property; sibling declarations do not become its value. */
139
- const bindingValues = (context, node, seen = new Set()) => {
140
- const definition = variableOf(context, node)?.defs.find((entry) => entry.type === "Variable")
141
- if (!definition) return []
142
- const path = patternPath(definition.node.id, node.name)
143
- if (path === null) return []
144
- return path.reduce((values, key) => values.flatMap((value) => propertyValues(context, value, key, seen)), [definition.node.init]).filter(Boolean)
145
- }
146
-
147
- /** Static object spreads retain the same published-prop obligations as ordinary JSX attributes. */
148
- const propertyValues = (context, input, key, seen = new Set()) => {
149
- const node = unwrap(input)
150
- if (!node || seen.has(node)) return []
151
- seen.add(node)
152
- if (node.type === "Identifier") {
153
- return bindingValues(context, node, seen).flatMap((value) => propertyValues(context, value, key, seen))
154
- }
155
- if (node.type === "MemberExpression") {
156
- const member = node.computed ? stringOf(node.property) : node.property.name
157
- return propertyValues(context, node.object, member, seen).flatMap((value) => propertyValues(context, value, key, seen))
158
- }
159
- if (node.type === "ConditionalExpression") return [node.consequent, node.alternate].flatMap((branch) => propertyValues(context, branch, key, seen))
160
- if (node.type !== "ObjectExpression") return []
161
- return node.properties.flatMap((property) => {
162
- if (property.type === "SpreadElement") return propertyValues(context, property.argument, key, seen)
163
- return propertyName(context, property) === key ? [property.value] : []
164
- })
165
- }
166
-
167
- const propStrings = (context, entry, name, mode = "classes") => {
168
- if (entry.type === "JSXSpreadAttribute") {
169
- const values = propertyValues(context, entry.argument, name)
170
- return values.length ? values.flatMap((value) => authoredStrings(context, value, new Set(), mode)) : typedPropertyStrings(context, entry.argument, name, mode)
171
- }
172
- return entry.name.name === name ? authoredStrings(context, attributeValue(entry), new Set(), mode) : []
173
- }
174
-
175
- /** Only a fully known unpainted frame removes the accordion's bounded surface context. */
176
- const unpaintedFrame = (context, input, seen = new Set()) => {
177
- const node = unwrap(input)
178
- if (!node || seen.has(node)) return false
179
- seen.add(node)
180
- const text = stringOf(node)
181
- if (text !== null) return text === "unpainted"
182
- if (node.type === "ConditionalExpression") return [node.consequent, node.alternate].every((branch) => unpaintedFrame(context, branch, new Set(seen)))
183
- const values = node.type === "Identifier" ? bindingValues(context, node)
184
- : node.type === "MemberExpression" ? propertyValues(context, node.object, node.computed ? stringOf(node.property) : node.property.name) : []
185
- return values.length > 0 && values.every((value) => unpaintedFrame(context, value, new Set(seen)))
186
- }
187
-
188
- /** A spread overrides preceding frame props; an unresolved spread may install a bounded frame. */
189
- const frameEffect = (context, input, prior, seen = new Set()) => {
190
- const node = unwrap(input)
191
- if (!node || seen.has(node)) return "painted"
192
- seen.add(node)
193
- if (node.type === "ObjectExpression") {
194
- let effect = prior
195
- for (const property of node.properties) {
196
- const key = property.type === "Property" ? propertyName(context, property) : null
197
- const value = property.type === "SpreadElement" ? frameEffect(context, property.argument, effect, new Set(seen))
198
- : key === "frame" ? unpaintedFrame(context, property.value) ? "unpainted" : "painted" : key === null ? "painted" : "absent"
199
- if (value !== "absent") effect = value
200
- }
201
- return effect
202
- }
203
- const values = node.type === "Identifier" ? bindingValues(context, node)
204
- : node.type === "MemberExpression" ? propertyValues(context, node.object, node.computed ? stringOf(node.property) : node.property.name)
205
- : node.type === "ConditionalExpression" ? [node.consequent, node.alternate] : []
206
- const effects = values.map((value) => frameEffect(context, value, prior, new Set(seen)))
207
- return effects.length > 0 && effects.every((effect) => effect === effects[0]) ? effects[0] : "painted"
208
- }
209
-
210
- /** Resolve authored style objects without treating unrelated object siblings or conditions as CSS. */
211
- const stylePaint = (context, input, seen = new Set()) => {
212
- const node = unwrap(input?.type === "JSXExpressionContainer" ? input.expression : input)
213
- if (!node || seen.has(node)) return []
214
- seen.add(node)
215
- if (node.type === "Identifier") {
216
- const values = bindingValues(context, node)
217
- return values.length ? values.flatMap((value) => stylePaint(context, value, seen))
218
- : typedClassStrings(context, node, "style-keys").filter((entry) => paintProperty(entry.text)).map((entry) => entry.node)
219
- }
220
- if (node.type === "MemberExpression") {
221
- const member = node.computed ? stringOf(node.property) : node.property.name
222
- const values = propertyValues(context, node.object, member)
223
- return values.length ? values.flatMap((value) => stylePaint(context, value, seen))
224
- : typedClassStrings(context, node, "style-keys").filter((entry) => paintProperty(entry.text)).map((entry) => entry.node)
225
- }
226
- if (node.type === "ConditionalExpression") return [node.consequent, node.alternate].flatMap((branch) => stylePaint(context, branch, seen))
227
- if (node.type === "LogicalExpression") return (node.operator === "&&" ? [node.right] : [node.left, node.right]).flatMap((branch) => stylePaint(context, branch, seen))
228
- if (node.type !== "ObjectExpression") return []
229
- return node.properties.flatMap((property) => {
230
- if (property.type === "SpreadElement") return stylePaint(context, property.argument, seen)
231
- return paintProperty(propertyName(context, property)) ? [property] : []
232
- })
233
- }
234
-
235
- /** An explicit section caption excludes the row's own content subtree. */
236
- const inSectionCaption = (context, node) => {
237
- for (const ancestor of surfaceAncestors(context, node.parent)) {
238
- if (isSurface(context, ancestor, node)) return false
239
- const owner = grammarName(context, ancestor.name)
240
- if (owner?.[0] !== "ListBox") continue
241
- if (owner[1] === "Item") return false
242
- if (["Section", "Header", "SectionHeader"].includes(owner[1])) return true
243
- }
244
- return false
245
- }
246
-
247
- /** ANATOMY-2: field variants belong to Grammar's context, including across app component boundaries. */
248
- export const nestedControlVariant = {
249
- meta: {
250
- type: "problem",
251
- docs: { description: "ANATOMY-2: Grammar context supplies field variants; nested Tabs cannot force primary." },
252
- schema: [],
253
- messages: {
254
- variant: "FE_NESTED_VARIANT: app fields cannot set a static variant. Omit variant so Grammar's surface context supplies the appropriate default.",
255
- tabs: "FE_NESTED_VARIANT: Tabs inside a Grammar surface cannot force variant=\"primary\". Omit variant so Grammar supplies secondary.",
256
- },
257
- },
258
- create(context) {
259
- return {
260
- JSXOpeningElement(node) {
261
- const name = grammarName(context, node.name)
262
- if (name?.length !== 1) return
263
- if (FIELDS.has(name[0])) {
264
- const variant = node.attributes.find((entry) => propStrings(context, entry, "variant", "variant").length > 0)
265
- if (variant) context.report({ node: variant, messageId: "variant" })
266
- } else if (name[0] === "Tabs") {
267
- const primary = node.attributes.find((entry) => propStrings(context, entry, "variant", "variant").some((value) => value.text === "primary"))
268
- if (primary && [...surfaceAncestors(context, node.parent)].some((ancestor) => isSurface(context, ancestor, node))) context.report({ node: primary, messageId: "tabs" })
269
- }
270
- },
271
- }
272
- },
273
- }
274
-
275
- /** App classes carry layout; Grammar owns surface paint. */
276
- export const noAppSurfacePaint = {
277
- meta: {
278
- type: "problem",
279
- docs: { description: "App composition CSS is layout-only; Grammar paints surfaces." },
280
- schema: [],
281
- messages: { surface: "FE_GRAMMAR_LAYOUT_ONLY: app CSS paints or styles Grammar content. Keep composition CSS layout-only and use Grammar's published surfaces, typography and tone props." },
282
- },
283
- create(context) {
284
- const reported = new Set()
285
- const checkClassEntries = (entries) => {
286
- for (const entry of entries) {
287
- if (!classTokens(entry.text).some(rawSurface) || reported.has(entry.node)) continue
288
- reported.add(entry.node)
289
- context.report({ node: entry.node, messageId: "surface" })
290
- }
291
- }
292
- const checkClasses = (value) => checkClassEntries(authoredStrings(context, value))
293
- const reportStyle = (nodes) => {
294
- for (const node of nodes) {
295
- if (reported.has(node)) continue
296
- reported.add(node)
297
- context.report({ node, messageId: "surface" })
298
- }
299
- }
300
- const checkStyle = (value) => {
301
- reportStyle(stylePaint(context, value))
302
- }
303
- return {
304
- JSXAttribute(node) {
305
- if (["className", "class", "classNames"].includes(node.name.name)) checkClasses(node.value)
306
- if (node.name.name === "style") checkStyle(node.value)
307
- },
308
- JSXSpreadAttribute(node) {
309
- for (const name of ["className", "class", "classNames"]) checkClassEntries(propStrings(context, node, name))
310
- const values = propertyValues(context, node.argument, "style")
311
- if (values.length) values.forEach(checkStyle)
312
- else reportStyle(typedPropertyStrings(context, node.argument, "style", "style-keys").filter((entry) => paintProperty(entry.text)).map((entry) => entry.node))
313
- },
314
- CallExpression(node) {
315
- if (node.callee.type === "Identifier" && CLASS_BUILDERS.has(node.callee.name)) checkClasses(node)
316
- },
317
- VariableDeclarator(node) {
318
- if (/[Cc]lass/.test(node.id.name ?? "") || /(?:^|[/\\])classNames\.[cm]?[jt]sx?$/.test(context.filename)) checkClasses(node.init)
319
- },
320
- Property(node) {
321
- if (["className", "class", "classNames"].includes(propertyName(context, node))) checkClasses(node.value)
322
- if (propertyName(context, node) === "style") checkStyle(node.value)
323
- },
324
- }
325
- },
326
- }
327
-
328
- /** PADDING-0: app padding wrappers cannot add a second horizontal inset to Grammar's list or section caption. */
329
- export const noListPanelPadding = {
330
- meta: {
331
- type: "problem",
332
- docs: { description: "PADDING-0: panel lists and section captions share the Grammar surface's content edge." },
333
- schema: [],
334
- messages: { padding: "FE_LIST_PANEL_PADDING: horizontal padding stacks with the Grammar list inset. Remove this wrapper padding and use ListBox inset=\"panel\" so rows and section captions share the panel content edge." },
335
- },
336
- create(context) {
337
- const reported = new Set()
338
- const check = (opening) => {
339
- const className = opening.attributes.find((entry) => ["className", "class"].some((name) => propStrings(context, entry, name).some((value) => classTokens(value.text).some(horizontalPadding))))
340
- if (!className || reported.has(className)) return
341
- reported.add(className)
342
- context.report({ node: className, messageId: "padding" })
343
- }
344
- return {
345
- JSXOpeningElement(node) {
346
- const ancestors = [...surfaceAncestors(context, node.parent)]
347
- const surfaceIndex = ancestors.findIndex((ancestor) => isSurface(context, ancestor, node))
348
- if (surfaceIndex < 0) return
349
- const name = grammarName(context, node.name)
350
- if (name?.[0] === "ListBox" && (name.length === 1 || ["Section", "Header", "SectionHeader"].includes(name[1]))) {
351
- check(node)
352
- for (const ancestor of ancestors.slice(0, surfaceIndex + 1)) check(ancestor)
353
- } else if (inSectionCaption(context, node)) check(node)
354
- },
355
- }
356
- },
357
- }
358
-
359
- /** BOUNDARY-1 case-5: only content bands carry inset; horizontal separators reach the surface edges. */
360
- export const dividerFullBleed = {
361
- meta: {
362
- type: "problem",
363
- docs: {
364
- description:
365
- "BOUNDARY-1 case-5: horizontal Grammar dividers are full-bleed.",
366
- },
367
- schema: [],
368
- messages: {
369
- padding:
370
- "FE_DIVIDER_FULL_BLEED: horizontal padding on this wrapper insets a Grammar Divider/Separator. Move padding to the sibling content bands so the divider reaches both surface edges.",
371
- },
372
- },
373
- create(context) {
374
- // Follow rendered same-file components, helper calls and JSX constants, with cycle protection.
375
- const containsDivider = (root, seen = new Set()) => {
376
- if (!root || seen.has(root)) return false
377
- seen.add(root)
378
- let found = false
379
- const follow = (node) => {
380
- found = containsDivider(node, seen) || found
381
- }
382
- walk(context.sourceCode, root, (node) => {
383
- if (found) return false
384
- if (isFn(node)) return false
385
- if (node.type === "JSXOpeningElement") {
386
- const name = grammarName(context, node.name)
387
- if (
388
- name?.length === 1 &&
389
- ["Divider", "Separator"].includes(name[0])
390
- ) {
391
- const orientations = node.attributes.flatMap((entry) =>
392
- propStrings(context, entry, "orientation", "variant"),
393
- )
394
- if (
395
- !orientations.length ||
396
- orientations.some((entry) => entry.text !== "vertical")
397
- )
398
- found = true
399
- } else if (node.name.type === "JSXIdentifier") {
400
- for (const definition of variableOf(context, node.name)?.defs ??
401
- []) {
402
- if (definition.type === "FunctionName")
403
- follow(definition.node.body)
404
- if (definition.type === "Variable")
405
- follow(functionOf(context, definition.node.init)?.body)
406
- }
407
- }
408
- }
409
- if (node.type === "CallExpression") {
410
- follow(functionOf(context, node.callee)?.body)
411
- for (const argument of node.arguments)
412
- if (isFn(argument)) follow(argument.body)
413
- }
414
- if (
415
- node.type === "Identifier" &&
416
- node.parent?.type === "JSXExpressionContainer"
417
- ) {
418
- for (const value of bindingValues(context, node)) follow(value)
419
- }
420
- return undefined
421
- })
422
- return found
423
- }
424
- return {
425
- JSXOpeningElement(node) {
426
- const padding = node.attributes.find((entry) =>
427
- propStrings(context, entry, "className").some((value) =>
428
- classTokens(value.text).some(horizontalPadding),
429
- ),
430
- )
431
- if (!padding) return
432
- if (node.parent.children.some((child) => containsDivider(child)))
433
- context.report({ node: padding, messageId: "padding" })
434
- },
435
- }
436
- },
437
- }
438
-
439
- /** The surface laws ship at error in every composing source tree. */
440
- export const rules = { "divider-full-bleed": dividerFullBleed, "nested-control-variant": nestedControlVariant, "no-list-panel-padding": noListPanelPadding, "no-app-surface-paint": noAppSurfacePaint }
441
-
442
- /** No app can lower or omit a surface law. */
443
- export const recommended = Object.fromEntries(Object.keys(rules).map((name) => [`starci-fe/${name}`, "error"]))