@womp/kookie-ui 0.9.1 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. package/components.css +145 -26
  2. package/dist/cjs/components/app-bar.d.ts +49 -0
  3. package/dist/cjs/components/app-bar.d.ts.map +1 -0
  4. package/dist/cjs/components/app-bar.js +2 -0
  5. package/dist/cjs/components/app-bar.js.map +7 -0
  6. package/dist/cjs/components/chatbar.js +2 -2
  7. package/dist/cjs/components/chatbar.js.map +4 -4
  8. package/dist/cjs/components/context-menu.js +1 -1
  9. package/dist/cjs/components/context-menu.js.map +4 -4
  10. package/dist/cjs/components/dropdown-menu.js +1 -1
  11. package/dist/cjs/components/dropdown-menu.js.map +4 -4
  12. package/dist/cjs/components/index.d.ts +2 -1
  13. package/dist/cjs/components/index.d.ts.map +1 -1
  14. package/dist/cjs/components/index.js +5 -5
  15. package/dist/cjs/components/index.js.map +4 -4
  16. package/dist/cjs/components/message-scroller.d.ts +6 -0
  17. package/dist/cjs/components/message-scroller.d.ts.map +1 -1
  18. package/dist/cjs/components/message-scroller.js +2 -2
  19. package/dist/cjs/components/message-scroller.js.map +4 -4
  20. package/dist/cjs/components/scroll-area.d.ts.map +1 -1
  21. package/dist/cjs/components/scroll-area.js +1 -1
  22. package/dist/cjs/components/scroll-area.js.map +4 -4
  23. package/dist/cjs/components/scroll-area.props.d.ts +10 -0
  24. package/dist/cjs/components/scroll-area.props.d.ts.map +1 -1
  25. package/dist/cjs/components/scroll-area.props.js +1 -1
  26. package/dist/cjs/components/scroll-area.props.js.map +2 -2
  27. package/dist/cjs/components/sidebar.js +1 -1
  28. package/dist/cjs/components/sidebar.js.map +4 -4
  29. package/dist/cjs/components/table.js +1 -1
  30. package/dist/cjs/components/table.js.map +4 -4
  31. package/dist/cjs/components/theme-panel.js +3 -3
  32. package/dist/cjs/components/theme-panel.js.map +4 -4
  33. package/dist/cjs/components/toolbar.d.ts +88 -36
  34. package/dist/cjs/components/toolbar.d.ts.map +1 -1
  35. package/dist/cjs/components/toolbar.js +4 -1
  36. package/dist/cjs/components/toolbar.js.map +4 -4
  37. package/dist/cjs/components/toolbar.props.d.ts +28 -0
  38. package/dist/cjs/components/toolbar.props.d.ts.map +1 -0
  39. package/dist/cjs/components/toolbar.props.js +2 -0
  40. package/dist/cjs/components/toolbar.props.js.map +7 -0
  41. package/dist/cjs/hooks/use-scroll-edges.d.ts +12 -0
  42. package/dist/cjs/hooks/use-scroll-edges.d.ts.map +1 -0
  43. package/dist/cjs/hooks/use-scroll-edges.js +2 -0
  44. package/dist/cjs/hooks/use-scroll-edges.js.map +7 -0
  45. package/dist/cjs/index.js +5 -5
  46. package/dist/cjs/index.js.map +4 -4
  47. package/dist/cjs/props/index.js +1 -1
  48. package/dist/cjs/props/index.js.map +2 -2
  49. package/dist/esm/{chunk-ABSKRHVM.js → chunk-36A5BYCN.js} +2 -2
  50. package/dist/esm/{chunk-73QGEY2I.js → chunk-3R4YLW4A.js} +2 -2
  51. package/dist/esm/{chunk-ZZLZGHZQ.js → chunk-4LDVSTXI.js} +2 -2
  52. package/dist/esm/{chunk-YQRSAP46.js → chunk-53SZ4Y23.js} +2 -2
  53. package/dist/esm/chunk-54JFIP5S.js +2 -0
  54. package/dist/esm/chunk-54JFIP5S.js.map +7 -0
  55. package/dist/esm/chunk-5BWLY27A.js +2 -0
  56. package/dist/esm/chunk-5BWLY27A.js.map +7 -0
  57. package/dist/esm/{chunk-X3JM2565.js → chunk-6G3MPL4A.js} +2 -2
  58. package/dist/esm/{chunk-DSJY5RI3.js → chunk-7JJT42J2.js} +2 -2
  59. package/dist/esm/chunk-C7EVYSWK.js +1 -0
  60. package/dist/esm/chunk-COWZAUZR.js +2 -0
  61. package/dist/esm/chunk-COWZAUZR.js.map +7 -0
  62. package/dist/esm/{chunk-ERY5TH2M.js → chunk-CYU6BLZ5.js} +2 -2
  63. package/dist/esm/{chunk-MWIIGYAJ.js → chunk-D6PHTHF2.js} +2 -2
  64. package/dist/esm/{chunk-YTD3AJAU.js → chunk-ENLFKWYV.js} +2 -2
  65. package/dist/esm/{chunk-4XGGX2V2.js → chunk-ISOLUTZF.js} +2 -2
  66. package/dist/esm/{chunk-LXWPEDPZ.js → chunk-IYCPYN4Z.js} +2 -2
  67. package/dist/esm/{chunk-RJP5QUXO.js → chunk-L5PID6BU.js} +2 -2
  68. package/dist/esm/{chunk-BW5CO4PZ.js → chunk-METRW4HG.js} +2 -2
  69. package/dist/esm/{chunk-PYHEBG5I.js → chunk-MJOXEUBT.js} +2 -2
  70. package/dist/esm/{chunk-PHC3QBV3.js → chunk-MZXHKM4V.js} +2 -2
  71. package/dist/esm/chunk-OIUCV5HK.js +2 -0
  72. package/dist/esm/chunk-OIUCV5HK.js.map +7 -0
  73. package/dist/esm/{chunk-NIM7DIVH.js → chunk-PQKJNRMR.js} +2 -2
  74. package/dist/esm/{chunk-65GAANJD.js → chunk-UYCPDGWA.js} +2 -2
  75. package/dist/esm/chunk-XLMTTSRO.js +2 -0
  76. package/dist/esm/chunk-XLMTTSRO.js.map +7 -0
  77. package/dist/esm/chunk-XPRPYCII.js +2 -0
  78. package/dist/esm/{chunk-5KICSAT7.js.map → chunk-XPRPYCII.js.map} +2 -2
  79. package/dist/esm/{chunk-QWVU2TW4.js → chunk-ZK7A2MZZ.js} +2 -2
  80. package/dist/esm/chunk-ZUOK3KFY.js +2 -0
  81. package/dist/esm/chunk-ZUOK3KFY.js.map +7 -0
  82. package/dist/esm/components/_internal/base-button.js +1 -1
  83. package/dist/esm/components/alert-dialog.js +1 -1
  84. package/dist/esm/components/app-bar.d.ts +49 -0
  85. package/dist/esm/components/app-bar.d.ts.map +1 -0
  86. package/dist/esm/components/app-bar.js +2 -0
  87. package/dist/esm/components/app-bar.js.map +7 -0
  88. package/dist/esm/components/button.js +1 -1
  89. package/dist/esm/components/chatbar.js +1 -1
  90. package/dist/esm/components/checkbox-cards.js +1 -1
  91. package/dist/esm/components/context-menu.js +1 -1
  92. package/dist/esm/components/dialog.js +1 -1
  93. package/dist/esm/components/dropdown-menu.js +1 -1
  94. package/dist/esm/components/icon-button.js +1 -1
  95. package/dist/esm/components/index.d.ts +2 -1
  96. package/dist/esm/components/index.d.ts.map +1 -1
  97. package/dist/esm/components/index.js +1 -1
  98. package/dist/esm/components/message-scroller.d.ts +6 -0
  99. package/dist/esm/components/message-scroller.d.ts.map +1 -1
  100. package/dist/esm/components/message-scroller.js +1 -1
  101. package/dist/esm/components/notice.js +1 -1
  102. package/dist/esm/components/scroll-area.d.ts.map +1 -1
  103. package/dist/esm/components/scroll-area.js +1 -1
  104. package/dist/esm/components/scroll-area.props.d.ts +10 -0
  105. package/dist/esm/components/scroll-area.props.d.ts.map +1 -1
  106. package/dist/esm/components/scroll-area.props.js +1 -1
  107. package/dist/esm/components/sheet.js +1 -1
  108. package/dist/esm/components/shell.js +1 -1
  109. package/dist/esm/components/sidebar.js +1 -1
  110. package/dist/esm/components/table.js +1 -1
  111. package/dist/esm/components/theme-panel.js +1 -1
  112. package/dist/esm/components/toggle-button.js +1 -1
  113. package/dist/esm/components/toggle-icon-button.js +1 -1
  114. package/dist/esm/components/toolbar.d.ts +88 -36
  115. package/dist/esm/components/toolbar.d.ts.map +1 -1
  116. package/dist/esm/components/toolbar.js +1 -1
  117. package/dist/esm/components/toolbar.props.d.ts +28 -0
  118. package/dist/esm/components/toolbar.props.d.ts.map +1 -0
  119. package/dist/esm/components/toolbar.props.js +2 -0
  120. package/dist/esm/components/toolbar.props.js.map +7 -0
  121. package/dist/esm/components/user-card.js +1 -1
  122. package/dist/esm/hooks/use-scroll-edges.d.ts +12 -0
  123. package/dist/esm/hooks/use-scroll-edges.d.ts.map +1 -0
  124. package/dist/esm/hooks/use-scroll-edges.js +2 -0
  125. package/dist/esm/hooks/use-scroll-edges.js.map +7 -0
  126. package/dist/esm/index.js +1 -1
  127. package/dist/esm/props/index.js +1 -1
  128. package/package.json +1 -1
  129. package/src/components/app-bar.css +204 -0
  130. package/src/components/app-bar.tsx +256 -0
  131. package/src/components/index.css +2 -0
  132. package/src/components/index.tsx +13 -1
  133. package/src/components/message-scroller.tsx +24 -5
  134. package/src/components/scroll-area.css +44 -0
  135. package/src/components/scroll-area.props.tsx +8 -0
  136. package/src/components/scroll-area.tsx +7 -1
  137. package/src/components/toolbar.css +87 -160
  138. package/src/components/toolbar.props.tsx +27 -0
  139. package/src/components/toolbar.tsx +259 -193
  140. package/src/hooks/use-scroll-edges.ts +46 -0
  141. package/src/styles/tokens/color.css +19 -0
  142. package/styles.css +159 -26
  143. package/tokens/base.css +14 -0
  144. package/tokens.css +14 -0
  145. package/dist/esm/chunk-47LMOIW3.js +0 -2
  146. package/dist/esm/chunk-47LMOIW3.js.map +0 -7
  147. package/dist/esm/chunk-5KICSAT7.js +0 -2
  148. package/dist/esm/chunk-CTA5ODAO.js +0 -2
  149. package/dist/esm/chunk-CTA5ODAO.js.map +0 -7
  150. package/dist/esm/chunk-JKS2YXMS.js +0 -1
  151. package/dist/esm/chunk-W4WWN3RC.js +0 -2
  152. package/dist/esm/chunk-W4WWN3RC.js.map +0 -7
  153. /package/dist/esm/{chunk-ABSKRHVM.js.map → chunk-36A5BYCN.js.map} +0 -0
  154. /package/dist/esm/{chunk-73QGEY2I.js.map → chunk-3R4YLW4A.js.map} +0 -0
  155. /package/dist/esm/{chunk-ZZLZGHZQ.js.map → chunk-4LDVSTXI.js.map} +0 -0
  156. /package/dist/esm/{chunk-YQRSAP46.js.map → chunk-53SZ4Y23.js.map} +0 -0
  157. /package/dist/esm/{chunk-X3JM2565.js.map → chunk-6G3MPL4A.js.map} +0 -0
  158. /package/dist/esm/{chunk-DSJY5RI3.js.map → chunk-7JJT42J2.js.map} +0 -0
  159. /package/dist/esm/{chunk-JKS2YXMS.js.map → chunk-C7EVYSWK.js.map} +0 -0
  160. /package/dist/esm/{chunk-ERY5TH2M.js.map → chunk-CYU6BLZ5.js.map} +0 -0
  161. /package/dist/esm/{chunk-MWIIGYAJ.js.map → chunk-D6PHTHF2.js.map} +0 -0
  162. /package/dist/esm/{chunk-YTD3AJAU.js.map → chunk-ENLFKWYV.js.map} +0 -0
  163. /package/dist/esm/{chunk-4XGGX2V2.js.map → chunk-ISOLUTZF.js.map} +0 -0
  164. /package/dist/esm/{chunk-LXWPEDPZ.js.map → chunk-IYCPYN4Z.js.map} +0 -0
  165. /package/dist/esm/{chunk-RJP5QUXO.js.map → chunk-L5PID6BU.js.map} +0 -0
  166. /package/dist/esm/{chunk-BW5CO4PZ.js.map → chunk-METRW4HG.js.map} +0 -0
  167. /package/dist/esm/{chunk-PYHEBG5I.js.map → chunk-MJOXEUBT.js.map} +0 -0
  168. /package/dist/esm/{chunk-PHC3QBV3.js.map → chunk-MZXHKM4V.js.map} +0 -0
  169. /package/dist/esm/{chunk-NIM7DIVH.js.map → chunk-PQKJNRMR.js.map} +0 -0
  170. /package/dist/esm/{chunk-65GAANJD.js.map → chunk-UYCPDGWA.js.map} +0 -0
  171. /package/dist/esm/{chunk-QWVU2TW4.js.map → chunk-ZK7A2MZZ.js.map} +0 -0
