@dxos/react-ui 0.9.0 → 0.9.1-main.c7dcc2e112

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 (81) hide show
  1. package/dist/lib/browser/{chunk-7JFD5ZHZ.mjs → chunk-F3PLXTDA.mjs} +21 -25
  2. package/dist/lib/browser/{chunk-7JFD5ZHZ.mjs.map → chunk-F3PLXTDA.mjs.map} +3 -3
  3. package/dist/lib/browser/{chunk-5SPBSIWS.mjs → chunk-HFJYTGHU.mjs} +11 -1
  4. package/dist/lib/browser/chunk-HFJYTGHU.mjs.map +7 -0
  5. package/dist/lib/browser/index.mjs +1042 -856
  6. package/dist/lib/browser/index.mjs.map +4 -4
  7. package/dist/lib/browser/meta.json +1 -1
  8. package/dist/lib/browser/testing/index.mjs +5 -5
  9. package/dist/lib/browser/testing/index.mjs.map +2 -2
  10. package/dist/lib/browser/translations.mjs +1 -1
  11. package/dist/lib/node-esm/{chunk-TTEL2FP2.mjs → chunk-LLVUYLAM.mjs} +21 -25
  12. package/dist/lib/node-esm/{chunk-TTEL2FP2.mjs.map → chunk-LLVUYLAM.mjs.map} +3 -3
  13. package/dist/lib/node-esm/{chunk-FPVTVXND.mjs → chunk-OJYUPUCY.mjs} +11 -1
  14. package/dist/lib/node-esm/chunk-OJYUPUCY.mjs.map +7 -0
  15. package/dist/lib/node-esm/index.mjs +1042 -856
  16. package/dist/lib/node-esm/index.mjs.map +4 -4
  17. package/dist/lib/node-esm/meta.json +1 -1
  18. package/dist/lib/node-esm/testing/index.mjs +5 -5
  19. package/dist/lib/node-esm/testing/index.mjs.map +2 -2
  20. package/dist/lib/node-esm/translations.mjs +1 -1
  21. package/dist/types/src/components/Button/Button.d.ts +2 -0
  22. package/dist/types/src/components/Button/Button.d.ts.map +1 -1
  23. package/dist/types/src/components/Button/IconButton.d.ts +0 -1
  24. package/dist/types/src/components/Button/IconButton.d.ts.map +1 -1
  25. package/dist/types/src/components/Button/IconButton.stories.d.ts +34 -0
  26. package/dist/types/src/components/Button/IconButton.stories.d.ts.map +1 -1
  27. package/dist/types/src/components/Button/SystemIconButton.d.ts +20 -0
  28. package/dist/types/src/components/Button/SystemIconButton.d.ts.map +1 -0
  29. package/dist/types/src/components/Button/ToggleIconButton.d.ts +15 -0
  30. package/dist/types/src/components/Button/ToggleIconButton.d.ts.map +1 -0
  31. package/dist/types/src/components/Button/index.d.ts +2 -0
  32. package/dist/types/src/components/Button/index.d.ts.map +1 -1
  33. package/dist/types/src/components/Card/Card.d.ts +25 -19
  34. package/dist/types/src/components/Card/Card.d.ts.map +1 -1
  35. package/dist/types/src/components/Card/Card.stories.d.ts +1 -1
  36. package/dist/types/src/components/Card/Card.stories.d.ts.map +1 -1
  37. package/dist/types/src/components/Card/Card.theme.d.ts.map +1 -1
  38. package/dist/types/src/components/Clipboard/index.d.ts +1 -0
  39. package/dist/types/src/components/Clipboard/index.d.ts.map +1 -1
  40. package/dist/types/src/components/Column/Column.d.ts +19 -1
  41. package/dist/types/src/components/Column/Column.d.ts.map +1 -1
  42. package/dist/types/src/components/Column/Column.theme.d.ts +8 -0
  43. package/dist/types/src/components/Column/Column.theme.d.ts.map +1 -1
  44. package/dist/types/src/components/Input/constants.d.ts +3 -0
  45. package/dist/types/src/components/Input/constants.d.ts.map +1 -1
  46. package/dist/types/src/components/Message/Message.theme.d.ts.map +1 -1
  47. package/dist/types/src/components/Toast/Toast.d.ts.map +1 -1
  48. package/dist/types/src/components/Toast/Toast.theme.d.ts.map +1 -1
  49. package/dist/types/src/components/Tooltip/TextTooltip.d.ts +19 -0
  50. package/dist/types/src/components/Tooltip/TextTooltip.d.ts.map +1 -0
  51. package/dist/types/src/components/Tooltip/index.d.ts +1 -0
  52. package/dist/types/src/components/Tooltip/index.d.ts.map +1 -1
  53. package/dist/types/src/testing/Loading.d.ts.map +1 -1
  54. package/dist/types/src/translations.d.ts +10 -0
  55. package/dist/types/src/translations.d.ts.map +1 -1
  56. package/dist/types/tsconfig.tsbuildinfo +1 -1
  57. package/package.json +16 -16
  58. package/src/components/Button/Button.tsx +23 -2
  59. package/src/components/Button/IconButton.stories.tsx +81 -1
  60. package/src/components/Button/IconButton.tsx +2 -4
  61. package/src/components/Button/SystemIconButton.tsx +153 -0
  62. package/src/components/Button/ToggleIconButton.tsx +32 -0
  63. package/src/components/Button/index.ts +2 -0
  64. package/src/components/Card/Card.stories.tsx +69 -27
  65. package/src/components/Card/Card.theme.ts +27 -9
  66. package/src/components/Card/Card.tsx +165 -88
  67. package/src/components/Column/Column.theme.ts +32 -4
  68. package/src/components/Column/Column.tsx +36 -1
  69. package/src/components/Input/constants.ts +3 -0
  70. package/src/components/Message/Message.theme.ts +0 -10
  71. package/src/components/Message/Message.tsx +4 -4
  72. package/src/components/Popover/Popover.theme.ts +1 -1
  73. package/src/components/Toast/Toast.theme.ts +0 -6
  74. package/src/components/Toast/Toast.tsx +11 -10
  75. package/src/components/Tooltip/TextTooltip.tsx +54 -0
  76. package/src/components/Tooltip/index.ts +1 -0
  77. package/src/testing/Loading.tsx +2 -1
  78. package/src/testing/decorators/withLayout.tsx +2 -2
  79. package/src/translations.ts +10 -0
  80. package/dist/lib/browser/chunk-5SPBSIWS.mjs.map +0 -7
  81. package/dist/lib/node-esm/chunk-FPVTVXND.mjs.map +0 -7
