@12-apps/ui 6.50.0 → 6.51.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (222) hide show
  1. package/components-catalog.md +2 -2
  2. package/dist/_internal/components/data-display/Badge/Badge.js +4 -4
  3. package/dist/_internal/components/data-display/Badge/Badge.styles.js +4 -4
  4. package/dist/_internal/components/data-display/Badge/BadgeContent.js +4 -4
  5. package/dist/_internal/components/data-display/Carousel/Carousel.chrome.js +4 -4
  6. package/dist/_internal/components/data-display/Carousel/Carousel.js +4 -4
  7. package/dist/_internal/components/data-display/Carousel/Carousel.parts.js +4 -4
  8. package/dist/_internal/components/data-display/Carousel/Carousel.styles.js +4 -4
  9. package/dist/_internal/components/data-display/Chart/Chart.js +4 -4
  10. package/dist/_internal/components/data-display/Chart/SpecChart.js +4 -4
  11. package/dist/_internal/components/data-display/Chart/chart-context.js +4 -4
  12. package/dist/_internal/components/data-display/Chart/chart-internals.js +4 -4
  13. package/dist/_internal/components/data-display/Chart/chart-renderers.js +4 -4
  14. package/dist/_internal/components/data-display/DataGrid/DataGrid.js +6 -6
  15. package/dist/_internal/components/data-display/DataGrid/DataGrid.model.js +5 -5
  16. package/dist/_internal/components/data-display/DataViews/DataViewsGrid.js +16 -16
  17. package/dist/_internal/components/data-display/DataViews/DataViewsTableBase.js +17 -17
  18. package/dist/_internal/components/data-display/DataViews/base-list-card-geometry.js +8 -8
  19. package/dist/_internal/components/data-display/DataViews/base-list-card-slots.js +7 -7
  20. package/dist/_internal/components/data-display/DataViews/base-list-card.js +9 -9
  21. package/dist/_internal/components/data-display/DataViews/data-views-display-panel.js +7 -7
  22. package/dist/_internal/components/data-display/DataViews/data-views-display-tabs.js +6 -6
  23. package/dist/_internal/components/data-display/DataViews/data-views-grid-bodies.js +9 -9
  24. package/dist/_internal/components/data-display/DataViews/data-views-grid-parts.js +15 -15
  25. package/dist/_internal/components/data-display/DataViews/data-views-layout-context.js +5 -5
  26. package/dist/_internal/components/data-display/DataViews/data-views-scope-tabs.js +6 -6
  27. package/dist/_internal/components/data-display/DataViews/data-views-shell-toolbar.js +9 -9
  28. package/dist/_internal/components/data-display/DataViews/data-views-toolbar.js +8 -8
  29. package/dist/_internal/components/data-display/DataViews/list-card-rails.js +6 -6
  30. package/dist/_internal/components/data-display/FleetMap/FleetMap.js +4 -4
  31. package/dist/_internal/components/data-display/Sheet/Sheet.body.js +4 -4
  32. package/dist/_internal/components/data-display/Sheet/Sheet.js +4 -4
  33. package/dist/_internal/components/data-display/Sheet/Sheet.styles.js +4 -4
  34. package/dist/_internal/components/data-display/Table/Table.js +5 -5
  35. package/dist/_internal/components/data-display/index.js +7 -7
  36. package/dist/_internal/components/form/Command/Command.js +4 -4
  37. package/dist/_internal/components/form/Command/Command.styles.js +4 -4
  38. package/dist/_internal/components/form/Label/Label.js +4 -4
  39. package/dist/_internal/components/form/Label/Label.styles.js +4 -4
  40. package/dist/_internal/components/form/Menubar/Menubar.item.js +4 -4
  41. package/dist/_internal/components/form/Menubar/Menubar.js +4 -4
  42. package/dist/_internal/components/form/Menubar/Menubar.styles.js +4 -4
  43. package/dist/_internal/components/form/Menubar/Menubar.toolbar.js +4 -4
  44. package/dist/_internal/components/form/index.js +4 -4
  45. package/dist/_internal/components/index.js +23 -23
  46. package/dist/_internal/components/layout/Box/Box.js +7 -7
  47. package/dist/_internal/components/layout/SettingCard/SettingCard.js +9 -9
  48. package/dist/_internal/components/layout/SettingCard/SettingCard.parts.js +8 -8
  49. package/dist/_internal/components/layout/SettingCard/SettingGrid.js +9 -9
  50. package/dist/_internal/components/layout/SettingCard/SettingGroup.js +10 -10
  51. package/dist/_internal/components/layout/SettingCard/SettingSwitch.js +9 -9
  52. package/dist/_internal/components/layout/SettingCard/SettingToggle.js +10 -10
  53. package/dist/_internal/components/layout/Stack/Stack.js +8 -8
  54. package/dist/_internal/components/layout/index.js +13 -13
  55. package/dist/_internal/components/navigation/AppHeader/AppHeader.details.js +4 -4
  56. package/dist/_internal/components/navigation/AppHeader/AppHeader.identity.js +4 -4
  57. package/dist/_internal/components/navigation/SectionNav/SectionNav.bar.js +9 -9
  58. package/dist/_internal/components/navigation/SectionNav/SectionNav.helpers.js +5 -1
  59. package/dist/_internal/components/navigation/SectionNav/SectionNav.js +11 -11
  60. package/dist/_internal/components/navigation/SectionNav/SectionNav.parts.js +12 -6
  61. package/dist/_internal/components/navigation/SectionNav/SectionNav.primary.js +7 -7
  62. package/dist/_internal/components/navigation/SectionNav/SectionNav.rail.js +7 -7
  63. package/dist/_internal/components/navigation/SectionNav/SectionNav.sheet.js +7 -7
  64. package/dist/_internal/components/navigation/SectionNav/__stories__/fixtures.js +16 -4
  65. package/dist/_internal/components/navigation/SectionNav/__stories__/fixtures.js.map +1 -1
  66. package/dist/_internal/components/navigation/index.js +11 -11
  67. package/dist/_internal/components/utility/WorkflowStep/WorkflowStep.helpers.js +4 -4
  68. package/dist/_internal/components/utility/WorkflowStep/WorkflowStep.js +4 -4
  69. package/dist/_internal/components/utility/WorkflowStep/WorkflowStep.parts.js +4 -4
  70. package/dist/_internal/components/utility/WorkflowStep/WorkflowStepRow.js +4 -4
  71. package/dist/_internal/components/utility/index.js +4 -4
  72. package/dist/_internal/icons/Icon.js +7 -7
  73. package/dist/_internal/provider/UiProvider.js +6 -6
  74. package/dist/_internal/provider/mui-bridge.js +5 -5
  75. package/dist/_internal/provider/use-ui-theme.js +6 -6
  76. package/dist/_internal/tokens/density-overrides.js +3 -3
  77. package/dist/_internal/tokens/density-overrides.navigation.js +1 -1
  78. package/dist/_internal/tokens/density-overrides.selection.js +1 -1
  79. package/dist/_internal/tokens/density.js +4 -4
  80. package/dist/charts.js +4 -4
  81. package/dist/{chunk-XK77FY42.js → chunk-25MSGLNU.js} +4 -3
  82. package/dist/chunk-25MSGLNU.js.map +1 -0
  83. package/dist/{chunk-7VC6YBD3.js → chunk-3U45DRNS.js} +2 -2
  84. package/dist/{chunk-ZQCZMSLO.js → chunk-4B5WSVS7.js} +2 -2
  85. package/dist/{chunk-F4FOBTLJ.js → chunk-4BI5PCOM.js} +2 -2
  86. package/dist/{chunk-HVZ4HF4M.js → chunk-5N2UJOCT.js} +2 -2
  87. package/dist/{chunk-DFCX3FNB.js → chunk-5ZBFADBL.js} +2 -2
  88. package/dist/{chunk-GLKQT2DS.js → chunk-A436VORP.js} +33 -4
  89. package/dist/chunk-A436VORP.js.map +1 -0
  90. package/dist/{chunk-LWBR5U6V.js → chunk-ABPTS3XK.js} +2 -2
  91. package/dist/{chunk-43EQITNF.js → chunk-BHPLHGZP.js} +4 -4
  92. package/dist/{chunk-YMW2VYGU.js → chunk-DLO5V4AV.js} +2 -2
  93. package/dist/{chunk-7FQF5BC2.js → chunk-ESW4Z5I7.js} +2 -2
  94. package/dist/{chunk-ZPD6MGAO.js → chunk-FDJLYYDA.js} +4 -4
  95. package/dist/{chunk-LZL6HDTZ.js → chunk-FM5RBGG7.js} +2 -2
  96. package/dist/{chunk-V2SAZOT2.js → chunk-FYTC4LOD.js} +2 -2
  97. package/dist/{chunk-4ELI2MM3.js → chunk-GAXHYRX6.js} +9 -6
  98. package/dist/chunk-GAXHYRX6.js.map +1 -0
  99. package/dist/{chunk-CT45KNCT.js → chunk-GPTICJVN.js} +3 -3
  100. package/dist/{chunk-URGH4673.js → chunk-H556NMKE.js} +4 -4
  101. package/dist/{chunk-VOF6QVCF.js → chunk-HF7V3RI5.js} +2 -2
  102. package/dist/{chunk-NKJMOWY3.js → chunk-HJUR6ZY6.js} +2 -2
  103. package/dist/{chunk-RX7J6VRI.js → chunk-HKEBTPKY.js} +119 -42
  104. package/dist/chunk-HKEBTPKY.js.map +1 -0
  105. package/dist/{chunk-ZC3NEUNQ.js → chunk-I2LF53MJ.js} +2 -2
  106. package/dist/{chunk-ZFQVT6HZ.js → chunk-II7Z5UB6.js} +2 -2
  107. package/dist/{chunk-K65NSHQI.js → chunk-L3CQTVFU.js} +4 -4
  108. package/dist/{chunk-EY4GPLMF.js → chunk-MT7XMJUJ.js} +5 -5
  109. package/dist/{chunk-TELVW2OI.js → chunk-MYCR7FMI.js} +2 -2
  110. package/dist/{chunk-56TGECKZ.js → chunk-NP6SHLQ3.js} +4 -3
  111. package/dist/chunk-NP6SHLQ3.js.map +1 -0
  112. package/dist/{chunk-WB6BTUXQ.js → chunk-OWILUXWM.js} +2 -2
  113. package/dist/{chunk-VHVI3D4R.js → chunk-P3GBXE73.js} +11 -2
  114. package/dist/chunk-P3GBXE73.js.map +1 -0
  115. package/dist/{chunk-WSBTMZJM.js → chunk-P7YUOH2E.js} +2 -2
  116. package/dist/{chunk-VOP2BXKL.js → chunk-QWTPK3FT.js} +2 -2
  117. package/dist/{chunk-XQ6Y5Z7R.js → chunk-RCJOTNIW.js} +2 -2
  118. package/dist/{chunk-32JMSHEA.js → chunk-S4ZUZ6DS.js} +10 -6
  119. package/dist/chunk-S4ZUZ6DS.js.map +1 -0
  120. package/dist/{chunk-SWDDXMV7.js → chunk-SJ4IAZ3V.js} +4 -4
  121. package/dist/{chunk-FHWBOOS3.js → chunk-TH7RBREI.js} +2 -2
  122. package/dist/{chunk-SA2ERYVV.js → chunk-VAO63IIP.js} +3 -3
  123. package/dist/{chunk-43Q6CVPB.js → chunk-VELGOFCL.js} +127 -78
  124. package/dist/chunk-VELGOFCL.js.map +1 -0
  125. package/dist/{chunk-RCJYLDXV.js → chunk-VJBTFWDK.js} +4 -4
  126. package/dist/chunk-WNKRFMSI.js +113 -0
  127. package/dist/chunk-WNKRFMSI.js.map +1 -0
  128. package/dist/{chunk-LM2YOONY.js → chunk-XYHNXO6Q.js} +2 -2
  129. package/dist/{chunk-FT7BNFGS.js → chunk-YK4ZG5Y4.js} +2 -2
  130. package/dist/data-display/Badge.js +4 -4
  131. package/dist/data-display/Carousel.js +4 -4
  132. package/dist/data-display/Chart.js +4 -4
  133. package/dist/data-display/DataGrid.js +6 -6
  134. package/dist/data-display/DataViews.js +20 -20
  135. package/dist/data-display/FleetMap.js +4 -4
  136. package/dist/data-display/Sheet.js +4 -4
  137. package/dist/data-display/Table.js +5 -5
  138. package/dist/form/Command.js +4 -4
  139. package/dist/form/Label.js +4 -4
  140. package/dist/form/Menubar.js +4 -4
  141. package/dist/icons.js +7 -7
  142. package/dist/layout/Box.js +7 -7
  143. package/dist/layout/SettingCard.js +13 -13
  144. package/dist/layout/Stack.js +8 -8
  145. package/dist/navigation/AppHeader.js +4 -4
  146. package/dist/navigation/SectionNav.js +11 -11
  147. package/dist/provider.js +7 -7
  148. package/dist/tokens.js +4 -4
  149. package/dist/types/components/navigation/SectionNav/SectionNav.bar.d.ts.map +1 -1
  150. package/dist/types/components/navigation/SectionNav/SectionNav.helpers.d.ts +12 -1
  151. package/dist/types/components/navigation/SectionNav/SectionNav.helpers.d.ts.map +1 -1
  152. package/dist/types/components/navigation/SectionNav/SectionNav.parts.d.ts +36 -2
  153. package/dist/types/components/navigation/SectionNav/SectionNav.parts.d.ts.map +1 -1
  154. package/dist/types/components/navigation/SectionNav/SectionNav.primary.d.ts +22 -4
  155. package/dist/types/components/navigation/SectionNav/SectionNav.primary.d.ts.map +1 -1
  156. package/dist/types/components/navigation/SectionNav/SectionNav.rail.d.ts.map +1 -1
  157. package/dist/types/components/navigation/SectionNav/SectionNav.sheet.d.ts.map +1 -1
  158. package/dist/types/components/navigation/SectionNav/SectionNav.types.d.ts +81 -5
  159. package/dist/types/components/navigation/SectionNav/SectionNav.types.d.ts.map +1 -1
  160. package/dist/types/components/navigation/SectionNav/__stories__/fixtures.d.ts +11 -1
  161. package/dist/types/components/navigation/SectionNav/__stories__/fixtures.d.ts.map +1 -1
  162. package/dist/types/components/navigation/SectionNav/index.d.ts +1 -1
  163. package/dist/types/components/navigation/SectionNav/index.d.ts.map +1 -1
  164. package/dist/types/tokens/density-overrides.d.ts.map +1 -1
  165. package/dist/types/tokens/density-overrides.navigation.d.ts.map +1 -1
  166. package/dist/types/tokens/density-overrides.selection.d.ts.map +1 -1
  167. package/dist/utility/WorkflowStep.js +4 -4
  168. package/package.json +1 -1
  169. package/src/components/navigation/SectionNav/SectionNav.bar.tsx +161 -88
  170. package/src/components/navigation/SectionNav/SectionNav.helpers.ts +16 -1
  171. package/src/components/navigation/SectionNav/SectionNav.md +40 -4
  172. package/src/components/navigation/SectionNav/SectionNav.parts.tsx +59 -1
  173. package/src/components/navigation/SectionNav/SectionNav.primary.tsx +107 -30
  174. package/src/components/navigation/SectionNav/SectionNav.rail.tsx +154 -41
  175. package/src/components/navigation/SectionNav/SectionNav.sheet.tsx +6 -2
  176. package/src/components/navigation/SectionNav/SectionNav.types.ts +82 -5
  177. package/src/components/navigation/SectionNav/__stories__/fixtures.tsx +21 -1
  178. package/src/components/navigation/SectionNav/index.ts +1 -0
  179. package/src/tokens/density-overrides.navigation.ts +15 -2
  180. package/src/tokens/density-overrides.selection.ts +31 -14
  181. package/src/tokens/density-overrides.ts +27 -11
  182. package/dist/chunk-32JMSHEA.js.map +0 -1
  183. package/dist/chunk-43Q6CVPB.js.map +0 -1
  184. package/dist/chunk-4ELI2MM3.js.map +0 -1
  185. package/dist/chunk-56TGECKZ.js.map +0 -1
  186. package/dist/chunk-GLKQT2DS.js.map +0 -1
  187. package/dist/chunk-RX7J6VRI.js.map +0 -1
  188. package/dist/chunk-RZ4SBSLP.js +0 -62
  189. package/dist/chunk-RZ4SBSLP.js.map +0 -1
  190. package/dist/chunk-VHVI3D4R.js.map +0 -1
  191. package/dist/chunk-XK77FY42.js.map +0 -1
  192. /package/dist/{chunk-7VC6YBD3.js.map → chunk-3U45DRNS.js.map} +0 -0
  193. /package/dist/{chunk-ZQCZMSLO.js.map → chunk-4B5WSVS7.js.map} +0 -0
  194. /package/dist/{chunk-F4FOBTLJ.js.map → chunk-4BI5PCOM.js.map} +0 -0
  195. /package/dist/{chunk-HVZ4HF4M.js.map → chunk-5N2UJOCT.js.map} +0 -0
  196. /package/dist/{chunk-DFCX3FNB.js.map → chunk-5ZBFADBL.js.map} +0 -0
  197. /package/dist/{chunk-LWBR5U6V.js.map → chunk-ABPTS3XK.js.map} +0 -0
  198. /package/dist/{chunk-43EQITNF.js.map → chunk-BHPLHGZP.js.map} +0 -0
  199. /package/dist/{chunk-YMW2VYGU.js.map → chunk-DLO5V4AV.js.map} +0 -0
  200. /package/dist/{chunk-7FQF5BC2.js.map → chunk-ESW4Z5I7.js.map} +0 -0
  201. /package/dist/{chunk-ZPD6MGAO.js.map → chunk-FDJLYYDA.js.map} +0 -0
  202. /package/dist/{chunk-LZL6HDTZ.js.map → chunk-FM5RBGG7.js.map} +0 -0
  203. /package/dist/{chunk-V2SAZOT2.js.map → chunk-FYTC4LOD.js.map} +0 -0
  204. /package/dist/{chunk-CT45KNCT.js.map → chunk-GPTICJVN.js.map} +0 -0
  205. /package/dist/{chunk-URGH4673.js.map → chunk-H556NMKE.js.map} +0 -0
  206. /package/dist/{chunk-VOF6QVCF.js.map → chunk-HF7V3RI5.js.map} +0 -0
  207. /package/dist/{chunk-NKJMOWY3.js.map → chunk-HJUR6ZY6.js.map} +0 -0
  208. /package/dist/{chunk-ZC3NEUNQ.js.map → chunk-I2LF53MJ.js.map} +0 -0
  209. /package/dist/{chunk-ZFQVT6HZ.js.map → chunk-II7Z5UB6.js.map} +0 -0
  210. /package/dist/{chunk-K65NSHQI.js.map → chunk-L3CQTVFU.js.map} +0 -0
  211. /package/dist/{chunk-EY4GPLMF.js.map → chunk-MT7XMJUJ.js.map} +0 -0
  212. /package/dist/{chunk-TELVW2OI.js.map → chunk-MYCR7FMI.js.map} +0 -0
  213. /package/dist/{chunk-WB6BTUXQ.js.map → chunk-OWILUXWM.js.map} +0 -0
  214. /package/dist/{chunk-WSBTMZJM.js.map → chunk-P7YUOH2E.js.map} +0 -0
  215. /package/dist/{chunk-VOP2BXKL.js.map → chunk-QWTPK3FT.js.map} +0 -0
  216. /package/dist/{chunk-XQ6Y5Z7R.js.map → chunk-RCJOTNIW.js.map} +0 -0
  217. /package/dist/{chunk-SWDDXMV7.js.map → chunk-SJ4IAZ3V.js.map} +0 -0
  218. /package/dist/{chunk-FHWBOOS3.js.map → chunk-TH7RBREI.js.map} +0 -0
  219. /package/dist/{chunk-SA2ERYVV.js.map → chunk-VAO63IIP.js.map} +0 -0
  220. /package/dist/{chunk-RCJYLDXV.js.map → chunk-VJBTFWDK.js.map} +0 -0
  221. /package/dist/{chunk-LM2YOONY.js.map → chunk-XYHNXO6Q.js.map} +0 -0
  222. /package/dist/{chunk-FT7BNFGS.js.map → chunk-YK4ZG5Y4.js.map} +0 -0