@@ -1,211 +1,272 @@
1
+ 'use client';
2
+
1
3
  import * as React from 'react';
2
4
  import classNames from 'classnames';
3
-
4
- import { Card } from './card.js';
5
- import { Flex } from './flex.js';
6
- import { Heading } from './heading.js';
7
-
8
- import type { CardProps } from './card.js';
9
- import type { FlexProps } from './flex.js';
10
-
11
- // ============================================================================
12
- // Toolbar Root
13
- // ============================================================================
14
-
15
- type ToolbarRootElement = React.ElementRef<'div'>;
16
- type Anchor = 'top' | 'bottom' | 'left' | 'right';
17
- type SpaceScale = '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9';
18
-
19
- interface ToolbarRootProps extends Omit<CardProps, 'asChild'> {
20
- /**
21
- * Which edge to anchor to. Sets position: sticky and the appropriate edge.
22
- * @default "top"
23
- */
24
- anchor?: Anchor;
25
- /**
26
- * Floating mode adds margin from edges for a detached look with absolute positioning.
27
- * When false (default), uses inset mode with negative margins to go edge-to-edge inside Cards.
28
- * @default false
29
- */
30
- floating?: boolean;
31
- /**
32
- * Margin offset from edges when floating. Uses space scale values.
33
- * @default "2"
34
- */
35
- offset?: SpaceScale;
5
+ import { Toolbar as ToolbarPrimitive } from 'radix-ui';
6
+
7
+ import { toolbarRootPropDefs } from './toolbar.props.js';
8
+ import { Button } from './button.js';
9
+ import { IconButton } from './icon-button.js';
10
+ import { Text } from './text.js';
11
+ import { extractProps } from '../helpers/extract-props.js';
12
+ import { marginPropDefs } from '../props/margin.props.js';
13
+
14
+ import type { ButtonProps } from './button.js';
15
+ import type { TextProps } from './text.js';
16
+ import type { MarginProps } from '../props/margin.props.js';
17
+ import type { ComponentPropsWithout, RemovedProps } from '../helpers/component-props.js';
18
+ import type { GetPropDefTypes } from '../props/prop-def.js';
19
+
20
+ /*
21
+ * Toolbar takes Kookie UI v2's contract (v2: components/toolbar), built on Radix instead of
22
+ * Base UI. A toolbar is a row of controls with ONE tab stop: `role="toolbar"`, and the arrow
23
+ * keys move focus between its controls. That keyboard is the reason it is a component.
24
+ *
25
+ * Its `size` reaches every control inside it that does not state its own. The row itself paints
26
+ * nothing; a `ToolbarGroup` draws a quiet well around controls that belong together, and a
27
+ * `ToolbarSeparator` draws a gray hairline between clusters. Use a `Flex` to cluster controls
28
+ * without a well: the row spaces the clusters with `space-between`.
29
+ *
30
+ * The band anchored to a pane's edge (Left / Center / Right / Title) that this name used to
31
+ * mean is `AppBar` since 0.11.0.
32
+ */
33
+
34
+ type ToolbarSize = NonNullable<GetPropDefTypes<typeof toolbarRootPropDefs>['size']>;
35
+ type ToolbarOrientation = NonNullable<GetPropDefTypes<typeof toolbarRootPropDefs>['orientation']>;
36
+
37
+ interface ToolbarContextValue {
38
+ size: ToolbarSize;
39
+ orientation: ToolbarOrientation;
36
40
  }
37
41
 
38
- const ToolbarRoot = React.forwardRef<ToolbarRootElement, ToolbarRootProps>(
39
- (props, forwardedRef) => {
40
- const {
41
- anchor = 'top',
42
- floating = false,
43
- offset = '2',
44
- size = '1',
45
- variant = 'ghost',
46
- material,
47
- className,
48
- style,
49
- children,
50
- ...restProps
51
- } = props;
52
-
53
- const isHorizontal = anchor === 'top' || anchor === 'bottom';
54
- const internalRef = React.useRef<HTMLDivElement>(null);
55
-
56
- // Merge refs
57
- const mergedRef = React.useCallback(
58
- (node: HTMLDivElement | null) => {
59
- (internalRef as React.MutableRefObject<HTMLDivElement | null>).current = node;
60
- if (typeof forwardedRef === 'function') {
61
- forwardedRef(node);
62
- } else if (forwardedRef) {
63
- forwardedRef.current = node;
64
- }
65
- },
66
- [forwardedRef],
67
- );
68
-
69
- // Expose height/width as CSS variable on parent element
70
- React.useEffect(() => {
71
- const element = internalRef.current;
72
- if (!element) return;
42
+ const ToolbarContext = React.createContext<ToolbarContextValue | null>(null);
73
43
 
74
- const parent = element.parentElement;
75
- if (!parent) return;
44
+ /** Set by a `ToolbarGroup`: its children rest quiet, and `disabled` turns them all off. */
45
+ const ToolbarGroupContext = React.createContext<{ disabled: boolean } | null>(null);
76
46
 
77
- const update = () => {
78
- let dimension = isHorizontal ? element.offsetHeight : element.offsetWidth;
79
-
80
- // For floating mode, include the margin in the total space
81
- if (floating) {
82
- const computedStyle = getComputedStyle(element);
83
- const margin = parseFloat(computedStyle.marginTop) || 0;
84
- dimension += margin * 2;
85
- }
86
-
87
- if (isHorizontal) {
88
- parent.style.setProperty('--rt-toolbar-height', `${dimension}px`);
89
- } else {
90
- parent.style.setProperty('--rt-toolbar-width', `${dimension}px`);
91
- }
92
- };
47
+ function useToolbarContext(consumer: string): ToolbarContextValue {
48
+ const context = React.useContext(ToolbarContext);
49
+ if (!context) {
50
+ throw new Error(`${consumer} must be rendered inside a Toolbar.`);
51
+ }
52
+ return context;
53
+ }
93
54
 
94
- update();
55
+ // ============================================================================
56
+ // Toolbar (root)
57
+ // ============================================================================
95
58
 
96
- const resizeObserver = new ResizeObserver(update);
97
- resizeObserver.observe(element);
59
+ type ToolbarOwnProps = GetPropDefTypes<typeof toolbarRootPropDefs>;
60
+
61
+ interface ToolbarProps
62
+ extends ComponentPropsWithout<typeof ToolbarPrimitive.Root, RemovedProps | 'orientation'>,
63
+ ToolbarOwnProps,
64
+ MarginProps {}
65
+
66
+ /**
67
+ * A row of controls with one keyboard tab stop. The arrow keys move focus between the controls.
68
+ *
69
+ * Give it an `aria-label` (or `aria-labelledby`) that says what the controls act on.
70
+ *
71
+ * @example
72
+ * ```tsx
73
+ * <Toolbar aria-label="Formatting">
74
+ * <ToolbarGroup>
75
+ * <ToolbarButton iconOnly aria-label="Bold" aria-pressed={bold} onClick={toggleBold}>
76
+ * <HugeiconsIcon icon={TextBoldIcon} strokeWidth={1.75} />
77
+ * </ToolbarButton>
78
+ * </ToolbarGroup>
79
+ * <ToolbarSeparator />
80
+ * <ToolbarButton>Share</ToolbarButton>
81
+ * </Toolbar>
82
+ * ```
83
+ */
84
+ const ToolbarRoot = React.forwardRef<HTMLDivElement, ToolbarProps>((props, forwardedRef) => {
85
+ const size: ToolbarSize = props.size ?? toolbarRootPropDefs.size.default;
86
+ const orientation: ToolbarOrientation = props.orientation ?? toolbarRootPropDefs.orientation.default;
87
+ const { className, orientation: _orientation, children, ...rootProps } = extractProps(
88
+ props,
89
+ toolbarRootPropDefs,
90
+ marginPropDefs,
91
+ );
92
+
93
+ const context = React.useMemo(() => ({ size, orientation }), [size, orientation]);
94
+
95
+ return (
96
+ <ToolbarPrimitive.Root
97
+ {...rootProps}
98
+ ref={forwardedRef}
99
+ orientation={orientation}
100
+ className={classNames('rt-Toolbar', className)}
101
+ >
102
+ <ToolbarContext.Provider value={context}>{children}</ToolbarContext.Provider>
103
+ </ToolbarPrimitive.Root>
104
+ );
105
+ });
106
+ ToolbarRoot.displayName = 'Toolbar';
98
107
 
99
- return () => resizeObserver.disconnect();
100
- }, [isHorizontal, floating]);
108
+ // ============================================================================
109
+ // ToolbarGroup
110
+ // ============================================================================
101
111
 
102
- const computedStyle = floating
103
- ? { margin: `var(--space-${offset})`, ...style }
104
- : style;
112
+ interface ToolbarGroupProps extends ComponentPropsWithout<'div', RemovedProps> {
113
+ /** Turns every control in the group off at once. */
114
+ disabled?: boolean;
115
+ }
105
116
 
117
+ /**
118
+ * Controls that belong together, drawn as one quiet well that stands level with a button
119
+ * beside it. Buttons inside it rest as `ghost` unless they state a `variant`.
120
+ * To cluster controls without a well, use a `Flex`.
121
+ */
122
+ const ToolbarGroup = React.forwardRef<HTMLDivElement, ToolbarGroupProps>(
123
+ ({ className, disabled = false, children, ...props }, forwardedRef) => {
124
+ const { orientation } = useToolbarContext('ToolbarGroup');
125
+ const group = React.useMemo(() => ({ disabled }), [disabled]);
106
126
  return (
107
- <Card
108
- ref={mergedRef}
109
- size={size}
110
- variant={variant}
111
- material={material}
112
- data-anchor={anchor}
113
- data-floating={floating || undefined}
114
- className={classNames('rt-Toolbar', className)}
115
- style={computedStyle}
116
- {...restProps}
127
+ <div
128
+ role="group"
129
+ data-orientation={orientation}
130
+ data-disabled={disabled || undefined}
131
+ {...props}
132
+ ref={forwardedRef}
133
+ className={classNames('rt-ToolbarGroup', className)}
117
134
  >
118
- <div className="rt-ToolbarInner">
119
- {children}
120
- </div>
121
- </Card>
135
+ <ToolbarGroupContext.Provider value={group}>{children}</ToolbarGroupContext.Provider>
136
+ </div>
122
137
  );
123
138
  },
124
139
  );
125
- ToolbarRoot.displayName = 'Toolbar.Root';
140
+ ToolbarGroup.displayName = 'Toolbar.Group';
126
141
 
127
142
  // ============================================================================
128
- // Toolbar Left (for horizontal) / Start (alias)
143
+ // ToolbarButton
129
144
  // ============================================================================
130
145
 
131
- type ToolbarSectionElement = React.ElementRef<'div'>;
132
- type ToolbarSectionProps = FlexProps;
146
+ type ToolbarButtonBaseProps = Omit<ButtonProps, 'as' | 'asChild'>;
133
147
 
134
- const ToolbarLeft = React.forwardRef<ToolbarSectionElement, ToolbarSectionProps>(
135
- ({ gap = '2', align = 'center', className, children, ...props }, forwardedRef) => (
136
- <Flex
137
- ref={forwardedRef}
138
- align={align}
139
- gap={gap}
140
- className={classNames('rt-ToolbarSection', 'rt-ToolbarLeft', className)}
141
- {...props}
142
- >
143
- {children}
144
- </Flex>
145
- ),
146
- );
147
- ToolbarLeft.displayName = 'Toolbar.Left';
148
+ /** A labelled button: takes every `Button` prop. */
149
+ type ToolbarTextButtonProps = ToolbarButtonBaseProps & {
150
+ iconOnly?: false;
151
+ };
148
152
 
149
- // ============================================================================
150
- // Toolbar Center
151
- // ============================================================================
153
+ /** An icon-only button: renders an `IconButton`, and the type requires an `aria-label`. */
154
+ type ToolbarIconButtonProps = Omit<ToolbarButtonBaseProps, 'aria-label' | 'aria-labelledby' | 'overrideStyles'> & {
155
+ iconOnly: true;
156
+ /** The control's accessible name. An icon has no words of its own, so this is required. */
157
+ 'aria-label': string;
158
+ };
152
159
 
153
- const ToolbarCenter = React.forwardRef<ToolbarSectionElement, ToolbarSectionProps>(
154
- ({ gap = '2', align = 'center', justify = 'center', className, children, ...props }, forwardedRef) => (
155
- <Flex
156
- ref={forwardedRef}
157
- align={align}
158
- justify={justify}
159
- gap={gap}
160
- className={classNames('rt-ToolbarSection', 'rt-ToolbarCenter', className)}
161
- {...props}
162
- >
163
- {children}
164
- </Flex>
165
- ),
166
- );
167
- ToolbarCenter.displayName = 'Toolbar.Center';
160
+ type ToolbarButtonProps = ToolbarTextButtonProps | ToolbarIconButtonProps;
161
+
162
+ function isPressed(value: React.AriaAttributes['aria-pressed']) {
163
+ return value === true || value === 'true';
164
+ }
165
+
166
+ /**
167
+ * A `Button` that joins the toolbar's keyboard navigation, so a row of them is one tab stop.
168
+ *
169
+ * Set `iconOnly` (with an `aria-label`) for a symbol-only control. Set `aria-pressed` for a
170
+ * control that toggles; a pressed button wears Button's pressed look.
171
+ *
172
+ * It takes the toolbar's `size`, and rests as a gray `soft` button (gray `ghost` inside a
173
+ * `ToolbarGroup`). A stated `size`, `variant` or `color` wins; a stated `variant` without a
174
+ * `color` takes Button's own default colour. It throws outside a `Toolbar`.
175
+ */
176
+ const ToolbarButton = React.forwardRef<HTMLButtonElement, ToolbarButtonProps>((props, forwardedRef) => {
177
+ const toolbar = useToolbarContext('ToolbarButton');
178
+ const group = React.useContext(ToolbarGroupContext);
179
+ const { iconOnly, size, variant, color, disabled, ...rest } = props;
180
+
181
+ const pressed = isPressed(props['aria-pressed']);
182
+ const stateProps =
183
+ 'data-state' in props || props['aria-pressed'] === undefined ? {} : { 'data-state': pressed ? 'on' : 'off' };
184
+
185
+ const shared = {
186
+ size: size ?? toolbar.size,
187
+ variant: variant ?? (group ? 'ghost' : 'soft'),
188
+ // The gray rest is a pair with the default variant: a caller who states a variant gets
189
+ // Button's own colour for it unless they state a colour too.
190
+ color: color ?? (variant ? undefined : 'gray'),
191
+ ...stateProps,
192
+ } as const;
193
+
194
+ return (
195
+ <ToolbarPrimitive.Button asChild ref={forwardedRef} disabled={disabled || group?.disabled || undefined}>
196
+ {iconOnly ? (
197
+ <IconButton {...(rest as Omit<ToolbarIconButtonProps, 'iconOnly'>)} {...shared} />
198
+ ) : (
199
+ <Button {...(rest as Omit<ToolbarTextButtonProps, 'iconOnly'>)} {...shared} />
200
+ )}
201
+ </ToolbarPrimitive.Button>
202
+ );
203
+ });
204
+ ToolbarButton.displayName = 'Toolbar.Button';
168
205
 
169
206
  // ============================================================================
170
- // Toolbar Right (for horizontal) / End (alias)
207
+ // ToolbarSeparator
171
208
  // ============================================================================
172
209
 
173
- const ToolbarRight = React.forwardRef<ToolbarSectionElement, ToolbarSectionProps>(
174
- ({ gap = '2', align = 'center', justify = 'end', className, children, ...props }, forwardedRef) => (
175
- <Flex
176
- ref={forwardedRef}
177
- align={align}
178
- justify={justify}
179
- gap={gap}
180
- className={classNames('rt-ToolbarSection', 'rt-ToolbarRight', className)}
181
- {...props}
182
- >
183
- {children}
184
- </Flex>
185
- ),
210
+ type ToolbarSeparatorProps = ComponentPropsWithout<typeof ToolbarPrimitive.Separator, RemovedProps | 'orientation'>;
211
+
212
+ /**
213
+ * A gray hairline between two clusters, announced as a separator. Its direction is always
214
+ * opposite to the toolbar's.
215
+ */
216
+ const ToolbarSeparator = React.forwardRef<HTMLDivElement, ToolbarSeparatorProps>(
217
+ ({ className, ...props }, forwardedRef) => {
218
+ useToolbarContext('ToolbarSeparator');
219
+ return (
220
+ <ToolbarPrimitive.Separator
221
+ {...props}
222
+ ref={forwardedRef}
223
+ className={classNames('rt-ToolbarSeparator', className)}
224
+ />
225
+ );
226
+ },
186
227
  );
187
- ToolbarRight.displayName = 'Toolbar.Right';
228
+ ToolbarSeparator.displayName = 'Toolbar.Separator';
188
229
 
189
230
  // ============================================================================
190
- // Toolbar Title
231
+ // ToolbarTitle
191
232
  // ============================================================================
192
233
 
193
- type ToolbarTitleElement = React.ElementRef<typeof Heading>;
194
- type ToolbarTitleProps = React.ComponentPropsWithoutRef<typeof Heading>;
234
+ /** The title's text step: one step above the label of a button at the same size. */
235
+ const TITLE_SIZE = { '1': '2', '2': '3', '3': '4', '4': '5' } as const satisfies Record<ToolbarSize, TextProps['size']>;
195
236
 
196
- const ToolbarTitle = React.forwardRef<ToolbarTitleElement, ToolbarTitleProps>(
197
- ({ className, size = '2', weight = 'medium', truncate = true, children, ...props }, forwardedRef) => (
198
- <Heading
199
- ref={forwardedRef}
200
- size={size}
201
- weight={weight}
202
- truncate={truncate}
203
- className={classNames('rt-ToolbarTitle', className)}
204
- {...props}
205
- >
206
- {children}
207
- </Heading>
208
- ),
237
+ interface ToolbarTitleProps {
238
+ /** The title text. With none, the title renders nothing. */
239
+ children?: React.ReactNode;
240
+ className?: string;
241
+ style?: React.CSSProperties;
242
+ /** The id of the title, so that the toolbar's `aria-labelledby` can point at it. */
243
+ id?: string;
244
+ }
245
+
246
+ /**
247
+ * What the row is about, at its leading edge. One line: it truncates rather than wrapping.
248
+ * Its text step follows the toolbar's `size`, and it takes more air from the controls beside it
249
+ * than they take from each other.
250
+ */
251
+ const ToolbarTitle = React.forwardRef<HTMLSpanElement, ToolbarTitleProps>(
252
+ ({ className, children, style, id }, forwardedRef) => {
253
+ const { size } = useToolbarContext('ToolbarTitle');
254
+ if (children === undefined || children === null || children === false) return null;
255
+ return (
256
+ <Text
257
+ ref={forwardedRef}
258
+ as="span"
259
+ size={TITLE_SIZE[size]}
260
+ weight="medium"
261
+ truncate
262
+ id={id}
263
+ style={style}
264
+ className={classNames('rt-ToolbarTitle', className)}
265
+ >
266
+ {children}
267
+ </Text>
268
+ );
269
+ },
209
270
  );
210
271
  ToolbarTitle.displayName = 'Toolbar.Title';
211
272
 
@@ -213,30 +274,35 @@ ToolbarTitle.displayName = 'Toolbar.Title';
213
274
  // Exports
214
275
  // ============================================================================
215
276
 
216
- const Toolbar = ToolbarRoot as typeof ToolbarRoot & {
217
- Left: typeof ToolbarLeft;
218
- Center: typeof ToolbarCenter;
219
- Right: typeof ToolbarRight;
220
- Title: typeof ToolbarTitle;
221
- };
222
-
223
- Toolbar.Left = ToolbarLeft;
224
- Toolbar.Center = ToolbarCenter;
225
- Toolbar.Right = ToolbarRight;
226
- Toolbar.Title = ToolbarTitle;
277
+ const Toolbar = Object.assign(ToolbarRoot, {
278
+ Root: ToolbarRoot,
279
+ Group: ToolbarGroup,
280
+ Button: ToolbarButton,
281
+ Separator: ToolbarSeparator,
282
+ Title: ToolbarTitle,
283
+ });
227
284
 
228
285
  export {
229
286
  Toolbar,
287
+ ToolbarGroup,
288
+ ToolbarButton,
289
+ ToolbarSeparator,
290
+ ToolbarTitle,
230
291
  ToolbarRoot as Root,
231
- ToolbarLeft as Left,
232
- ToolbarCenter as Center,
233
- ToolbarRight as Right,
292
+ ToolbarGroup as Group,
293
+ ToolbarButton as Button,
294
+ ToolbarSeparator as Separator,
234
295
  ToolbarTitle as Title,
235
296
  };
236
297
  export type {
237
- ToolbarRootProps as RootProps,
238
- ToolbarSectionProps as LeftProps,
239
- ToolbarSectionProps as CenterProps,
240
- ToolbarSectionProps as RightProps,
298
+ ToolbarProps,
299
+ ToolbarGroupProps,
300
+ ToolbarButtonProps,
301
+ ToolbarSeparatorProps,
302
+ ToolbarTitleProps,
303
+ ToolbarProps as RootProps,
304
+ ToolbarGroupProps as GroupProps,
305
+ ToolbarButtonProps as ButtonProps,
306
+ ToolbarSeparatorProps as SeparatorProps,
241
307
  ToolbarTitleProps as TitleProps,
242
308
  };
@@ -0,0 +1,46 @@
1
+ import * as React from 'react';
2
+
3
+ /**
4
+ * Publishes how much content is hidden past each block edge of a scroller, as
5
+ * `--scroll-overflow-start` and `--scroll-overflow-end` (px) on the element.
6
+ * The scroll fade reads them, so an edge fades only while content is hidden
7
+ * there, and the fade ramps in over the first pixels of scrolling.
8
+ *
9
+ * Writes happen at most once per frame, on scroll and when the element or its
10
+ * content resizes. Nothing re-renders.
11
+ */
12
+ export function useScrollEdges<T extends HTMLElement>(enabled: boolean) {
13
+ const [element, setElement] = React.useState<T | null>(null);
14
+
15
+ React.useEffect(() => {
16
+ if (!enabled || !element) return;
17
+ let frame = 0;
18
+ const write = () => {
19
+ frame = 0;
20
+ const start = Math.max(0, element.scrollTop);
21
+ const end = Math.max(0, element.scrollHeight - element.clientHeight - element.scrollTop);
22
+ element.style.setProperty('--scroll-overflow-start', `${start}px`);
23
+ element.style.setProperty('--scroll-overflow-end', `${end}px`);
24
+ };
25
+ const schedule = () => {
26
+ if (!frame) frame = requestAnimationFrame(write);
27
+ };
28
+ write();
29
+ element.addEventListener('scroll', schedule, { passive: true });
30
+ const observer = typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(schedule);
31
+ observer?.observe(element);
32
+ for (const child of Array.from(element.children)) observer?.observe(child);
33
+ const mutations = typeof MutationObserver === 'undefined' ? null : new MutationObserver(schedule);
34
+ mutations?.observe(element, { childList: true });
35
+ return () => {
36
+ if (frame) cancelAnimationFrame(frame);
37
+ element.removeEventListener('scroll', schedule);
38
+ observer?.disconnect();
39
+ mutations?.disconnect();
40
+ element.style.removeProperty('--scroll-overflow-start');
41
+ element.style.removeProperty('--scroll-overflow-end');
42
+ };
43
+ }, [enabled, element]);
44
+
45
+ return setElement;
46
+ }
@@ -17,6 +17,25 @@
17
17
  --color-dialog-solid: white;
18
18
  --color-dialog-translucent: rgba(255, 255, 255, var(--dialog-opacity));
19
19
 
20
+ /*
21
+ * v2's names for the page, its text and its neutral scale, so code written against either
22
+ * system reads the same tokens. Aliases only: the values stay v1's and follow the appearance.
23
+ */
24
+ --color-page: var(--color-background);
25
+ --color-text: var(--gray-12);
26
+ --neutral-1: var(--gray-1);
27
+ --neutral-2: var(--gray-2);
28
+ --neutral-3: var(--gray-3);
29
+ --neutral-4: var(--gray-4);
30
+ --neutral-5: var(--gray-5);
31
+ --neutral-6: var(--gray-6);
32
+ --neutral-7: var(--gray-7);
33
+ --neutral-8: var(--gray-8);
34
+ --neutral-9: var(--gray-9);
35
+ --neutral-10: var(--gray-10);
36
+ --neutral-11: var(--gray-11);
37
+ --neutral-12: var(--gray-12);
38
+
20
39
  /* Material system defaults (solid) - controls color-mix blending for component backgrounds */
21
40
  --material-blend: 100%;
22
41
  --material-alpha: 0;