@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.
- package/README.md +7 -6
- package/dist/docs/AGENTS.md +8 -5
- package/dist/docs/index.md +143 -135
- package/dist/docs/marketing/introduction/getting-started.md +241 -0
- package/dist/docs/{components → product/components}/accordion.md +8 -8
- package/dist/docs/{components → product/components}/address-card.md +5 -5
- package/dist/docs/{components → product/components}/agent-logo.md +5 -5
- package/dist/docs/{components → product/components}/alert-dialog.md +11 -11
- package/dist/docs/{components → product/components}/alert.md +11 -11
- package/dist/docs/{components → product/components}/amount-input.md +13 -13
- package/dist/docs/{components → product/components}/amount.md +8 -8
- package/dist/docs/{components → product/components}/app-shell.md +13 -13
- package/dist/docs/{components → product/components}/asset-chip.md +11 -11
- package/dist/docs/{components → product/components}/autocomplete.md +10 -9
- package/dist/docs/{components → product/components}/avatar.md +8 -8
- package/dist/docs/{components → product/components}/badge.md +9 -9
- package/dist/docs/{components → product/components}/badged-logo.md +6 -6
- package/dist/docs/{components → product/components}/banner.md +8 -8
- package/dist/docs/{components → product/components}/bar-chart.md +7 -7
- package/dist/docs/{components → product/components}/breadcrumbs.md +8 -8
- package/dist/docs/{components → product/components}/button-group.md +8 -8
- package/dist/docs/{components → product/components}/button.md +17 -17
- package/dist/docs/{components → product/components}/calendar.md +8 -8
- package/dist/docs/{components → product/components}/callout.md +5 -5
- package/dist/docs/{components → product/components}/checkbox-group.md +13 -13
- package/dist/docs/{components → product/components}/checkbox.md +12 -12
- package/dist/docs/{components → product/components}/choice-group.md +14 -14
- package/dist/docs/{components → product/components}/choicebox.md +9 -9
- package/dist/docs/{components → product/components}/code.md +5 -5
- package/dist/docs/{components → product/components}/combobox.md +9 -9
- package/dist/docs/{components → product/components}/container.md +7 -7
- package/dist/docs/{components → product/components}/copy-button.md +13 -13
- package/dist/docs/{components → product/components}/date-picker.md +11 -11
- package/dist/docs/{components → product/components}/dialog.md +17 -17
- package/dist/docs/{components → product/components}/drawer.md +11 -11
- package/dist/docs/{components → product/components}/empty.md +9 -9
- package/dist/docs/{components → product/components}/field.md +14 -14
- package/dist/docs/{components → product/components}/fieldset.md +6 -6
- package/dist/docs/{components → product/components}/frame.md +9 -9
- package/dist/docs/{components → product/components}/gauge.md +7 -7
- package/dist/docs/{components → product/components}/get-started-visual.md +4 -4
- package/dist/docs/{components → product/components}/grid.md +7 -7
- package/dist/docs/{components → product/components}/hero-visual.md +4 -4
- package/dist/docs/{components → product/components}/hex.md +9 -9
- package/dist/docs/{components → product/components}/icon-tile.md +8 -8
- package/dist/docs/{components → product/components}/identicon.md +7 -7
- package/dist/docs/{components → product/components}/inline.md +7 -7
- package/dist/docs/{components → product/components}/inset.md +8 -8
- package/dist/docs/{components → product/components}/kbd.md +8 -8
- package/dist/docs/{components → product/components}/list-group.md +24 -24
- package/dist/docs/{components → product/components}/loading-dots.md +8 -8
- package/dist/docs/{components → product/components}/menu.md +14 -14
- package/dist/docs/{components → product/components}/native-select.md +7 -7
- package/dist/docs/{components → product/components}/network-logo.md +7 -7
- package/dist/docs/{components → product/components}/number-field.md +8 -8
- package/dist/docs/{components → product/components}/otp-field.md +9 -9
- package/dist/docs/{components → product/components}/pagination.md +8 -8
- package/dist/docs/{components → product/components}/popover.md +11 -11
- package/dist/docs/{components → product/components}/progress.md +10 -10
- package/dist/docs/{components → product/components}/qr.md +8 -8
- package/dist/docs/{components → product/components}/radio-group.md +10 -10
- package/dist/docs/{components → product/components}/secret-value.md +8 -8
- package/dist/docs/{components → product/components}/select.md +13 -13
- package/dist/docs/{components → product/components}/separator.md +7 -7
- package/dist/docs/{components → product/components}/sidebar-menu.md +11 -11
- package/dist/docs/{components → product/components}/skeleton.md +9 -9
- package/dist/docs/{components → product/components}/slider.md +8 -8
- package/dist/docs/{components → product/components}/snippet.md +10 -10
- package/dist/docs/{components → product/components}/spinner.md +12 -12
- package/dist/docs/{components → product/components}/split-bar.md +5 -5
- package/dist/docs/{components → product/components}/split-button.md +11 -11
- package/dist/docs/{components → product/components}/stack.md +8 -8
- package/dist/docs/{components → product/components}/status-chip.md +7 -7
- package/dist/docs/{components → product/components}/status-indicator.md +6 -6
- package/dist/docs/{components → product/components}/status-row.md +7 -7
- package/dist/docs/{components → product/components}/stepper.md +9 -9
- package/dist/docs/{components → product/components}/success-hero.md +5 -5
- package/dist/docs/{components → product/components}/surface.md +11 -11
- package/dist/docs/{components → product/components}/switch.md +11 -11
- package/dist/docs/{components → product/components}/table.md +16 -16
- package/dist/docs/{components → product/components}/tabs.md +11 -11
- package/dist/docs/{components → product/components}/tempo-logo.md +4 -4
- package/dist/docs/{components → product/components}/text-input.md +14 -14
- package/dist/docs/{components → product/components}/textarea.md +7 -7
- package/dist/docs/{components → product/components}/toast.md +11 -11
- package/dist/docs/{components → product/components}/token-logo.md +16 -16
- package/dist/docs/{components → product/components}/token.md +10 -10
- package/dist/docs/{components → product/components}/tooltip.md +11 -11
- package/dist/docs/{components → product/components}/verification-code.md +4 -4
- package/dist/docs/{components → product/components}/visual.md +5 -5
- package/dist/docs/product/components.md +203 -0
- package/dist/docs/{foundations → product/foundations}/accessibility.md +16 -16
- package/dist/docs/{foundations → product/foundations}/agents.md +17 -17
- package/dist/docs/{foundations → product/foundations}/color.md +7 -7
- package/dist/docs/{foundations → product/foundations}/content/tone.md +7 -7
- package/dist/docs/{foundations → product/foundations}/content/voice.md +2 -2
- package/dist/docs/{foundations → product/foundations}/content/writing-style.md +8 -8
- package/dist/docs/{foundations → product/foundations}/iconography.md +11 -11
- package/dist/docs/{foundations → product/foundations}/intent.md +28 -28
- package/dist/docs/{foundations → product/foundations}/layout.md +22 -22
- package/dist/docs/product/foundations/linter.md +496 -0
- package/dist/docs/{foundations → product/foundations}/motion.md +7 -7
- package/dist/docs/{foundations → product/foundations}/radius-and-padding.md +3 -3
- package/dist/docs/{foundations → product/foundations}/typography.md +4 -4
- package/dist/docs/{introduction → product/introduction}/faq.md +22 -21
- package/dist/docs/{introduction → product/introduction}/getting-started.md +4 -4
- package/dist/docs/{patterns → product/patterns}/blockchain-values.md +20 -20
- package/dist/docs/{patterns → product/patterns}/dashboards.md +28 -28
- package/dist/docs/{patterns → product/patterns}/destructive-actions.md +16 -16
- package/dist/docs/{patterns → product/patterns}/empty-states.md +14 -14
- package/dist/docs/{patterns → product/patterns}/errors-and-validation.md +23 -23
- package/dist/docs/{patterns → product/patterns}/feedback-and-status.md +31 -31
- package/dist/docs/{patterns → product/patterns}/forms.md +24 -24
- package/dist/docs/{patterns → product/patterns}/loading.md +19 -19
- package/dist/docs/{patterns → product/patterns}/navigation.md +23 -23
- package/dist/docs/{patterns → product/patterns}/payments.md +20 -20
- package/dist/docs/{patterns → product/patterns}/responsive-layout.md +23 -23
- package/dist/docs/{patterns → product/patterns}/tables-and-lists.md +19 -19
- package/dist/docs/{products → product/products}/tempo-console.md +12 -12
- package/dist/docs/{products → product/products}/tempo-performance.md +11 -11
- package/dist/docs/{products → product/products}/tempo-wallet.md +12 -12
- package/dist/marketing/index.d.ts +4 -0
- package/dist/marketing/index.d.ts.map +1 -0
- package/dist/marketing/index.js +4 -0
- package/dist/marketing/index.js.map +1 -0
- package/dist/marketing/index.js.zyzz.json +1 -0
- package/dist/marketing/vars.d.ts +907 -0
- package/dist/marketing/vars.d.ts.map +1 -0
- package/dist/marketing/vars.js +15 -0
- package/dist/marketing/vars.js.map +1 -0
- package/dist/marketing/vars.js.zyzz.json +1 -0
- package/dist/marketing/zyzz.config.d.ts +4512 -0
- package/dist/marketing/zyzz.config.d.ts.map +1 -0
- package/dist/marketing/zyzz.config.js +63 -0
- package/dist/marketing/zyzz.config.js.map +1 -0
- package/dist/marketing/zyzz.config.js.zyzz.json +1 -0
- package/dist/oxlint/index.d.ts +12 -1
- package/dist/oxlint/index.d.ts.map +1 -1
- package/dist/oxlint/index.js +39 -1
- package/dist/oxlint/index.js.map +1 -1
- package/dist/oxlint/internal/Docs.d.ts +9 -0
- package/dist/oxlint/internal/Docs.d.ts.map +1 -0
- package/dist/oxlint/internal/Docs.js +15 -0
- package/dist/oxlint/internal/Docs.js.map +1 -0
- package/dist/oxlint/internal/Imports.d.ts +1 -1
- package/dist/oxlint/internal/Imports.d.ts.map +1 -1
- package/dist/oxlint/internal/Imports.js +2 -0
- package/dist/oxlint/internal/Imports.js.map +1 -1
- package/dist/oxlint/internal/Styles.d.ts +40 -0
- package/dist/oxlint/internal/Styles.d.ts.map +1 -0
- package/dist/oxlint/internal/Styles.js +194 -0
- package/dist/oxlint/internal/Styles.js.map +1 -0
- package/dist/oxlint/internal/Tokens.d.ts +16 -0
- package/dist/oxlint/internal/Tokens.d.ts.map +1 -0
- package/dist/oxlint/internal/Tokens.js +39 -0
- package/dist/oxlint/internal/Tokens.js.map +1 -0
- package/dist/oxlint/rules/NoCrossSurface.d.ts +2 -2
- package/dist/oxlint/rules/NoCrossSurface.js +3 -3
- package/dist/oxlint/rules/NoCrossSurface.js.map +1 -1
- package/dist/oxlint/rules/NoCustomValues.d.ts +11 -0
- package/dist/oxlint/rules/NoCustomValues.d.ts.map +1 -0
- package/dist/oxlint/rules/NoCustomValues.js +150 -0
- package/dist/oxlint/rules/NoCustomValues.js.map +1 -0
- package/dist/oxlint/rules/NoSingleKeyShortcuts.d.ts +11 -0
- package/dist/oxlint/rules/NoSingleKeyShortcuts.d.ts.map +1 -0
- package/dist/oxlint/rules/NoSingleKeyShortcuts.js +158 -0
- package/dist/oxlint/rules/NoSingleKeyShortcuts.js.map +1 -0
- package/dist/oxlint/rules/PreferComponent.d.ts +3 -2
- package/dist/oxlint/rules/PreferComponent.d.ts.map +1 -1
- package/dist/oxlint/rules/PreferComponent.js +11 -4
- package/dist/oxlint/rules/PreferComponent.js.map +1 -1
- package/dist/oxlint/rules/PreferLayout.d.ts +9 -0
- package/dist/oxlint/rules/PreferLayout.d.ts.map +1 -0
- package/dist/oxlint/rules/PreferLayout.js +90 -0
- package/dist/oxlint/rules/PreferLayout.js.map +1 -0
- package/dist/oxlint/rules/PreferProgress.d.ts +9 -0
- package/dist/oxlint/rules/PreferProgress.d.ts.map +1 -0
- package/dist/oxlint/rules/PreferProgress.js +43 -0
- package/dist/oxlint/rules/PreferProgress.js.map +1 -0
- package/dist/oxlint/rules/PreferSurface.d.ts +9 -0
- package/dist/oxlint/rules/PreferSurface.d.ts.map +1 -0
- package/dist/oxlint/rules/PreferSurface.js +55 -0
- package/dist/oxlint/rules/PreferSurface.js.map +1 -0
- package/dist/oxlint/rules/RequireAccessibleName.d.ts.map +1 -1
- package/dist/oxlint/rules/RequireAccessibleName.js +3 -1
- package/dist/oxlint/rules/RequireAccessibleName.js.map +1 -1
- package/dist/oxlint/rules/RequireScrollRegion.d.ts +11 -0
- package/dist/oxlint/rules/RequireScrollRegion.d.ts.map +1 -0
- package/dist/oxlint/rules/RequireScrollRegion.js +71 -0
- package/dist/oxlint/rules/RequireScrollRegion.js.map +1 -0
- package/dist/web/icons/index.js.zyzz.json +1 -0
- package/package.json +6 -1
- package/src/marketing/README.md +21 -0
- package/src/marketing/index.ts +3 -0
- package/src/marketing/vars.ts +751 -0
- package/src/marketing/zyzz.config.ts +376 -0
- package/src/oxlint/index.ts +40 -1
- package/src/oxlint/internal/Docs.ts +18 -0
- package/src/oxlint/internal/Imports.ts +2 -1
- package/src/oxlint/internal/Styles.ts +240 -0
- package/src/oxlint/internal/Tokens.ts +43 -0
- package/src/oxlint/rules/NoCrossSurface.ts +3 -3
- package/src/oxlint/rules/NoCustomValues.ts +170 -0
- package/src/oxlint/rules/NoSingleKeyShortcuts.ts +190 -0
- package/src/oxlint/rules/PreferComponent.ts +11 -4
- package/src/oxlint/rules/PreferLayout.ts +96 -0
- package/src/oxlint/rules/PreferProgress.ts +49 -0
- package/src/oxlint/rules/PreferSurface.ts +70 -0
- package/src/oxlint/rules/RequireAccessibleName.ts +3 -1
- package/src/oxlint/rules/RequireScrollRegion.ts +92 -0
- package/dist/docs/components.md +0 -203
- 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
|
|
7
|
-
* is reported. Without it, the rule reports a file that imports from
|
|
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
|
+
}
|