@tempoxyz/ds 0.3.10 → 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 +12 -0
- package/dist/docs/index.md +145 -0
- package/dist/docs/marketing/introduction/getting-started.md +241 -0
- package/dist/docs/product/components/accordion.md +489 -0
- package/dist/docs/product/components/address-card.md +147 -0
- package/dist/docs/product/components/agent-logo.md +194 -0
- package/dist/docs/product/components/alert-dialog.md +631 -0
- package/dist/docs/product/components/alert.md +448 -0
- package/dist/docs/product/components/amount-input.md +305 -0
- package/dist/docs/product/components/amount.md +794 -0
- package/dist/docs/product/components/app-shell.md +559 -0
- package/dist/docs/product/components/asset-chip.md +294 -0
- package/dist/docs/product/components/autocomplete.md +620 -0
- package/dist/docs/product/components/avatar.md +321 -0
- package/dist/docs/product/components/badge.md +339 -0
- package/dist/docs/product/components/badged-logo.md +116 -0
- package/dist/docs/product/components/banner.md +379 -0
- package/dist/docs/product/components/bar-chart.md +208 -0
- package/dist/docs/product/components/breadcrumbs.md +380 -0
- package/dist/docs/product/components/button-group.md +286 -0
- package/dist/docs/product/components/button.md +650 -0
- package/dist/docs/product/components/calendar.md +375 -0
- package/dist/docs/product/components/callout.md +96 -0
- package/dist/docs/product/components/checkbox-group.md +521 -0
- package/dist/docs/product/components/checkbox.md +467 -0
- package/dist/docs/product/components/choice-group.md +516 -0
- package/dist/docs/product/components/choicebox.md +641 -0
- package/dist/docs/product/components/code.md +100 -0
- package/dist/docs/product/components/combobox.md +939 -0
- package/dist/docs/product/components/container.md +257 -0
- package/dist/docs/product/components/copy-button.md +642 -0
- package/dist/docs/product/components/date-picker.md +514 -0
- package/dist/docs/product/components/dialog.md +1332 -0
- package/dist/docs/product/components/drawer.md +966 -0
- package/dist/docs/product/components/empty.md +340 -0
- package/dist/docs/product/components/field.md +570 -0
- package/dist/docs/product/components/fieldset.md +398 -0
- package/dist/docs/product/components/frame.md +434 -0
- package/dist/docs/product/components/gauge.md +299 -0
- package/dist/docs/product/components/get-started-visual.md +117 -0
- package/dist/docs/product/components/grid.md +288 -0
- package/dist/docs/product/components/hero-visual.md +155 -0
- package/dist/docs/product/components/hex.md +487 -0
- package/dist/docs/product/components/icon-tile.md +292 -0
- package/dist/docs/product/components/identicon.md +371 -0
- package/dist/docs/product/components/inline.md +233 -0
- package/dist/docs/product/components/inset.md +225 -0
- package/dist/docs/product/components/kbd.md +423 -0
- package/dist/docs/product/components/list-group.md +981 -0
- package/dist/docs/product/components/loading-dots.md +237 -0
- package/dist/docs/product/components/menu.md +934 -0
- package/dist/docs/product/components/native-select.md +204 -0
- package/dist/docs/product/components/network-logo.md +248 -0
- package/dist/docs/product/components/number-field.md +543 -0
- package/dist/docs/product/components/otp-field.md +616 -0
- package/dist/docs/product/components/pagination.md +516 -0
- package/dist/docs/product/components/popover.md +544 -0
- package/dist/docs/product/components/progress.md +388 -0
- package/dist/docs/product/components/qr.md +314 -0
- package/dist/docs/product/components/radio-group.md +528 -0
- package/dist/docs/product/components/secret-value.md +406 -0
- package/dist/docs/product/components/select.md +751 -0
- package/dist/docs/product/components/separator.md +379 -0
- package/dist/docs/product/components/sidebar-menu.md +922 -0
- package/dist/docs/product/components/skeleton.md +517 -0
- package/dist/docs/product/components/slider.md +539 -0
- package/dist/docs/product/components/snippet.md +424 -0
- package/dist/docs/product/components/spinner.md +216 -0
- package/dist/docs/product/components/split-bar.md +139 -0
- package/dist/docs/product/components/split-button.md +464 -0
- package/dist/docs/product/components/stack.md +299 -0
- package/dist/docs/product/components/status-chip.md +226 -0
- package/dist/docs/product/components/status-indicator.md +119 -0
- package/dist/docs/product/components/status-row.md +174 -0
- package/dist/docs/product/components/stepper.md +451 -0
- package/dist/docs/product/components/success-hero.md +157 -0
- package/dist/docs/product/components/surface.md +639 -0
- package/dist/docs/product/components/switch.md +472 -0
- package/dist/docs/product/components/table.md +967 -0
- package/dist/docs/product/components/tabs.md +627 -0
- package/dist/docs/product/components/tempo-logo.md +185 -0
- package/dist/docs/product/components/text-input.md +624 -0
- package/dist/docs/product/components/textarea.md +352 -0
- package/dist/docs/product/components/toast.md +535 -0
- package/dist/docs/product/components/token-logo.md +371 -0
- package/dist/docs/product/components/token.md +247 -0
- package/dist/docs/product/components/tooltip.md +490 -0
- package/dist/docs/product/components/verification-code.md +116 -0
- package/dist/docs/product/components/visual.md +249 -0
- package/dist/docs/product/components.md +203 -0
- package/dist/docs/product/foundations/accessibility.md +377 -0
- package/dist/docs/product/foundations/agents.md +150 -0
- package/dist/docs/product/foundations/color.md +522 -0
- package/dist/docs/product/foundations/content/tone.md +194 -0
- package/dist/docs/product/foundations/content/voice.md +194 -0
- package/dist/docs/product/foundations/content/writing-style.md +406 -0
- package/dist/docs/product/foundations/iconography.md +282 -0
- package/dist/docs/product/foundations/intent.md +360 -0
- package/dist/docs/product/foundations/layout.md +508 -0
- package/dist/docs/product/foundations/linter.md +496 -0
- package/dist/docs/product/foundations/motion.md +151 -0
- package/dist/docs/product/foundations/radius-and-padding.md +86 -0
- package/dist/docs/product/foundations/typography.md +266 -0
- package/dist/docs/product/introduction/faq.md +133 -0
- package/dist/docs/product/introduction/getting-started.md +377 -0
- package/dist/docs/product/patterns/blockchain-values.md +709 -0
- package/dist/docs/product/patterns/dashboards.md +638 -0
- package/dist/docs/product/patterns/destructive-actions.md +626 -0
- package/dist/docs/product/patterns/empty-states.md +612 -0
- package/dist/docs/product/patterns/errors-and-validation.md +808 -0
- package/dist/docs/product/patterns/feedback-and-status.md +518 -0
- package/dist/docs/product/patterns/forms.md +679 -0
- package/dist/docs/product/patterns/loading.md +630 -0
- package/dist/docs/product/patterns/navigation.md +397 -0
- package/dist/docs/product/patterns/payments.md +765 -0
- package/dist/docs/product/patterns/responsive-layout.md +997 -0
- package/dist/docs/product/patterns/tables-and-lists.md +964 -0
- package/dist/docs/product/products/tempo-console.md +671 -0
- package/dist/docs/product/products/tempo-performance.md +437 -0
- package/dist/docs/product/products/tempo-wallet.md +438 -0
- 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
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/** Points native HTML controls and structures at the Web components that replace them. @module */
|
|
2
2
|
import type { ESTree, Rule } from '@oxlint/plugins'
|
|
3
|
+
import * as Docs from '../internal/Docs.ts'
|
|
3
4
|
|
|
4
5
|
// Each element maps to the component that renders it with Tempo styles, states, and semantics.
|
|
5
6
|
const elements: Record<string, string> = {
|
|
6
7
|
button: 'Button',
|
|
7
8
|
details: 'Accordion',
|
|
8
9
|
dialog: 'Dialog',
|
|
10
|
+
dl: 'ListGroup',
|
|
9
11
|
fieldset: 'Fieldset',
|
|
10
12
|
hr: 'Separator',
|
|
11
13
|
kbd: 'Kbd',
|
|
@@ -35,8 +37,9 @@ const inputs: Record<string, string> = {
|
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
/**
|
|
38
|
-
* Reports a native element that a `@tempoxyz/ds` component replaces, such as `<button
|
|
39
|
-
* `<input type="checkbox"
|
|
40
|
+
* Reports a native element that a `@tempoxyz/ds` component replaces, such as `<button>`,
|
|
41
|
+
* `<input type="checkbox">`, or a `<dl>` of labels and values. The message links the docs page of
|
|
42
|
+
* the component. Elements inside a `render` prop are skipped, since the component that
|
|
40
43
|
* receives them supplies the styles.
|
|
41
44
|
*/
|
|
42
45
|
export const rule: Rule = {
|
|
@@ -45,7 +48,7 @@ export const rule: Rule = {
|
|
|
45
48
|
description: 'Prefer `@tempoxyz/ds` components over the native elements they replace.',
|
|
46
49
|
},
|
|
47
50
|
messages: {
|
|
48
|
-
component: "Use '{{component}}' from '@tempoxyz/ds' instead of <{{element}}>.",
|
|
51
|
+
component: "Use '{{component}}' from '@tempoxyz/ds' instead of <{{element}}>. See {{url}}",
|
|
49
52
|
},
|
|
50
53
|
schema: [
|
|
51
54
|
{
|
|
@@ -95,7 +98,11 @@ export const rule: Rule = {
|
|
|
95
98
|
|
|
96
99
|
if (replacement)
|
|
97
100
|
context.report({
|
|
98
|
-
data: {
|
|
101
|
+
data: {
|
|
102
|
+
component: replacement,
|
|
103
|
+
element: node.name.name,
|
|
104
|
+
url: Docs.component(replacement),
|
|
105
|
+
},
|
|
99
106
|
messageId: 'component',
|
|
100
107
|
node,
|
|
101
108
|
})
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/** Points plain flex and grid wrappers at `Stack`, `Inline`, and `Grid`. @module */
|
|
2
|
+
import type { Rule } from '@oxlint/plugins'
|
|
3
|
+
import * as Docs from '../internal/Docs.ts'
|
|
4
|
+
import * as Styles from '../internal/Styles.ts'
|
|
5
|
+
|
|
6
|
+
// Block elements that a layout component renders through `render`.
|
|
7
|
+
const tags = new Set([
|
|
8
|
+
'article',
|
|
9
|
+
'aside',
|
|
10
|
+
'div',
|
|
11
|
+
'footer',
|
|
12
|
+
'form',
|
|
13
|
+
'header',
|
|
14
|
+
'main',
|
|
15
|
+
'nav',
|
|
16
|
+
'ol',
|
|
17
|
+
'section',
|
|
18
|
+
'ul',
|
|
19
|
+
])
|
|
20
|
+
|
|
21
|
+
// Declarations that the layout components set. Any other declaration makes the element more than a
|
|
22
|
+
// layout wrapper, so the rule skips it.
|
|
23
|
+
const layout = new Set([
|
|
24
|
+
'alignItems',
|
|
25
|
+
'columnGap',
|
|
26
|
+
'display',
|
|
27
|
+
'flexDirection',
|
|
28
|
+
'flexWrap',
|
|
29
|
+
'gap',
|
|
30
|
+
'gridTemplateColumns',
|
|
31
|
+
'justifyContent',
|
|
32
|
+
'rowGap',
|
|
33
|
+
])
|
|
34
|
+
|
|
35
|
+
// Equal tracks that `columns` or `fit` on `Grid` produce.
|
|
36
|
+
const tracks = /^repeat\(\s*(?:[1-46]|12|auto-fill|auto-fit)\s*,/
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Reports a block element whose only styles lay out its children with flex or grid and a gap.
|
|
40
|
+
* A column maps to `Stack`, a row to `Inline`, and equal grid tracks to `Grid`. An element
|
|
41
|
+
* with a `className`, a spread that the rule cannot read, or any other declaration passes.
|
|
42
|
+
*/
|
|
43
|
+
export const rule: Rule = {
|
|
44
|
+
meta: {
|
|
45
|
+
docs: {
|
|
46
|
+
description: 'Use `Stack`, `Inline`, or `Grid` instead of a plain flex or grid wrapper.',
|
|
47
|
+
},
|
|
48
|
+
messages: {
|
|
49
|
+
layout:
|
|
50
|
+
"Use '{{component}}' from '@tempoxyz/ds' instead of laying out <{{element}}> with {{display}} and a gap. See {{url}}",
|
|
51
|
+
},
|
|
52
|
+
schema: [],
|
|
53
|
+
type: 'suggestion',
|
|
54
|
+
},
|
|
55
|
+
create(context) {
|
|
56
|
+
return Styles.visit((element) => {
|
|
57
|
+
if (!tags.has(element.tag) || element.open) return
|
|
58
|
+
|
|
59
|
+
const declarations = element.declarations
|
|
60
|
+
|
|
61
|
+
if (![...declarations.keys()].every((property) => layout.has(property))) return
|
|
62
|
+
if (!['columnGap', 'gap', 'rowGap'].some((property) => declarations.has(property))) return
|
|
63
|
+
|
|
64
|
+
const display = Styles.text(declarations.get('display'))
|
|
65
|
+
const component = (() => {
|
|
66
|
+
if (display === 'flex' || display === 'inline-flex') {
|
|
67
|
+
const direction = declarations.has('flexDirection')
|
|
68
|
+
? Styles.text(declarations.get('flexDirection'))
|
|
69
|
+
: 'row'
|
|
70
|
+
|
|
71
|
+
if (direction === 'column') return 'Stack'
|
|
72
|
+
if (direction === 'row') return 'Inline'
|
|
73
|
+
return undefined
|
|
74
|
+
}
|
|
75
|
+
if (display !== 'grid' && display !== 'inline-grid') return undefined
|
|
76
|
+
if (!declarations.has('gridTemplateColumns')) return 'Stack'
|
|
77
|
+
|
|
78
|
+
const columns = Styles.text(declarations.get('gridTemplateColumns'))
|
|
79
|
+
|
|
80
|
+
return columns !== undefined && tracks.test(columns) ? 'Grid' : undefined
|
|
81
|
+
})()
|
|
82
|
+
|
|
83
|
+
if (component)
|
|
84
|
+
context.report({
|
|
85
|
+
data: {
|
|
86
|
+
component,
|
|
87
|
+
display: display?.replace('inline-', '') ?? '',
|
|
88
|
+
element: element.tag,
|
|
89
|
+
url: Docs.component(component),
|
|
90
|
+
},
|
|
91
|
+
messageId: 'layout',
|
|
92
|
+
node: element.node,
|
|
93
|
+
})
|
|
94
|
+
})
|
|
95
|
+
},
|
|
96
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/** Points hand-built bars at `Progress` and `ListGroup.Meter`. @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
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Reports an empty `div` or `span` whose inline `width` or `inlineSize` is a computed percentage,
|
|
8
|
+
* such as a template literal that ends in `%`, which draws the fill of a hand-built bar. An element
|
|
9
|
+
* with children passes, since a resizable pane sets its width the same way.
|
|
10
|
+
*/
|
|
11
|
+
export const rule: Rule = {
|
|
12
|
+
meta: {
|
|
13
|
+
docs: { description: 'Use `Progress` or `ListGroup.Meter` instead of a hand-built bar.' },
|
|
14
|
+
messages: {
|
|
15
|
+
bar: "Use 'Progress' from '@tempoxyz/ds', or 'ListGroup.Meter' in a ListGroup row, instead of drawing a bar with <{{element}}> and a percentage width. See {{url}}",
|
|
16
|
+
},
|
|
17
|
+
schema: [],
|
|
18
|
+
type: 'suggestion',
|
|
19
|
+
},
|
|
20
|
+
create(context) {
|
|
21
|
+
function percentage(node: ESTree.Node | undefined) {
|
|
22
|
+
if (node?.type === 'TemplateLiteral')
|
|
23
|
+
return node.expressions.length > 0 && node.quasis.at(-1)?.value.cooked?.trim() === '%'
|
|
24
|
+
if (node?.type === 'BinaryExpression')
|
|
25
|
+
return node.operator === '+' && node.right.type === 'Literal' && node.right.value === '%'
|
|
26
|
+
return false
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return Styles.visit((element) => {
|
|
30
|
+
if (element.tag !== 'div' && element.tag !== 'span') return
|
|
31
|
+
|
|
32
|
+
const parent = element.node.parent
|
|
33
|
+
const empty =
|
|
34
|
+
parent.type === 'JSXElement' &&
|
|
35
|
+
parent.children.every((child) => child.type === 'JSXText' && child.value.trim() === '')
|
|
36
|
+
|
|
37
|
+
if (!empty) return
|
|
38
|
+
if (
|
|
39
|
+
percentage(element.declarations.get('width')) ||
|
|
40
|
+
percentage(element.declarations.get('inlineSize'))
|
|
41
|
+
)
|
|
42
|
+
context.report({
|
|
43
|
+
data: { element: element.tag, url: Docs.component('Progress') },
|
|
44
|
+
messageId: 'bar',
|
|
45
|
+
node: element.node,
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
},
|
|
49
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/** Points native elements styled as cards at `Surface`. @module */
|
|
2
|
+
import type { Rule } from '@oxlint/plugins'
|
|
3
|
+
import * as Docs from '../internal/Docs.ts'
|
|
4
|
+
import * as Styles from '../internal/Styles.ts'
|
|
5
|
+
|
|
6
|
+
// Elements that render a card's container. Inline elements and controls draw other components.
|
|
7
|
+
const tags = new Set(['article', 'aside', 'div', 'li', 'section'])
|
|
8
|
+
|
|
9
|
+
// Values that draw nothing, so the declaration does not make the element a card.
|
|
10
|
+
const empty = /^(?:0(?:\.0+)?(?:px|rem)?|initial|none|transparent|unset)$/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Reports an `article`, `aside`, `div`, `li`, or `section` whose styles draw a card: a corner
|
|
14
|
+
* radius and padding with a border or a background. A pill radius, such as `full`, and a
|
|
15
|
+
* positioned element, such as a hand-built popover, pass.
|
|
16
|
+
*/
|
|
17
|
+
export const rule: Rule = {
|
|
18
|
+
meta: {
|
|
19
|
+
docs: { description: 'Use `Surface` instead of a native element styled as a card.' },
|
|
20
|
+
messages: {
|
|
21
|
+
surface:
|
|
22
|
+
"Use 'Surface' from '@tempoxyz/ds' instead of styling <{{element}}> as a card. It sets the radius and padding together. See {{url}}",
|
|
23
|
+
},
|
|
24
|
+
schema: [],
|
|
25
|
+
type: 'suggestion',
|
|
26
|
+
},
|
|
27
|
+
create(context) {
|
|
28
|
+
return Styles.visit((element) => {
|
|
29
|
+
if (!tags.has(element.tag)) return
|
|
30
|
+
|
|
31
|
+
const declarations = element.declarations
|
|
32
|
+
|
|
33
|
+
function set(property: string) {
|
|
34
|
+
const node = declarations.get(property)
|
|
35
|
+
|
|
36
|
+
if (!node) return false
|
|
37
|
+
|
|
38
|
+
const value = Styles.text(node)
|
|
39
|
+
|
|
40
|
+
return value === undefined || !empty.test(value)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const properties = [...declarations.keys()]
|
|
44
|
+
const position = Styles.text(declarations.get('position'))
|
|
45
|
+
const radius = properties.some((property) => {
|
|
46
|
+
if (!property.endsWith('Radius') || !set(property)) return false
|
|
47
|
+
|
|
48
|
+
const value = Styles.text(declarations.get(property))
|
|
49
|
+
|
|
50
|
+
return value === undefined || !/^(?:50%|9{3,}(?:px)?|full)$/.test(value)
|
|
51
|
+
})
|
|
52
|
+
const padding = properties.some((property) => property.startsWith('padding') && set(property))
|
|
53
|
+
const border = properties.some(
|
|
54
|
+
(property) =>
|
|
55
|
+
/^border(?:Block|Bottom|Inline|Left|Right|Top)?(?:End|Start)?(?:Style|Width)?$/.test(
|
|
56
|
+
property,
|
|
57
|
+
) && set(property),
|
|
58
|
+
)
|
|
59
|
+
const background = set('background') || set('backgroundColor')
|
|
60
|
+
|
|
61
|
+
if (position === 'absolute' || position === 'fixed' || position === 'sticky') return
|
|
62
|
+
if (radius && padding && (border || background))
|
|
63
|
+
context.report({
|
|
64
|
+
data: { element: element.tag, url: Docs.component('Surface') },
|
|
65
|
+
messageId: 'surface',
|
|
66
|
+
node: element.node,
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
},
|
|
70
|
+
}
|
|
@@ -13,8 +13,10 @@ const list = new Intl.ListFormat('en', { type: 'disjunction' })
|
|
|
13
13
|
const web = ['aria-label', 'aria-labelledby']
|
|
14
14
|
const mobile = ['accessibilityLabel', 'accessibilityLabelledBy', 'aria-label', 'aria-labelledby']
|
|
15
15
|
|
|
16
|
-
// Components from each surface whose rendered output has no text to name it.
|
|
16
|
+
// Components from each surface whose rendered output has no text to name it. Marketing ships no
|
|
17
|
+
// components yet.
|
|
17
18
|
const targets: Record<Imports.Surface, Record<string, Target>> = {
|
|
19
|
+
marketing: {},
|
|
18
20
|
mobile: { IconButton: { names: mobile } },
|
|
19
21
|
web: {
|
|
20
22
|
Button: { names: web, when: { attribute: 'shape', value: 'icon' } },
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/** Requires a name, a role, and a tab stop on native scroll containers. @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
|
+
|
|
6
|
+
const list = new Intl.ListFormat('en', { type: 'conjunction' })
|
|
7
|
+
|
|
8
|
+
// Native elements and component names that take focus, so the region's content holds a tab stop.
|
|
9
|
+
const focusable =
|
|
10
|
+
/^(?:a|button|input|select|summary|textarea|.*(?:Button|Input|Link|Select|Trigger))$/
|
|
11
|
+
|
|
12
|
+
const overflows = ['overflow', 'overflowBlock', 'overflowInline', 'overflowX', 'overflowY']
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Reports a native element that scrolls, through `overflow` set to `auto` or `scroll`, without
|
|
16
|
+
* `role`, `tabIndex`, and `aria-label` or `aria-labelledby`. Keyboards scroll only a focusable
|
|
17
|
+
* region, and screen readers announce the region by its name. An element with a spread that the
|
|
18
|
+
* rule cannot read passes, since the spread can supply the attributes, and so does a container whose
|
|
19
|
+
* content holds a link, a button, a field, or an element with `tabIndex`.
|
|
20
|
+
*/
|
|
21
|
+
export const rule: Rule = {
|
|
22
|
+
meta: {
|
|
23
|
+
docs: { description: 'Name scroll containers and make them focusable.' },
|
|
24
|
+
messages: {
|
|
25
|
+
region:
|
|
26
|
+
'This scrolling <{{element}}> needs {{missing}}, so keyboard and screen reader users can reach and identify it. See {{url}}',
|
|
27
|
+
},
|
|
28
|
+
schema: [],
|
|
29
|
+
type: 'problem',
|
|
30
|
+
},
|
|
31
|
+
create(context) {
|
|
32
|
+
function attribute(node: ESTree.JSXOpeningElement, names: readonly string[]) {
|
|
33
|
+
return node.attributes.some(
|
|
34
|
+
(attribute) =>
|
|
35
|
+
attribute.type === 'JSXAttribute' &&
|
|
36
|
+
attribute.name.type === 'JSXIdentifier' &&
|
|
37
|
+
names.includes(attribute.name.name),
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Whether the JSX inside `node` renders an element that takes focus.
|
|
42
|
+
function interactive(node: ESTree.Node): boolean {
|
|
43
|
+
if (node.type === 'JSXOpeningElement') {
|
|
44
|
+
const name = context.sourceCode.getText(node.name).split('.').at(-1) ?? ''
|
|
45
|
+
|
|
46
|
+
if (focusable.test(name) || attribute(node, ['href', 'tabIndex'])) return true
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return (context.sourceCode.visitorKeys[node.type] ?? []).some((field) => {
|
|
50
|
+
const value: unknown = Reflect.get(node, field)
|
|
51
|
+
|
|
52
|
+
return (Array.isArray(value) ? value : [value]).some(
|
|
53
|
+
(child) => child && typeof child === 'object' && 'type' in child && interactive(child),
|
|
54
|
+
)
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return Styles.visit((element) => {
|
|
59
|
+
if (element.spread) return
|
|
60
|
+
|
|
61
|
+
const scrolls = overflows.some((property) => {
|
|
62
|
+
const value = Styles.text(element.declarations.get(property))
|
|
63
|
+
|
|
64
|
+
return value !== undefined && /\b(?:auto|scroll)\b/.test(value)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
if (!scrolls) return
|
|
68
|
+
if (
|
|
69
|
+
element.node.parent.type === 'JSXElement' &&
|
|
70
|
+
element.node.parent.children.some(interactive)
|
|
71
|
+
)
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
const missing = [
|
|
75
|
+
...(attribute(element.node, ['role']) ? [] : ['role="region"']),
|
|
76
|
+
...(attribute(element.node, ['tabIndex']) ? [] : ['tabIndex={0}']),
|
|
77
|
+
...(attribute(element.node, ['aria-label', 'aria-labelledby']) ? [] : ['an aria-label']),
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
if (missing.length > 0)
|
|
81
|
+
context.report({
|
|
82
|
+
data: {
|
|
83
|
+
element: element.tag,
|
|
84
|
+
missing: list.format(missing),
|
|
85
|
+
url: Docs.page('patterns/responsive-layout#scroll-wide-content-inside-its-own-region'),
|
|
86
|
+
},
|
|
87
|
+
messageId: 'region',
|
|
88
|
+
node: element.node,
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
},
|
|
92
|
+
}
|