@@ -5,25 +5,23 @@
5
5
  import { Primitive } from '@radix-ui/react-primitive';
6
6
  import { Slot } from '@radix-ui/react-slot';
7
7
  import DOMPurify from 'dompurify';
8
- import React, { CSSProperties, JSX, MouseEventHandler, type ReactNode, forwardRef, useId, useMemo } from 'react';
8
+ import React, { CSSProperties, MouseEventHandler, type ReactNode, forwardRef, useId, useMemo } from 'react';
9
+ import { useTranslation } from 'react-i18next';
9
10
 
10
11
  import { iconSize } from '@dxos/ui-theme';
11
- import { type Density } from '@dxos/ui-types';
12
+ import { type Density, type SlottableProps } from '@dxos/ui-types';
13
+
14
+ import { translationKey } from '#translations';
12
15
 
13
16
  import { useThemeContext } from '../../hooks';
14
17
  import { composable, composableProps, slottable } from '../../util';
15
18
  import { type ThemedClassName } from '../../util';
16
- import { Button } from '../Button';
19
+ import { Button, IconButton } from '../Button';
17
20
  import { Column } from '../Column';
18
- import { Icon, IconBlock, type IconBlockProps, type IconProps } from '../Icon';
21
+ import { Icon } from '../Icon';
19
22
  import { Image, type ImageProps } from '../Image';
20
- import {
21
- Toolbar,
22
- type ToolbarActionIconButtonProps,
23
- ToolbarDragHandleProps,
24
- type ToolbarMenuProps,
25
- type ToolbarRootProps,
26
- } from '../Toolbar';
23
+ import { DropdownMenu } from '../Menu';
24
+ import { type ToolbarActionIconButtonProps, type ToolbarDragHandleProps, type ToolbarMenuProps } from '../Toolbar';
27
25
 
28
26
  //
29
27
  // Root
@@ -82,22 +80,34 @@ CardRoot.displayName = CARD_ROOT_NAME;
82
80
 
83
81
  const CARD_HEADER_NAME = 'Card.Header';
84
82
 
85
- type CardHeaderProps = ToolbarRootProps;
83
+ type CardHeaderProps = SlottableProps;
86
84
 
87
85
  /**
88
- * Top "header" slot of a Card. Despite the name, this renders as an ARIA
89
- * toolbar (`role="toolbar"`) so its action children get keyboard navigation
90
- * for free`<header>` is allowed to contain a toolbar.
86
+ * Top header row of a Card. Renders as a `<header>` and shares `Card.Row`'s subgrid +
87
+ * `data-slot` layout: `Card.Block` children land in the gutters, anonymous children
88
+ * (e.g. `Card.Title`) in the center. Sets `--icon-size` 5 — header icons are larger than
89
+ * row icons (which use 4). Not a toolbar: most headers carry 0–1 controls, so `role="toolbar"`
90
+ * was inaccurate; controls are reached by normal tab order.
91
91
  */
