@tempoxyz/ds 0.3.11 → 0.3.13

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 (212) hide show
  1. package/README.md +7 -6
  2. package/dist/docs/AGENTS.md +8 -5
  3. package/dist/docs/index.md +143 -135
  4. package/dist/docs/marketing/introduction/getting-started.md +241 -0
  5. package/dist/docs/{components → product/components}/accordion.md +8 -8
  6. package/dist/docs/{components → product/components}/address-card.md +5 -5
  7. package/dist/docs/{components → product/components}/agent-logo.md +5 -5
  8. package/dist/docs/{components → product/components}/alert-dialog.md +11 -11
  9. package/dist/docs/{components → product/components}/alert.md +11 -11
  10. package/dist/docs/{components → product/components}/amount-input.md +13 -13
  11. package/dist/docs/{components → product/components}/amount.md +8 -8
  12. package/dist/docs/{components → product/components}/app-shell.md +13 -13
  13. package/dist/docs/{components → product/components}/asset-chip.md +11 -11
  14. package/dist/docs/{components → product/components}/autocomplete.md +10 -9
  15. package/dist/docs/{components → product/components}/avatar.md +8 -8
  16. package/dist/docs/{components → product/components}/badge.md +9 -9
  17. package/dist/docs/{components → product/components}/badged-logo.md +6 -6
  18. package/dist/docs/{components → product/components}/banner.md +8 -8
  19. package/dist/docs/{components → product/components}/bar-chart.md +7 -7
  20. package/dist/docs/{components → product/components}/breadcrumbs.md +8 -8
  21. package/dist/docs/{components → product/components}/button-group.md +8 -8
  22. package/dist/docs/{components → product/components}/button.md +17 -17
  23. package/dist/docs/{components → product/components}/calendar.md +8 -8
  24. package/dist/docs/{components → product/components}/callout.md +5 -5
  25. package/dist/docs/{components → product/components}/checkbox-group.md +13 -13
  26. package/dist/docs/{components → product/components}/checkbox.md +12 -12
  27. package/dist/docs/{components → product/components}/choice-group.md +14 -14
  28. package/dist/docs/{components → product/components}/choicebox.md +9 -9
  29. package/dist/docs/{components → product/components}/code.md +5 -5
  30. package/dist/docs/{components → product/components}/combobox.md +9 -9
  31. package/dist/docs/{components → product/components}/container.md +7 -7
  32. package/dist/docs/{components → product/components}/copy-button.md +13 -13
  33. package/dist/docs/{components → product/components}/date-picker.md +11 -11
  34. package/dist/docs/{components → product/components}/dialog.md +17 -17
  35. package/dist/docs/{components → product/components}/drawer.md +11 -11
  36. package/dist/docs/{components → product/components}/empty.md +9 -9
  37. package/dist/docs/{components → product/components}/field.md +14 -14
  38. package/dist/docs/{components → product/components}/fieldset.md +6 -6
  39. package/dist/docs/{components → product/components}/frame.md +9 -9
  40. package/dist/docs/{components → product/components}/gauge.md +7 -7
  41. package/dist/docs/{components → product/components}/get-started-visual.md +4 -4
  42. package/dist/docs/{components → product/components}/grid.md +7 -7
  43. package/dist/docs/{components → product/components}/hero-visual.md +4 -4
  44. package/dist/docs/{components → product/components}/hex.md +9 -9
  45. package/dist/docs/{components → product/components}/icon-tile.md +8 -8
  46. package/dist/docs/{components → product/components}/identicon.md +7 -7
  47. package/dist/docs/{components → product/components}/inline.md +7 -7
  48. package/dist/docs/{components → product/components}/inset.md +8 -8
  49. package/dist/docs/{components → product/components}/kbd.md +8 -8
  50. package/dist/docs/{components → product/components}/list-group.md +24 -24
  51. package/dist/docs/{components → product/components}/loading-dots.md +8 -8
  52. package/dist/docs/{components → product/components}/menu.md +14 -14
  53. package/dist/docs/{components → product/components}/native-select.md +7 -7
  54. package/dist/docs/{components → product/components}/network-logo.md +7 -7
  55. package/dist/docs/{components → product/components}/number-field.md +8 -8
  56. package/dist/docs/{components → product/components}/otp-field.md +9 -9
  57. package/dist/docs/{components → product/components}/pagination.md +8 -8
  58. package/dist/docs/{components → product/components}/popover.md +11 -11
  59. package/dist/docs/{components → product/components}/progress.md +10 -10
  60. package/dist/docs/{components → product/components}/qr.md +8 -8
  61. package/dist/docs/{components → product/components}/radio-group.md +10 -10
  62. package/dist/docs/{components → product/components}/secret-value.md +8 -8
  63. package/dist/docs/{components → product/components}/select.md +13 -13
  64. package/dist/docs/{components → product/components}/separator.md +7 -7
  65. package/dist/docs/{components → product/components}/sidebar-menu.md +11 -11
  66. package/dist/docs/{components → product/components}/skeleton.md +9 -9
  67. package/dist/docs/{components → product/components}/slider.md +8 -8
  68. package/dist/docs/{components → product/components}/snippet.md +10 -10
  69. package/dist/docs/{components → product/components}/spinner.md +12 -12
  70. package/dist/docs/{components → product/components}/split-bar.md +5 -5
  71. package/dist/docs/{components → product/components}/split-button.md +11 -11
  72. package/dist/docs/{components → product/components}/stack.md +8 -8
  73. package/dist/docs/{components → product/components}/status-chip.md +7 -7
  74. package/dist/docs/{components → product/components}/status-indicator.md +6 -6
  75. package/dist/docs/{components → product/components}/status-row.md +7 -7
  76. package/dist/docs/{components → product/components}/stepper.md +9 -9
  77. package/dist/docs/{components → product/components}/success-hero.md +5 -5
  78. package/dist/docs/{components → product/components}/surface.md +11 -11
  79. package/dist/docs/{components → product/components}/switch.md +11 -11
  80. package/dist/docs/{components → product/components}/table.md +16 -16
  81. package/dist/docs/{components → product/components}/tabs.md +11 -11
  82. package/dist/docs/{components → product/components}/tempo-logo.md +4 -4
  83. package/dist/docs/{components → product/components}/text-input.md +14 -14
  84. package/dist/docs/{components → product/components}/textarea.md +7 -7
  85. package/dist/docs/{components → product/components}/toast.md +11 -11
  86. package/dist/docs/{components → product/components}/token-logo.md +16 -16
  87. package/dist/docs/{components → product/components}/token.md +10 -10
  88. package/dist/docs/{components → product/components}/tooltip.md +11 -11
  89. package/dist/docs/{components → product/components}/verification-code.md +4 -4
  90. package/dist/docs/{components → product/components}/visual.md +5 -5
  91. package/dist/docs/product/components.md +203 -0
  92. package/dist/docs/{foundations → product/foundations}/accessibility.md +16 -16
  93. package/dist/docs/{foundations → product/foundations}/agents.md +17 -17
  94. package/dist/docs/{foundations → product/foundations}/color.md +7 -7
  95. package/dist/docs/{foundations → product/foundations}/content/tone.md +7 -7
  96. package/dist/docs/{foundations → product/foundations}/content/voice.md +2 -2
  97. package/dist/docs/{foundations → product/foundations}/content/writing-style.md +8 -8
  98. package/dist/docs/{foundations → product/foundations}/iconography.md +11 -11
  99. package/dist/docs/{foundations → product/foundations}/intent.md +28 -28
  100. package/dist/docs/{foundations → product/foundations}/layout.md +22 -22
  101. package/dist/docs/product/foundations/linter.md +496 -0
  102. package/dist/docs/{foundations → product/foundations}/motion.md +7 -7
  103. package/dist/docs/{foundations → product/foundations}/radius-and-padding.md +3 -3
  104. package/dist/docs/{foundations → product/foundations}/typography.md +4 -4
  105. package/dist/docs/{introduction → product/introduction}/faq.md +22 -21
  106. package/dist/docs/{introduction → product/introduction}/getting-started.md +4 -4
  107. package/dist/docs/{patterns → product/patterns}/blockchain-values.md +20 -20
  108. package/dist/docs/{patterns → product/patterns}/dashboards.md +28 -28
  109. package/dist/docs/{patterns → product/patterns}/destructive-actions.md +16 -16
  110. package/dist/docs/{patterns → product/patterns}/empty-states.md +14 -14
  111. package/dist/docs/{patterns → product/patterns}/errors-and-validation.md +23 -23
  112. package/dist/docs/{patterns → product/patterns}/feedback-and-status.md +31 -31
  113. package/dist/docs/{patterns → product/patterns}/forms.md +24 -24
  114. package/dist/docs/{patterns → product/patterns}/loading.md +19 -19
  115. package/dist/docs/{patterns → product/patterns}/navigation.md +23 -23
  116. package/dist/docs/{patterns → product/patterns}/payments.md +20 -20
  117. package/dist/docs/{patterns → product/patterns}/responsive-layout.md +23 -23
  118. package/dist/docs/{patterns → product/patterns}/tables-and-lists.md +19 -19
  119. package/dist/docs/{products → product/products}/tempo-console.md +12 -12
  120. package/dist/docs/{products → product/products}/tempo-performance.md +11 -11
  121. package/dist/docs/{products → product/products}/tempo-wallet.md +12 -12
  122. package/dist/marketing/index.d.ts +4 -0
  123. package/dist/marketing/index.d.ts.map +1 -0
  124. package/dist/marketing/index.js +4 -0
  125. package/dist/marketing/index.js.map +1 -0
  126. package/dist/marketing/index.js.zyzz.json +1 -0
  127. package/dist/marketing/vars.d.ts +907 -0
  128. package/dist/marketing/vars.d.ts.map +1 -0
  129. package/dist/marketing/vars.js +15 -0
  130. package/dist/marketing/vars.js.map +1 -0
  131. package/dist/marketing/vars.js.zyzz.json +1 -0
  132. package/dist/marketing/zyzz.config.d.ts +4512 -0
  133. package/dist/marketing/zyzz.config.d.ts.map +1 -0
  134. package/dist/marketing/zyzz.config.js +63 -0
  135. package/dist/marketing/zyzz.config.js.map +1 -0
  136. package/dist/marketing/zyzz.config.js.zyzz.json +1 -0
  137. package/dist/oxlint/index.d.ts +12 -1
  138. package/dist/oxlint/index.d.ts.map +1 -1
  139. package/dist/oxlint/index.js +39 -1
  140. package/dist/oxlint/index.js.map +1 -1
  141. package/dist/oxlint/internal/Docs.d.ts +9 -0
  142. package/dist/oxlint/internal/Docs.d.ts.map +1 -0
  143. package/dist/oxlint/internal/Docs.js +15 -0
  144. package/dist/oxlint/internal/Docs.js.map +1 -0
  145. package/dist/oxlint/internal/Imports.d.ts +1 -1
  146. package/dist/oxlint/internal/Imports.d.ts.map +1 -1
  147. package/dist/oxlint/internal/Imports.js +2 -0
  148. package/dist/oxlint/internal/Imports.js.map +1 -1
  149. package/dist/oxlint/internal/Styles.d.ts +40 -0
  150. package/dist/oxlint/internal/Styles.d.ts.map +1 -0
  151. package/dist/oxlint/internal/Styles.js +194 -0
  152. package/dist/oxlint/internal/Styles.js.map +1 -0
  153. package/dist/oxlint/internal/Tokens.d.ts +16 -0
  154. package/dist/oxlint/internal/Tokens.d.ts.map +1 -0
  155. package/dist/oxlint/internal/Tokens.js +39 -0
  156. package/dist/oxlint/internal/Tokens.js.map +1 -0
  157. package/dist/oxlint/rules/NoCrossSurface.d.ts +2 -2
  158. package/dist/oxlint/rules/NoCrossSurface.js +3 -3
  159. package/dist/oxlint/rules/NoCrossSurface.js.map +1 -1
  160. package/dist/oxlint/rules/NoCustomValues.d.ts +11 -0
  161. package/dist/oxlint/rules/NoCustomValues.d.ts.map +1 -0
  162. package/dist/oxlint/rules/NoCustomValues.js +150 -0
  163. package/dist/oxlint/rules/NoCustomValues.js.map +1 -0
  164. package/dist/oxlint/rules/NoSingleKeyShortcuts.d.ts +11 -0
  165. package/dist/oxlint/rules/NoSingleKeyShortcuts.d.ts.map +1 -0
  166. package/dist/oxlint/rules/NoSingleKeyShortcuts.js +158 -0
  167. package/dist/oxlint/rules/NoSingleKeyShortcuts.js.map +1 -0
  168. package/dist/oxlint/rules/PreferComponent.d.ts +3 -2
  169. package/dist/oxlint/rules/PreferComponent.d.ts.map +1 -1
  170. package/dist/oxlint/rules/PreferComponent.js +11 -4
  171. package/dist/oxlint/rules/PreferComponent.js.map +1 -1
  172. package/dist/oxlint/rules/PreferLayout.d.ts +9 -0
  173. package/dist/oxlint/rules/PreferLayout.d.ts.map +1 -0
  174. package/dist/oxlint/rules/PreferLayout.js +90 -0
  175. package/dist/oxlint/rules/PreferLayout.js.map +1 -0
  176. package/dist/oxlint/rules/PreferProgress.d.ts +9 -0
  177. package/dist/oxlint/rules/PreferProgress.d.ts.map +1 -0
  178. package/dist/oxlint/rules/PreferProgress.js +43 -0
  179. package/dist/oxlint/rules/PreferProgress.js.map +1 -0
  180. package/dist/oxlint/rules/PreferSurface.d.ts +9 -0
  181. package/dist/oxlint/rules/PreferSurface.d.ts.map +1 -0
  182. package/dist/oxlint/rules/PreferSurface.js +55 -0
  183. package/dist/oxlint/rules/PreferSurface.js.map +1 -0
  184. package/dist/oxlint/rules/RequireAccessibleName.d.ts.map +1 -1
  185. package/dist/oxlint/rules/RequireAccessibleName.js +3 -1
  186. package/dist/oxlint/rules/RequireAccessibleName.js.map +1 -1
  187. package/dist/oxlint/rules/RequireScrollRegion.d.ts +11 -0
  188. package/dist/oxlint/rules/RequireScrollRegion.d.ts.map +1 -0
  189. package/dist/oxlint/rules/RequireScrollRegion.js +71 -0
  190. package/dist/oxlint/rules/RequireScrollRegion.js.map +1 -0
  191. package/dist/web/icons/index.js.zyzz.json +1 -0
  192. package/package.json +6 -1
  193. package/src/marketing/README.md +21 -0
  194. package/src/marketing/index.ts +3 -0
  195. package/src/marketing/vars.ts +751 -0
  196. package/src/marketing/zyzz.config.ts +376 -0
  197. package/src/oxlint/index.ts +40 -1
  198. package/src/oxlint/internal/Docs.ts +18 -0
  199. package/src/oxlint/internal/Imports.ts +2 -1
  200. package/src/oxlint/internal/Styles.ts +240 -0
  201. package/src/oxlint/internal/Tokens.ts +43 -0
  202. package/src/oxlint/rules/NoCrossSurface.ts +3 -3
  203. package/src/oxlint/rules/NoCustomValues.ts +170 -0
  204. package/src/oxlint/rules/NoSingleKeyShortcuts.ts +190 -0
  205. package/src/oxlint/rules/PreferComponent.ts +11 -4
  206. package/src/oxlint/rules/PreferLayout.ts +96 -0
  207. package/src/oxlint/rules/PreferProgress.ts +49 -0
  208. package/src/oxlint/rules/PreferSurface.ts +70 -0
  209. package/src/oxlint/rules/RequireAccessibleName.ts +3 -1
  210. package/src/oxlint/rules/RequireScrollRegion.ts +92 -0
  211. package/dist/docs/components.md +0 -203
  212. package/dist/docs/foundations/linter.md +0 -296
