@dxos/react-ui 0.9.1-main.c7dcc2e112 → 0.9.1-staging.ee54ba693a

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 (239) hide show
  1. package/dist/lib/browser/{chunk-HFJYTGHU.mjs → chunk-CMHHVHX3.mjs} +4 -1
  2. package/dist/lib/browser/chunk-CMHHVHX3.mjs.map +7 -0
  3. package/dist/lib/browser/{chunk-F3PLXTDA.mjs → chunk-GTHV4WYK.mjs} +123 -232
  4. package/dist/lib/browser/chunk-GTHV4WYK.mjs.map +7 -0
  5. package/dist/lib/browser/index.mjs +1069 -976
  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-LLVUYLAM.mjs → chunk-3VSKWNCQ.mjs} +123 -232
  12. package/dist/lib/node-esm/chunk-3VSKWNCQ.mjs.map +7 -0
  13. package/dist/lib/node-esm/{chunk-OJYUPUCY.mjs → chunk-P4KFQGJI.mjs} +4 -1
  14. package/dist/lib/node-esm/chunk-P4KFQGJI.mjs.map +7 -0
  15. package/dist/lib/node-esm/index.mjs +1069 -976
  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/Avatars/Avatar.d.ts +1 -1
  22. package/dist/types/src/components/Avatars/Avatar.d.ts.map +1 -1
  23. package/dist/types/src/components/Breadcrumb/Breadcrumb.d.ts +3 -3
  24. package/dist/types/src/components/Breadcrumb/Breadcrumb.d.ts.map +1 -1
  25. package/dist/types/src/components/Button/Button.d.ts +2 -2
  26. package/dist/types/src/components/Button/Button.d.ts.map +1 -1
  27. package/dist/types/src/components/Button/IconButton.stories.d.ts +3 -0
  28. package/dist/types/src/components/Button/IconButton.stories.d.ts.map +1 -1
  29. package/dist/types/src/components/Button/MicButton.d.ts +24 -0
  30. package/dist/types/src/components/Button/MicButton.d.ts.map +1 -0
  31. package/dist/types/src/components/Button/MicButton.stories.d.ts +57 -0
  32. package/dist/types/src/components/Button/MicButton.stories.d.ts.map +1 -0
  33. package/dist/types/src/components/Button/SystemIconButton.d.ts +15 -2
  34. package/dist/types/src/components/Button/SystemIconButton.d.ts.map +1 -1
  35. package/dist/types/src/components/Button/SystemIconButton.stories.d.ts +51 -0
  36. package/dist/types/src/components/Button/SystemIconButton.stories.d.ts.map +1 -0
  37. package/dist/types/src/components/Button/ToggleGroup.d.ts +2 -2
  38. package/dist/types/src/components/Button/ToggleGroup.d.ts.map +1 -1
  39. package/dist/types/src/components/Button/index.d.ts +1 -0
  40. package/dist/types/src/components/Button/index.d.ts.map +1 -1
  41. package/dist/types/src/components/Calendar/Calendar.d.ts.map +1 -1
  42. package/dist/types/src/components/Calendar/Calendar.theme.d.ts.map +1 -1
  43. package/dist/types/src/components/Card/Card.d.ts +8 -8
  44. package/dist/types/src/components/Card/Card.d.ts.map +1 -1
  45. package/dist/types/src/components/Card/Card.stories.d.ts +2 -2
  46. package/dist/types/src/components/Card/Card.stories.d.ts.map +1 -1
  47. package/dist/types/src/components/Carousel/Carousel.d.ts +29 -6
  48. package/dist/types/src/components/Carousel/Carousel.d.ts.map +1 -1
  49. package/dist/types/src/components/Carousel/Carousel.stories.d.ts +48 -2
  50. package/dist/types/src/components/Carousel/Carousel.stories.d.ts.map +1 -1
  51. package/dist/types/src/components/Column/Column.d.ts +2 -2
  52. package/dist/types/src/components/Column/Column.d.ts.map +1 -1
  53. package/dist/types/src/components/Dialog/AlertDialog.d.ts +2 -2
  54. package/dist/types/src/components/Dialog/AlertDialog.d.ts.map +1 -1
  55. package/dist/types/src/components/Dialog/Dialog.d.ts +1 -1
  56. package/dist/types/src/components/Dialog/Dialog.d.ts.map +1 -1
  57. package/dist/types/src/components/Dialog/Dialog.stories.d.ts +2 -2
  58. package/dist/types/src/components/Dialog/Dialog.stories.d.ts.map +1 -1
  59. package/dist/types/src/components/Icon/Icon.stories.d.ts +5 -0
  60. package/dist/types/src/components/Icon/Icon.stories.d.ts.map +1 -1
  61. package/dist/types/src/components/Input/Input.d.ts +4 -1
  62. package/dist/types/src/components/Input/Input.d.ts.map +1 -1
  63. package/dist/types/src/components/Input/Input.stories.d.ts +3 -3
  64. package/dist/types/src/components/Input/Input.stories.d.ts.map +1 -1
  65. package/dist/types/src/components/Input/Input.theme.d.ts +10 -10
  66. package/dist/types/src/components/Input/Input.theme.d.ts.map +1 -1
  67. package/dist/types/src/components/Input/SegmentedInput.d.ts +8 -8
  68. package/dist/types/src/components/Input/SegmentedInput.d.ts.map +1 -1
  69. package/dist/types/src/components/Input/index.d.ts +0 -1
  70. package/dist/types/src/components/Input/index.d.ts.map +1 -1
  71. package/dist/types/src/components/Main/Main.d.ts +3 -3
  72. package/dist/types/src/components/Main/Main.d.ts.map +1 -1
  73. package/dist/types/src/components/Menu/ContextMenu.d.ts +1 -1
  74. package/dist/types/src/components/Menu/ContextMenu.d.ts.map +1 -1
  75. package/dist/types/src/components/Menu/DropdownMenu.d.ts +2 -2
  76. package/dist/types/src/components/Menu/DropdownMenu.d.ts.map +1 -1
  77. package/dist/types/src/components/Message/Message.d.ts +1 -1
  78. package/dist/types/src/components/Message/Message.d.ts.map +1 -1
  79. package/dist/types/src/components/Message/Message.stories.d.ts +2 -2
  80. package/dist/types/src/components/Message/Message.stories.d.ts.map +1 -1
  81. package/dist/types/src/components/Panel/Panel.d.ts +1 -1
  82. package/dist/types/src/components/Panel/Panel.d.ts.map +1 -1
  83. package/dist/types/src/components/Panel/Panel.theme.d.ts.map +1 -1
  84. package/dist/types/src/components/Popover/Popover.d.ts +2 -2
  85. package/dist/types/src/components/Popover/Popover.d.ts.map +1 -1
  86. package/dist/types/src/components/ScrollArea/ScrollArea.d.ts +2 -0
  87. package/dist/types/src/components/ScrollArea/ScrollArea.d.ts.map +1 -1
  88. package/dist/types/src/components/ScrollArea/ScrollArea.stories.d.ts +1 -0
  89. package/dist/types/src/components/ScrollArea/ScrollArea.stories.d.ts.map +1 -1
  90. package/dist/types/src/components/ScrollContainer/ScrollContainer.d.ts +1 -1
  91. package/dist/types/src/components/ScrollContainer/ScrollContainer.d.ts.map +1 -1
  92. package/dist/types/src/components/ScrollContainer/ScrollContainer.stories.d.ts +2 -2
  93. package/dist/types/src/components/ScrollContainer/ScrollContainer.stories.d.ts.map +1 -1
  94. package/dist/types/src/components/Select/Select.d.ts +1 -1
  95. package/dist/types/src/components/Select/Select.d.ts.map +1 -1
  96. package/dist/types/src/components/Select/Select.stories.d.ts +2 -2
  97. package/dist/types/src/components/Select/Select.stories.d.ts.map +1 -1
  98. package/dist/types/src/components/Separator/Separator.d.ts.map +1 -1
  99. package/dist/types/src/components/Splitter/Splitter.d.ts +30 -16
  100. package/dist/types/src/components/Splitter/Splitter.d.ts.map +1 -1
  101. package/dist/types/src/components/Splitter/Splitter.stories.d.ts +6 -1
  102. package/dist/types/src/components/Splitter/Splitter.stories.d.ts.map +1 -1
  103. package/dist/types/src/components/Splitter/Splitter.theme.d.ts +3 -1
  104. package/dist/types/src/components/Splitter/Splitter.theme.d.ts.map +1 -1
  105. package/dist/types/src/components/Tag/Tag.d.ts +1 -1
  106. package/dist/types/src/components/Tag/Tag.d.ts.map +1 -1
  107. package/dist/types/src/components/Tag/Tag.theme.d.ts +1 -1
  108. package/dist/types/src/components/Tag/Tag.theme.d.ts.map +1 -1
  109. package/dist/types/src/components/Toast/Toast.d.ts +1 -1
  110. package/dist/types/src/components/Toast/Toast.d.ts.map +1 -1
  111. package/dist/types/src/components/Toast/Toast.theme.d.ts.map +1 -1
  112. package/dist/types/src/components/Toolbar/Toolbar.d.ts +1 -1
  113. package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +1 -1
  114. package/dist/types/src/components/Tooltip/Tooltip.d.ts +3 -3
  115. package/dist/types/src/components/Tooltip/Tooltip.d.ts.map +1 -1
  116. package/dist/types/src/components/Tooltip/Tooltip.stories.d.ts +2 -2
  117. package/dist/types/src/components/Tooltip/Tooltip.stories.d.ts.map +1 -1
  118. package/dist/types/src/components/index.d.ts +0 -1
  119. package/dist/types/src/components/index.d.ts.map +1 -1
  120. package/dist/types/src/index.d.ts +1 -1
  121. package/dist/types/src/index.d.ts.map +1 -1
  122. package/dist/types/src/primitives/ThemeProvider/TranslationsProvider.d.ts +1 -8
  123. package/dist/types/src/primitives/ThemeProvider/TranslationsProvider.d.ts.map +1 -1
  124. package/dist/types/src/theme/defaultTheme.d.ts.map +1 -1
  125. package/dist/types/src/theme/index.d.ts +0 -2
  126. package/dist/types/src/theme/index.d.ts.map +1 -1
  127. package/dist/types/src/translations.d.ts +3 -0
  128. package/dist/types/src/translations.d.ts.map +1 -1
  129. package/dist/types/src/util/slots.d.ts +1 -1
  130. package/dist/types/src/util/slots.d.ts.map +1 -1
  131. package/dist/types/tsconfig.tsbuildinfo +1 -1
  132. package/package.json +22 -21
  133. package/src/components/Avatars/Avatar.stories.tsx +2 -2
  134. package/src/components/Avatars/Avatar.theme.ts +1 -1
  135. package/src/components/Avatars/Avatar.tsx +3 -3
  136. package/src/components/Breadcrumb/Breadcrumb.tsx +6 -6
  137. package/src/components/Button/Button.tsx +2 -2
  138. package/src/components/Button/MicButton.stories.tsx +60 -0
  139. package/src/components/Button/MicButton.tsx +117 -0
  140. package/src/components/Button/SystemIconButton.stories.tsx +91 -0
  141. package/src/components/Button/SystemIconButton.tsx +113 -2
  142. package/src/components/Button/ToggleGroup.tsx +2 -2
  143. package/src/components/Button/index.ts +1 -0
  144. package/src/components/Calendar/Calendar.theme.ts +2 -0
  145. package/src/components/Calendar/Calendar.tsx +3 -3
  146. package/src/components/Card/Card.stories.tsx +2 -2
  147. package/src/components/Card/Card.tsx +11 -11
  148. package/src/components/Carousel/Carousel.stories.tsx +42 -18
  149. package/src/components/Carousel/Carousel.tsx +169 -33
  150. package/src/components/Column/Column.tsx +4 -7
  151. package/src/components/Dialog/AlertDialog.stories.tsx +2 -2
  152. package/src/components/Dialog/AlertDialog.tsx +13 -13
  153. package/src/components/Dialog/Dialog.stories.tsx +3 -4
  154. package/src/components/Dialog/Dialog.theme.ts +1 -1
  155. package/src/components/Dialog/Dialog.tsx +12 -12
  156. package/src/components/Icon/Icon.stories.tsx +23 -0
  157. package/src/components/Icon/Icon.theme.ts +1 -1
  158. package/src/components/Input/Input.stories.tsx +13 -5
  159. package/src/components/Input/Input.theme.ts +17 -39
  160. package/src/components/Input/Input.tsx +30 -13
  161. package/src/components/Input/SegmentedInput.tsx +50 -41
  162. package/src/components/Input/index.ts +0 -1
  163. package/src/components/Main/Main.tsx +3 -3
  164. package/src/components/MediaPlayer/MediaPlayer.tsx +4 -4
  165. package/src/components/Menu/ContextMenu.tsx +8 -8
  166. package/src/components/Menu/DropdownMenu.tsx +12 -13
  167. package/src/components/Message/Message.stories.tsx +2 -2
  168. package/src/components/Message/Message.theme.ts +1 -1
  169. package/src/components/Message/Message.tsx +2 -2
  170. package/src/components/Panel/Panel.theme.ts +2 -1
  171. package/src/components/Panel/Panel.tsx +1 -1
  172. package/src/components/Popover/Popover.tsx +9 -9
  173. package/src/components/ScrollArea/ScrollArea.tsx +7 -4
  174. package/src/components/ScrollContainer/ScrollContainer.stories.tsx +2 -2
  175. package/src/components/ScrollContainer/ScrollContainer.tsx +2 -2
  176. package/src/components/Select/Select.stories.tsx +2 -2
  177. package/src/components/Select/Select.theme.ts +1 -1
  178. package/src/components/Select/Select.tsx +14 -14
  179. package/src/components/Separator/Separator.tsx +1 -0
  180. package/src/components/Splitter/Splitter.stories.tsx +84 -20
  181. package/src/components/Splitter/Splitter.theme.ts +20 -3
  182. package/src/components/Splitter/Splitter.tsx +320 -47
  183. package/src/components/Tag/Tag.stories.tsx +1 -1
  184. package/src/components/Tag/Tag.theme.ts +1 -1
  185. package/src/components/Tag/Tag.tsx +3 -5
  186. package/src/components/Toast/Toast.theme.ts +3 -2
  187. package/src/components/Toast/Toast.tsx +5 -5
  188. package/src/components/Toolbar/Toolbar.tsx +10 -10
  189. package/src/components/Tooltip/Tooltip.stories.tsx +2 -2
  190. package/src/components/Tooltip/Tooltip.tsx +3 -3
  191. package/src/components/index.ts +0 -1
  192. package/src/exemplars/tabster.stories.tsx +1 -1
  193. package/src/index.ts +1 -1
  194. package/src/primitives/ThemeProvider/TranslationsProvider.tsx +10 -30
  195. package/src/testing/decorators/withLayout.tsx +4 -4
  196. package/src/theme/defaultTheme.ts +0 -4
  197. package/src/theme/index.ts +0 -2
  198. package/src/translations.ts +8 -0
  199. package/src/util/slots.ts +5 -5
  200. package/dist/lib/browser/chunk-F3PLXTDA.mjs.map +0 -7
  201. package/dist/lib/browser/chunk-HFJYTGHU.mjs.map +0 -7
  202. package/dist/lib/node-esm/chunk-LLVUYLAM.mjs.map +0 -7
  203. package/dist/lib/node-esm/chunk-OJYUPUCY.mjs.map +0 -7
  204. package/dist/types/src/components/Input/constants.d.ts +0 -5
  205. package/dist/types/src/components/Input/constants.d.ts.map +0 -1
  206. package/dist/types/src/components/List/List.d.ts +0 -29
  207. package/dist/types/src/components/List/List.d.ts.map +0 -1
  208. package/dist/types/src/components/List/List.stories.d.ts +0 -16
  209. package/dist/types/src/components/List/List.stories.d.ts.map +0 -1
  210. package/dist/types/src/components/List/List.theme.d.ts +0 -7
  211. package/dist/types/src/components/List/List.theme.d.ts.map +0 -1
  212. package/dist/types/src/components/List/ListDropIndicator.d.ts +0 -13
  213. package/dist/types/src/components/List/ListDropIndicator.d.ts.map +0 -1
  214. package/dist/types/src/components/List/Tree.d.ts +0 -23
  215. package/dist/types/src/components/List/Tree.d.ts.map +0 -1
  216. package/dist/types/src/components/List/Tree.stories.d.ts +0 -15
  217. package/dist/types/src/components/List/Tree.stories.d.ts.map +0 -1
  218. package/dist/types/src/components/List/TreeDropIndicator.d.ts +0 -8
  219. package/dist/types/src/components/List/TreeDropIndicator.d.ts.map +0 -1
  220. package/dist/types/src/components/List/Treegrid.d.ts +0 -33
  221. package/dist/types/src/components/List/Treegrid.d.ts.map +0 -1
  222. package/dist/types/src/components/List/Treegrid.stories.d.ts +0 -12
  223. package/dist/types/src/components/List/Treegrid.stories.d.ts.map +0 -1
  224. package/dist/types/src/components/List/Treegrid.theme.d.ts +0 -7
  225. package/dist/types/src/components/List/Treegrid.theme.d.ts.map +0 -1
  226. package/dist/types/src/components/List/index.d.ts +0 -4
  227. package/dist/types/src/components/List/index.d.ts.map +0 -1
  228. package/src/components/Input/constants.ts +0 -8
  229. package/src/components/List/List.stories.tsx +0 -234
  230. package/src/components/List/List.theme.ts +0 -47
  231. package/src/components/List/List.tsx +0 -171
  232. package/src/components/List/ListDropIndicator.tsx +0 -69
  233. package/src/components/List/Tree.stories.tsx +0 -87
  234. package/src/components/List/Tree.tsx +0 -74
  235. package/src/components/List/TreeDropIndicator.tsx +0 -70
  236. package/src/components/List/Treegrid.stories.tsx +0 -156
  237. package/src/components/List/Treegrid.theme.ts +0 -35
  238. package/src/components/List/Treegrid.tsx +0 -187
  239. package/src/components/List/index.ts +0 -7
