@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.
Files changed (210) hide show
  1. package/README.md +7 -6
  2. package/dist/docs/AGENTS.md +12 -0
  3. package/dist/docs/index.md +145 -0
  4. package/dist/docs/marketing/introduction/getting-started.md +241 -0
  5. package/dist/docs/product/components/accordion.md +489 -0
  6. package/dist/docs/product/components/address-card.md +147 -0
  7. package/dist/docs/product/components/agent-logo.md +194 -0
  8. package/dist/docs/product/components/alert-dialog.md +631 -0
  9. package/dist/docs/product/components/alert.md +448 -0
  10. package/dist/docs/product/components/amount-input.md +305 -0
  11. package/dist/docs/product/components/amount.md +794 -0
  12. package/dist/docs/product/components/app-shell.md +559 -0
  13. package/dist/docs/product/components/asset-chip.md +294 -0
  14. package/dist/docs/product/components/autocomplete.md +620 -0
  15. package/dist/docs/product/components/avatar.md +321 -0
  16. package/dist/docs/product/components/badge.md +339 -0
  17. package/dist/docs/product/components/badged-logo.md +116 -0
  18. package/dist/docs/product/components/banner.md +379 -0
  19. package/dist/docs/product/components/bar-chart.md +208 -0
  20. package/dist/docs/product/components/breadcrumbs.md +380 -0
  21. package/dist/docs/product/components/button-group.md +286 -0
  22. package/dist/docs/product/components/button.md +650 -0
  23. package/dist/docs/product/components/calendar.md +375 -0
  24. package/dist/docs/product/components/callout.md +96 -0
  25. package/dist/docs/product/components/checkbox-group.md +521 -0
  26. package/dist/docs/product/components/checkbox.md +467 -0
  27. package/dist/docs/product/components/choice-group.md +516 -0
  28. package/dist/docs/product/components/choicebox.md +641 -0
  29. package/dist/docs/product/components/code.md +100 -0
  30. package/dist/docs/product/components/combobox.md +939 -0
  31. package/dist/docs/product/components/container.md +257 -0
  32. package/dist/docs/product/components/copy-button.md +642 -0
  33. package/dist/docs/product/components/date-picker.md +514 -0
  34. package/dist/docs/product/components/dialog.md +1332 -0
  35. package/dist/docs/product/components/drawer.md +966 -0
  36. package/dist/docs/product/components/empty.md +340 -0
  37. package/dist/docs/product/components/field.md +570 -0
  38. package/dist/docs/product/components/fieldset.md +398 -0
  39. package/dist/docs/product/components/frame.md +434 -0
  40. package/dist/docs/product/components/gauge.md +299 -0
  41. package/dist/docs/product/components/get-started-visual.md +117 -0
  42. package/dist/docs/product/components/grid.md +288 -0
  43. package/dist/docs/product/components/hero-visual.md +155 -0
  44. package/dist/docs/product/components/hex.md +487 -0
  45. package/dist/docs/product/components/icon-tile.md +292 -0
  46. package/dist/docs/product/components/identicon.md +371 -0
  47. package/dist/docs/product/components/inline.md +233 -0
  48. package/dist/docs/product/components/inset.md +225 -0
  49. package/dist/docs/product/components/kbd.md +423 -0
  50. package/dist/docs/product/components/list-group.md +981 -0
  51. package/dist/docs/product/components/loading-dots.md +237 -0
  52. package/dist/docs/product/components/menu.md +934 -0
  53. package/dist/docs/product/components/native-select.md +204 -0
  54. package/dist/docs/product/components/network-logo.md +248 -0
  55. package/dist/docs/product/components/number-field.md +543 -0
  56. package/dist/docs/product/components/otp-field.md +616 -0
  57. package/dist/docs/product/components/pagination.md +516 -0
  58. package/dist/docs/product/components/popover.md +544 -0
  59. package/dist/docs/product/components/progress.md +388 -0
  60. package/dist/docs/product/components/qr.md +314 -0
  61. package/dist/docs/product/components/radio-group.md +528 -0
  62. package/dist/docs/product/components/secret-value.md +406 -0
  63. package/dist/docs/product/components/select.md +751 -0
  64. package/dist/docs/product/components/separator.md +379 -0
  65. package/dist/docs/product/components/sidebar-menu.md +922 -0
  66. package/dist/docs/product/components/skeleton.md +517 -0
  67. package/dist/docs/product/components/slider.md +539 -0
  68. package/dist/docs/product/components/snippet.md +424 -0
  69. package/dist/docs/product/components/spinner.md +216 -0
  70. package/dist/docs/product/components/split-bar.md +139 -0
  71. package/dist/docs/product/components/split-button.md +464 -0
  72. package/dist/docs/product/components/stack.md +299 -0
  73. package/dist/docs/product/components/status-chip.md +226 -0
  74. package/dist/docs/product/components/status-indicator.md +119 -0
  75. package/dist/docs/product/components/status-row.md +174 -0
  76. package/dist/docs/product/components/stepper.md +451 -0
  77. package/dist/docs/product/components/success-hero.md +157 -0
  78. package/dist/docs/product/components/surface.md +639 -0
  79. package/dist/docs/product/components/switch.md +472 -0
  80. package/dist/docs/product/components/table.md +967 -0
  81. package/dist/docs/product/components/tabs.md +627 -0
  82. package/dist/docs/product/components/tempo-logo.md +185 -0
  83. package/dist/docs/product/components/text-input.md +624 -0
  84. package/dist/docs/product/components/textarea.md +352 -0
  85. package/dist/docs/product/components/toast.md +535 -0
  86. package/dist/docs/product/components/token-logo.md +371 -0
  87. package/dist/docs/product/components/token.md +247 -0
  88. package/dist/docs/product/components/tooltip.md +490 -0
  89. package/dist/docs/product/components/verification-code.md +116 -0
  90. package/dist/docs/product/components/visual.md +249 -0
  91. package/dist/docs/product/components.md +203 -0
  92. package/dist/docs/product/foundations/accessibility.md +377 -0
  93. package/dist/docs/product/foundations/agents.md +150 -0
  94. package/dist/docs/product/foundations/color.md +522 -0
  95. package/dist/docs/product/foundations/content/tone.md +194 -0
  96. package/dist/docs/product/foundations/content/voice.md +194 -0
  97. package/dist/docs/product/foundations/content/writing-style.md +406 -0
  98. package/dist/docs/product/foundations/iconography.md +282 -0
  99. package/dist/docs/product/foundations/intent.md +360 -0
  100. package/dist/docs/product/foundations/layout.md +508 -0
  101. package/dist/docs/product/foundations/linter.md +496 -0
  102. package/dist/docs/product/foundations/motion.md +151 -0
  103. package/dist/docs/product/foundations/radius-and-padding.md +86 -0
  104. package/dist/docs/product/foundations/typography.md +266 -0
  105. package/dist/docs/product/introduction/faq.md +133 -0
  106. package/dist/docs/product/introduction/getting-started.md +377 -0
  107. package/dist/docs/product/patterns/blockchain-values.md +709 -0
  108. package/dist/docs/product/patterns/dashboards.md +638 -0
  109. package/dist/docs/product/patterns/destructive-actions.md +626 -0
  110. package/dist/docs/product/patterns/empty-states.md +612 -0
  111. package/dist/docs/product/patterns/errors-and-validation.md +808 -0
  112. package/dist/docs/product/patterns/feedback-and-status.md +518 -0
  113. package/dist/docs/product/patterns/forms.md +679 -0
  114. package/dist/docs/product/patterns/loading.md +630 -0
  115. package/dist/docs/product/patterns/navigation.md +397 -0
  116. package/dist/docs/product/patterns/payments.md +765 -0
  117. package/dist/docs/product/patterns/responsive-layout.md +997 -0
  118. package/dist/docs/product/patterns/tables-and-lists.md +964 -0
  119. package/dist/docs/product/products/tempo-console.md +671 -0
  120. package/dist/docs/product/products/tempo-performance.md +437 -0
  121. package/dist/docs/product/products/tempo-wallet.md +438 -0
  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
@@ -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>` or
39
- * `<input type="checkbox">`. Elements inside a `render` prop are skipped, since the component that
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: { component: replacement, element: node.name.name },
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
+ }