@@ -0,0 +1,240 @@
1
+ /** Reads the style declarations that native JSX elements apply in a file. @module */
2
+ import type { ESTree, Visitor } from '@oxlint/plugins'
3
+
4
+ /** Declarations keyed by CSS property, with each value as written. */
5
+ export type Declarations = ReadonlyMap<string, ESTree.Node>
6
+
7
+ /** A native element, such as a `div`, and the declarations it applies. */
8
+ export type Element = {
9
+ /**
10
+ * Declarations from the `style()` and `variants()` definitions that its spreads call, then
11
+ * from its inline `style` prop, which wins.
12
+ */
13
+ declarations: Declarations
14
+ /** The opening element. */
15
+ node: ESTree.JSXOpeningElement
16
+ /** Whether a `className` or a spread that is not a known style call can add declarations. */
17
+ open: boolean
18
+ /** Whether a spread that is not a known style call can add attributes. */
19
+ spread: boolean
20
+ /** The tag name, such as `div`. */
21
+ tag: string
22
+ }
23
+
24
+ type Pending = {
25
+ calls: readonly string[]
26
+ inline: Declarations
27
+ node: ESTree.JSXOpeningElement
28
+ open: boolean
29
+ spread: boolean
30
+ tag: string
31
+ }
32
+
33
+ /** Returns the Zyzz helper that a callee names, `style` or `variants`, directly or as a member of a config. */
34
+ export function helper(node: ESTree.Node): 'style' | 'variants' | undefined {
35
+ const name = (() => {
36
+ if (node.type === 'Identifier') return node.name
37
+ if (node.type === 'MemberExpression' && !node.computed && node.property.type === 'Identifier')
38
+ return node.property.name
39
+ return undefined
40
+ })()
41
+
42
+ return name === 'style' || name === 'variants' ? name : undefined
43
+ }
44
+
45
+ /** Returns the property name of a non-computed object key. */
46
+ export function key(property: ESTree.Node): string | undefined {
47
+ if (property.type !== 'Property' || property.computed) return undefined
48
+ if (property.key.type === 'Identifier') return property.key.name
49
+ if (property.key.type === 'Literal') return String(property.key.value)
50
+ return undefined
51
+ }
52
+
53
+ /**
54
+ * Returns the text of a literal value without its `!custom` suffix, such as `16px` for
55
+ * `'16px !custom'` and `2` for `2`. Returns `undefined` for a computed value.
56
+ */
57
+ export function text(node: ESTree.Node | undefined): string | undefined {
58
+ if (node?.type === 'UnaryExpression' && node.operator === '-') {
59
+ const value = text(node.argument)
60
+
61
+ return value === undefined ? undefined : `-${value}`
62
+ }
63
+ if (
64
+ node?.type === 'Literal' &&
65
+ (typeof node.value === 'number' || typeof node.value === 'string')
66
+ )
67
+ return String(node.value)
68
+ .replace(/\s*!custom$/, '')
69
+ .trim()
70
+ if (node?.type === 'TemplateLiteral' && node.expressions.length === 0)
71
+ return node.quasis
72
+ .map((quasi) => quasi.value.cooked ?? '')
73
+ .join('')
74
+ .replace(/\s*!custom$/, '')
75
+ .trim()
76
+ return undefined
77
+ }
78
+
79
+ /**
80
+ * Returns a visitor that calls `fn` once the file is read, with each native element outside a
81
+ * `render` prop. A spread resolves when it calls a `style()` or `variants()` definition in the
82
+ * same file by name, such as `{...styles.card()}` for `styles.card` in `namespace styles`.
83
+ */
84
+ export function visit(fn: visit.Callback): Visitor {
85
+ const definitions = new Map<string, Declarations>()
86
+ const pending: Pending[] = []
87
+
88
+ return {
89
+ CallExpression(node) {
90
+ const kind = helper(node.callee)
91
+ const argument = node.arguments[0]
92
+
93
+ if (!kind || argument?.type !== 'ObjectExpression') return
94
+
95
+ const name = path(node)
96
+
97
+ if (name) definitions.set(name, kind === 'style' ? read(argument) : variants(argument))
98
+ },
99
+ JSXOpeningElement(node) {
100
+ if (node.name.type !== 'JSXIdentifier' || !/^[a-z]/.test(node.name.name)) return
101
+
102
+ for (let parent: ESTree.Node | null = node.parent; parent; parent = parent.parent)
103
+ if (
104
+ parent.type === 'JSXAttribute' &&
105
+ parent.name.type === 'JSXIdentifier' &&
106
+ parent.name.name === 'render'
107
+ )
108
+ return
109
+
110
+ const calls: string[] = []
111
+ const inline = new Map<string, ESTree.Node>()
112
+ let open = false
113
+ let spread = false
114
+
115
+ for (const attribute of node.attributes) {
116
+ if (attribute.type === 'JSXSpreadAttribute') {
117
+ const call =
118
+ attribute.argument.type === 'CallExpression'
119
+ ? name(attribute.argument.callee)
120
+ : undefined
121
+
122
+ if (call) calls.push(call)
123
+ else spread = true
124
+ continue
125
+ }
126
+ if (attribute.name.type !== 'JSXIdentifier') continue
127
+ if (attribute.name.name === 'className') open = true
128
+ if (attribute.name.name !== 'style' || attribute.value?.type !== 'JSXExpressionContainer')
129
+ continue
130
+
131
+ const expression = attribute.value.expression
132
+ const objects = expression.type === 'ArrayExpression' ? expression.elements : [expression]
133
+
134
+ for (const object of objects)
135
+ if (object?.type === 'ObjectExpression')
136
+ for (const [field, value] of read(object)) inline.set(field, value)
137
+ }
138
+
139
+ pending.push({ calls, inline, node, open, spread, tag: node.name.name })
140
+ },
141
+ 'Program:exit'() {
142
+ for (const element of pending) {
143
+ const declarations = new Map<string, ESTree.Node>()
144
+ let open = element.open || element.spread
145
+ let spread = element.spread
146
+
147
+ for (const call of element.calls) {
148
+ const definition = definitions.get(call)
149
+
150
+ if (!definition) {
151
+ open = true
152
+ spread = true
153
+ continue
154
+ }
155
+ for (const [field, value] of definition) declarations.set(field, value)
156
+ }
157
+ for (const [field, value] of element.inline) declarations.set(field, value)
158
+ fn({ declarations, node: element.node, open, spread, tag: element.tag })
159
+ }
160
+ },
161
+ }
162
+ }
163
+
164
+ export declare namespace visit {
165
+ /** Receives each native element once the whole file is read. */
166
+ type Callback = (element: Element) => void
167
+ }
168
+
169
+ // An identifier or a chain of identifiers, such as `styles.card`.
170
+ function name(node: ESTree.Node): string | undefined {
171
+ if (node.type === 'Identifier') return node.name
172
+ if (node.type !== 'MemberExpression' || node.computed || node.property.type !== 'Identifier')
173
+ return undefined
174
+
175
+ const object = name(node.object)
176
+
177
+ return object === undefined ? undefined : `${object}.${node.property.name}`
178
+ }
179
+
180
+ // The name a definition is applied by: its variable, its object keys, and its namespaces.
181
+ function path(node: ESTree.Node): string | undefined {
182
+ const parts: string[] = []
183
+ let current: ESTree.Node = node
184
+
185
+ for (let parent = current.parent; parent; current = parent, parent = parent.parent) {
186
+ if (parent.type === 'Property') {
187
+ const property = key(parent)
188
+
189
+ if (property === undefined || parent.value !== current) return undefined
190
+ parts.unshift(property)
191
+ continue
192
+ }
193
+ if (parent.type === 'ObjectExpression') continue
194
+ if (parent.type !== 'VariableDeclarator' || parent.id.type !== 'Identifier') return undefined
195
+ parts.unshift(parent.id.name)
196
+ break
197
+ }
198
+ if (parts.length === 0) return undefined
199
+ for (let parent = current.parent; parent; parent = parent.parent)
200
+ if (parent.type === 'TSModuleDeclaration' && parent.id.type === 'Identifier')
201
+ parts.unshift(parent.id.name)
202
+ return parts.join('.')
203
+ }
204
+
205
+ // Top-level declarations. Nested objects, such as `selectors` and queries, apply conditionally.
206
+ function read(object: ESTree.ObjectExpression): Declarations {
207
+ const declarations = new Map<string, ESTree.Node>()
208
+
209
+ for (const property of object.properties) {
210
+ const field = key(property)
211
+
212
+ if (field === undefined || property.type !== 'Property') continue
213
+ if (property.value.type === 'ObjectExpression') continue
214
+ declarations.set(field, property.value)
215
+ }
216
+ return declarations
217
+ }
218
+
219
+ // The base declarations of a recipe, then the declarations of every option of every axis.
220
+ function variants(object: ESTree.ObjectExpression): Declarations {
221
+ const declarations = new Map<string, ESTree.Node>()
222
+
223
+ for (const property of object.properties) {
224
+ if (property.type !== 'Property' || property.value.type !== 'ObjectExpression') continue
225
+
226
+ const section = key(property)
227
+
228
+ if (section === 'base')
229
+ for (const [field, value] of read(property.value)) declarations.set(field, value)
230
+ if (section !== 'variants') continue
231
+ for (const axis of property.value.properties) {
232
+ if (axis.type !== 'Property' || axis.value.type !== 'ObjectExpression') continue
233
+ for (const option of axis.value.properties)
234
+ if (option.type === 'Property' && option.value.type === 'ObjectExpression')
235
+ for (const [field, value] of read(option.value))
236
+ if (!declarations.has(field)) declarations.set(field, value)
237
+ }
238
+ }
239
+ return declarations
240
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * The Web token scales that lint messages suggest, as `src/web/vars.ts` defines them. The published
3
+ * `vars` module compiles to an empty object, so the plugin keeps its own copy of these scales.
4
+ * @module
5
+ */
6
+
7
+ /** Container widths, keyed by their size in px. */
8
+ export const container: readonly number[] = [
9
+ 256, 288, 320, 384, 448, 512, 576, 672, 768, 896, 1024, 1152, 1280,
10
+ ]
11
+
12
+ /** Returns the token key of a spacing or container size in px, such as `0-5` for 0.5. */
13
+ export function key(px: number): string {
14
+ return String(px).replace('.', '-')
15
+ }
16
+
17
+ /** Corner radii in px, by Platform name. `full` takes the spacing step `999`. */
18
+ export const radius: Readonly<Record<string, number>> = {
19
+ '2xl': 64,
20
+ '2xs': 8,
21
+ '3xs': 4,
22
+ full: 999,
23
+ lg: 32,
24
+ md: 24,
25
+ none: 0,
26
+ sm: 16,
27
+ xl: 40,
28
+ xs: 12,
29
+ }
30
+
31
+ /** Spacing steps in px. A key writes a fraction with a hyphen, such as `0-5` for 0.5px. */
32
+ export const spacing: readonly number[] = [
33
+ -24, -20, -16, -12, -10, -8, -6, -4, 0, 0.5, 1, 1.5, 2, 3, 4, 6, 8, 10, 12, 16, 20, 24, 28, 32,
34
+ 36, 40, 44, 48, 52, 56, 60, 64, 72, 80, 100, 120, 140, 160, 180, 200, 240, 280, 320, 360, 999,
35
+ ]
36
+
37
+ /** Stacking layers, from the app shell overlay up to toasts. */
38
+ export const zIndex: Readonly<Record<string, number>> = {
39
+ modal: 60,
40
+ overlay: 40,
41
+ popover: 50,
42
+ toast: 1000,
43
+ }
@@ -3,8 +3,8 @@ import type { ESTree, Rule } from '@oxlint/plugins'
3
3
  import * as Imports from '../internal/Imports.ts'
4
4
 
5
5
  /**
6
- * Reports an import from the other surface. With `surface` set, every import from the other surface
7
- * is reported. Without it, the rule reports a file that imports from both surfaces.
6
+ * Reports an import from another surface. With `surface` set, every import from another surface
7
+ * is reported. Without it, the rule reports a file that imports from more than one surface.
8
8
  */
9
9
  export const rule: Rule = {
10
10
  meta: {
@@ -18,7 +18,7 @@ export const rule: Rule = {
18
18
  schema: [
19
19
  {
20
20
  additionalProperties: false,
21
- properties: { surface: { enum: ['mobile', 'web'], type: 'string' } },
21
+ properties: { surface: { enum: ['marketing', 'mobile', 'web'], type: 'string' } },
22
22
  type: 'object',
23
23
  },
24
24
  ],
@@ -0,0 +1,170 @@
1
+ /** Finds `!custom` lengths and layers in Zyzz styles that equal a Web token. @module */
2
+ import type { ESTree, Rule } from '@oxlint/plugins'
3
+ import * as Docs from '../internal/Docs.ts'
4
+ import * as Styles from '../internal/Styles.ts'
5
+ import * as Tokens from '../internal/Tokens.ts'
6
+
7
+ // Sizes that also accept a container width.
8
+ const containers =
9
+ /^(?:flexBasis|inlineSize|max(?:InlineSize|Width)|min(?:InlineSize|Width)|width)$/
10
+
11
+ // The longhands that set each part of a shorthand with two, three, or four parts.
12
+ const sides: Record<number, readonly string[]> = {
13
+ 2: ['Block', 'Inline'],
14
+ 3: ['Top', 'Inline', 'Bottom'],
15
+ 4: ['Top', 'Right', 'Bottom', 'Left'],
16
+ }
17
+
18
+ // Sizes and offsets, which accept spacing steps.
19
+ const sizes =
20
+ /^(?:blockSize|bottom|flexBasis|height|inlineSize|inset\w*|left|max(?:BlockSize|Height|InlineSize|Width)|min(?:BlockSize|Height|InlineSize|Width)|right|textIndent|top|width)$/
21
+
22
+ // Properties on the spacing scale.
23
+ const spacing = /^(?:columnGap|gap|margin\w*|padding\w*|rowGap|scrollMargin\w*|scrollPadding\w*)$/
24
+
25
+ const urls = {
26
+ container: Docs.page('foundations/layout#widths'),
27
+ radius: Docs.page('foundations/radius-and-padding'),
28
+ spacing: Docs.page('foundations/layout#spacing'),
29
+ zIndex: Docs.page('foundations/linter#use-tokens-for-custom-values'),
30
+ }
31
+
32
+ // A length in px, or `undefined` for any other value.
33
+ function px(value: string) {
34
+ const match = /^(-?(?:\d*\.)?\d+)(px|rem)$/.exec(value)
35
+
36
+ if (!match?.[1]) return undefined
37
+ return Number(match[1]) * (match[2] === 'rem' ? 16 : 1)
38
+ }
39
+
40
+ /**
41
+ * Reports a `!custom` value in a `style()` or `variants()` definition that equals a Web token:
42
+ * a px or rem length on a spacing, radius, size, or container property that equals a step of its
43
+ * scale, and a number on `zIndex` that equals a layer. The message names the token to use.
44
+ * A `!custom` value that no token equals, such as `'13px !custom'`, is the sanctioned escape
45
+ * hatch and does not report.
46
+ */
47
+ export const rule: Rule = {
48
+ meta: {
49
+ docs: { description: 'Use the Web token that a `!custom` length or layer equals.' },
50
+ messages: {
51
+ token:
52
+ "'{{property}}' sets the custom value {{value}}, which equals a token. Use {{tokens}}. See {{url}}",
53
+ },
54
+ schema: [],
55
+ type: 'suggestion',
56
+ },
57
+ create(context) {
58
+ // The advice for a property and its value, or `undefined` when no token replaces it.
59
+ function advise(property: string, value: string) {
60
+ if (property === 'zIndex') {
61
+ if (!/^-?\d+$/.test(value)) return undefined
62
+
63
+ const layer = Object.entries(Tokens.zIndex).find((entry) => entry[1] === Number(value))
64
+
65
+ if (!layer) return undefined
66
+ return { tokens: `the zIndex token '${layer[0]}'`, url: urls.zIndex }
67
+ }
68
+ if (property.endsWith('Radius')) {
69
+ const length = px(value)
70
+
71
+ if (length === undefined) return undefined
72
+
73
+ const step = Object.entries(Tokens.radius).find((entry) => entry[1] === length)
74
+
75
+ if (!step) return undefined
76
+ return { tokens: `the radius token '${step[0]}'`, url: urls.radius }
77
+ }
78
+ if (spacing.test(property)) {
79
+ const parts = value.split(/\s+/)
80
+ const lengths = parts.map((part) => (part === 'auto' ? 'auto' : px(part)))
81
+
82
+ if (lengths.some((length) => length === undefined)) return undefined
83
+ if (!lengths.some((length) => typeof length === 'number' && length !== 0)) return undefined
84
+ if (
85
+ lengths.some((length) => typeof length === 'number' && !Tokens.spacing.includes(length))
86
+ )
87
+ return undefined
88
+
89
+ const keys = lengths.map((length) =>
90
+ typeof length === 'number' ? Tokens.key(length) : 'auto',
91
+ )
92
+ const longhands = (() => {
93
+ if (property === 'gap') return ['rowGap', 'columnGap']
94
+
95
+ const suffixes = sides[parts.length]
96
+
97
+ return suffixes?.map((suffix) => `${property}${suffix}`)
98
+ })()
99
+
100
+ if (parts.length === 1)
101
+ return { tokens: `the spacing token '${keys[0]}'`, url: urls.spacing }
102
+ if (!longhands || longhands.length !== parts.length) return undefined
103
+ return {
104
+ tokens: `spacing tokens on each side: ${longhands.map((longhand, index) => `${longhand} '${keys[index]}'`).join(', ')}`,
105
+ url: urls.spacing,
106
+ }
107
+ }
108
+ if (sizes.test(property)) {
109
+ const length = px(value)
110
+
111
+ if (length === undefined || length === 0) return undefined
112
+ if (Tokens.spacing.includes(length))
113
+ return {
114
+ tokens: `the spacing token '${Tokens.key(length)}'`,
115
+ url: urls.spacing,
116
+ }
117
+ if (containers.test(property) && Tokens.container.includes(length))
118
+ return {
119
+ tokens: `the container token '${Tokens.key(length)}'`,
120
+ url: urls.container,
121
+ }
122
+ }
123
+ return undefined
124
+ }
125
+
126
+ function check(object: ESTree.ObjectExpression) {
127
+ for (const property of object.properties) {
128
+ if (property.type !== 'Property') continue
129
+ if (property.value.type === 'ObjectExpression') {
130
+ check(property.value)
131
+ continue
132
+ }
133
+
134
+ const name = Styles.key(property)
135
+ const node = property.value
136
+ const custom =
137
+ (node.type === 'Literal' &&
138
+ typeof node.value === 'string' &&
139
+ node.value.endsWith('!custom')) ||
140
+ (node.type === 'TemplateLiteral' &&
141
+ node.expressions.length === 0 &&
142
+ node.quasis
143
+ .map((quasi) => quasi.value.cooked ?? '')
144
+ .join('')
145
+ .endsWith('!custom')) ||
146
+ (name === 'zIndex' && node.type === 'Literal' && typeof node.value === 'number')
147
+ const value = Styles.text(node)
148
+
149
+ if (name === undefined || !custom || value === undefined) continue
150
+
151
+ const advice = advise(name, value)
152
+
153
+ if (advice)
154
+ context.report({
155
+ data: { property: name, value: context.sourceCode.getText(node), ...advice },
156
+ messageId: 'token',
157
+ node,
158
+ })
159
+ }
160
+ }
161
+
162
+ return {
163
+ CallExpression(node) {
164
+ if (!Styles.helper(node.callee)) return
165
+ for (const argument of node.arguments.slice(0, 1))
166
+ if (argument.type === 'ObjectExpression') check(argument)
167
+ },
168
+ }
169
+ },
170
+ }
@@ -0,0 +1,190 @@
1
+ /** Finds single-key shortcuts that listen on the whole page. @module */
2
+ import type { ESTree, Rule, Scope } from '@oxlint/plugins'
3
+ import * as Docs from '../internal/Docs.ts'
4
+
5
+ const events = new Set(['keydown', 'keypress', 'keyup'])
6
+ // Hooks that return the function they receive.
7
+ const hooks = new Set(['useCallback', 'useEffectEvent', 'useEvent'])
8
+ const modifiers = new Set(['altKey', 'ctrlKey', 'getModifierState', 'metaKey'])
9
+ const targets = new Set(['document', 'globalThis', 'self', 'window'])
10
+
11
+ /**
12
+ * Reports a `keydown`, `keyup`, or `keypress` listener on `window` or `document` that compares
13
+ * `key` with one printable character, or `code` with a letter or digit key, without reading a
14
+ * modifier. A character key on the whole page fires while people type or dictate, which WCAG 2.1.4
15
+ * Character Key Shortcuts forbids unless people can turn it off or remap it. A handler that reads
16
+ * `metaKey`, `ctrlKey`, `altKey`, or `getModifierState` passes.
17
+ */
18
+ export const rule: Rule = {
19
+ meta: {
20
+ docs: { description: 'Require a modifier on page-wide keyboard shortcuts.' },
21
+ messages: {
22
+ shortcut:
23
+ "'{{key}}' is a single-key shortcut on the whole page. Require a modifier, such as metaKey or ctrlKey, or let people turn it off or remap it (WCAG 2.1.4). Show it with 'Kbd' from '@tempoxyz/ds'. See {{url}}",
24
+ },
25
+ schema: [],
26
+ type: 'problem',
27
+ },
28
+ create(context) {
29
+ const url = Docs.page('foundations/accessibility#shortcuts')
30
+
31
+ // The character that `node` compares with a key, such as `k` for `'k'` or `'KeyK'`.
32
+ function character(node: ESTree.Node, property: string) {
33
+ if (node.type !== 'Literal' || typeof node.value !== 'string') return undefined
34
+ if (property === 'code')
35
+ return /^(?:Digit\d|Key[A-Z])$/.test(node.value) ? node.value : undefined
36
+ return [...node.value].length === 1 && /\S/.test(node.value) ? node.value : undefined
37
+ }
38
+
39
+ // `event.key` or `event.code`, also through `toLowerCase()` or `toUpperCase()`.
40
+ function read(node: ESTree.Node): string | undefined {
41
+ if (
42
+ node.type === 'CallExpression' &&
43
+ node.callee.type === 'MemberExpression' &&
44
+ node.callee.property.type === 'Identifier' &&
45
+ /^to(?:Lower|Upper)Case$/.test(node.callee.property.name)
46
+ )
47
+ return read(node.callee.object)
48
+ if (node.type !== 'MemberExpression' || node.computed || node.property.type !== 'Identifier')
49
+ return undefined
50
+ return node.property.name === 'key' || node.property.name === 'code'
51
+ ? node.property.name
52
+ : undefined
53
+ }
54
+
55
+ function resolve(node: ESTree.Node): ESTree.Node | undefined {
56
+ if (node.type === 'ArrowFunctionExpression' || node.type === 'FunctionExpression') return node
57
+ if (
58
+ node.type === 'CallExpression' &&
59
+ node.callee.type === 'Identifier' &&
60
+ hooks.has(node.callee.name) &&
61
+ node.arguments[0]
62
+ )
63
+ return resolve(node.arguments[0])
64
+ if (node.type !== 'Identifier') return undefined
65
+
66
+ for (
67
+ let scope: Scope | null = context.sourceCode.getScope(node);
68
+ scope;
69
+ scope = scope.upper
70
+ ) {
71
+ const variable = scope.set.get(node.name)
72
+
73
+ if (!variable) continue
74
+
75
+ const definition = variable.defs[0]?.node
76
+
77
+ if (definition?.type === 'FunctionDeclaration') return definition
78
+ if (definition?.type === 'VariableDeclarator' && definition.init)
79
+ return resolve(definition.init)
80
+ return undefined
81
+ }
82
+ return undefined
83
+ }
84
+
85
+ function walk(node: ESTree.Node, fn: (node: ESTree.Node) => void) {
86
+ fn(node)
87
+ for (const field of context.sourceCode.visitorKeys[node.type] ?? []) {
88
+ const value: unknown = Reflect.get(node, field)
89
+
90
+ for (const child of Array.isArray(value) ? value : [value])
91
+ if (child && typeof child === 'object' && 'type' in child) walk(child, fn)
92
+ }
93
+ }
94
+
95
+ function check(handler: ESTree.Node) {
96
+ const found: { character: string; node: ESTree.Node }[] = []
97
+ let modified = false
98
+
99
+ walk(handler, (node) => {
100
+ if (
101
+ node.type === 'MemberExpression' &&
102
+ node.property.type === 'Identifier' &&
103
+ modifiers.has(node.property.name)
104
+ )
105
+ modified = true
106
+ if (node.type === 'BinaryExpression' && ['!=', '!==', '==', '==='].includes(node.operator))
107
+ for (const [left, right] of [
108
+ [node.left, node.right],
109
+ [node.right, node.left],
110
+ ] as const) {
111
+ const property = read(left)
112
+ const value = property && character(right, property)
113
+
114
+ if (value) found.push({ character: value, node: right })
115
+ }
116
+ if (node.type === 'SwitchStatement') {
117
+ const property = read(node.discriminant)
118
+
119
+ if (property)
120
+ for (const entry of node.cases) {
121
+ const value = entry.test && character(entry.test, property)
122
+
123
+ if (value && entry.test) found.push({ character: value, node: entry.test })
124
+ }
125
+ }
126
+ if (
127
+ node.type === 'CallExpression' &&
128
+ node.callee.type === 'MemberExpression' &&
129
+ node.callee.property.type === 'Identifier' &&
130
+ node.callee.property.name === 'includes' &&
131
+ node.callee.object.type === 'ArrayExpression' &&
132
+ node.arguments[0]
133
+ ) {
134
+ const property = read(node.arguments[0])
135
+
136
+ if (property)
137
+ for (const item of node.callee.object.elements) {
138
+ const value = item && character(item, property)
139
+
140
+ if (value && item) found.push({ character: value, node: item })
141
+ }
142
+ }
143
+ })
144
+
145
+ if (modified) return
146
+ for (const entry of found)
147
+ context.report({
148
+ data: { key: entry.character, url },
149
+ messageId: 'shortcut',
150
+ node: entry.node,
151
+ })
152
+ }
153
+
154
+ return {
155
+ CallExpression(node) {
156
+ const callee = node.callee
157
+
158
+ if (
159
+ callee.type !== 'MemberExpression' ||
160
+ callee.property.type !== 'Identifier' ||
161
+ callee.property.name !== 'addEventListener'
162
+ )
163
+ return
164
+
165
+ const target = (() => {
166
+ if (callee.object.type === 'Identifier') return callee.object.name
167
+ if (
168
+ callee.object.type === 'MemberExpression' &&
169
+ callee.object.property.type === 'Identifier'
170
+ )
171
+ return callee.object.property.name
172
+ return undefined
173
+ })()
174
+ const [event, listener] = node.arguments
175
+
176
+ if (!target || !(targets.has(target) || target === 'body')) return
177
+ if (
178
+ event?.type !== 'Literal' ||
179
+ typeof event.value !== 'string' ||
180
+ !events.has(event.value)
181
+ )
182
+ return
183
+
184
+ const handler = listener && resolve(listener)
185
+
186
+ if (handler) check(handler)
187
+ },
188
+ }
189
+ },
190
+ }