92
- const CardHeader = composable<HTMLDivElement, CardHeaderProps>(({ children, classNames, ...props }, forwardedRef) => {
93
- const { tx } = useThemeContext();
92
+ const CardHeader = slottable<HTMLDivElement, CardHeaderProps>(
93
+ ({ children, asChild, style, ...props }, forwardedRef) => {
94
+ const { tx } = useThemeContext();
95
+ const { className, ...rest } = composableProps(props);
96
+ // `@radix-ui/react-primitive` has no `header` node; the intrinsic element handles asChild via Slot.
97
+ const Comp = asChild ? Slot : 'header';
94
98
 
95
- return (
96
- <Toolbar.Root {...props} style={iconSize(5)} classNames={[tx('card.header', {}), classNames]} ref={forwardedRef}>
97
- {children}
98
- </Toolbar.Root>
99
- );
100
- });
99
+ return (
100
+ <Comp
101
+ {...rest}
102
+ style={{ ...iconSize(5), ...style }}
103
+ className={tx('card.header', {}, className)}
104
+ ref={forwardedRef}
105
+ >
106
+ {children}
107
+ </Comp>
108
+ );
109
+ },
110
+ );
101
111
 
102
112
  CardHeader.displayName = CARD_HEADER_NAME;
103
113
 
@@ -109,13 +119,27 @@ const CARD_DRAG_HANDLE_NAME = 'Card.DragHandle';
109
119
 
110
120
  type CardDragHandleProps = ToolbarDragHandleProps;
111
121
 
112
- const CardDragHandle = forwardRef<HTMLButtonElement, CardDragHandleProps>((props, forwardedRef) => {
113
- return (
114
- <CardIconBlock>
115
- <Toolbar.DragHandle {...props} ref={forwardedRef} />
116
- </CardIconBlock>
117
- );
118
- });
122
+ const CardDragHandle = forwardRef<HTMLButtonElement, CardDragHandleProps>(
123
+ ({ testId = 'drag-handle', label }, forwardedRef) => {
124
+ const { t } = useTranslation(translationKey);
125
+ return (
126
+ <CardBlock>
127
+ <IconButton
128
+ data-testid={testId}
129
+ tabIndex={-1}
130
+ noTooltip
131
+ iconOnly
132
+ icon='ph--dots-six-vertical--regular'
133
+ variant='ghost'
134
+ label={label ?? t('toolbar-drag-handle.label')}
135
+ classNames='dx-focus-ring-none cursor-pointer text-base-fg'
136
+ disabled={!forwardedRef}
137
+ ref={forwardedRef}
138
+ />
139
+ </CardBlock>
140
+ );
141
+ },
142
+ );
119
143
 
120
144
  CardDragHandle.displayName = CARD_DRAG_HANDLE_NAME;
121
145
 
@@ -127,13 +151,28 @@ const CARD_ACTION_ICON_BUTTON_NAME = 'Card.ActionIconButton';
127
151
 
128
152
  type CardActionIconButtonProps = ToolbarActionIconButtonProps;
129
153
 
130
- const CardActionIconButton = forwardRef<HTMLButtonElement, CardActionIconButtonProps>((props, forwardedRef) => {
131
- return (
132
- <CardIconBlock>
133
- <Toolbar.ActionIconButton {...props} ref={forwardedRef} />
134
- </CardIconBlock>
135
- );
136
- });
154
+ const CARD_ACTION_ICONS = { close: 'ph--x--regular', delete: 'ph--trash--regular' } as const;
155
+
156
+ const CARD_ACTION_LABEL_KEYS = { close: 'toolbar-close.label', delete: 'toolbar-delete.label' } as const;
157
+
158
+ const CardActionIconButton = forwardRef<HTMLButtonElement, CardActionIconButtonProps>(
159
+ ({ action, onClick, label }, forwardedRef) => {
160
+ const { t } = useTranslation(translationKey);
161
+ return (
162
+ <CardBlock end>
163
+ <IconButton
164
+ iconOnly
165
+ icon={CARD_ACTION_ICONS[action]}
166
+ variant='ghost'
167
+ label={label ?? t(CARD_ACTION_LABEL_KEYS[action])}
168
+ classNames='cursor-pointer'
169
+ onClick={onClick}
170
+ ref={forwardedRef}
171
+ />
172
+ </CardBlock>
173
+ );
174
+ },
175
+ );
137
176
 
138
177
  CardActionIconButton.displayName = CARD_ACTION_ICON_BUTTON_NAME;
139
178
 
@@ -145,43 +184,67 @@ const CARD_MENU_NAME = 'Card.Menu';
145
184
 
