@tempoxyz/ds 0.3.0 → 0.3.1

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 (173) hide show
  1. package/dist/web/components/Accordion.js.zyzz.json +1 -1
  2. package/dist/web/components/AgentLogo.js.zyzz.json +1 -1
  3. package/dist/web/components/Alert.css +1 -1
  4. package/dist/web/components/Alert.d.ts +7 -2
  5. package/dist/web/components/Alert.d.ts.map +1 -1
  6. package/dist/web/components/Alert.js +12 -7
  7. package/dist/web/components/Alert.js.map +1 -1
  8. package/dist/web/components/Alert.js.zyzz.json +1 -1
  9. package/dist/web/components/AlertDialog.js.zyzz.json +1 -1
  10. package/dist/web/components/Amount.js.zyzz.json +1 -1
  11. package/dist/web/components/AmountInput.js.zyzz.json +1 -1
  12. package/dist/web/components/AppShell.css +1 -1
  13. package/dist/web/components/AppShell.d.ts +108 -0
  14. package/dist/web/components/AppShell.d.ts.map +1 -1
  15. package/dist/web/components/AppShell.js +72 -24
  16. package/dist/web/components/AppShell.js.map +1 -1
  17. package/dist/web/components/AppShell.js.zyzz.json +1 -1
  18. package/dist/web/components/AssetChip.js.zyzz.json +1 -1
  19. package/dist/web/components/Autocomplete.js.zyzz.json +1 -1
  20. package/dist/web/components/Avatar.js.zyzz.json +1 -1
  21. package/dist/web/components/Badge.css +1 -1
  22. package/dist/web/components/Badge.d.ts +1 -1
  23. package/dist/web/components/Badge.d.ts.map +1 -1
  24. package/dist/web/components/Badge.js.map +1 -1
  25. package/dist/web/components/Badge.js.zyzz.json +1 -1
  26. package/dist/web/components/Banner.css +1 -1
  27. package/dist/web/components/Banner.js.zyzz.json +1 -1
  28. package/dist/web/components/BarChart.css +1 -1
  29. package/dist/web/components/BarChart.js.zyzz.json +1 -1
  30. package/dist/web/components/Breadcrumbs.js.zyzz.json +1 -1
  31. package/dist/web/components/Button.css +1 -1
  32. package/dist/web/components/Button.d.ts +10 -6
  33. package/dist/web/components/Button.d.ts.map +1 -1
  34. package/dist/web/components/Button.js +4 -3
  35. package/dist/web/components/Button.js.map +1 -1
  36. package/dist/web/components/Button.js.zyzz.json +1 -1
  37. package/dist/web/components/ButtonGroup.js.zyzz.json +1 -1
  38. package/dist/web/components/Calendar.css +1 -1
  39. package/dist/web/components/Calendar.js.zyzz.json +1 -1
  40. package/dist/web/components/Checkbox.js.zyzz.json +1 -1
  41. package/dist/web/components/CheckboxGroup.js.zyzz.json +1 -1
  42. package/dist/web/components/ChoiceGroup.css +1 -1
  43. package/dist/web/components/ChoiceGroup.js.zyzz.json +1 -1
  44. package/dist/web/components/Choicebox.js.zyzz.json +1 -1
  45. package/dist/web/components/Combobox.js.zyzz.json +1 -1
  46. package/dist/web/components/Container.js.zyzz.json +1 -1
  47. package/dist/web/components/CopyButton.js.zyzz.json +1 -1
  48. package/dist/web/components/DatePicker.css +1 -1
  49. package/dist/web/components/DatePicker.js.zyzz.json +1 -1
  50. package/dist/web/components/Dialog.d.ts.map +1 -1
  51. package/dist/web/components/Dialog.js +1 -1
  52. package/dist/web/components/Dialog.js.map +1 -1
  53. package/dist/web/components/Dialog.js.zyzz.json +1 -1
  54. package/dist/web/components/Drawer.js.zyzz.json +1 -1
  55. package/dist/web/components/Empty.css +1 -1
  56. package/dist/web/components/Empty.js.zyzz.json +1 -1
  57. package/dist/web/components/Field.js.zyzz.json +1 -1
  58. package/dist/web/components/Fieldset.js.zyzz.json +1 -1
  59. package/dist/web/components/Frame.css +1 -1
  60. package/dist/web/components/Frame.js.zyzz.json +1 -1
  61. package/dist/web/components/Gauge.js.zyzz.json +1 -1
  62. package/dist/web/components/Grid.js.zyzz.json +1 -1
  63. package/dist/web/components/Hex.css +1 -1
  64. package/dist/web/components/Hex.js.zyzz.json +1 -1
  65. package/dist/web/components/IconTile.css +1 -1
  66. package/dist/web/components/IconTile.js.zyzz.json +1 -1
  67. package/dist/web/components/Identicon.js.zyzz.json +1 -1
  68. package/dist/web/components/Inline.js.zyzz.json +1 -1
  69. package/dist/web/components/Inset.js.zyzz.json +1 -1
  70. package/dist/web/components/Kbd.css +1 -1
  71. package/dist/web/components/Kbd.js.zyzz.json +1 -1
  72. package/dist/web/components/ListGroup.css +1 -1
  73. package/dist/web/components/ListGroup.d.ts +55 -8
  74. package/dist/web/components/ListGroup.d.ts.map +1 -1
  75. package/dist/web/components/ListGroup.js +23 -19
  76. package/dist/web/components/ListGroup.js.map +1 -1
  77. package/dist/web/components/ListGroup.js.zyzz.json +1 -1
  78. package/dist/web/components/LoadingDots.js.zyzz.json +1 -1
  79. package/dist/web/components/Menu.css +1 -1
  80. package/dist/web/components/Menu.js.zyzz.json +1 -1
  81. package/dist/web/components/NetworkLogo.js.zyzz.json +1 -1
  82. package/dist/web/components/NumberField.js.zyzz.json +1 -1
  83. package/dist/web/components/OtpField.css +1 -1
  84. package/dist/web/components/OtpField.js.zyzz.json +1 -1
  85. package/dist/web/components/Pagination.css +1 -1
  86. package/dist/web/components/Pagination.js.zyzz.json +1 -1
  87. package/dist/web/components/Popover.js.zyzz.json +1 -1
  88. package/dist/web/components/Progress.js.zyzz.json +1 -1
  89. package/dist/web/components/Qr.js.zyzz.json +1 -1
  90. package/dist/web/components/RadioGroup.js.zyzz.json +1 -1
  91. package/dist/web/components/SecretValue.css +1 -1
  92. package/dist/web/components/SecretValue.js.zyzz.json +1 -1
  93. package/dist/web/components/Select.css +1 -1
  94. package/dist/web/components/Select.d.ts.map +1 -1
  95. package/dist/web/components/Select.js +2 -2
  96. package/dist/web/components/Select.js.map +1 -1
  97. package/dist/web/components/Select.js.zyzz.json +1 -1
  98. package/dist/web/components/Separator.js.zyzz.json +1 -1
  99. package/dist/web/components/SidebarMenu.css +1 -1
  100. package/dist/web/components/SidebarMenu.d.ts +52 -0
  101. package/dist/web/components/SidebarMenu.d.ts.map +1 -1
  102. package/dist/web/components/SidebarMenu.js +47 -16
  103. package/dist/web/components/SidebarMenu.js.map +1 -1
  104. package/dist/web/components/SidebarMenu.js.zyzz.json +1 -1
  105. package/dist/web/components/Skeleton.css +1 -1
  106. package/dist/web/components/Skeleton.js.zyzz.json +1 -1
  107. package/dist/web/components/Slider.js.zyzz.json +1 -1
  108. package/dist/web/components/Snippet.js.zyzz.json +1 -1
  109. package/dist/web/components/Spinner.js.zyzz.json +1 -1
  110. package/dist/web/components/SplitBar.js.zyzz.json +1 -1
  111. package/dist/web/components/SplitButton.js.zyzz.json +1 -1
  112. package/dist/web/components/Stack.js.zyzz.json +1 -1
  113. package/dist/web/components/StatusChip.css +1 -1
  114. package/dist/web/components/StatusChip.js.zyzz.json +1 -1
  115. package/dist/web/components/StatusRow.js.zyzz.json +1 -1
  116. package/dist/web/components/Stepper.js.zyzz.json +1 -1
  117. package/dist/web/components/Surface.css +1 -1
  118. package/dist/web/components/Surface.d.ts +85 -0
  119. package/dist/web/components/Surface.d.ts.map +1 -1
  120. package/dist/web/components/Surface.js +6 -3
  121. package/dist/web/components/Surface.js.map +1 -1
  122. package/dist/web/components/Surface.js.zyzz.json +1 -1
  123. package/dist/web/components/Switch.js.zyzz.json +1 -1
  124. package/dist/web/components/Table.css +1 -1
  125. package/dist/web/components/Table.js.zyzz.json +1 -1
  126. package/dist/web/components/Tabs.js.zyzz.json +1 -1
  127. package/dist/web/components/TempoLogo.js.zyzz.json +1 -1
  128. package/dist/web/components/TextInput.js.zyzz.json +1 -1
  129. package/dist/web/components/Textarea.css +1 -1
  130. package/dist/web/components/Textarea.js.zyzz.json +1 -1
  131. package/dist/web/components/Toast.css +1 -1
  132. package/dist/web/components/Toast.js.zyzz.json +1 -1
  133. package/dist/web/components/Token.js.zyzz.json +1 -1
  134. package/dist/web/components/TokenLogo.js.zyzz.json +1 -1
  135. package/dist/web/components/Tooltip.js.zyzz.json +1 -1
  136. package/dist/web/components/VerificationCode.css +1 -1
  137. package/dist/web/components/VerificationCode.js.zyzz.json +1 -1
  138. package/dist/web/components/Visual.js.zyzz.json +1 -1
  139. package/dist/web/components/internal/Field.css +1 -1
  140. package/dist/web/components/internal/Field.js.zyzz.json +1 -1
  141. package/dist/web/components/internal/InputGroup.css +1 -1
  142. package/dist/web/components/internal/InputGroup.js.zyzz.json +1 -1
  143. package/dist/web/components/internal/Listbox.css +1 -1
  144. package/dist/web/components/internal/Listbox.js.zyzz.json +1 -1
  145. package/dist/web/components/internal/TabBar.d.ts +33 -0
  146. package/dist/web/components/internal/TabBar.d.ts.map +1 -0
  147. package/dist/web/components/internal/TabBar.js +30 -0
  148. package/dist/web/components/internal/TabBar.js.map +1 -0
  149. package/dist/web/components/internal/Text.d.ts +9 -0
  150. package/dist/web/components/internal/Text.d.ts.map +1 -0
  151. package/dist/web/components/internal/Text.js +35 -0
  152. package/dist/web/components/internal/Text.js.map +1 -0
  153. package/dist/web/index.js.zyzz.json +1 -1
  154. package/dist/web/vars.d.ts +26 -3
  155. package/dist/web/vars.d.ts.map +1 -1
  156. package/dist/web/vars.js.map +1 -1
  157. package/dist/web/vars.js.zyzz.json +1 -1
  158. package/dist/web/zyzz.config.d.ts +130 -15
  159. package/dist/web/zyzz.config.d.ts.map +1 -1
  160. package/dist/web/zyzz.config.js.zyzz.json +1 -1
  161. package/package.json +1 -1
  162. package/src/web/components/Alert.tsx +25 -5
  163. package/src/web/components/AppShell.tsx +273 -8
  164. package/src/web/components/Badge.tsx +4 -2
  165. package/src/web/components/Button.tsx +14 -10
  166. package/src/web/components/Dialog.tsx +7 -1
  167. package/src/web/components/ListGroup.tsx +95 -14
  168. package/src/web/components/Select.tsx +2 -0
  169. package/src/web/components/SidebarMenu.tsx +97 -3
  170. package/src/web/components/Surface.tsx +105 -1
  171. package/src/web/components/internal/TabBar.ts +52 -0
  172. package/src/web/components/internal/Text.ts +39 -0
  173. package/src/web/vars.ts +20 -7
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tempoxyz/ds",
3
3
  "type": "module",