@@ -2,31 +2,125 @@
2
2
  // Copyright 2026 DXOS.org
3
3
  //
4
4
 
5
- import { createContextScope } from '@radix-ui/react-context';
5
+ import { useComposedRefs } from '@radix-ui/react-compose-refs';
6
+ import { createContext } from '@radix-ui/react-context';
6
7
  import { Primitive } from '@radix-ui/react-primitive';
7
8
  import { Slot } from '@radix-ui/react-slot';
8
- import React from 'react';
9
+ import { useControllableState } from '@radix-ui/react-use-controllable-state';
10
+ import React, {
11
+ type CSSProperties,
12
+ type KeyboardEvent,
13
+ type PointerEvent,
14
+ type RefObject,
15
+ useCallback,
16
+ useEffect,
17
+ useRef,
18
+ useState,
19
+ } from 'react';
20
+
21
+ import { type SlottableProps } from '@dxos/ui-types';
9
22
 
10
23
  import { useThemeContext } from '../../hooks';
11
24
  import { composableProps, slottable } from '../../util';
12
- import { ThemedClassName } from '../../util';
13
25
 
14
- type ScopedProps<P> = P & { __scopeSplitter?: any };
26
+ type SplitterOrientation = 'horizontal' | 'vertical';
27
+
28
+ // Animated panel visibility: collapse to the start panel, the end panel, or show both split at `size`.
29
+ type SplitterMode = 'start' | 'end' | 'split';
30
+
31
+ type Position = 'start' | 'end';
15
32
 