@@ -31,10 +31,13 @@ screen.
31
31
  - **layout**: `'bar' | 'rail'`.
32
32
  - **label**: the navigation landmark's accessible name.
33
33
  - **destinations**: `SectionNavDestination[]`, each with `id`, `label`, `icon`,
34
- `href`, `badge?` and `active?`.
35
- - **primary**: a `SectionNavMenu` (`label`, `icon`, `title`, `groups`). In the
36
- bar it is the raised button, whose name becomes `copy.close` while its sheet
37
- is open.
34
+ `href` OR `onSelect`, `badge?`, `active?`, `disabled?`, `dimmed?`,
35
+ `loading?` and `dataTestId?`.
36
+ - **primary**: a `SectionNavMenu` (`label`, `icon`, `title`, `groups`) or a
37
+ `SectionNavAction` (`label`, `icon`, `onSelect`, `disabled?`, `loading?`). In
38
+ the bar it is the raised button. A menu opens its sheet, and the button's name
39
+ becomes `copy.close` while it is open. An action runs on tap and its label is
40
+ drawn under the button.
38
41
  - **more**: a `SectionNavMenu`. In the bar it is the last slot, and its badge
39
42
  is the sum of its entries' badges.
40
43
  - **back**, **heading**: the rail only.
@@ -48,6 +51,36 @@ screen.
48
51
  named packs; `SECTION_NAV_COPY` is the locale pack.