4
- "version": "0.3.0",
4
+ "version": "0.3.1",
5
5
  "license": "MIT",
6
6
  "files": [
7
7
  "DESIGN.md",
@@ -48,7 +48,8 @@ export namespace Alert {
48
48
 
49
49
  /**
50
50
  * A text `<button>` with a 44px tall target, in the alert's strong type. Place it inside
51
- * `Alert.Content`, after the message. Name the action it performs, such as Review fees.
51
+ * `Alert.Content`, after the message, or at the end of the row, where its label sits in line
52
+ * with the first line. Name the action it performs, such as Review fees.
52
53
  */
53
54
  export function Action(props: Action.Props) {
54
55
  const { className, vars, ...attributes } = props
@@ -70,7 +71,8 @@ export namespace Alert {
70
71
 
71
72
  /**
72
73
  * Stacks the title, message, and action in a `<div>` that fills the row and can shrink, so long
73
- * text wraps beside the indicator.
74
+ * text wraps beside the indicator. The message flows as text. When it sits beside a paragraph or
75
+ * a list, wrap it in a `p`, so the blocks keep their 4px gap.
74
76
  */
75
77
  export function Content(props: Content.Props) {
76
78
  const { className, vars, ...attributes } = props
@@ -176,13 +178,28 @@ namespace styles {
176
178
  { id: 'tds/web/alert/action' },
177
179
  )
178
180
 
181
+ // The message flows as text, so inline elements such as `strong` stay on its lines. The title,
182
+ // the action and block children each take a line of their own, 4px from the content beside
183
+ // them.
179
184
  export const content = style(
180
185
  {
181
- display: 'flex',
186
+ display: 'block',
182
187
  flex: '1 !custom',
183
- flexDirection: 'column',
184
188
  minWidth: '0',
185
- rowGap: '4',
189
+ selectors: {
190
+ '& > [data-alert-action]': {
191
+ display: 'flex',
192
+ marginBlockStart: '4',
193
+ width: 'fit-content !custom',
194
+ },
195
+ '& > [data-alert-title]': { marginBlockEnd: '4' },
196
+ // The browser's block margins of paragraphs and lists give way to the 4px gap. `:where`
197
+ // keeps the reset below any class that sets a margin of its own.
198
+ '& > :where(p, ul, ol)': { marginBlock: '0' },
199
+ // A list sits 4px below the message, and a paragraph 4px below the element before it. A
200
+ // list follows text in practice, which CSS cannot select, so it always takes the gap.
201
+ '& > :is(ul, ol), & > * + :is(p, div)': { marginBlockStart: '4' },
202
+ },
186
203
  },
187
204
  { id: 'tds/web/alert/content' },
188
205
  )
@@ -260,6 +277,9 @@ namespace styles {
260
277
  padding: '16',
261
278
  typography: 'body.b3',
262
279
  selectors: {
280
+ // An action at the end of the row centers its 44px target on the 16px first line, so its
281
+ // label stays in line with the message.
282
+ '& > [data-alert-action]': { marginBlock: 'calc((1rem - 2.75rem) / 2) !custom' },
263
283
  '& [data-alert-action], & [data-alert-title]': { typography: 'body.b3Strong' },
264
284
  '& [data-alert-icon]': { height: '16', width: '16' },
265
285
  },
@@ -1,4 +1,5 @@
1
1
  import * as BaseUi from '@base-ui/react/drawer'
2
+ import { useRender } from '@base-ui/react/use-render'
2
3
  import {
3
4
  type ComponentPropsWithRef,
4
5
  createContext,
@@ -9,8 +10,11 @@ import {
9
10
  import type * as zyzz from 'zyzz'
10
11
  import { keyframes } from 'zyzz/web'
11
12
  import { style, variants, vars } from '../zyzz.config.js'
13
+ import * as TabBar from './internal/TabBar.js'
14
+ import * as Text from './internal/Text.js'
12
15
 
13
16
  type Context = {
17
+ collapse: boolean
14
18
  container: HTMLDivElement | null
15
19
  handle: BaseUi.Drawer.Handle<unknown>
16
20
  open: boolean
@@ -19,10 +23,11 @@ type Context = {
19
23
 
20
24
  const context = createContext<Context | undefined>(undefined)
21
25
 
22
- const compactQuery = '(max-width: 1023px)'
26
+ // The sidebar opens as a drawer below 1024px, or below 768px when it collapses to its icons from
27
+ // 768px to 1023px. The breakpoints are literals because media queries cannot read CSS variables.
28
+ function useCompact(collapse: boolean) {
29
+ const compactQuery = collapse ? '(max-width: 767px)' : '(max-width: 1023px)'
23
30
 
24
- // The viewport breakpoint is a literal because media queries cannot read CSS variables.
25
- function useCompact() {
26
31
  return useSyncExternalStore(
27
32
  (notify) => {
28
33
  const query = window.matchMedia(compactQuery)
@@ -112,7 +117,9 @@ export namespace AppShell {
112
117
  export function Root(props: Root.Props) {
113
118
  const {
114
119
  align,
120
+ children,
115
121
  className,
122
+ collapse = false,
116
123
  defaultOpen = false,
117
124
  onOpenChange,
118
125
  open,
@@ -121,6 +128,7 @@ export namespace AppShell {
121
128
  ...attributes
122
129
  } = props
123
130
  const [handle] = useState(() => BaseUi.Drawer.createHandle<unknown>())
131
+ const [tabBar] = useState(TabBar.create)
124
132
  const [uncontrolled, setUncontrolled] = useState(defaultOpen)
125
133
  const [container, setContainer] = useState<HTMLDivElement | null>(null)
126
134
  const current = open ?? uncontrolled
@@ -128,6 +136,7 @@ export namespace AppShell {
128
136
  return (
129
137
  <context.Provider
130
138
  value={{
139
+ collapse,
131
140
  container,
132
141
  handle,
133
142
  open: current,
@@ -140,13 +149,19 @@ export namespace AppShell {
140
149
  <div
141
150
  {...attributes}
142
151
  {...styles.root({ align, className, style: props.style, vars })}
152
+ data-app-shell-collapse={collapse ? '' : undefined}
143
153
  data-state={current ? 'open' : 'closed'}
144
154
  ref={(node) => {
145
155
  setContainer(node)
146
156
  if (typeof ref === 'function') ref(node)
147
157
  else if (ref) ref.current = node
148
158
  }}
149
- />
159
+ >
160
+ <TabBar.Context.Provider value={tabBar}>
161
+ {children}
162
+ <BottomBar />
163
+ </TabBar.Context.Provider>
164
+ </div>
150
165
  </context.Provider>
151
166
  )
152
167
  }
@@ -154,6 +169,14 @@ export namespace AppShell {
154
169
  export namespace Root {
155
170
  export type Props = ComponentPropsWithRef<'div'> &
156
171
  zyzz.Props.Variants<typeof styles.root> & {
172
+ /**
173
+ * Collapses the inline sidebar to a 64px column of icons from 768px to 1023px, and opens
174
+ * the drawer only below 768px. Give every `SidebarMenu.Item` an icon as its first child.
175
+ * Absent, the sidebar opens as a drawer below 1024px.
176
+ * @default false
177
+ */
178
+ collapse?: boolean | undefined
179
+
157
180
  /**
158
181
  * Initial drawer state when `open` is uncontrolled.
159
182
  * @default false
@@ -177,10 +200,18 @@ export namespace AppShell {
177
200
  export function Sidebar(props: Sidebar.Props) {
178
201
  const { className, vars, ...attributes } = props
179
202
  const shell = useShell()
180
- const compact = useCompact()
203
+ const compact = useCompact(shell.collapse)
181
204
  const sidebar = styles.sidebar({ className, style: props.style, vars })
182
205
 
183
- if (!compact) return <aside {...attributes} {...styles.inline(sidebar)} />
206
+ if (!compact)
207
+ return (
208
+ <aside
209
+ {...attributes}
210
+ {...styles.inline(sidebar)}
211
+ data-app-shell-rail={shell.collapse ? '' : undefined}
212
+ data-app-shell-sidebar="inline"
213
+ />
214
+ )
184
215
 
185
216
  return (
186
217
  <BaseUi.Drawer.Root
@@ -189,7 +220,8 @@ export namespace AppShell {
189
220
  open={shell.open}
190
221
  swipeDirection="right"
191
222
  >
192
- <BaseUi.Drawer.Portal container={shell.container}>
223
+ {/* The closed drawer stays mounted, so its items register in the bottom bar. */}
224
+ <BaseUi.Drawer.Portal container={shell.container} keepMounted>
193
225
  <BaseUi.Drawer.Backdrop {...styles.backdrop()} />
194
226
  <BaseUi.Drawer.Viewport {...styles.viewport()}>
195
227
  <BaseUi.Drawer.Popup
@@ -292,6 +324,54 @@ export namespace AppShell {
292
324
  }
293
325
  }
294
326
 
327
+ /**
328
+ * Repeats the `SidebarMenu.Item`s that set `tabBar` in a bar fixed to the bottom edge below 768px:
329
+ * `tab` items as icons in a pill `<nav>`, then each `action` item as a floating action button.
330
+ * Renders nothing while no item sets `tabBar`.
331
+ */
332
+ function BottomBar() {
333
+ const store = use(TabBar.Context)!
334
+ const entries = useSyncExternalStore(store.subscribe, store.snapshot, store.snapshot)
335
+ const tabs = entries.filter(([, entry]) => entry.variant === 'tab')
336
+ const actions = entries.filter(([, entry]) => entry.variant === 'action')
337
+
338
+ if (entries.length === 0) return null
339
+
340
+ return (
341
+ <div {...styles.bottomBar()} data-app-shell-bottom-bar="">
342
+ {tabs.length > 0 && (
343
+ <nav aria-label="Main" {...styles.tabBar()}>
344
+ {tabs.map(([id, entry]) => (
345
+ <BottomBarItem entry={entry} key={id} />
346
+ ))}
347
+ </nav>
348
+ )}
349
+ {actions.map(([id, entry]) => (
350
+ <BottomBarItem entry={entry} key={id} />
351
+ ))}
352
+ </div>
353
+ )
354
+ }
355
+
356
+ /**
357
+ * Renders the element of a `SidebarMenu.Item` with the attributes and `render` of the item, and
358
+ * shows only its first element, such as its icon. Its text stays in the accessible name.
359
+ */
360
+ function BottomBarItem(props: { entry: TabBar.Entry }) {
361
+ const { entry } = props
362
+
363
+ return useRender({
364
+ defaultTagName: 'a',
365
+ props: {
366
+ ...entry.attributes,
367
+ 'aria-current': entry.active ? 'page' : undefined,
368
+ ...(entry.variant === 'tab' ? styles.tab({ active: entry.active }) : styles.action()),
369
+ children: Text.wrap(entry.children, styles.label()),
370
+ },
371
+ render: entry.render,
372
+ })
373
+ }
374
+
295
375
  const fadeIn = keyframes(
296
376
  { from: { opacity: 0 }, to: { opacity: 1 } },
297
377
  { id: 'tds/web/app-shell/fade-in' },
@@ -303,6 +383,53 @@ const fadeOut = keyframes(
303
383
  )
304
384
 
305
385
  namespace styles {
386
+ // The 56px circle of the Platform Floating Action Button, filled like the primary button.
387
+ export const action = style(
388
+ {
389
+ alignItems: 'center',
390
+ backgroundColor: 'inverse.background.primary',
391
+ borderRadius: 'full',
392
+ boxShadow: 'large',
393
+ boxSizing: 'border-box',
394
+ color: 'inverse.content.primary',
395
+ cornerShape: 'round',
396
+ cursor: 'pointer',
397
+ display: 'flex',
398
+ flexShrink: '0 !custom',
399
+ height: '56',
400
+ justifyContent: 'center',
401
+ outlineColor: 'transparent !custom',
402
+ outlineOffset: '2',
403
+ outlineStyle: 'solid !custom',
404
+ outlineWidth: '2',
405
+ position: 'relative',
406
+ textDecoration: 'none !custom',
407
+ transitionDuration: '160',
408
+ transitionProperty: 'background-color, outline-color, scale !custom',
409
+ transitionTimingFunction: 'snappy',
410
+ width: '56',
411
+ selectors: {
412
+ '& > :first-child': { flexShrink: '0 !custom', fontSize: '24' },
413
+ '& > :not(:first-child)': {
414
+ clipPath: 'inset(50%) !custom',
415
+ height: '1px !custom',
416
+ overflow: 'hidden',
417
+ position: 'absolute',
418
+ whiteSpace: 'nowrap',
419
+ width: '1px !custom',
420
+ },
421
+ '&:active': { scale: 'press', transitionDuration: '100' },
422
+ '&:focus-visible': { outlineColor: 'border.focus' },
423
+ '&:hover': { backgroundColor: 'action.primary.fill.hover' },
424
+ },
425
+ '@media (prefers-reduced-motion: reduce)': {
426
+ transitionProperty: 'background-color, outline-color !custom',
427
+ selectors: { '&:active': { scale: 'none !custom' } },
428
+ },
429
+ },
430
+ { id: 'tds/web/app-shell/action' },
431
+ )
432
+
306
433
  export const backdrop = style(
307
434
  {
308
435
  backgroundColor: 'neutral.100',
@@ -350,6 +477,22 @@ namespace styles {
350
477
  { id: 'tds/web/app-shell/bar' },
351
478
  )
352
479
 
480
+ // Floats over the content, inset by the 24px page margin of the Mobile layout and 16px from the
481
+ // bottom edge. It sits below the drawer and its backdrop.
482
+ export const bottomBar = style(
483
+ {
484
+ alignItems: 'center',
485
+ bottom: 'calc(1rem + env(safe-area-inset-bottom)) !custom',
486
+ columnGap: '12',
487
+ display: 'none',
488
+ insetInline: '24',
489
+ position: 'fixed',
490
+ zIndex: '39 !custom',
491
+ '@media (max-width: 767px)': { display: 'flex' },
492
+ },
493
+ { id: 'tds/web/app-shell/bottom-bar' },
494
+ )
495
+
353
496
  export const bars = style(
354
497
  { display: 'block', height: '16', position: 'relative', width: '16' },
355
498
  { id: 'tds/web/app-shell/bars' },
@@ -375,6 +518,9 @@ namespace styles {
375
518
  },
376
519
  '&[data-starting-style]': { transform: 'translateX(100%) !custom' },
377
520
  '&[data-swiping]': { transitionDuration: '0', userSelect: 'none' },
521
+ // The closed drawer stays mounted with the `hidden` attribute, which the flex layout of
522
+ // the sidebar would override.
523
+ '&[hidden]': { display: 'none !custom' },
378
524
  },
379
525
  '@media (prefers-reduced-motion: reduce)': {
380
526
  transitionProperty: 'opacity !custom',
@@ -411,8 +557,11 @@ namespace styles {
411
557
  paddingInline: '16',
412
558
  '@media (min-width: 640px)': { marginInline: '-2rem !custom', paddingInline: '24' },
413
559
  // Below 1024px the fixed trigger covers the end of the bar, so header actions end 8px
414
- // before it: its 40px plus its 16px inset.
560
+ // before it: its 40px plus its 16px inset. A collapsed sidebar hides it from 768px.
415
561
  '@media (max-width: 1023px)': { paddingInlineEnd: '64' },
562
+ '@media (min-width: 768px) and (max-width: 1023px)': {
563
+ selectors: { '[data-app-shell-collapse] > main > &': { paddingInlineEnd: '24' } },
564
+ },
416
565
  },
417
566
  },
418
567
  { id: 'tds/web/app-shell/header' },
@@ -454,6 +603,15 @@ namespace styles {
454
603
  '@media (max-width: 1023px)': {
455
604
  selectors: { '&::after, &::before': { display: 'none !custom' } },
456
605
  },
606
+ // The bottom bar covers the end of the page, so content can scroll clear of it: 16px
607
+ // below the bar, its 56px, and 16px above it.
608
+ '@media (max-width: 767px)': {
609
+ selectors: {
610
+ '&:has(> [data-app-shell-bottom-bar]) > main': {
611
+ paddingBottom: 'calc(5.5rem + env(safe-area-inset-bottom)) !custom',
612
+ },
613
+ },
614
+ },
457
615
  },
458
616
  defaultVariants: { align: 'center' },
459
617
  variants: {
@@ -504,6 +662,19 @@ namespace styles {
504
662
  { id: 'tds/web/app-shell/sidebar' },
505
663
  )
506
664
 
665
+ // Keeps the label of a bottom bar item in its accessible name without painting it.
666
+ export const label = style(
667
+ {
668
+ clipPath: 'inset(50%) !custom',
669
+ height: '1px !custom',
670
+ overflow: 'hidden',
671
+ position: 'absolute',
672
+ whiteSpace: 'nowrap',
673
+ width: '1px !custom',
674
+ },
675
+ { id: 'tds/web/app-shell/label' },
676
+ )
677
+
507
678
  // Follows `sidebar`, so its compact `display: none` overrides the sidebar's `display: flex` in
508
679
  // server-rendered markup, which always emits the wide layout.
509
680
  export const inline = style(
@@ -517,6 +688,12 @@ namespace styles {
517
688
  position: 'sticky',
518
689
  width: 'var(--sidebar-width, 16rem) !custom',
519
690
  '@media (max-width: 1023px)': { display: 'none !custom' },
691
+ // A collapsed sidebar stays inline as a column of icons, which `SidebarMenu` centers.
692
+ '@media (min-width: 768px) and (max-width: 1023px)': {
693
+ selectors: {
694
+ '&[data-app-shell-rail]': { display: 'flex !custom', width: '4rem !custom' },
695
+ },
696
+ },
520
697
  },
521
698
  { id: 'tds/web/app-shell/inline' },
522
699
  )
@@ -538,6 +715,15 @@ namespace styles {
538
715
  // Classic scrollbars keep their own lane instead of covering content at the edge.
539
716
  scrollbarGutter: 'stable',
540
717
  scrollbarWidth: 'thin',
718
+ // The collapsed sidebar has no room for a scrollbar lane beside its 40px icons.
719
+ '@media (min-width: 768px) and (max-width: 1023px)': {
720
+ selectors: {
721
+ '[data-app-shell-rail] &': {
722
+ scrollbarGutter: 'auto !custom',
723
+ scrollbarWidth: 'none !custom',
724
+ },
725
+ },
726
+ },
541
727
  selectors: {
542
728
  // Curtains fade content into the sidebar at whichever end has more to scroll.
543
729
  '&::after, &::before': {
@@ -598,6 +784,81 @@ namespace styles {
598
784
  { id: 'tds/web/app-shell/sidebar-header' },
599
785
  )
600
786
 
787
+ // An icon-only item of the Platform Tab Bar, which fills its share of the pill.
788
+ export const tab = variants(
789
+ {
790
+ base: {
791
+ alignItems: 'center',
792
+ borderRadius: 'full',
793
+ boxSizing: 'border-box',
794
+ color: 'content.secondary',
795
+ cornerShape: 'round',
796
+ cursor: 'pointer',
797
+ display: 'flex',
798
+ flex: '1 !custom',
799
+ height: '100% !custom',
800
+ justifyContent: 'center',
801
+ minWidth: '0',
802
+ outlineColor: 'transparent !custom',
803
+ outlineOffset: '-2',
804
+ outlineStyle: 'solid !custom',
805
+ outlineWidth: '2',
806
+ position: 'relative',
807
+ textDecoration: 'none !custom',
808
+ transitionDuration: '160',
809
+ transitionProperty: 'background-color, color, outline-color, scale !custom',
810
+ transitionTimingFunction: 'snappy',
811
+ selectors: {
812
+ '& > :first-child': { flexShrink: '0 !custom', fontSize: '24' },
813
+ '& > :not(:first-child)': {
814
+ clipPath: 'inset(50%) !custom',
815
+ height: '1px !custom',
816
+ overflow: 'hidden',
817
+ position: 'absolute',
818
+ whiteSpace: 'nowrap',
819
+ width: '1px !custom',
820
+ },
821
+ '&:active': { scale: 'press', transitionDuration: '100' },
822
+ '&:focus-visible': { outlineColor: 'border.focus' },
823
+ '&:hover': { color: 'content.primary' },
824
+ },
825
+ '@media (prefers-reduced-motion: reduce)': {
826
+ transitionProperty: 'background-color, color, outline-color !custom',
827
+ selectors: { '&:active': { scale: 'none !custom' } },
828
+ },
829
+ },
830
+ defaultVariants: { active: false },
831
+ variants: {
832
+ active: {
833
+ false: {},
834
+ true: { backgroundColor: 'container.regular', color: 'content.primary' },
835
+ },
836
+ },
837
+ },
838
+ { id: 'tds/web/app-shell/tab' },
839
+ )
840
+
841
+ // The 56px pill of the Platform Tab Bar, which takes the width beside the action button.
842
+ export const tabBar = style(
843
+ {
844
+ alignItems: 'center',
845
+ backgroundColor: 'background.secondary',
846
+ borderColor: 'line.secondary',
847
+ borderRadius: 'full',
848
+ borderStyle: 'solid',
849
+ borderWidth: 'regular',
850
+ boxShadow: 'regular',
851
+ boxSizing: 'border-box',
852
+ cornerShape: 'round',
853
+ display: 'flex',
854
+ flex: '1 !custom',
855
+ height: '56',
856
+ minWidth: '0',
857
+ padding: '4',
858
+ },
859
+ { id: 'tds/web/app-shell/tab-bar' },
860
+ )
861
+
601
862
  export const title = variants(
602
863
  {
603
864
  base: {
@@ -654,6 +915,9 @@ namespace styles {
654
915
  },
655
916
  },
656
917
  '@media (min-width: 1024px)': { display: 'none !custom' },
918
+ '@media (min-width: 768px) and (max-width: 1023px)': {
919
+ selectors: { '[data-app-shell-collapse] &': { display: 'none !custom' } },
920
+ },
657
921
  '@media (prefers-reduced-motion: reduce)': {
658
922
  transitionProperty: 'background-color, outline-color, outline-offset !custom',
659
923
  selectors: { '&:active': { transform: 'none !custom' } },
@@ -670,6 +934,7 @@ namespace styles {
670
934
  justifyContent: 'flex-end',
671
935
  position: 'fixed',
672
936
  zIndex: 'popover',
937
+ selectors: { '&[hidden]': { display: 'none !custom' } },
673
938
  },
674
939
  { id: 'tds/web/app-shell/viewport' },
675
940
  )
@@ -104,8 +104,10 @@ namespace styles {
104
104
  // The accent text follows the inverse content color, which turns black in dark mode.
105
105
  accent: { backgroundColor: 'accent.blue.background', color: 'inverse.content.primary' },
106
106
  elevated: { backgroundColor: 'background.elevated', color: 'content.primary' },
107
- // The inverse fill stays dark in both schemes, so its text stays white.
108
- inverse: { backgroundColor: 'inverse.background.secondary', color: 'neutral.000' },
107
+ inverse: {
108
+ backgroundColor: 'inverse.background.secondary',
109
+ color: 'inverse.content.primary',
110
+ },
109
111
  outlined: {
110
112
  backgroundColor: 'transparent !custom',
111
113
  borderColor: 'line.secondary',
@@ -12,6 +12,7 @@ import { Spinner } from './Spinner.js'
12
12
  * - Use `Button` for actions and `Button.Link` for navigation.
13
13
  * - Label with a specific verb, such as Send payment. Give `shape="icon"` buttons, which render as circles, an `aria-label`.
14
14
  * - Keep one `primary` button per view or section, and give the others `secondary` or `tertiary`.
15
+ * - Use `danger` for the action that confirms a destructive outcome, such as Revoke key.
15
16
  * - Set `appearance="inverse"` on an inverse surface, such as a dark banner on a light page, and
16
17
  * `appearance="overlay"` over an image or video.
17
18
  * - Use `large` for the actions in a dialog footer. Keep `xsmall` pills for actions inside a row
@@ -162,18 +163,11 @@ namespace styles {
162
163
  selectors: { '& > span:first-child': { minWidth: '0 !custom' } },
163
164
  },
164
165
  },
165
- // The Platform Icon Button keeps its primary fill dark in both schemes, so its icon stays
166
- // white. Hover and press take the primary action fills, which turn light in dark mode, so
167
- // the icon takes the inverse content color over them.
166
+ // The Platform Icon Button fills its primary circle a step lighter than the primary
167
+ // Button in light mode.
168
168
  {
169
169
  when: { appearance: 'base', shape: 'icon', variant: 'primary' },
170
- style: {
171
- backgroundColor: 'inverse.background.secondary',
172
- color: 'neutral.000',
173
- selectors: {
174
- '&:is(:hover, :active):not(:disabled)': { color: 'inverse.content.primary' },
175
- },
176
- },
170
+ style: { backgroundColor: 'inverse.background.secondary' },
177
171
  },
178
172
  // The focus ring of the page blends into an inverse surface, so an inverse button draws
179
173
  // its ring in the inverse text color, 2px out, where the surface separates it from a fill
@@ -278,6 +272,16 @@ namespace styles {
278
272
  // Each pressed fill follows its hover fill, so it wins while the pointer stays over the
279
273
  // button.
280
274
  variant: {
275
+ // The Platform Button has no pressed design for `danger`, so a press keeps the hover fill.
276
+ danger: {
277
+ backgroundColor: 'action.danger.fill.rest',
278
+ color: 'action.danger.label.rest',
279
+ selectors: {
280
+ '&:is(:hover, :active):not(:disabled)': {
281
+ backgroundColor: 'action.danger.fill.hover',
282
+ },
283
+ },
284
+ },
281
285
  // The Platform Button paints its primary fill and label with the inverse colors.
282
286
  primary: {
283
287
  backgroundColor: 'inverse.background.primary',
@@ -75,7 +75,13 @@ export namespace Dialog {
75
75
  export function Body(props: Body.Props) {
76
76
  const { className, vars, ...attributes } = props
77
77
 
78
- return <div {...attributes} {...styles.body({ className, style: props.style, vars })} />
78
+ return (
79
+ <div
80
+ {...attributes}
81
+ data-dialog-body=""
82
+ {...styles.body({ className, style: props.style, vars })}
83
+ />
84
+ )
79
85
  }
80
86
 
81
87
  export namespace Body {