16
- // TODO(burdon): Generalize horizontal/vertical and change to start/end.
17
- type Mode = 'top' | 'bottom' | 'split';
33
+ //
34
+ // Context
35
+ //
36
+
37
+ const SPLITTER_NAME = 'Splitter';
18
38
 
19
39
  type SplitterContextValue = {
20
- mode: Mode;
21
- ratio?: number;
40
+ orientation: SplitterOrientation;
41
+ mode: SplitterMode;
42
+ /** Which panel `size` measures; the other panel fills the remainder. */
43
+ anchor: Position;
44
+ /** The anchored panel's extent in rem, or `undefined` for an even (50/50) split. */
45
+ size: number | undefined;
22
46
  transition: number;
47
+ resizable: boolean;
48
+ /** Lower bound (rem) applied to both panels. */
49
+ minSize: number;
50
+ dragging: boolean;
51
+ /** True only briefly after a `mode` change, so the collapse animates but layout reflows (resize) do not. */
52
+ animating: boolean;
53
+ rootRef: RefObject<HTMLDivElement | null>;
54
+ setSize: (size: number) => void;
55
+ setDragging: (dragging: boolean) => void;
23
56
  };
24
57
 
25
- const SPLITTER_NAME = 'Splitter';
58
+ const [SplitterProvider, useSplitterContext] = createContext<SplitterContextValue>(SPLITTER_NAME);
59
+
60
+ const getRem = (): number => parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
26
61
 