49
52
  - **dataTestId**: the prefix for every test id (`section-nav`).
50
53
 
54
+ ## A bar of verbs
55
+
56
+ The same bar pins a SCREEN's actions at its foot: give destinations an
57
+ `onSelect` instead of an `href`, and `primary` a `SectionNavAction`. A bar of
58
+ places and a bar of verbs are one component, so they cannot drift into two
59
+ looks one tap apart.
60
+
61
+ - `disabled` draws the slot dimmed and makes it a native disabled button, even
62
+ when it carries an `href`: a link cannot be disabled, and one that looks
63
+ dimmed but still navigates is a lie. A tap does nothing; `onSelect` is not
64
+ called.
65
+ - `dimmed` is for an act the screen cannot do now but should EXPLAIN when
66
+ tapped. It is drawn exactly like `disabled` (the `text.disabled` colour) and
67
+ reports `aria-disabled="true"`, but it is NOT natively disabled: it stays
68
+ focusable, its `onSelect` still fires, and the host answers with a message
69
+ (why not, and what would make it possible). It is never lit.
70
+ - `loading` replaces the icon with a spinner and reports `aria-busy` and
71
+ `aria-disabled="true"` until the write it started answers. It stays
72
+ focusable, so the keyboard keeps its place, and a tap is ignored. The same
73
+ holds for a loading action `primary`. Native `disabled` is used only for
74
+ `disabled`.
75
+ - `active` on an action slot marks a TOGGLE that is on: it lights the slot and
76
+ reports `aria-pressed="true"` (`false` when `active: false`). Only a link
77
+ reports `aria-current="page"`; an action is not a page.
78
+ - A menu entry, and a whole menu, can be `disabled` too. In the bar a disabled
79
+ menu's trigger is inert and opens no sheet; in the rail, where the menu is
80
+ listed rather than behind a trigger, every row of it is inert.
81
+ - `dataTestId` on a destination, entry, action or menu replaces the id the nav
82
+ would derive, for a host whose suites already drive their own.
83
+
51
84
  A menu's **groups** carry `entries`. Each entry has an `href` (a link) or an