146
185
  type CardMenuProps<T extends any | void = void> = ToolbarMenuProps<T>;
147
186
 
148
- function CardMenu<T extends any | void = void>({ context, items, ...props }: CardMenuProps<T>) {
187
+ function CardMenu<T extends any | void = void>({ context, items }: CardMenuProps<T>) {
188
+ const { t } = useTranslation(translationKey);
149
189
  return (
150
- <CardIconBlock>
151
- <Toolbar.Menu {...props} context={context} items={items ?? []} />
152
- </CardIconBlock>
190
+ <CardBlock end>
191
+ <DropdownMenu.Root>
192
+ <DropdownMenu.Trigger disabled={!items?.length} asChild>
193
+ <IconButton
194
+ iconOnly
195
+ variant='ghost'
196
+ icon='ph--dots-three-vertical--regular'
197
+ label={t('toolbar-menu.label')}
198
+ />
199
+ </DropdownMenu.Trigger>
200
+ {(items?.length ?? 0) > 0 && (
201
+ <DropdownMenu.Portal>
202
+ <DropdownMenu.Content>
203
+ <DropdownMenu.Viewport>
204
+ {items?.map(({ label, onClick: onSelect }, index) => (
205
+ // `context` is the generic payload threaded to each handler; the cast is the
206
+ // generic boundary (T may be `void`, so `context` is typed `T | undefined`).
207
+ <DropdownMenu.Item key={index} onSelect={() => onSelect(context as T)}>
208
+ {label}
209
+ </DropdownMenu.Item>
210
+ ))}
211
+ </DropdownMenu.Viewport>
212
+ <DropdownMenu.Arrow />
213
+ </DropdownMenu.Content>
214
+ </DropdownMenu.Portal>
215
+ )}
216
+ </DropdownMenu.Root>
217
+ </CardBlock>
153
218
  );
154
219
  }
155
220
 
156
221
  CardMenu.displayName = CARD_MENU_NAME;
157
222
 
158
223
  //
159
- // Icon
224
+ // Block
160
225
  //
161
226
 
162
- const CARD_ICON_NAME = 'Card.Icon';
227
+ const CARD_BLOCK_NAME = 'Card.Block';
163
228
 
164
- function CardIcon(props: IconProps) {
165
- return (
166
- <CardIconBlock>
167
- <Icon {...props} />
168
- </CardIconBlock>
169
- );
170
- }
229
+ type CardBlockProps = SlottableProps<{ end?: boolean; compact?: boolean; square?: boolean }>;
171
230
 
172
- CardIcon.displayName = CARD_ICON_NAME;
173
-
174
- //
175
- // IconBlock
176
- //
177
-
178
- const CARD_ICON_BLOCK_NAME = 'Card.IconBlock';
231
+ /**
232
+ * Leading (default) or trailing (`end`) gutter slot of a Card row/header. Sized to the
233
+ * rail-item square so a passive `<Icon>` aligns with an `IconButton`. Defaults text color to
234
+ * `subdued` so a decorative `<Icon>` child needs no styling; interactive children set their own.
235
+ */
236
+ const CardBlock = composable<HTMLDivElement, CardBlockProps>(({ children, ...props }, forwardedRef) => {
237
+ const { tx } = useThemeContext();
238
+ const { className, ...rest } = composableProps(props);
179
239
 
180
- const CardIconBlock = forwardRef<HTMLDivElement, IconBlockProps>((props, forwardedRef) => {
181
- return <IconBlock {...props} ref={forwardedRef} />;
240
+ return (
241
+ <Column.Block {...rest} classNames={tx('card.block', {}, className)} ref={forwardedRef}>
242
+ {children}
243
+ </Column.Block>
244
+ );
182
245
  });
183
246
 
184
- CardIconBlock.displayName = CARD_ICON_BLOCK_NAME;
247
+ CardBlock.displayName = CARD_BLOCK_NAME;
185
248
 
186
249
  //
187
250
  // Title
@@ -271,32 +334,28 @@ CardSection.displayName = CARD_SECTION_NAME;
271
334
 
272
335
  const CARD_ROW_NAME = 'Card.Row';
273
336
 
274
- type CardRowProps = { icon?: string | JSX.Element; fullWidth?: boolean };
337
+ type CardRowProps = { fullWidth?: boolean };
275
338
 
276
339
  /**
277
340
  * A row inside a Card.
278
- * - Default: spans all 3 columns and establishes a subgrid so children align to the Card's columns.
279
- * An optional `icon` lands in the first column; when omitted, the first column is left empty.
280
- * - `fullWidth`: spans all columns without a subgrid — children do their own internal layout.
281
- * The `icon` prop is ignored in this mode.
341
+ * - Default: spans all 3 columns as a subgrid; children align via `data-slot` — `Card.Block`
342
+ * lands in the gutters (start/end), anonymous children in the center. Sets `--icon-size` 4.
343
+ * - `fullWidth`: spans all columns without a subgrid — children do their own internal layout;
344
+ * `Card.Block` placement is inert in this mode.
282
345
  */
283
346
  const CardRow = slottable<HTMLDivElement, CardRowProps>(
284
- ({ children, asChild, icon: iconProp, fullWidth, ...props }, forwardedRef) => {
347
+ ({ children, asChild, fullWidth, style, ...props }, forwardedRef) => {
285
348
  const { tx } = useThemeContext();
286
349
  const { className, ...rest } = composableProps(props);
287
350
  const Comp = asChild ? Slot : Primitive.div;
288
- const icon =
289
- typeof iconProp === 'string' ? (
290
- <CardIcon classNames='text-subdued' icon={iconProp as string} size={4} />
291
- ) : iconProp ? (
292
- iconProp
293
- ) : (
294
- <div />
295
- );
296
351
 
297
352
  return (
298
- <Comp {...rest} className={tx('card.row', { fullWidth }, className)} ref={forwardedRef}>
299
- {!fullWidth && icon}
353
+ <Comp
354
+ {...rest}
355
+ style={{ ...iconSize(4), ...style }}
356
+ className={tx('card.row', { fullWidth }, className)}
357
+ ref={forwardedRef}
358
+ >
300
359
  {children}
301
360
  </Comp>
302
361
  );
@@ -379,15 +438,21 @@ type CardPosterProps = ThemedClassName<
379
438
  Omit<ImageProps, 'src' | 'alt' | 'classNames'>
380
439
  >;
381
440
 
382
- function CardPoster({ classNames, alt, aspect: aspectProp, image, icon, ...imageProps }: CardPosterProps) {
441
+ function CardPoster({
442
+ classNames,
443
+ alt,
444
+ aspect: aspectProp,
445
+ image,
446
+ icon,
447
+ fit = 'cover',
448
+ ...imageProps
449
+ }: CardPosterProps) {
383
450
  const { tx } = useThemeContext();
384
451
  const aspect = aspectProp === 'auto' ? 'aspect-auto' : 'aspect-video';
385
452
 
386
453
  if (image) {
387
454
  return (
388
- <div className='col-span-full'>
389
- <Image classNames={[tx('card.poster', {}), aspect, classNames]} src={image} alt={alt} {...imageProps} />
390
- </div>
455
+ <Image classNames={[tx('card.poster', {}), aspect, classNames]} src={image} alt={alt} fit={fit} {...imageProps} />
391
456
  );
392
457
  }
393
458
 
@@ -414,9 +479,17 @@ function CardAction({ icon, actionIcon = 'ph--arrow-right--regular', label, onCl
414
479
  const { tx } = useThemeContext();
415
480
  return (
416
481
  <Button variant='ghost' classNames={tx('card.action', {})} onClick={onClick}>
417
- {icon ? <CardIcon classNames='text-subdued' icon={icon} size={4} /> : <div />}
418
- <span className={tx('card.action-label', {}, !actionIcon ? 'col-span-2' : undefined)}>{label}</span>
419
- {actionIcon && <CardIcon icon={actionIcon} size={4} />}
482
+ {icon && (
483
+ <CardBlock>
484
+ <Icon icon={icon} size={4} />
485
+ </CardBlock>
486
+ )}
487
+ <span className={tx('card.action-label', {})}>{label}</span>
488
+ {actionIcon && (
489
+ <CardBlock end>
490
+ <Icon icon={actionIcon} size={4} />
491
+ </CardBlock>
492
+ )}
420
493
  </Button>
421
494
  );
422
495
  }
@@ -435,9 +508,13 @@ function CardLink({ label, href }: CardLinkProps) {
435
508
  const { tx } = useThemeContext();
436
509
  return (
437
510
  <a className={tx('card.link', {})} data-variant='ghost' href={href} target='_blank' rel='noreferrer'>
438
- <CardIcon classNames='text-subdued' icon='ph--link--regular' />
511
+ <CardBlock>
512
+ <Icon icon='ph--link--regular' size={4} />
513
+ </CardBlock>
439
514
  <span className={tx('card.link-label', {})}>{label}</span>
440
- <CardIcon classNames='invisible group-hover:visible' icon='ph--arrow-square-out--regular' />
515
+ <CardBlock end classNames='invisible group-hover:visible'>
516
+ <Icon icon='ph--arrow-square-out--regular' size={4} />
517
+ </CardBlock>
441
518
  </a>
442
519
  );
443
520
  }
@@ -454,12 +531,11 @@ export const Card = {
454
531
  // Header
455
532
  Header: CardHeader,
456
533
 
457
- // Header parts
458
- IconBlock: CardIconBlock,
534
+ // Header / row parts
535
+ Block: CardBlock,
459
536
  DragHandle: CardDragHandle,
460
537
  ActionIconButton: CardActionIconButton,
461
538
  Menu: CardMenu,
462
- Icon: CardIcon,
463
539
  Title: CardTitle,
464
540
 
465
541
  // Body
@@ -478,6 +554,7 @@ export const Card = {
478
554
  export type {
479
555
  CardRootProps,
480
556
  CardHeaderProps,
557
+ CardBlockProps,
481
558
  CardDragHandleProps,
482
559
  CardActionIconButtonProps,
483
560
  CardMenuProps,
@@ -9,20 +9,47 @@ import { withColumn } from './withColumn';
9
9
 
10
10
  export type ColumnStyleProps = {};
11
11
 
12
+ export type ColumnBlockStyleProps = {
13
+ /** Trailing gutter (column 3) instead of the default leading gutter (column 1). */
14
+ end?: boolean;
15
+ compact?: boolean;
16
+ /** Constrain to a square (1:1) slot rather than the default rail-item width. */
17
+ square?: boolean;
18
+ };
19
+
12
20
  const root: ComponentFunction<ColumnStyleProps> = (_, ...etc) => {
13
21
  return mx('dx-column-root grid', ...etc);
14
22
  };
15
23
 
16
24
  /**
17
- * Three-column icon-slot row: spans all 3 columns of the parent Column.Root grid.
18
- * Uses CSS subgrid to inherit column sizing from the parent Column.
19
- * Children map to: [col-1: icon/slot] [col-2: content] [col-3: icon/action].
25
+ * Three-column subgrid row spanning all 3 columns of the parent Column.Root grid.
26
+ * Placement is explicit, not source-order based:
27
+ * - `Column.Block` self-places into column 1 (leading) or column 3 (trailing, `end`) and
28
+ * carries the `dx-gutter` marker class.
29
+ * - Every other (content) child is placed in column 2 via `[&>*:not(.dx-gutter)]`.
30
+ * Class-based placement (rather than `[data-slot=…]`) is used because Tailwind does not
31
+ * generate arbitrary variants that nest a square-bracket attribute selector.
32
+ * Robust to conditional children: a falsy leading slot never shifts content into a gutter.
20
33
  * NOTE: Must not use overflow-hidden here since it will clip input focus rings.
21
34
  */
22
35
  const row: ComponentFunction<ColumnStyleProps> = (_, ...etc) => {
23
- return mx('col-span-3 grid grid-cols-subgrid', ...etc);
36
+ return mx('col-span-3 grid grid-cols-subgrid', '[&>*:not(.dx-gutter)]:col-start-2', ...etc);
24
37
  };
25
38
 
39
+ /**
40
+ * Gutter slot geometry: a `--dx-rail-item` square that centers its child, so a passive
41
+ * `<Icon>` and an interactive `IconButton` line up to the pixel. Self-places into column 1
42
+ * (default) or column 3 (`end`); the `dx-gutter` marker keeps it out of the content track.
43
+ */
44
+ const block: ComponentFunction<ColumnBlockStyleProps> = ({ end, compact, square }, ...etc) =>
45
+ mx(
46
+ 'dx-gutter grid place-items-center [&>img]:max-w-[1.5rem]',
47
+ end ? 'col-start-3' : 'col-start-1',
48
+ square ? 'aspect-square' : 'w-[var(--dx-rail-item)]',
49
+ compact ? '' : 'h-[var(--dx-rail-item)]',
50
+ ...etc,
51
+ );
52
+
26
53
  /**
27
54
  * Bleed placement: spans all 3 columns of the parent Column.Root grid (gutter-to-gutter).
28
55
  * Use for `ScrollArea`, full-width dividers, tables, or any content that should ignore gutters.
@@ -43,6 +70,7 @@ const center: ComponentFunction<ColumnStyleProps> = (_, ...etc) => {
43
70
  export const columnTheme = {
44
71
  root,
45
72
  row,
73
+ block,
46
74
  bleed,
47
75
  center,
48
76
  };
@@ -152,6 +152,40 @@ const ColumnCenter = slottable<HTMLDivElement>(({ children, asChild, ...props },
152
152
 
153
153
  ColumnCenter.displayName = COLUMN_CENTER_NAME;
154
154
 
155
+ //
156
+ // Block
157
+ //
158
+
159
+ const COLUMN_BLOCK_NAME = 'Column.Block';
160
+
161
+ type ColumnBlockProps = SlottableProps<{ end?: boolean; compact?: boolean; square?: boolean }>;
162
+
163
+ /**
164
+ * A gutter slot inside a Column.Row. Sized to `--dx-rail-item` and centers its child so a passive
165
+ * `<Icon>` and an interactive `IconButton` align to the pixel. `end` opts into the trailing gutter
166
+ * (column 3); default is the leading gutter (column 1). Placement is via `data-slot`, so it is
167
+ * robust to conditional rendering and source order.
168
+ */
169
+ const ColumnBlock = slottable<HTMLDivElement, ColumnBlockProps>(
170
+ ({ children, asChild, end, compact, square, ...props }, forwardedRef) => {
171
+ const { tx } = useThemeContext();
172
+ const { className, ...rest } = composableProps(props);
173
+ const Comp = asChild ? Slot : Primitive.div;
174
+ return (
175
+ <Comp
176
+ {...rest}
177
+ data-slot={end ? 'end' : 'start'}
178
+ className={tx('column.block', { end, compact, square }, className)}
179
+ ref={forwardedRef}
180
+ >
181
+ {children}
182
+ </Comp>
183
+ );
184
+ },
185
+ );
186
+
187
+ ColumnBlock.displayName = COLUMN_BLOCK_NAME;
188
+
155
189
  //
156
190
  // Column
157
191
  //
@@ -159,8 +193,9 @@ ColumnCenter.displayName = COLUMN_CENTER_NAME;
159
193
  export const Column = {
160
194
  Root: ColumnRoot,
161
195
  Row: ColumnRow,
196
+ Block: ColumnBlock,
162
197
  Bleed: ColumnBleed,
163
198
  Center: ColumnCenter,
164
199
  };
165
200
 
166
- export type { ColumnRootProps, ColumnRowProps, ColumnBleedProps, ColumnCenterProps };
201
+ export type { ColumnRootProps, ColumnRowProps, ColumnBlockProps, ColumnBleedProps, ColumnCenterProps };
@@ -2,4 +2,7 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
+ /**
6
+ * @deprecated Create TW variable.
7
+ */
5
8
  export const inputTextLabel = 'py-1 text-sm text-description';
@@ -22,14 +22,6 @@ const title: ComponentFunction<MessageStyleProps> = (_, etc) => {
22
22
  return mx('col-start-2 overflow-hidden truncate', etc);
23
23
  };
24
24
 
25
- const icon: ComponentFunction<MessageStyleProps> = (_, etc) => {
26
- return mx('col-start-1 grid place-items-center', etc);
27
- };
28
-
29
- const close: ComponentFunction<MessageStyleProps> = (_, etc) => {
30
- return mx('col-start-3', etc);
31
- };
32
-
33
25
  const content: ComponentFunction<MessageStyleProps> = (_, etc) => {
34
26
  return mx('col-start-2 flex flex-col first:font-medium pb-1.5', etc);
35
27
  };
@@ -37,8 +29,6 @@ const content: ComponentFunction<MessageStyleProps> = (_, etc) => {
37
29
  export const messageTheme: Theme<MessageStyleProps> = {
38
30
  root,
39
31
  header,
40
- icon,
41
32
  title,
42
- close,
43
33
  content,
44
34
  };
@@ -141,15 +141,15 @@ const MessageTitle = forwardRef<HTMLDivElement, MessageTitleProps>(
141
141
  return (
142
142
  <Column.Row classNames={tx('message.header', {}, classNames)} ref={forwardedRef}>
143
143
  {icon && (
144
- <div className={tx('message.icon', { valence })}>
144
+ <Column.Block>
145
145
  <Icon icon={icon} />
146
- </div>
146
+ </Column.Block>
147
147
  )}
148
148
  <h2 className={tx('message.title', {}, classNames)} id={titleId}>
149
149
  {children}
150
150
  </h2>
151
151
  {onClose && (
152
- <div className={tx('message.close', {})}>
152
+ <Column.Block end>
153
153
  <IconButton
154
154
  variant='ghost'
155
155
  icon='ph--x--regular'
@@ -158,7 +158,7 @@ const MessageTitle = forwardRef<HTMLDivElement, MessageTitleProps>(
158
158
  label={t('toolbar-close.label')}
159
159
  onClick={onClose}
160
160
  />
161
- </div>
161
+ </Column.Block>
162
162
  )}
163
163
  </Column.Row>
164
164
  );
@@ -13,7 +13,7 @@ export type PopoverStyleProps = Partial<{
13
13
 
14
14
  const content: ComponentFunction<PopoverStyleProps> = ({ elevation }, ...etc) =>
15
15
  mx(
16
- 'dx-popover-surface border border-separator rounded-sm',
16
+ 'dx-popover-surface overflow-hidden border border-separator rounded-sm',
17
17
  surfaceShadow({ elevation: 'positioned' }),
18
18
  surfaceZIndex({ elevation, level: 'menu' }),
19
19
  'dx-focus-ring',
@@ -32,12 +32,8 @@ const grid: ComponentFunction<ToastStyleProps> = (_props, ...etc) => mx('gap-y-1
32
32
 
33
33
  const header: ComponentFunction<ToastStyleProps> = (_props, ...etc) => mx('items-center', ...etc);
34
34
 
35
- const icon: ComponentFunction<ToastStyleProps> = (_props, ...etc) => mx('col-start-1 grid place-items-center', ...etc);
36
-
37
35
  const title: ComponentFunction<ToastStyleProps> = (_props, ...etc) => mx('col-start-2 truncate font-medium', ...etc);
38
36
 
39
- const close: ComponentFunction<ToastStyleProps> = (_props, ...etc) => mx('col-start-3', ...etc);
40
-
41
37
  const description: ComponentFunction<ToastStyleProps> = (_props, ...etc) =>
42
38
  mx('col-start-2 overflow-hidden text-description', ...etc);
43
39
 
@@ -48,9 +44,7 @@ export const toastTheme: Theme<ToastStyleProps> = {
48
44
  root,
49
45
  grid,
50
46
  header,
51
- icon,
52
47
  title,
53
- close,
54
48
  description,
55
49
  actions,
56
50
  };
@@ -71,22 +71,23 @@ const ToastTitle = forwardRef<HTMLHeadingElement, ToastTitleProps>(
71
71
  return (
72
72
  <Column.Row classNames={tx('toast.header', {})}>
73
73
  {icon && (
74
- <div className={tx('toast.icon', {})}>
74
+ <Column.Block>
75
75
  <Icon icon={icon} size={5} />
76
- </div>
76
+ </Column.Block>
77
77
  )}
78
78
  <ToastPrimitive.Title {...props} className={tx('toast.title', {}, classNames)} ref={forwardedRef}>
79
79
  {children}
80
80
  </ToastPrimitive.Title>
81
81
  {onClose && (
82
- <IconButton
83
- variant='ghost'
84
- icon='ph--x--regular'
85
- iconOnly
86
- label={t('toolbar-close.label')}
87
- classNames={tx('toast.close', {})}
88
- onClick={onClose}
89
- />
82
+ <Column.Block end>
83
+ <IconButton
84
+ variant='ghost'
85
+ icon='ph--x--regular'
86
+ iconOnly
87
+ label={t('toolbar-close.label')}
88
+ onClick={onClose}
89
+ />
90
+ </Column.Block>
90
91
  )}
91
92
  </Column.Row>
92
93
  );
@@ -0,0 +1,54 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { useComposedRefs } from '@radix-ui/react-compose-refs';
6
+ import React, {
7
+ type ComponentPropsWithoutRef,
8
+ type PropsWithChildren,
9
+ type SyntheticEvent,
10
+ forwardRef,
11
+ useCallback,
12
+ useRef,
13
+ } from 'react';
14
+
15
+ import { Tooltip, type TooltipScopedProps, type TooltipTriggerProps } from './Tooltip';
16
+
17
+ export type TextTooltipProps = PropsWithChildren<
18
+ {
19
+ text: string;
20
+ asChild?: boolean;
21
+ onlyWhenTruncating?: boolean;
22
+ truncateQuery?: string;
23
+ } & Pick<TooltipTriggerProps, 'side'> &
24
+ ComponentPropsWithoutRef<'button'>
25
+ >;
26
+
27
+ export const TextTooltip = forwardRef<HTMLButtonElement, TooltipScopedProps<TextTooltipProps>>(
28
+ (
29
+ { __scopeTooltip, text, children, onlyWhenTruncating, asChild = true, side, truncateQuery, ...props },
30
+ forwardedRef,
31
+ ) => {
32
+ const content = useRef<HTMLButtonElement | null>(null);
33
+ const ref = useComposedRefs(content, forwardedRef);
34
+ const handleInteract = useCallback(
35
+ (event: SyntheticEvent) => {
36
+ if (onlyWhenTruncating && content.current) {
37
+ const element: HTMLElement | null = truncateQuery
38
+ ? content.current.querySelector(truncateQuery)
39
+ : content.current;
40
+ if (!element || element.scrollWidth <= element.offsetWidth) {
41
+ event.preventDefault();
42
+ }
43
+ }
44
+ },
45
+ [onlyWhenTruncating, truncateQuery],
46
+ );
47
+
48
+ return (
49
+ <Tooltip.Trigger asChild={asChild} {...props} content={text} side={side} onInteract={handleInteract} ref={ref}>
50
+ {children}
51
+ </Tooltip.Trigger>
52
+ );
53
+ },
54
+ );