27
- const [createSplitterContext, createSplitterScope] = createContextScope(SPLITTER_NAME);
62
+ // Lower-bound clamp applied to both panels: the start panel can't drop below `minSize` nor exceed the
63
+ // container minus `minSize` (reserving room for the end panel, which also carries the min).
64
+ const clampStyle = (minSize: number, orientation: SplitterOrientation): CSSProperties =>
65
+ orientation === 'horizontal'
66
+ ? { minInlineSize: `${minSize}rem`, maxInlineSize: `calc(100% - ${minSize}rem)` }
67
+ : { minBlockSize: `${minSize}rem`, maxBlockSize: `calc(100% - ${minSize}rem)` };
28
68
 
29
- const [SplitterProvider, useSplitterContext] = createSplitterContext<SplitterContextValue>(SPLITTER_NAME);
69
+ const endMinStyle = (minSize: number, orientation: SplitterOrientation): CSSProperties =>
70
+ orientation === 'horizontal' ? { minInlineSize: `${minSize}rem` } : { minBlockSize: `${minSize}rem` };
71
+
72
+ // In `split` mode the anchored panel is fixed at `size` rem (clamped) and the other fills the rest; when
73
+ // `size` is undefined both panels grow equally (50/50). Otherwise one panel fills (flex-grow) and the
74
+ // other collapses to zero (content stays mounted, clipped).
75
+ const panelStyle = (
76
+ position: Position,
77
+ mode: SplitterMode,
78
+ anchor: Position,
79
+ size: number | undefined,
80
+ minSize: number,
81
+ orientation: SplitterOrientation,
82
+ ): CSSProperties => {
83
+ if (mode !== 'split') {
84
+ const fills = (mode === 'start' && position === 'start') || (mode === 'end' && position === 'end');
85
+ return { flexGrow: fills ? 1 : 0, flexShrink: 1, flexBasis: 0 };
86
+ }
87
+
88
+ // No explicit size: split evenly.
89
+ if (size === undefined) {
90
+ return { flexGrow: 1, flexShrink: 1, flexBasis: 0, ...endMinStyle(minSize, orientation) };
91
+ }
92
+
93
+ return position === anchor
94
+ ? { flexGrow: 0, flexShrink: 1, flexBasis: `${size}rem`, ...clampStyle(minSize, orientation) }
95
+ : { flexGrow: 1, flexShrink: 1, flexBasis: 0, ...endMinStyle(minSize, orientation) };
96
+ };
97
+
98
+ // Absolute position for the handle, centered on the split point (`size` rem from the anchored edge, or
99
+ // the middle for an even split). The offset is clamped to the same bounds as the anchored panel
100
+ // (`minSize` .. `100% - minSize`) so a sudden container shrink (e.g. opening devtools) that caps the
101
+ // panel keeps the handle pinned to the panel edge instead of floating into the other panel.
102
+ const handlePosition = (
103
+ orientation: SplitterOrientation,
104
+ anchor: Position,
105
+ size: number | undefined,
106
+ minSize: number,
107
+ ): CSSProperties => {
108
+ if (size === undefined) {
109
+ return orientation === 'horizontal'
110
+ ? { insetBlock: 0, insetInlineStart: '50%', transform: 'translateX(-50%)' }
111
+ : { insetInline: 0, insetBlockStart: '50%', transform: 'translateY(-50%)' };
112
+ }
113
+
114
+ const offset = `clamp(${minSize}rem, ${size}rem, calc(100% - ${minSize}rem))`;
115
+ if (orientation === 'horizontal') {
116
+ return anchor === 'end'
117
+ ? { insetBlock: 0, insetInlineEnd: offset, transform: 'translateX(50%)' }
118
+ : { insetBlock: 0, insetInlineStart: offset, transform: 'translateX(-50%)' };
119
+ }
120
+ return anchor === 'end'
121
+ ? { insetInline: 0, insetBlockEnd: offset, transform: 'translateY(50%)' }
122
+ : { insetInline: 0, insetBlockStart: offset, transform: 'translateY(-50%)' };
123
+ };
30
124
 