52
85
  `onSelect` (an action). `layout: 'grid'` draws three tiles across in the sheet;
53
86
  `list` (the default) draws one row per entry with its `description`. Choosing
@@ -81,6 +114,9 @@ the slot reports the sheet through `aria-expanded`.
81
114
 
82
115
  The raised round button on its own, for a host that already draws its own bar:
83
116
  drop it between two tabs in a flex row. Without `open` it is a plain action.
117
+ With `captioned` its label is drawn under it, hidden from assistive tech so
118
+ the button is named once. `disabled` is native; `loading` keeps it focusable
119
+ with `aria-busy` and `aria-disabled`, and drops the click.
84
120
  With `open` it reports `aria-expanded`, takes `closeLabel` as its name, and its
85
121
  icon turns 45° while open.
86
122
 
@@ -1,4 +1,5 @@
1
1
  import Box from '@mui/material/Box/index.js';
2
+ import CircularProgress from '@mui/material/CircularProgress/index.js';
2
3
  import type { SxProps, Theme } from '@mui/material/styles/index.js';
3
4
  import type { ElementType, ReactNode } from 'react';
4
5
 
@@ -6,6 +7,7 @@ import { rem } from '../../../tokens/scales';
6
7
  import { Badge } from '../../data-display/Badge';
7
8
 
8
9
  import { shownCount } from './SectionNav.helpers';
10
+ import type { SectionNavDestination } from './SectionNav.types';
9
11
 
10
12
  /**
11
13
  * An icon with its count on the shoulder.
@@ -59,21 +61,76 @@ export function focusRing(theme: Theme): SxProps<Theme> {
59
61
  * `href`, a button for an `onSelect`.
60
62
  *
61
63
  * Returned as props for a `Box`, so the control stays ONE interactive
62
- * element — never a button wrapped around a link.
64
+ * element — never a button wrapped around a link. Whatever it would lead to,
65
+ * a control that is not live is a BUTTON: a link cannot be disabled, and one
66
+ * that looks dimmed but still navigates is the lie this rules out.
67
+ *
68
+ * - `inert` — a native `disabled` button: no focus, no click.
69
+ * - `busy` — a write is in flight: `aria-disabled`, still focusable, and the
70
+ * click is dropped, so the keyboard keeps its place until the write answers.
71
+ * - `dimmed` — `aria-disabled`, still focusable, and the click STILL runs, so
72
+ * the host can explain why the act cannot be done now.
63
73
  */
64
74
  export function controlProps({
65
75
  href,
66
76
  onClick,
67
77
  linkComponent,
78
+ inert = false,
79
+ busy = false,
80
+ dimmed = false,
68
81
  }: {
69
82
  href: string | undefined;
70
83
  onClick?: () => void;
71
84
  linkComponent: ElementType | undefined;
85
+ inert?: boolean;
86
+ busy?: boolean;
87
+ dimmed?: boolean;
72
88
  }): Record<string, unknown> {
89
+ if (inert) return { component: 'button', type: 'button', disabled: true };
90
+ if (busy) return { component: 'button', type: 'button', 'aria-disabled': 'true' };
91
+ if (dimmed) return { component: 'button', type: 'button', 'aria-disabled': 'true', onClick };
73
92
  if (href !== undefined) return { component: linkComponent ?? 'a', href, onClick };
74
93
  return { component: 'button', type: 'button', onClick };
75
94
  }
76
95
 
96
+ /**
97
+ * A destination's control: `controlProps` from its own state, plus
98
+ * `aria-pressed` when it is an action slot that is a toggle. A link reports
99
+ * where the viewer is through `aria-current` instead — the row draws that.
100
+ */
101
+ export function destinationControl(
102
+ destination: SectionNavDestination,
103
+ linkComponent: ElementType | undefined,
104
+ onClick: () => void,
105
+ ): Record<string, unknown> {
106
+ return {
107
+ ...controlProps({
108
+ href: destination.href,
109
+ linkComponent,
110
+ inert: destination.disabled === true,
111
+ busy: destination.loading === true,
112
+ dimmed: destination.dimmed === true,
113
+ onClick,
114
+ }),
115
+ 'aria-pressed': destination.href === undefined ? destination.active : undefined,
116
+ };
117
+ }
118
+
119
+ /**
120
+ * The selector for a control drawn as not live: natively disabled, or
121
+ * `aria-disabled` (in flight, or dimmed) and still focusable.
122
+ */
123
+ export const NOT_LIVE = '&:disabled, &[aria-disabled="true"]';
124
+
125
+ /**
126
+ * The icon, or the spinner that stands in for it while the slot's write is in
127
+ * flight — the same box, so the label under it does not move.
128
+ */
129
+ export function SlotIcon({ icon, loading }: { icon: ReactNode; loading: boolean }): React.JSX.Element {
130
+ if (!loading) return <>{icon}</>;
131
+ return <CircularProgress size="1em" color="inherit" thickness={5} data-testid="section-nav-spinner" />;
132
+ }
133
+
77
134
  /** The base reset every clickable in the nav shares: no underline, no button chrome. */