31
125
  //
32
126
  // Root
@@ -34,18 +128,88 @@ const [SplitterProvider, useSplitterContext] = createSplitterContext<SplitterCon
34
128
 
35
129
  const ROOT_NAME = 'Splitter.Root';
36
130
 
37
- type SplitterRootProps = Partial<SplitterContextValue>;
131
+ type SplitterRootElementProps = {
132
+ orientation?: SplitterOrientation;
133
+ mode?: SplitterMode;
134
+ /** Which panel `size` measures (defaults to `start`); the other panel fills the remainder. */
135
+ anchor?: Position;
136
+ /** The anchored panel's extent in rem (controlled). */
137
+ size?: number;
138
+ defaultSize?: number;
139
+ onSizeChange?: (size: number) => void;
140
+ transition?: number;
141
+ resizable?: boolean;
142
+ /** Lower bound (rem) applied to both panels. */
143
+ minSize?: number;
144
+ };
145
+
146
+ type SplitterRootProps = SlottableProps<SplitterRootElementProps>;
38
147
 
39
- const SplitterRoot = slottable<HTMLDivElement, SplitterRootProps>(
40
- ({ asChild, mode = 'top', ratio = 0.5, transition = 250, children, ...props }, forwardedRef) => {
148
+ const SplitterRoot = slottable<HTMLDivElement, SplitterRootElementProps>(
149
+ (
150
+ {
151
+ asChild,
152
+ children,
153
+ orientation = 'vertical',
154
+ mode = 'split',
155
+ anchor = 'start',
156
+ size: sizeProp,
157
+ defaultSize,
158
+ onSizeChange,
159
+ transition = 250,
160
+ resizable = false,
161
+ minSize = 0,
162
+ ...props
163
+ },
164
+ forwardedRef,
165
+ ) => {
41
166
  const { tx } = useThemeContext();
42
- const { __scopeSplitter, ...rest } = props as ScopedProps<typeof props>;
43
- const { className, ...restProps } = composableProps(rest);
167
+ const rootRef = useRef<HTMLDivElement>(null);
168
+ const composedRef = useComposedRefs(forwardedRef, rootRef);
169
+ const [size, setSize] = useControllableState({
170
+ prop: sizeProp,
171
+ defaultProp: defaultSize,
172
+ onChange: onSizeChange,
173
+ });
174
+ const [dragging, setDragging] = useState(false);
175
+
176
+ // Animate ONLY for a brief window right after a `mode` change (the collapse). The rest of the time the
177
+ // transition is off, so layout reflows from a container/window resize never animate (no jitter) — this
178
+ // avoids relying on observing/throttling resize events at all.
179
+ const [animating, setAnimating] = useState(false);
180
+ const previousMode = useRef(mode);
181
+ useEffect(() => {
182
+ if (previousMode.current === mode) {
183
+ return;
184
+ }
185
+ previousMode.current = mode;
186
+ if (transition <= 0) {
187
+ return;
188
+ }
189
+ setAnimating(true);
190
+ const timer = setTimeout(() => setAnimating(false), transition);
191
+ return () => clearTimeout(timer);
192
+ }, [mode, transition]);
193
+
194
+ const { className, ...rest } = composableProps(props);
44
195
  const Comp = asChild ? Slot : Primitive.div;
45
196
 
46
197
  return (
47
- <SplitterProvider scope={__scopeSplitter} mode={mode} ratio={ratio} transition={transition}>
48
- <Comp {...restProps} ref={forwardedRef} className={tx('splitter.root', {}, className)}>
198
+ <SplitterProvider
199
+ orientation={orientation}
200
+ mode={mode}
201
+ anchor={anchor}
202
+ size={size}
203
+ transition={transition}
204
+ resizable={resizable}
205
+ minSize={minSize}
206
+ animating={animating}
207
+ dragging={dragging}
208
+ rootRef={rootRef}
209
+ setSize={setSize}
210
+ setDragging={setDragging}
211
+ >
212
+ <Comp {...rest} ref={composedRef} className={tx('splitter.root', { orientation }, className)}>
49
213
  {children}
50
214
  </Comp>
51
215
  </SplitterProvider>
@@ -61,42 +225,28 @@ SplitterRoot.displayName = ROOT_NAME;
61
225
 
62
226
  const PANEL_NAME = 'Splitter.Panel';
63
227
 
64
- type SplitterPanelProps = ThemedClassName<{
65
- position: 'top' | 'bottom';
66
- }>;
228
+ type SplitterPanelProps = SlottableProps<{ position: Position }>;
67
229
 
68
- const SplitterPanel = slottable<HTMLDivElement, SplitterPanelProps>(
69
- ({ classNames, asChild, children, position, style, ...props }, forwardedRef) => {
230
+ const SplitterPanel = slottable<HTMLDivElement, { position: Position }>(
231
+ ({ asChild, children, position, ...props }, forwardedRef) => {
70
232
  const { tx } = useThemeContext();
71
- const { __scopeSplitter, ...rest } = props as ScopedProps<typeof props>;
233
+ const { orientation, mode, anchor, size, minSize, transition, dragging, animating } =
234
+ useSplitterContext(PANEL_NAME);
235
+ const { className, style, ...rest } = composableProps(props);
72
236
  const Comp = asChild ? Slot : Primitive.div;
73
- const { mode, ratio = 0.5, transition } = useSplitterContext(PANEL_NAME, __scopeSplitter);
74
- const { className, ...restProps } = composableProps(rest);
75
-
76
- // Calculate position and height based on mode and ratio.
77
- const isTopPanel = position === 'top';
78
- const topOffset = isTopPanel ? '0%' : mode === 'top' ? '100%' : mode === 'bottom' ? '0%' : `${ratio * 100}%`;
79
- const height = isTopPanel
80
- ? mode === 'top'
81
- ? '100%'
82
- : mode === 'bottom'
83
- ? '0%'
84
- : `${ratio * 100}%`
85
- : mode === 'bottom'
86
- ? '100%'
87
- : mode === 'top'
88
- ? '0%'
89
- : `${(1 - ratio) * 100}%`;
237
+
238
+ // Only animate during the brief post-mode-change window (collapse), never while dragging or on a plain
239
+ // container/window resize — so the panels track layout reflows instantly without jitter.
240
+ const animate = transition > 0 && animating && !dragging;
90
241
 
91
242
  return (
92
243
  <Comp
93
- {...restProps}
244
+ {...rest}
94
245
  ref={forwardedRef}
95
246
  className={tx('splitter.panel', {}, className)}
96
247
  style={{
97
- top: topOffset,
98
- height,
99
- transition: `top ${transition}ms, height ${transition}ms ease-out`,
248
+ ...panelStyle(position, mode, anchor, size, minSize, orientation),
249
+ transition: animate ? `flex-grow ${transition}ms ease-out, flex-basis ${transition}ms ease-out` : undefined,
100
250
  ...style,
101
251
  }}
102
252
  >
@@ -108,6 +258,128 @@ const SplitterPanel = slottable<HTMLDivElement, SplitterPanelProps>(
108
258
 
109
259
  SplitterPanel.displayName = PANEL_NAME;
110
260
 
261
+ //
262
+ // Handle
263
+ //
264
+
265
+ const HANDLE_NAME = 'Splitter.Handle';
266
+
267
+ type SplitterHandleProps = SlottableProps;
268
+
269
+ const SplitterHandle = slottable<HTMLDivElement>(({ asChild, children, ...props }, forwardedRef) => {
270
+ const { tx } = useThemeContext();
271
+ const { orientation, anchor, size, resizable, minSize, rootRef, setSize, setDragging } =
272
+ useSplitterContext(HANDLE_NAME);
273
+ const { className, ...rest } = composableProps(props);
274
+ const Comp = asChild ? Slot : Primitive.div;
275
+
276
+ // Container extent (rem) along the split axis; the upper bound is `extent - minSize`.
277
+ const extentRem = useCallback((): number => {
278
+ const rect = rootRef.current?.getBoundingClientRect();
279
+ if (!rect) {
280
+ return (size ?? minSize) + minSize;
281
+ }
282
+ return (orientation === 'horizontal' ? rect.width : rect.height) / getRem();
283
+ }, [orientation, rootRef, size, minSize]);
284
+
285
+ const handlePointerDown = useCallback(
286
+ (event: PointerEvent<HTMLDivElement>) => {
287
+ if (event.button !== 0) {
288
+ return;
289
+ }
290
+ event.preventDefault();
291
+ event.currentTarget.setPointerCapture(event.pointerId);
292
+ setDragging(true);
293
+ },
294
+ [setDragging],
295
+ );
296
+
297
+ // Map the pointer position within the Root to the anchored panel's extent (rem), clamped so neither
298
+ // panel drops below `minSize`. For an `end` anchor the extent is measured from the far edge.
299
+ const handlePointerMove = useCallback(
300
+ (event: PointerEvent<HTMLDivElement>) => {
301
+ if (!event.currentTarget.hasPointerCapture(event.pointerId)) {
302
+ return;
303
+ }
304
+ const root = rootRef.current;
305
+ if (!root) {
306
+ return;
307
+ }
308
+ const rect = root.getBoundingClientRect();
309
+ const rem = getRem();
310
+ const offset = (orientation === 'horizontal' ? event.clientX - rect.left : event.clientY - rect.top) / rem;
311
+ const extent = (orientation === 'horizontal' ? rect.width : rect.height) / rem;
312
+ const next = anchor === 'end' ? extent - offset : offset;
313
+ setSize(Math.min(Math.max(next, minSize), Math.max(minSize, extent - minSize)));
314
+ },
315
+ [orientation, anchor, minSize, rootRef, setSize],
316
+ );
317
+
318
+ const handlePointerUp = useCallback(
319
+ (event: PointerEvent<HTMLDivElement>) => {
320
+ if (event.currentTarget.hasPointerCapture(event.pointerId)) {
321
+ event.currentTarget.releasePointerCapture(event.pointerId);
322
+ }
323
+ setDragging(false);
324
+ },
325
+ [setDragging],
326
+ );
327
+
328
+ // Arrow keys nudge the start panel's size (rem); Home/End jump to the bounds.
329
+ // Keeps the resize affordance usable without a pointer.
330
+ const handleKeyDown = useCallback(
331
+ (event: KeyboardEvent<HTMLDivElement>) => {
332
+ const horizontal = orientation === 'horizontal';
333
+ const step = event.shiftKey ? 4 : 1;
334
+ // Start from the current size, or the midpoint when the split is even (unsized).
335
+ const base = size ?? extentRem() / 2;
336
+ let next: number | undefined;
337
+ if (event.key === (horizontal ? 'ArrowRight' : 'ArrowDown')) {
338
+ next = base + step;
339
+ } else if (event.key === (horizontal ? 'ArrowLeft' : 'ArrowUp')) {
340
+ next = base - step;
341
+ } else if (event.key === 'Home') {
342
+ next = minSize;
343
+ } else if (event.key === 'End') {
344
+ next = extentRem() - minSize;
345
+ }
346
+ if (next === undefined) {
347
+ return;
348
+ }
349
+ event.preventDefault();
350
+ setSize(Math.min(Math.max(next, minSize), Math.max(minSize, extentRem() - minSize)));
351
+ },
352
+ [orientation, size, minSize, extentRem, setSize],
353
+ );
354
+
355
+ if (!resizable) {
356
+ return null;
357
+ }
358
+
359
+ return (
360
+ <Comp
361
+ {...rest}
362
+ ref={forwardedRef}
363
+ role='separator'
364
+ tabIndex={0}
365
+ aria-orientation={orientation === 'horizontal' ? 'vertical' : 'horizontal'}
366
+ aria-valuemin={Math.round(minSize)}
367
+ aria-valuenow={size !== undefined ? Math.round(size) : undefined}
368
+ className={tx('splitter.handle', { orientation }, className)}
369
+ style={handlePosition(orientation, anchor, size, minSize)}
370
+ onPointerDown={handlePointerDown}
371
+ onPointerMove={handlePointerMove}
372
+ onPointerUp={handlePointerUp}
373
+ onPointerCancel={handlePointerUp}
374
+ onKeyDown={handleKeyDown}
375
+ >
376
+ {children}
377
+ </Comp>
378
+ );
379
+ });
380
+
381
+ SplitterHandle.displayName = HANDLE_NAME;
382
+
111
383
  //
112
384
  // Splitter
113
385
  //
@@ -115,8 +387,9 @@ SplitterPanel.displayName = PANEL_NAME;
115
387
  const Splitter = {
116
388
  Root: SplitterRoot,
117
389
  Panel: SplitterPanel,
390
+ Handle: SplitterHandle,
118
391
  };
119
392
 
120
- export { Splitter, createSplitterScope };
393
+ export { Splitter };
121
394
 
122
- export type { Mode as SplitterMode, SplitterRootProps, SplitterPanelProps };
395
+ export type { SplitterHandleProps, SplitterMode, SplitterOrientation, SplitterPanelProps, SplitterRootProps };
@@ -19,7 +19,7 @@ const meta = {
19
19
  render: () => (
20
20
  <div>
21
21
  {palettes.map((palette) => (
22
- <Tag key={palette} palette={palette}>
22
+ <Tag key={palette} hue={palette}>
23
23
  {palette}
24
24
  </Tag>
25
25
  ))}
@@ -12,7 +12,7 @@ import {
12
12
  } from '@dxos/ui-types';
13
13
 
14
14
  export type TagStyleProps = {
15
- palette?: ChromaticPalette | NeutralPalette | MessageValence;
15
+ hue?: ChromaticPalette | NeutralPalette | MessageValence;
16
16
  };
17
17
 
18
18
  const root: ComponentFunction<TagStyleProps> = (_, ...etc) => mx('dx-tag', ...etc);
@@ -12,16 +12,14 @@ import { useThemeContext } from '../../hooks';
12
12
  import { type ThemedClassName } from '../../util';
13
13
 
14
14
  export type TagProps = ThemedClassName<ComponentPropsWithRef<typeof Primitive.span>> & {
15
- palette?: NeutralPalette | ChromaticPalette | MessageValence;
16
15
  asChild?: boolean;
16
+ hue?: NeutralPalette | ChromaticPalette | MessageValence;
17
17
  };
18
18
 
19
19
  export const Tag = forwardRef<HTMLSpanElement, TagProps>(
20
- ({ asChild, palette = 'neutral', classNames, ...props }, forwardedRef) => {
20
+ ({ asChild, hue = 'neutral', classNames, ...props }, forwardedRef) => {
21
21
  const { tx } = useThemeContext();
22
22
  const Comp = asChild ? Slot : Primitive.span;
23
- return (
24
- <Comp {...props} className={tx('tag.root', { palette }, classNames)} data-hue={palette} ref={forwardedRef} />
25
- );
23
+ return <Comp {...props} className={tx('tag.root', { hue }, classNames)} data-hue={hue} ref={forwardedRef} />;
26
24
  },
27
25
  );
@@ -9,8 +9,9 @@ export type ToastStyleProps = {};
9
9
 
10
10
  const viewport: ComponentFunction<ToastStyleProps> = (_props, ...etc) =>
11
11
  mx(
12
- // TODO(burdon): block-end should take into account status bar.
13
- 'z-40 fixed bottom-[calc(env(safe-area-inset-bottom)+1rem)] inset-start-[calc(env(safe-area-inset-left)+1rem)] inset-end-[calc(env(safe-area-inset-right)+1rem)] w-auto md:end-[calc(env(safe-area-inset-right)+1rem)] md:left-auto md:w-full md:max-w-sm',
12
+ 'z-40 fixed bottom-[calc(env(safe-area-inset-bottom)+1rem)]',
13
+ 'inset-start-[calc(env(safe-area-inset-left)+1rem)] inset-end-[calc(env(safe-area-inset-right)+1rem)]',
14
+ 'w-auto md:end-[calc(env(safe-area-inset-right)+4rem)] md:left-auto md:w-full md:max-w-sm',
14
15
  'rounded-md flex flex-col gap-2',
15
16
  ...etc,
16
17
  );
@@ -162,12 +162,12 @@ export const Toast = {
162
162
  };
163
163
 
164
164
  export type {
165
+ ToastActionProps,
166
+ ToastActionsProps,
167
+ ToastCloseProps,
168
+ ToastDescriptionProps,
165
169
  ToastProviderProps,
166
- ToastViewportProps,
167
170
  ToastRootProps,
168
171
  ToastTitleProps,
169
- ToastDescriptionProps,
170
- ToastActionsProps,
171
- ToastActionProps,
172
- ToastCloseProps,
172
+ ToastViewportProps,
173
173
  };
@@ -366,19 +366,19 @@ export const Toolbar = {
366
366
  };
367
367
 
368
368
  export type {
369
- ToolbarRootProps,
370
- ToolbarTextProps,
369
+ ToolbarActionIconButtonAction,
370
+ ToolbarActionIconButtonProps,
371
371
  ToolbarButtonProps,
372
+ ToolbarDragHandleProps,
372
373
  ToolbarIconButtonProps,
373
374
  ToolbarLinkProps,
374
- ToolbarToggleProps,
375
- ToolbarToggleGroupProps,
376
- ToolbarToggleGroupItemProps,
377
- ToolbarToggleGroupIconItemProps,
378
- ToolbarSeparatorProps,
379
- ToolbarDragHandleProps,
380
- ToolbarActionIconButtonAction,
381
- ToolbarActionIconButtonProps,
382
375
  ToolbarMenuItem,
383
376
  ToolbarMenuProps,
377
+ ToolbarRootProps,
378
+ ToolbarSeparatorProps,
379
+ ToolbarTextProps,
380
+ ToolbarToggleGroupIconItemProps,
381
+ ToolbarToggleGroupItemProps,
382
+ ToolbarToggleGroupProps,
383
+ ToolbarToggleProps,
384
384
  };
@@ -11,12 +11,12 @@ import { withTheme } from '../../testing';
11
11
  import { Button } from '../Button';
12
12
  import { Tooltip } from './Tooltip';
13
13
 
14
- type DefaultStoryProps = {
14
+ type StoryArgs = {
15
15
  tooltips: { label: string; content: string }[];
16
16
  defaultOpen?: boolean;
17
17
  };
18
18
 
19
- const DefaultStory = ({ tooltips, defaultOpen }: DefaultStoryProps) => {
19
+ const DefaultStory = ({ tooltips, defaultOpen }: StoryArgs) => {
20
20
  return (
21
21
  <Tooltip.Provider defaultOpen={defaultOpen}>
22
22
  <div className='w-32'>
@@ -493,12 +493,12 @@ type TooltipContentImplProps = Omit<PopperContentProps, 'onPlaced'> & {
493
493
  * Event handler called when the escape key is down.
494
494
  * Can be prevented.
495
495
  */
496
- onEscapeKeyDown?: DismissableLayerProps['onEscapeKeyDown'];
496
+ 'onEscapeKeyDown'?: DismissableLayerProps['onEscapeKeyDown'];
497
497
  /**
498
498
  * Event handler called when the a `pointerdown` event happens outside of the `Tooltip`.
499
499
  * Can be prevented.
500
500
  */
501
- onPointerDownOutside?: DismissableLayerProps['onPointerDownOutside'];
501
+ 'onPointerDownOutside'?: DismissableLayerProps['onPointerDownOutside'];
502
502
  };
503
503
 
504
504
  const TooltipContentImpl = forwardRef<TooltipContentImplElement, TooltipContentImplProps>(
@@ -765,4 +765,4 @@ export const Tooltip = {
765
765
 
766
766
  export { createTooltipScope, useTooltipContext };
767
767
 
768
- export type { TooltipProviderProps, TooltipTriggerProps, TooltipScopedProps, TooltipSide };
768
+ export type { TooltipProviderProps, TooltipScopedProps, TooltipSide, TooltipTriggerProps };
@@ -18,7 +18,6 @@ export * from './Icon';
18
18
  export * from './Image';
19
19
  export * from './Input';
20
20
  export * from './Link';
21
- export * from './List';
22
21
  export * from './Main';
23
22
  export * from './MediaPlayer';
24
23
  export * from './Menu';
@@ -4,8 +4,8 @@
4
4
 
5
5
  import {
6
6
  useArrowNavigationGroup,
7
- useFocusFinders,
8
7
  useFocusableGroup,
8
+ useFocusFinders,
9
9
  useMergedTabsterAttributes_unstable,
10
10
  } from '@fluentui/react-tabster';
11
11
  import { type Decorator, type Meta, type StoryObj } from '@storybook/react-vite';
package/src/index.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- export { type Resource, type TFunction } from 'i18next';
5
+ export { type Resource, type TFunction } from '@dxos/i18n';
6
6
  export { Trans } from 'react-i18next';
7
7
 
8
8
  export * from '@dxos/react-hooks';
@@ -3,31 +3,14 @@
3
3
  //
4
4
 
5
5
  import { type Locale, enUS as dtLocaleEnUs } from 'date-fns/locale';
6
- import i18Next, { type Resource } from 'i18next';
7
6
  import React, { type ReactNode, Suspense, createContext, useContext, useEffect, useState } from 'react';
8
- import { initReactI18next, useTranslation as useI18NextTranslation } from 'react-i18next';
7
+ import { I18nextProvider, useTranslation as useI18NextTranslation } from 'react-i18next';
8
+
9
+ import { type Resource, addResources, i18n } from '@dxos/i18n';
9
10
 
10
- const initialLng = 'en-US';
11
11
  const initialNs = 'dxos-common';
12
12
  const initialDtLocale = dtLocaleEnUs;
13
13
 
14
- export const resources = {
15
- [initialLng]: {
16
- [initialNs]: {
17
- 'loading translations': 'Loading translations…',
18
- },
19
- },
20
- } as const satisfies Resource;
21
-
22
- void i18Next.use(initReactI18next).init({
23
- resources,
24
- lng: initialLng,
25
- defaultNS: initialNs,
26
- interpolation: {
27
- escapeValue: false,
28
- },
29
- });
30
-
31
14
  export interface TranslationsProviderProps {
32
15
  children?: ReactNode;
33
16
  // TODO(wittjosiah): Rename to `placeholder` to match ClientProvider?
@@ -59,14 +42,9 @@ export const TranslationsProvider = ({
59
42
  const [loaded, setLoaded] = useState(false);
60
43
  useEffect(() => {
61
44
  setLoaded(false);
45
+ // Convenience for standalone/storybook callers; the i18next instance lifecycle is owned by `@dxos/i18n`.
62
46
  if (resourceExtensions && resourceExtensions.length) {
63
- resourceExtensions.forEach((resource) => {
64
- Object.keys(resource).forEach((language) => {
65
- Object.keys(resource[language]).forEach((ns) => {
66
- i18Next.addResourceBundle(language, ns, resource[language][ns]);
67
- });
68
- });
69
- });
47
+ addResources(resourceExtensions);
70
48
  }
71
49
 
72
50
  setLoaded(true);
@@ -75,8 +53,10 @@ export const TranslationsProvider = ({
75
53
  // TODO(thure): This is not ideal, but i18next was causing `Suspense` to not render the fallback even when the child was asking for namespaces yet to be added.
76
54
  // TODO(burdon): Fallbacks should only appear after a short delay, and if the displayed then be visible for 500mx to avoid startup flickering.
77
55
  return (
78
- <TranslationsContext.Provider value={{ appNs: appNs ?? initialNs, dtLocale: dtLocale ?? initialDtLocale }}>
79
- <Suspense fallback={fallback}>{loaded ? children : fallback}</Suspense>
80
- </TranslationsContext.Provider>
56
+ <I18nextProvider i18n={i18n}>
57
+ <TranslationsContext.Provider value={{ appNs: appNs ?? initialNs, dtLocale: dtLocale ?? initialDtLocale }}>
58
+ <Suspense fallback={fallback}>{loaded ? children : fallback}</Suspense>
59
+ </TranslationsContext.Provider>
60
+ </I18nextProvider>
81
61
  );
82
62
  };