78
135
  export const CONTROL_RESET = {
79
136
  border: 0,
@@ -83,6 +140,7 @@ export const CONTROL_RESET = {
83
140
  textDecoration: 'none',
84
141
  cursor: 'pointer',
85
142
  margin: 0,
143
+ [NOT_LIVE]: { cursor: 'default' },
86
144
  } as const;
87
145
 
88
146
  export { Box };
@@ -1,12 +1,17 @@
1
- import { useTheme } from '@mui/material/styles/index.js';
1
+ import Typography from '@mui/material/Typography/index.js';
2
+ import { useTheme, type SxProps, type Theme } from '@mui/material/styles/index.js';
2
3
  import type { ReactNode } from 'react';
3
4
 
4
5
  import { rem } from '../../../tokens/scales';
5
6
 
6
- import { Box, CONTROL_RESET, focusRing } from './SectionNav.parts';
7
+ import { Box, CONTROL_RESET, NOT_LIVE, SlotIcon, focusRing } from './SectionNav.parts';
7
8
 
8
9
  export interface RaisedActionButtonProps {
9
- /** The button's accessible name — it has no visible label. */
10
+ /**
11
+ * The button's accessible name. Drawn under the button only when
12
+ * `captioned`; the caption is hidden from assistive tech, so the name is
13
+ * read once either way.
14
+ */
10
15
  label: string;
11
16
  icon: ReactNode;
12
17
  onClick: () => void;
@@ -18,6 +23,19 @@ export interface RaisedActionButtonProps {
18
23
  open?: boolean;
19
24
  /** The name it takes while `open` — the same control closes what it opened. REQUIRED with `open`. */
20
25
  closeLabel?: string;
26
+ /**
27
+ * Draw `label` under the button. A bar of VERBS names the act it is for; a
28
+ * bar of places can leave its one action to the icon.
29
+ */
30
+ captioned?: boolean;
31
+ /** Dimmed and not operable: a native `disabled` button. */
32
+ disabled?: boolean;
33
+ /**
34
+ * A write it started is in flight: a spinner replaces the icon, it reports
35
+ * `aria-busy` and `aria-disabled`, and a tap is ignored. It stays focusable,
36
+ * so the keyboard does not lose its place while the write answers.
37
+ */
38
+ loading?: boolean;
21
39
  dataTestId?: string;
22
40
  }
23
41
 
@@ -26,8 +44,9 @@ export interface RaisedActionButtonProps {
26
44
  * the row that is an ACTION rather than a place.
27
45
  *
28
46
  * Lifted half out of the bar so it reads as different in kind from the tabs
29
- * beside it, and unlabelled so it costs the row less width than a tab: that
30
- * width is what lets a bar keep four labelled tabs AND this button at 320px.
47
+ * beside it. Uncaptioned it costs the row less width than a tab: that width is
48
+ * what lets a bar keep four labelled tabs AND this button at 320px. A bar of
49
+ * verbs captions it, naming the act under the button.
31
50
  *
32
51
  * `SectionNav` draws it for its `primary` menu. It is exported on its own for
33
52
  * a bar the host already draws — drop it between two tabs, in a flex row.
@@ -38,44 +57,102 @@ export function RaisedActionButton({
38
57
  onClick,
39
58
  open,
40
59
  closeLabel,
60
+ captioned = false,
61
+ disabled = false,
62
+ loading = false,
41
63
  dataTestId = 'raised-action',
42
64
  }: RaisedActionButtonProps): React.JSX.Element {
43
65
  const theme = useTheme();
44
- const size = rem(theme, 52);
45
66
  const expanded = open === true;
46
67
  return (
47
- <Box sx={{ flex: 'none', display: 'flex', alignItems: 'flex-start', justifyContent: 'center', px: 0.5 }}>
68
+ <Box
69
+ sx={{
70
+ flex: 'none',
71
+ display: 'flex',
72
+ flexDirection: 'column',
73
+ alignItems: 'center',
74
+ justifyContent: 'flex-start',
75
+ px: 0.5,
76
+ }}
77
+ >
48
78
  <Box
49
79
  component="button"
50
80
  type="button"
51
- onClick={onClick}
81
+ {...raisedState({ onClick, disabled, loading })}
52
82
  aria-label={expanded && closeLabel ? closeLabel : label}
53
83
  aria-expanded={open}
54
84
  data-testid={dataTestId}
55
- sx={{
56
- ...CONTROL_RESET,
57
- ...focusRing(theme),
58
- width: size,
59
- height: size,
60
- // Lifted half out of the bar: the one control that is not a place.
61
- marginTop: rem(theme, -18),
62
- borderRadius: '50%',
63
- display: 'flex',
64
- alignItems: 'center',
65
- justifyContent: 'center',
66
- bgcolor: 'primary.main',
67
- color: 'primary.contrastText',
68
- boxShadow: theme.shadows[6],
69
- '& svg': {
70
- fontSize: rem(theme, 26),
71
- transition: theme.transitions.create('transform', { duration: theme.transitions.duration.shorter }),
72
- transform: expanded ? 'rotate(45deg)' : 'none',
73
- },
74
- '@media (prefers-reduced-motion: reduce)': { '& svg': { transition: 'none' } },
75
- }}
85
+ sx={raisedSx(theme, expanded)}
76
86
  >
77
- {icon}
87
+ <SlotIcon icon={icon} loading={loading} />
78
88
  </Box>
89
+ {captioned ? <RaisedCaption label={label} inert={disabled || loading} /> : null}
79
90
  </Box>
80
91
  );
81
92
  }
93
+
94
+ /**
95
+ * The button's operability. Only `disabled` is native: an in-flight button
96
+ * keeps its focus, reports `aria-busy` and `aria-disabled`, and drops the click.
97
+ */
98
+ function raisedState({
99
+ onClick,
100
+ disabled,
101
+ loading,
102
+ }: {
103
+ onClick: () => void;
104
+ disabled: boolean;
105
+ loading: boolean;
106
+ }): Record<string, unknown> {
107
+ const busy = loading && !disabled;
108
+ return {
109
+ onClick: busy ? undefined : onClick,
110
+ disabled,
111
+ 'aria-disabled': busy ? 'true' : undefined,
112
+ 'aria-busy': loading || undefined,
113
+ };
114
+ }
115
+
116
+ /**
117
+ * The label under a captioned button. Hidden from assistive tech: the button
118
+ * already carries the same words as its name, and reading them twice is noise.
119
+ */
120
+ function RaisedCaption({ label, inert }: { label: string; inert: boolean }): React.JSX.Element {
121
+ return (
122
+ <Typography
123
+ component="span"
124
+ variant="caption"
125
+ aria-hidden
126
+ sx={{ lineHeight: 1.3, mt: 0.25, whiteSpace: 'nowrap', color: inert ? 'text.disabled' : 'text.primary' }}
127
+ >
128
+ {label}
129
+ </Typography>
130
+ );
131
+ }
132
+
133
+ /** The round button: lifted half out of the bar, its icon turning while what it opened is open. */
134
+ function raisedSx(theme: Theme, expanded: boolean): SxProps<Theme> {
135
+ const size = rem(theme, 52);
136
+ return {
137
+ ...CONTROL_RESET,
138
+ ...focusRing(theme),
139
+ width: size,
140
+ height: size,
141
+ // Lifted half out of the bar: the one control that is not a place.
142
+ marginTop: rem(theme, -18),
143
+ borderRadius: '50%',
144
+ display: 'flex',
145
+ alignItems: 'center',
146
+ justifyContent: 'center',
147
+ bgcolor: 'primary.main',
148
+ color: 'primary.contrastText',
149
+ boxShadow: theme.shadows[6],
150
+ [NOT_LIVE]: { cursor: 'default', bgcolor: 'action.disabledBackground', color: 'text.disabled', boxShadow: 'none' },
151
+ '& svg': {
152
+ fontSize: rem(theme, 26),
153
+ transition: theme.transitions.create('transform', { duration: theme.transitions.duration.shorter }),
154
+ transform: expanded ? 'rotate(45deg)' : 'none',
155
+ },
156
+ '@media (prefers-reduced-motion: reduce)': { '& svg': { transition: 'none' } },
157
+ } as SxProps<Theme>;
158
+ }
@@ -5,8 +5,24 @@ import type { ElementType, ReactNode } from 'react';
5
5
  import type { SectionNavCopy } from '../../../copy';
6
6
  import { rem } from '../../../tokens/scales';
7
7
 
8
- import { Box, CONTROL_RESET, CountedIcon, controlProps, focusRing } from './SectionNav.parts';
9
- import type { SectionNavMenu, SectionNavProps } from './SectionNav.types';
8
+ import { destinationState, isMenu } from './SectionNav.helpers';
9
+ import {
10
+ Box,
11
+ CONTROL_RESET,
12
+ CountedIcon,
13
+ NOT_LIVE,
14
+ SlotIcon,
15
+ controlProps,
16
+ destinationControl,
17
+ focusRing,
18
+ } from './SectionNav.parts';
19
+ import type {
20
+ SectionNavAction,
21
+ SectionNavBack,
22
+ SectionNavDestination,
23
+ SectionNavMenu,
24
+ SectionNavProps,
25
+ } from './SectionNav.types';
10
26
 
11
27
  /**
12
28
  * One rail row: icon, label, and the count at the far end.
@@ -14,6 +30,10 @@ import type { SectionNavMenu, SectionNavProps } from './SectionNav.types';
14
30
  * Destinations and menu entries draw the same row — in the rail there is room
15
31
  * for everything, so the difference between "a place" and "an action" is the
16
32
  * section it sits in, not its shape.
33
+ *
34
+ * `active` is the look; `current` is what `aria-current` reports, and is a
35
+ * link's claim only — an action row that is ON says so with `aria-pressed`,
36
+ * which arrives in `control`. Omitted, `current` follows `active`.
17
37
  */
18
38
  function RailRow({
19
39
  label,
@@ -21,24 +41,29 @@ function RailRow({
21
41
  icon,
22
42
  count,
23
43
  active,
44
+ current = active,
24
45
  copy,
25
46
  testId,
26
47
  control,
48
+ loading = false,
27
49
  }: {
28
50
  label: string;
29
51
  description?: string;
30
52
  icon: ReactNode;
31
53
  count: number | undefined;
32
54
  active: boolean;
55
+ current?: boolean;
33
56
  copy: SectionNavCopy;
34
57
  testId: string;
35
58
  control: Record<string, unknown>;
59
+ loading?: boolean;
36
60
  }): React.JSX.Element {
37
61
  const theme = useTheme();
38
62
  return (
39
63
  <Box
40
64
  {...control}
41
- aria-current={active ? 'page' : undefined}
65
+ aria-current={current ? 'page' : undefined}
66
+ aria-busy={loading || undefined}
42
67
  data-testid={testId}
43
68
  sx={{
44
69
  ...CONTROL_RESET,
@@ -56,9 +81,15 @@ function RailRow({
56
81
  bgcolor: active ? 'action.selected' : 'transparent',
57
82
  '&:hover': { bgcolor: active ? 'action.selected' : 'action.hover' },
58
83
  '& svg': { color: 'primary.main', fontSize: rem(theme, 20) },
84
+ [NOT_LIVE]: { color: 'text.disabled', '& svg': { color: 'text.disabled' } },
59
85
  }}
60
86
  >
61
- <CountedIcon icon={icon} count={count} label={copy.badge} testId={`${testId}-badge`} />
87
+ <CountedIcon
88
+ icon={<SlotIcon icon={icon} loading={loading} />}
89
+ count={count}
90
+ label={copy.badge}
91
+ testId={`${testId}-badge`}
92
+ />
62
93
  <Box component="span" sx={{ display: 'flex', flexDirection: 'column', minWidth: 0 }}>
63
94
  <Typography component="span" variant="body2" sx={{ fontWeight: active ? 700 : 500 }}>
64
95
  {label}
@@ -82,7 +113,11 @@ function RailHeading({ children }: { children: ReactNode }): React.JSX.Element {
82
113
  );
83
114
  }
84
115
 
85
- /** A menu's groups, listed under its title — nothing folded away on a wide screen. */
116
+ /**
117
+ * A menu's groups, listed under its title — nothing folded away on a wide
118
+ * screen. A `disabled` menu has no trigger here to refuse, so every row of it
119
+ * is inert instead.
120
+ */
86
121
  function RailMenu({
87
122
  menu,
88
123
  linkComponent,
@@ -113,8 +148,13 @@ function RailMenu({
113
148
  count={entry.badge}
114
149
  active={entry.active === true}
115
150
  copy={copy}
116
- testId={`${testId}-entry-${entry.id}`}
117
- control={controlProps({ href: entry.href, linkComponent, onClick: () => entry.onSelect?.() })}
151
+ testId={entry.dataTestId ?? `${testId}-entry-${entry.id}`}
152
+ control={controlProps({
153
+ href: entry.href,
154
+ linkComponent,
155
+ inert: menu.disabled === true || entry.disabled === true,
156
+ onClick: () => entry.onSelect?.(),
157
+ })}
118
158
  />
119
159
  ))}
120
160
  </Box>
@@ -123,6 +163,104 @@ function RailMenu({
123
163
  );
124
164
  }
125
165
 
166
+ /** The way back out of the section, at the top of the rail. */
167
+ function RailBack({
168
+ back,
169
+ linkComponent,
170
+ testId,
171
+ }: {
172
+ back: SectionNavBack;
173
+ linkComponent: ElementType | undefined;
174
+ testId: string;
175
+ }): React.JSX.Element {
176
+ const theme = useTheme();
177
+ return (
178
+ <Box
179
+ {...controlProps({ href: back.href, linkComponent })}
180
+ data-testid={testId}
181
+ sx={{
182
+ ...CONTROL_RESET,
183
+ ...focusRing(theme),
184
+ display: 'flex',
185
+ alignItems: 'center',
186
+ gap: 1,
187
+ px: 1.25,
188
+ py: 0.75,
189
+ mb: 0.5,
190
+ borderRadius: 1,
191
+ color: 'text.secondary',
192
+ '&:hover': { bgcolor: 'action.hover', color: 'text.primary' },
193
+ '& svg': { fontSize: rem(theme, 20) },
194
+ }}
195
+ >
196
+ {back.icon}
197
+ <Typography component="span" variant="body2" sx={{ fontWeight: 600 }}>
198
+ {back.label}
199
+ </Typography>
200
+ </Box>
201
+ );
202
+ }
203
+
204
+ /** A primary that ACTS, as one row: there is no sheet to list on a wide screen. */
205
+ function RailAction({
206
+ action,
207
+ linkComponent,
208
+ copy,
209
+ testId,
210
+ }: {
211
+ action: SectionNavAction;
212
+ linkComponent: ElementType | undefined;
213
+ copy: SectionNavCopy;
214
+ testId: string;
215
+ }): React.JSX.Element {
216
+ return (
217
+ <RailRow
218
+ label={action.label}
219
+ icon={action.icon}
220
+ count={undefined}
221
+ active={false}
222
+ copy={copy}
223
+ testId={action.dataTestId ?? testId}
224
+ loading={action.loading === true}
225
+ control={controlProps({
226
+ href: undefined,
227
+ linkComponent,
228
+ inert: action.disabled === true,
229
+ busy: action.loading === true,
230
+ onClick: action.onSelect,
231
+ })}
232
+ />
233
+ );
234
+ }
235
+
236
+ /** A destination as a row: a link, or an action (a toggle when it carries `active`). */
237
+ function RailDestination({
238
+ destination,
239
+ linkComponent,
240
+ copy,
241
+ testId,
242
+ }: {
243
+ destination: SectionNavDestination;
244
+ linkComponent: ElementType | undefined;
245
+ copy: SectionNavCopy;
246
+ testId: string;
247
+ }): React.JSX.Element {
248
+ const { current, lit } = destinationState(destination);
249
+ return (
250
+ <RailRow
251
+ label={destination.label}
252
+ icon={destination.icon}
253
+ count={destination.badge}
254
+ active={lit}
255
+ current={current}
256
+ copy={copy}
257
+ testId={testId}
258
+ loading={destination.loading === true}
259
+ control={destinationControl(destination, linkComponent, () => destination.onSelect?.())}
260
+ />
261
+ );
262
+ }
263
+
126
264
  /**
127
265
  * The wide-screen layout: a vertical rail — the way back, the destinations,
128
266
  * then every menu's entries.
@@ -144,7 +282,6 @@ export function SectionNavRail({
144
282
  Pick<SectionNavProps, 'primary' | 'more' | 'back' | 'heading'> & {
145
283
  linkComponent: ElementType | undefined;
146
284
  }): React.JSX.Element {
147
- const theme = useTheme();
148
285
  return (
149
286
  <Box
150
287
  component="nav"
@@ -161,45 +298,21 @@ export function SectionNavRail({
161
298
  bgcolor: 'background.paper',
162
299
  }}
163
300
  >
164
- {back ? (
165
- <Box
166
- {...controlProps({ href: back.href, linkComponent })}
167
- data-testid={`${dataTestId}-back`}
168
- sx={{
169
- ...CONTROL_RESET,
170
- ...focusRing(theme),
171
- display: 'flex',
172
- alignItems: 'center',
173
- gap: 1,
174
- px: 1.25,
175
- py: 0.75,
176
- mb: 0.5,
177
- borderRadius: 1,
178
- color: 'text.secondary',
179
- '&:hover': { bgcolor: 'action.hover', color: 'text.primary' },
180
- '& svg': { fontSize: rem(theme, 20) },
181
- }}
182
- >
183
- {back.icon}
184
- <Typography component="span" variant="body2" sx={{ fontWeight: 600 }}>
185
- {back.label}
186
- </Typography>
187
- </Box>
188
- ) : null}
301
+ {back ? <RailBack back={back} linkComponent={linkComponent} testId={`${dataTestId}-back`} /> : null}
189
302
  {heading ? <RailHeading>{heading}</RailHeading> : null}
190
303
  {destinations.map((destination) => (
191
- <RailRow
304
+ <RailDestination
192
305
  key={destination.id}
193
- label={destination.label}
194
- icon={destination.icon}
195
- count={destination.badge}
196
- active={destination.active === true}
306
+ destination={destination}
307
+ linkComponent={linkComponent}
197
308
  copy={copy}
198
- testId={`${dataTestId}-dest-${destination.id}`}
199
- control={controlProps({ href: destination.href, linkComponent })}
309
+ testId={destination.dataTestId ?? `${dataTestId}-dest-${destination.id}`}
200
310
  />
201
311
  ))}
202
- {primary ? (
312
+ {primary && !isMenu(primary) ? (
313
+ <RailAction action={primary} linkComponent={linkComponent} copy={copy} testId={`${dataTestId}-primary`} />
314
+ ) : null}
315
+ {isMenu(primary) ? (
203
316
  <RailMenu menu={primary} linkComponent={linkComponent} copy={copy} testId={`${dataTestId}-primary`} />
204
317
  ) : null}
205
318
  {more ? <RailMenu menu={more} linkComponent={linkComponent} copy={copy} testId={`${dataTestId}-more`} /> : null}
@@ -25,13 +25,14 @@ function GridEntry({ entry, linkComponent, copy, testId, onDone }: EntryProps):
25
25
  {...controlProps({
26
26
  href: entry.href,
27
27
  linkComponent,
28
+ inert: entry.disabled === true,
28
29
  onClick: () => {
29
30
  entry.onSelect?.();
30
31
  onDone();
31
32
  },
32
33
  })}
33
34
  aria-current={entry.active ? 'page' : undefined}
34
- data-testid={`${testId}-entry-${entry.id}`}
35
+ data-testid={entry.dataTestId ?? `${testId}-entry-${entry.id}`}
35
36
  sx={{
36
37
  ...CONTROL_RESET,
37
38
  ...focusRing(theme),
@@ -48,6 +49,7 @@ function GridEntry({ entry, linkComponent, copy, testId, onDone }: EntryProps):
48
49
  bgcolor: entry.active ? 'action.selected' : 'action.hover',
49
50
  color: entry.active ? 'primary.main' : 'text.primary',
50
51
  '& svg': { color: 'primary.main' },
52
+ '&:disabled': { color: 'text.disabled', '& svg': { color: 'text.disabled' } },
51
53
  }}
52
54
  >
53
55
  <CountedIcon icon={entry.icon} count={entry.badge} label={copy.badge} testId={`${testId}-badge-${entry.id}`} />
@@ -66,13 +68,14 @@ function ListEntry({ entry, linkComponent, copy, testId, onDone }: EntryProps):
66
68
  {...controlProps({
67
69
  href: entry.href,
68
70
  linkComponent,
71
+ inert: entry.disabled === true,
69
72
  onClick: () => {
70
73
  entry.onSelect?.();
71
74
  onDone();
72
75
  },
73
76
  })}
74
77
  aria-current={entry.active ? 'page' : undefined}
75
- data-testid={`${testId}-entry-${entry.id}`}
78
+ data-testid={entry.dataTestId ?? `${testId}-entry-${entry.id}`}
76
79
  sx={{
77
80
  ...CONTROL_RESET,
78
81
  ...focusRing(theme),
@@ -87,6 +90,7 @@ function ListEntry({ entry, linkComponent, copy, testId, onDone }: EntryProps):
87
90
  textAlign: 'start',
88
91
  bgcolor: entry.active ? 'action.selected' : 'action.hover',
89
92
  '& svg': { color: 'primary.main' },
93
+ '&:disabled': { color: 'text.disabled', '& svg': { color: 'text.disabled' } },
90
94
  }}
91
95
  >
92
96
  <CountedIcon icon={entry.icon} count={entry.badge} label={copy.badge} testId={`${testId}-badge-${entry.id}`} />