@womp/kookie-ui 0.10.0 → 0.12.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 (113) hide show
  1. package/components.css +314 -218
  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.d.ts +26 -2
  7. package/dist/cjs/components/chatbar.d.ts.map +1 -1
  8. package/dist/cjs/components/chatbar.js +2 -2
  9. package/dist/cjs/components/chatbar.js.map +3 -3
  10. package/dist/cjs/components/index.d.ts +2 -1
  11. package/dist/cjs/components/index.d.ts.map +1 -1
  12. package/dist/cjs/components/index.js +5 -5
  13. package/dist/cjs/components/index.js.map +4 -4
  14. package/dist/cjs/components/toolbar.d.ts +88 -36
  15. package/dist/cjs/components/toolbar.d.ts.map +1 -1
  16. package/dist/cjs/components/toolbar.js +4 -1
  17. package/dist/cjs/components/toolbar.js.map +4 -4
  18. package/dist/cjs/components/toolbar.props.d.ts +28 -0
  19. package/dist/cjs/components/toolbar.props.d.ts.map +1 -0
  20. package/dist/cjs/components/toolbar.props.js +2 -0
  21. package/dist/cjs/components/toolbar.props.js.map +7 -0
  22. package/dist/cjs/index.js +5 -5
  23. package/dist/cjs/index.js.map +4 -4
  24. package/dist/esm/{chunk-73QGEY2I.js → chunk-3R4YLW4A.js} +2 -2
  25. package/dist/esm/{chunk-ZZLZGHZQ.js → chunk-4LDVSTXI.js} +2 -2
  26. package/dist/esm/chunk-54JFIP5S.js +2 -0
  27. package/dist/esm/chunk-54JFIP5S.js.map +7 -0
  28. package/dist/esm/{chunk-X3JM2565.js → chunk-6G3MPL4A.js} +2 -2
  29. package/dist/esm/{chunk-DSJY5RI3.js → chunk-7JJT42J2.js} +2 -2
  30. package/dist/esm/chunk-C7EVYSWK.js +1 -0
  31. package/dist/esm/{chunk-ERY5TH2M.js → chunk-CYU6BLZ5.js} +2 -2
  32. package/dist/esm/{chunk-25LBWKEV.js → chunk-D6PHTHF2.js} +2 -2
  33. package/dist/esm/chunk-EGMUQQRW.js +2 -0
  34. package/dist/esm/chunk-EGMUQQRW.js.map +7 -0
  35. package/dist/esm/{chunk-4XGGX2V2.js → chunk-ISOLUTZF.js} +2 -2
  36. package/dist/esm/{chunk-LXWPEDPZ.js → chunk-IYCPYN4Z.js} +2 -2
  37. package/dist/esm/{chunk-RJP5QUXO.js → chunk-L5PID6BU.js} +2 -2
  38. package/dist/esm/{chunk-BW5CO4PZ.js → chunk-METRW4HG.js} +2 -2
  39. package/dist/esm/{chunk-PYHEBG5I.js → chunk-MJOXEUBT.js} +2 -2
  40. package/dist/esm/{chunk-4D2OAD6I.js → chunk-OIUCV5HK.js} +2 -2
  41. package/dist/esm/{chunk-65GAANJD.js → chunk-UYCPDGWA.js} +2 -2
  42. package/dist/esm/chunk-XLMTTSRO.js +2 -0
  43. package/dist/esm/chunk-XLMTTSRO.js.map +7 -0
  44. package/dist/esm/{chunk-QWVU2TW4.js → chunk-ZK7A2MZZ.js} +2 -2
  45. package/dist/esm/chunk-ZUOK3KFY.js +2 -0
  46. package/dist/esm/chunk-ZUOK3KFY.js.map +7 -0
  47. package/dist/esm/components/_internal/base-button.js +1 -1
  48. package/dist/esm/components/alert-dialog.js +1 -1
  49. package/dist/esm/components/app-bar.d.ts +49 -0
  50. package/dist/esm/components/app-bar.d.ts.map +1 -0
  51. package/dist/esm/components/app-bar.js +2 -0
  52. package/dist/esm/components/app-bar.js.map +7 -0
  53. package/dist/esm/components/button.js +1 -1
  54. package/dist/esm/components/chatbar.d.ts +26 -2
  55. package/dist/esm/components/chatbar.d.ts.map +1 -1
  56. package/dist/esm/components/chatbar.js +1 -1
  57. package/dist/esm/components/checkbox-cards.js +1 -1
  58. package/dist/esm/components/dialog.js +1 -1
  59. package/dist/esm/components/icon-button.js +1 -1
  60. package/dist/esm/components/index.d.ts +2 -1
  61. package/dist/esm/components/index.d.ts.map +1 -1
  62. package/dist/esm/components/index.js +1 -1
  63. package/dist/esm/components/message-scroller.js +1 -1
  64. package/dist/esm/components/notice.js +1 -1
  65. package/dist/esm/components/sheet.js +1 -1
  66. package/dist/esm/components/shell.js +1 -1
  67. package/dist/esm/components/theme-panel.js +1 -1
  68. package/dist/esm/components/toggle-button.js +1 -1
  69. package/dist/esm/components/toggle-icon-button.js +1 -1
  70. package/dist/esm/components/toolbar.d.ts +88 -36
  71. package/dist/esm/components/toolbar.d.ts.map +1 -1
  72. package/dist/esm/components/toolbar.js +1 -1
  73. package/dist/esm/components/toolbar.props.d.ts +28 -0
  74. package/dist/esm/components/toolbar.props.d.ts.map +1 -0
  75. package/dist/esm/components/toolbar.props.js +2 -0
  76. package/dist/esm/components/toolbar.props.js.map +7 -0
  77. package/dist/esm/components/user-card.js +1 -1
  78. package/dist/esm/index.js +1 -1
  79. package/dist/esm/props/index.js +1 -1
  80. package/package.json +1 -1
  81. package/src/components/app-bar.css +204 -0
  82. package/src/components/app-bar.tsx +256 -0
  83. package/src/components/chatbar.css +75 -40
  84. package/src/components/chatbar.tsx +126 -49
  85. package/src/components/index.css +2 -0
  86. package/src/components/index.tsx +13 -1
  87. package/src/components/toolbar.css +87 -160
  88. package/src/components/toolbar.props.tsx +27 -0
  89. package/src/components/toolbar.tsx +259 -193
  90. package/src/styles/tokens/color.css +19 -0
  91. package/styles.css +328 -218
  92. package/tokens/base.css +14 -0
  93. package/tokens.css +14 -0
  94. package/dist/esm/chunk-47LMOIW3.js +0 -2
  95. package/dist/esm/chunk-47LMOIW3.js.map +0 -7
  96. package/dist/esm/chunk-JKS2YXMS.js +0 -1
  97. package/dist/esm/chunk-QK5RBHKV.js +0 -2
  98. package/dist/esm/chunk-QK5RBHKV.js.map +0 -7
  99. /package/dist/esm/{chunk-73QGEY2I.js.map → chunk-3R4YLW4A.js.map} +0 -0
  100. /package/dist/esm/{chunk-ZZLZGHZQ.js.map → chunk-4LDVSTXI.js.map} +0 -0
  101. /package/dist/esm/{chunk-X3JM2565.js.map → chunk-6G3MPL4A.js.map} +0 -0
  102. /package/dist/esm/{chunk-DSJY5RI3.js.map → chunk-7JJT42J2.js.map} +0 -0
  103. /package/dist/esm/{chunk-JKS2YXMS.js.map → chunk-C7EVYSWK.js.map} +0 -0
  104. /package/dist/esm/{chunk-ERY5TH2M.js.map → chunk-CYU6BLZ5.js.map} +0 -0
  105. /package/dist/esm/{chunk-25LBWKEV.js.map → chunk-D6PHTHF2.js.map} +0 -0
  106. /package/dist/esm/{chunk-4XGGX2V2.js.map → chunk-ISOLUTZF.js.map} +0 -0
  107. /package/dist/esm/{chunk-LXWPEDPZ.js.map → chunk-IYCPYN4Z.js.map} +0 -0
  108. /package/dist/esm/{chunk-RJP5QUXO.js.map → chunk-L5PID6BU.js.map} +0 -0
  109. /package/dist/esm/{chunk-BW5CO4PZ.js.map → chunk-METRW4HG.js.map} +0 -0
  110. /package/dist/esm/{chunk-PYHEBG5I.js.map → chunk-MJOXEUBT.js.map} +0 -0
  111. /package/dist/esm/{chunk-4D2OAD6I.js.map → chunk-OIUCV5HK.js.map} +0 -0
  112. /package/dist/esm/{chunk-65GAANJD.js.map → chunk-UYCPDGWA.js.map} +0 -0
  113. /package/dist/esm/{chunk-QWVU2TW4.js.map → chunk-ZK7A2MZZ.js.map} +0 -0
@@ -0,0 +1,256 @@
1
+ import * as React from 'react';
2
+ 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
+ // AppBar Root
13
+ // ============================================================================
14
+
15
+ type AppBarRootElement = 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 AppBarRootProps 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;
36
+ }
37
+
38
+ /**
39
+ * An app bar: the band anchored to one edge of a pane, with left, center and right sections
40
+ * and a title. It publishes its height (or width) to its parent as `--rt-app-bar-height`
41
+ * (`--rt-app-bar-width`) so content can pad itself clear of it.
42
+ *
43
+ * This component was called `Toolbar` until 0.11.0. It is not a `role="toolbar"`: for a row of
44
+ * controls with one tab stop and arrow-key navigation, use `Toolbar`.
45
+ */
46
+ const AppBarRoot = React.forwardRef<AppBarRootElement, AppBarRootProps>(
47
+ (props, forwardedRef) => {
48
+ const {
49
+ anchor = 'top',
50
+ floating = false,
51
+ offset = '2',
52
+ size = '1',
53
+ variant = 'ghost',
54
+ material,
55
+ className,
56
+ style,
57
+ children,
58
+ ...restProps
59
+ } = props;
60
+
61
+ const isHorizontal = anchor === 'top' || anchor === 'bottom';
62
+ const internalRef = React.useRef<HTMLDivElement>(null);
63
+
64
+ // Merge refs
65
+ const mergedRef = React.useCallback(
66
+ (node: HTMLDivElement | null) => {
67
+ (internalRef as React.MutableRefObject<HTMLDivElement | null>).current = node;
68
+ if (typeof forwardedRef === 'function') {
69
+ forwardedRef(node);
70
+ } else if (forwardedRef) {
71
+ forwardedRef.current = node;
72
+ }
73
+ },
74
+ [forwardedRef],
75
+ );
76
+
77
+ // Expose height/width as CSS variable on parent element
78
+ React.useEffect(() => {
79
+ const element = internalRef.current;
80
+ if (!element) return;
81
+
82
+ const parent = element.parentElement;
83
+ if (!parent) return;
84
+
85
+ const update = () => {
86
+ let dimension = isHorizontal ? element.offsetHeight : element.offsetWidth;
87
+
88
+ // For floating mode, include the margin in the total space
89
+ if (floating) {
90
+ const computedStyle = getComputedStyle(element);
91
+ const margin = parseFloat(computedStyle.marginTop) || 0;
92
+ dimension += margin * 2;
93
+ }
94
+
95
+ // `--rt-toolbar-*` are the names this bar published while it was called Toolbar.
96
+ // They are still written so existing layouts keep their offset; prefer `--rt-app-bar-*`.
97
+ if (isHorizontal) {
98
+ parent.style.setProperty('--rt-app-bar-height', `${dimension}px`);
99
+ parent.style.setProperty('--rt-toolbar-height', `${dimension}px`);
100
+ } else {
101
+ parent.style.setProperty('--rt-app-bar-width', `${dimension}px`);
102
+ parent.style.setProperty('--rt-toolbar-width', `${dimension}px`);
103
+ }
104
+ };
105
+
106
+ update();
107
+
108
+ const resizeObserver = new ResizeObserver(update);
109
+ resizeObserver.observe(element);
110
+
111
+ return () => resizeObserver.disconnect();
112
+ }, [isHorizontal, floating]);
113
+
114
+ const computedStyle = floating
115
+ ? { margin: `var(--space-${offset})`, ...style }
116
+ : style;
117
+
118
+ return (
119
+ <Card
120
+ ref={mergedRef}
121
+ size={size}
122
+ variant={variant}
123
+ material={material}
124
+ data-anchor={anchor}
125
+ data-floating={floating || undefined}
126
+ className={classNames('rt-AppBar', className)}
127
+ style={computedStyle}
128
+ {...restProps}
129
+ >
130
+ <div className="rt-AppBarInner">
131
+ {children}
132
+ </div>
133
+ </Card>
134
+ );
135
+ },
136
+ );
137
+ AppBarRoot.displayName = 'AppBar.Root';
138
+
139
+ // ============================================================================
140
+ // AppBar Left (for horizontal) / Start (alias)
141
+ // ============================================================================
142
+
143
+ type AppBarSectionElement = React.ElementRef<'div'>;
144
+ type AppBarSectionProps = FlexProps;
145
+
146
+ const AppBarLeft = React.forwardRef<AppBarSectionElement, AppBarSectionProps>(
147
+ ({ gap = '2', align = 'center', className, children, ...props }, forwardedRef) => (
148
+ <Flex
149
+ ref={forwardedRef}
150
+ align={align}
151
+ gap={gap}
152
+ className={classNames('rt-AppBarSection', 'rt-AppBarLeft', className)}
153
+ {...props}
154
+ >
155
+ {children}
156
+ </Flex>
157
+ ),
158
+ );
159
+ AppBarLeft.displayName = 'AppBar.Left';
160
+
161
+ // ============================================================================
162
+ // AppBar Center
163
+ // ============================================================================
164
+
165
+ const AppBarCenter = React.forwardRef<AppBarSectionElement, AppBarSectionProps>(
166
+ ({ gap = '2', align = 'center', justify = 'center', className, children, ...props }, forwardedRef) => (
167
+ <Flex
168
+ ref={forwardedRef}
169
+ align={align}
170
+ justify={justify}
171
+ gap={gap}
172
+ className={classNames('rt-AppBarSection', 'rt-AppBarCenter', className)}
173
+ {...props}
174
+ >
175
+ {children}
176
+ </Flex>
177
+ ),
178
+ );
179
+ AppBarCenter.displayName = 'AppBar.Center';
180
+
181
+ // ============================================================================
182
+ // AppBar Right (for horizontal) / End (alias)
183
+ // ============================================================================
184
+
185
+ const AppBarRight = React.forwardRef<AppBarSectionElement, AppBarSectionProps>(
186
+ ({ gap = '2', align = 'center', justify = 'end', className, children, ...props }, forwardedRef) => (
187
+ <Flex
188
+ ref={forwardedRef}
189
+ align={align}
190
+ justify={justify}
191
+ gap={gap}
192
+ className={classNames('rt-AppBarSection', 'rt-AppBarRight', className)}
193
+ {...props}
194
+ >
195
+ {children}
196
+ </Flex>
197
+ ),
198
+ );
199
+ AppBarRight.displayName = 'AppBar.Right';
200
+
201
+ // ============================================================================
202
+ // AppBar Title
203
+ // ============================================================================
204
+
205
+ type AppBarTitleElement = React.ElementRef<typeof Heading>;
206
+ type AppBarTitleProps = React.ComponentPropsWithoutRef<typeof Heading>;
207
+
208
+ const AppBarTitle = React.forwardRef<AppBarTitleElement, AppBarTitleProps>(
209
+ ({ className, size = '2', weight = 'medium', truncate = true, children, ...props }, forwardedRef) => (
210
+ <Heading
211
+ ref={forwardedRef}
212
+ size={size}
213
+ weight={weight}
214
+ truncate={truncate}
215
+ className={classNames('rt-AppBarTitle', className)}
216
+ {...props}
217
+ >
218
+ {children}
219
+ </Heading>
220
+ ),
221
+ );
222
+ AppBarTitle.displayName = 'AppBar.Title';
223
+
224
+ // ============================================================================
225
+ // Exports
226
+ // ============================================================================
227
+
228
+ const AppBar = AppBarRoot as typeof AppBarRoot & {
229
+ Root: typeof AppBarRoot;
230
+ Left: typeof AppBarLeft;
231
+ Center: typeof AppBarCenter;
232
+ Right: typeof AppBarRight;
233
+ Title: typeof AppBarTitle;
234
+ };
235
+
236
+ AppBar.Root = AppBarRoot;
237
+ AppBar.Left = AppBarLeft;
238
+ AppBar.Center = AppBarCenter;
239
+ AppBar.Right = AppBarRight;
240
+ AppBar.Title = AppBarTitle;
241
+
242
+ export {
243
+ AppBar,
244
+ AppBarRoot as Root,
245
+ AppBarLeft as Left,
246
+ AppBarCenter as Center,
247
+ AppBarRight as Right,
248
+ AppBarTitle as Title,
249
+ };
250
+ export type {
251
+ AppBarRootProps as RootProps,
252
+ AppBarSectionProps as LeftProps,
253
+ AppBarSectionProps as CenterProps,
254
+ AppBarSectionProps as RightProps,
255
+ AppBarTitleProps as TitleProps,
256
+ };
@@ -8,7 +8,9 @@
8
8
 
9
9
  /* Defaults */
10
10
  .rt-ChatbarRoot {
11
- --chatbar-attachment-thumb: var(--space-8);
11
+ --chatbar-item-lead: var(--space-5);
12
+ --chatbar-item-lead-radius: var(--radius-2);
13
+ --chatbar-item-padding: var(--space-1);
12
14
  /* A column: notices, then the box (which fills any height the consumer gives the root), then context. */
13
15
  display: flex;
14
16
  flex-direction: column;
@@ -116,58 +118,97 @@
116
118
  --scrollarea-scrollbar-size: 3px;
117
119
  }
118
120
 
119
- /* Attachment tile now uses Card + Flex, no custom layout needed */
121
+ /*
122
+ * Attachments and items are one pill: a lead tile (thumbnail or tinted icon),
123
+ * a label and a remove button. A custom `renderAttachment` keeps only the
124
+ * plain .rt-ChatbarAttachment hook.
125
+ */
126
+ .rt-ChatbarAttachment {
127
+ position: relative;
128
+ }
129
+
130
+ .rt-ChatbarItem {
131
+ display: inline-flex;
132
+ align-items: center;
133
+ gap: var(--space-2);
134
+ flex-shrink: 0;
135
+ box-sizing: border-box;
136
+ max-width: 240px;
137
+ padding: var(--chatbar-item-padding);
138
+ padding-right: calc(var(--chatbar-item-padding) + var(--space-1));
139
+ border: 1px solid var(--gray-a5);
140
+ border-radius: calc(var(--chatbar-item-lead-radius) + var(--chatbar-item-padding));
141
+ background-color: var(--color-panel-solid);
142
+ box-shadow: 0 1px 2px var(--gray-a3);
143
+ color: var(--gray-12);
144
+ }
145
+
146
+ .rt-ChatbarItem:where([data-status='error']) {
147
+ border-color: var(--red-a7);
148
+ }
120
149
 
121
- .rt-ChatbarAttachmentPreview {
122
- height: var(--chatbar-attachment-thumb);
123
- width: var(--chatbar-attachment-thumb);
124
- min-width: var(--chatbar-attachment-thumb);
150
+ .rt-ChatbarItemLead {
151
+ height: var(--chatbar-item-lead);
152
+ width: var(--chatbar-item-lead);
125
153
  flex-shrink: 0;
126
- border-radius: var(--radius-1);
154
+ border-radius: var(--chatbar-item-lead-radius);
127
155
  overflow: hidden;
128
156
  display: inline-flex;
129
157
  align-items: center;
130
158
  justify-content: center;
159
+ background: var(--accent-a3);
160
+ color: var(--accent-11);
161
+
162
+ /* stylelint-disable-next-line selector-max-type */
163
+ & :where(svg) {
164
+ width: calc(var(--chatbar-item-lead) * 0.6);
165
+ height: calc(var(--chatbar-item-lead) * 0.6);
166
+ }
167
+
168
+ &:where([data-kind='image']) {
169
+ background: var(--gray-a3);
170
+ }
171
+ }
172
+
173
+ /* An attachment's file icon and spinner stay neutral */
174
+ .rt-ChatbarAttachment .rt-ChatbarItemLead:where([data-kind='icon']) {
131
175
  background: var(--gray-a3);
176
+ color: var(--gray-11);
132
177
  }
133
178
 
134
179
  .rt-ChatbarAttachmentImage {
180
+ width: 100%;
135
181
  height: 100%;
136
- width: auto;
137
- object-fit: contain;
182
+ object-fit: cover;
138
183
  max-width: none;
139
184
  }
140
185
 
141
- /* PaperclipIcon handles its own sizing via IconButton */
142
-
143
- .rt-ChatbarAttachment {
144
- position: relative;
145
- background-color: var(--gray-1);
146
- border-radius: var(--radius-1);
147
- }
148
-
149
- /* Maximum width for non-image attachments */
150
- .rt-ChatbarAttachment:where([data-kind='file']) {
151
- max-width: 200px;
186
+ .rt-ChatbarItemLabel {
187
+ min-width: 0;
152
188
  }
153
189
 
154
- .rt-ChatbarAttachmentRemove {
155
- position: absolute;
156
- top: var(--chatbar-remove-offset);
157
- right: var(--chatbar-remove-offset);
190
+ .rt-ChatbarItemRemove {
191
+ flex-shrink: 0;
192
+ margin-left: calc(var(--space-1) * -1);
158
193
  }
159
194
 
160
- /* Size-dependent radius for attachment container and preview */
161
195
  @breakpoints {
162
- .rt-ChatbarRoot:where(.rt-r-size-1) :where(.rt-ChatbarAttachment, .rt-ChatbarAttachmentPreview) {
163
- border-radius: var(--radius-1);
164
- /* border-radius: var(--radius-1); */
165
- }
166
- .rt-ChatbarRoot:where(.rt-r-size-2) :where(.rt-ChatbarAttachment, .rt-ChatbarAttachmentPreview) {
167
- border-radius: var(--radius-2);
168
- }
169
- .rt-ChatbarRoot:where(.rt-r-size-3) :where(.rt-ChatbarAttachment, .rt-ChatbarAttachmentPreview) {
170
- border-radius: var(--radius-3);
196
+ .rt-ChatbarRoot {
197
+ &:where(.rt-r-size-1) {
198
+ --chatbar-item-lead: var(--space-4);
199
+ --chatbar-item-lead-radius: var(--radius-1);
200
+ --chatbar-item-padding: var(--space-1);
201
+ }
202
+ &:where(.rt-r-size-2) {
203
+ --chatbar-item-lead: var(--space-5);
204
+ --chatbar-item-lead-radius: var(--radius-2);
205
+ --chatbar-item-padding: var(--space-1);
206
+ }
207
+ &:where(.rt-r-size-3) {
208
+ --chatbar-item-lead: calc(var(--space-5) + var(--space-1));
209
+ --chatbar-item-lead-radius: var(--radius-2);
210
+ --chatbar-item-padding: calc(var(--space-1) * 1.5);
211
+ }
171
212
  }
172
213
  }
173
214
 
@@ -575,24 +616,18 @@
575
616
  &:where(.rt-r-size-1) {
576
617
  font-size: var(--font-size-1);
577
618
  line-height: var(--line-height-1);
578
- --chatbar-attachment-thumb: var(--space-8);
579
- --chatbar-remove-offset: calc(var(--space-1) * 0.5);
580
619
  /* --chatbar-attachments-radius: min(var(--radius-5), calc(var(--radius-1) + var(--space-1))); */
581
620
  --chatbar-attachments-radius: var(--radius-1);
582
621
  }
583
622
  &:where(.rt-r-size-2) {
584
623
  font-size: var(--font-size-2);
585
624
  line-height: var(--line-height-2);
586
- --chatbar-attachment-thumb: var(--space-9);
587
- --chatbar-remove-offset: var(--space-1);
588
625
  /* --chatbar-attachments-radius: min(var(--radius-6), calc(var(--radius-2) + var(--space-2))); */
589
626
  --chatbar-attachments-radius: var(--radius-2);
590
627
  }
591
628
  &:where(.rt-r-size-3) {
592
629
  font-size: var(--font-size-3);
593
630
  line-height: var(--line-height-3);
594
- --chatbar-attachment-thumb: var(--space-9);
595
- --chatbar-remove-offset: var(--space-2);
596
631
  /* --chatbar-attachments-radius: min(var(--radius-7), calc(var(--radius-3) + var(--space-3))); */
597
632
  --chatbar-attachments-radius: var(--radius-3);
598
633
  }
@@ -1030,18 +1030,22 @@ InlineEnd.displayName = 'Chatbar.InlineEnd';
1030
1030
  /**
1031
1031
  * Renders a horizontally scrollable list of attachments above the inline row.
1032
1032
  * Hidden when empty unless `forceMount`. Override per-item with `renderAttachment`.
1033
+ * `Chatbar.Item`s passed as children follow the attachments in the same row.
1033
1034
  */
1034
1035
  interface AttachmentsRowProps extends Omit<React.ComponentPropsWithoutRef<'div'>, 'children'> {
1035
1036
  asChild?: boolean;
1036
1037
  forceMount?: boolean;
1037
1038
  /** If provided, custom-render a tile; otherwise default tile is used */
1038
1039
  renderAttachment?: (attachment: ChatbarAttachment) => React.ReactNode;
1040
+ /** `Chatbar.Item`s: things the message refers to that are not files. */
1041
+ children?: React.ReactNode;
1039
1042
  }
1040
1043
 
1041
1044
  const AttachmentsRow = React.forwardRef<HTMLDivElement, AttachmentsRowProps>((props, forwardedRef) => {
1042
- const { asChild, forceMount, renderAttachment, className, ...divProps } = props;
1045
+ const { asChild, forceMount, renderAttachment, className, children, ...divProps } = props;
1043
1046
  const ctx = useChatbarContext();
1044
- if (ctx.attachments.length === 0 && !forceMount) return null;
1047
+ const hasItems = React.Children.toArray(children).length > 0;
1048
+ if (ctx.attachments.length === 0 && !hasItems && !forceMount) return null;
1045
1049
  const Comp = asChild ? Slot : 'div';
1046
1050
  return (
1047
1051
  <m.div layout="position" transition={LAYOUT_TRANSITION}>
@@ -1053,6 +1057,7 @@ const AttachmentsRow = React.forwardRef<HTMLDivElement, AttachmentsRowProps>((pr
1053
1057
  {renderAttachment?.(att)}
1054
1058
  </Attachment>
1055
1059
  ))}
1060
+ {children}
1056
1061
  </Flex>
1057
1062
  </ScrollArea>
1058
1063
  </Comp>
@@ -1061,6 +1066,50 @@ const AttachmentsRow = React.forwardRef<HTMLDivElement, AttachmentsRowProps>((pr
1061
1066
  });
1062
1067
  AttachmentsRow.displayName = 'Chatbar.AttachmentsRow';
1063
1068
 
1069
+ /**
1070
+ * Moves focus off a pill that is about to unmount, to the next pill's remove
1071
+ * button, the previous one's, or the text, so keyboard users are not dropped
1072
+ * to <body>.
1073
+ */
1074
+ function moveFocusOffPill(button: HTMLButtonElement, textareaRef: React.RefObject<HTMLTextAreaElement | null>) {
1075
+ const pill = button.closest('[role="listitem"]');
1076
+ const pills = pill?.parentElement ? Array.from(pill.parentElement.children) : [];
1077
+ const index = pill ? pills.indexOf(pill) : -1;
1078
+ const neighbour = pills[index + 1] ?? pills[index - 1];
1079
+ const nextButton = neighbour?.querySelector<HTMLButtonElement>('.rt-ChatbarItemRemove');
1080
+ if (nextButton) nextButton.focus();
1081
+ else textareaRef.current?.focus({ preventScroll: true });
1082
+ }
1083
+
1084
+ /** The pill an attachment and an item share: a lead tile, a label and ×. */
1085
+ const Pill: React.FC<{
1086
+ lead: React.ReactNode;
1087
+ leadKind: 'image' | 'icon';
1088
+ label: React.ReactNode;
1089
+ /** Read after the label by screen readers only, e.g. a file's size. */
1090
+ detail?: string;
1091
+ removeLabel: string;
1092
+ onRemove?: (event: React.MouseEvent<HTMLButtonElement>) => void;
1093
+ }> = ({ lead, leadKind, label, detail, removeLabel, onRemove }) => {
1094
+ const ctx = useChatbarContext();
1095
+ return (
1096
+ <>
1097
+ <span className="rt-ChatbarItemLead" data-kind={leadKind} aria-hidden>
1098
+ {lead}
1099
+ </span>
1100
+ <Text className="rt-ChatbarItemLabel" size={ctx.size} truncate>
1101
+ {label}
1102
+ {detail ? <VisuallyHidden>, {detail}</VisuallyHidden> : null}
1103
+ </Text>
1104
+ {onRemove && (
1105
+ <IconButton className="rt-ChatbarItemRemove" aria-label={removeLabel} size="1" variant="ghost" color="gray" onClick={onRemove}>
1106
+ <CloseIcon />
1107
+ </IconButton>
1108
+ )}
1109
+ </>
1110
+ );
1111
+ };
1112
+
1064
1113
  /** Default tile renderer for a single attachment. */
1065
1114
  interface AttachmentProps extends React.ComponentPropsWithoutRef<'div'> {
1066
1115
  attachment: ChatbarAttachment;
@@ -1079,71 +1128,98 @@ const Attachment = React.forwardRef<HTMLDivElement, AttachmentProps>((props, for
1079
1128
  const locked = ctx.disabled || ctx.readOnly;
1080
1129
 
1081
1130
  const handleRemove = (event: React.MouseEvent<HTMLButtonElement>) => {
1082
- // Move focus before the tile unmounts so keyboard users are not dropped to <body>
1083
- const tile = event.currentTarget.closest('[role="listitem"]');
1084
- const tiles = tile?.parentElement ? Array.from(tile.parentElement.children) : [];
1085
- const index = tile ? tiles.indexOf(tile) : -1;
1086
- const neighbour = tiles[index + 1] ?? tiles[index - 1];
1087
- const nextButton = neighbour?.querySelector<HTMLButtonElement>('.rt-ChatbarAttachmentRemove');
1088
- if (nextButton) nextButton.focus();
1089
- else ctx.textareaRef.current?.focus({ preventScroll: true });
1131
+ moveFocusOffPill(event.currentTarget, ctx.textareaRef);
1090
1132
  ctx.removeAttachment(attachment.id);
1091
1133
  };
1092
1134
 
1135
+ const lead =
1136
+ attachment.status === 'uploading' ? (
1137
+ <Spinner size="1" />
1138
+ ) : isImage ? (
1139
+ <img className="rt-ChatbarAttachmentImage" src={attachment.url} alt="" />
1140
+ ) : (
1141
+ <FileTextIcon />
1142
+ );
1143
+
1093
1144
  return (
1094
1145
  <Comp
1095
1146
  {...divProps}
1096
1147
  ref={forwardedRef}
1097
- className={classNames('rt-ChatbarAttachment', className)}
1148
+ className={classNames(children ? 'rt-ChatbarAttachment' : 'rt-ChatbarAttachment rt-ChatbarItem', className)}
1098
1149
  role="listitem"
1099
1150
  data-kind={isImage ? 'image' : 'file'}
1100
1151
  data-status={attachment.status}
1101
1152
  title={attachment.name}
1102
1153
  >
1103
1154
  {children ?? (
1104
- <Flex align="center" gap="2" pr={!isImage ? '6' : undefined}>
1105
- <div className="rt-ChatbarAttachmentPreview" aria-hidden>
1106
- {isImage ? <img className="rt-ChatbarAttachmentImage" src={attachment.url} alt="" /> : <FileTextIcon />}
1107
- </div>
1108
- {isImage ? (
1109
- <VisuallyHidden>
1110
- {attachment.name}, {sizeLabel}
1111
- {statusLabel ? `, ${statusLabel}` : ''}
1112
- </VisuallyHidden>
1113
- ) : (
1114
- <Flex direction="column" gap="0" minWidth="0">
1115
- <Text size={ctx.size} weight="medium" truncate>
1116
- {attachment.name}
1117
- </Text>
1118
- <Text
1119
- size="1"
1120
- color={attachment.status === 'error' ? 'red' : undefined}
1121
- emphasis={attachment.status === 'error' ? undefined : 'medium'}
1122
- >
1123
- {sizeLabel}
1124
- {statusLabel ? ` · ${statusLabel}` : ''}
1125
- </Text>
1126
- </Flex>
1127
- )}
1128
- {!locked && (
1129
- <IconButton
1130
- className="rt-ChatbarAttachmentRemove"
1131
- aria-label={`Remove ${attachment.name}`}
1132
- size="1"
1133
- variant="classic"
1134
- color="gray"
1135
- onClick={handleRemove}
1136
- >
1137
- <CloseIcon />
1138
- </IconButton>
1139
- )}
1140
- </Flex>
1155
+ <Pill
1156
+ lead={lead}
1157
+ leadKind={isImage && attachment.status !== 'uploading' ? 'image' : 'icon'}
1158
+ label={attachment.name}
1159
+ detail={statusLabel ? `${sizeLabel}, ${statusLabel}` : sizeLabel}
1160
+ removeLabel={`Remove ${attachment.name}`}
1161
+ onRemove={locked ? undefined : handleRemove}
1162
+ />
1141
1163
  )}
1142
1164
  </Comp>
1143
1165
  );
1144
1166
  });
1145
1167
  Attachment.displayName = 'Chatbar.Attachment';
1146
1168
 
1169
+ /**
1170
+ * Something the message refers to that is not a file: a scene object, a
1171
+ * picked mode, a selection. The same pill as an attachment, placed in
1172
+ * `Chatbar.AttachmentsRow` as a child. The app owns the list, so removing one
1173
+ * calls `onRemove` and nothing else; leave it out for a pill that cannot be
1174
+ * removed.
1175
+ */
1176
+ interface ItemProps extends Omit<React.ComponentPropsWithoutRef<'div'>, 'children' | 'color'> {
1177
+ /** The label. Also names the remove button: "Remove {label}". */
1178
+ children: string;
1179
+ /** Drawn in the lead tile, tinted with `color`. */
1180
+ icon?: React.ReactNode;
1181
+ /** A thumbnail for the lead tile, in place of the icon. */
1182
+ image?: string;
1183
+ /** Tint of the icon tile. Defaults to gray. */
1184
+ color?: (typeof accentColors)[number];
1185
+ onRemove?: () => void;
1186
+ /** Overrides the remove button's label. */
1187
+ removeLabel?: string;
1188
+ }
1189
+
1190
+ const Item = React.forwardRef<HTMLDivElement, ItemProps>((props, forwardedRef) => {
1191
+ const { children, icon, image, color, onRemove, removeLabel, className, ...divProps } = props;
1192
+ const ctx = useChatbarContext();
1193
+ const locked = ctx.disabled || ctx.readOnly;
1194
+
1195
+ const handleRemove = onRemove
1196
+ ? (event: React.MouseEvent<HTMLButtonElement>) => {
1197
+ moveFocusOffPill(event.currentTarget, ctx.textareaRef);
1198
+ onRemove();
1199
+ }
1200
+ : undefined;
1201
+
1202
+ return (
1203
+ <div
1204
+ {...divProps}
1205
+ ref={forwardedRef}
1206
+ className={classNames('rt-ChatbarItem', className)}
1207
+ role="listitem"
1208
+ data-accent-color={image ? undefined : (color ?? 'gray')}
1209
+ title={children}
1210
+ >
1211
+ <Pill
1212
+ lead={image ? <img className="rt-ChatbarAttachmentImage" src={image} alt="" /> : icon}
1213
+ leadKind={image ? 'image' : 'icon'}
1214
+ label={children}
1215
+ removeLabel={removeLabel ?? `Remove ${children}`}
1216
+ onRemove={locked ? undefined : handleRemove}
1217
+ />
1218
+ </div>
1219
+ );
1220
+ });
1221
+ Item.displayName = 'Chatbar.Item';
1222
+
1147
1223
  interface AttachTriggerProps extends React.ComponentPropsWithoutRef<'button'> {
1148
1224
  asChild?: boolean;
1149
1225
  /** Overrides Root `accept` for files chosen through this trigger */
@@ -1346,13 +1422,14 @@ const Send = React.forwardRef<HTMLButtonElement, SendProps>((props, forwardedRef
1346
1422
  });
1347
1423
  Send.displayName = 'Chatbar.Send';
1348
1424
 
1349
- export { Root, Textarea, InlineStart, InlineEnd, AttachmentsRow, Attachment, AttachTrigger, Row, RowStart, RowEnd, Send };
1425
+ export { Root, Textarea, InlineStart, InlineEnd, AttachmentsRow, Attachment, Item, AttachTrigger, Row, RowStart, RowEnd, Send };
1350
1426
  export type {
1351
1427
  RootProps as ChatbarRootProps,
1352
1428
  TextareaProps as ChatbarTextareaProps,
1353
1429
  InlineSlotProps as ChatbarInlineSlotProps,
1354
1430
  AttachmentsRowProps as ChatbarAttachmentsRowProps,
1355
1431
  AttachmentProps as ChatbarAttachmentProps,
1432
+ ItemProps as ChatbarItemProps,
1356
1433
  AttachTriggerProps as ChatbarAttachTriggerProps,
1357
1434
  RowProps as ChatbarRowProps,
1358
1435
  RowSlotProps as ChatbarRowSlotProps,
@@ -57,6 +57,8 @@
57
57
  @import './text-area.css';
58
58
  @import './text-field.css';
59
59
  @import './theme-panel.css';
60
+ /* AppBar keeps the place its old toolbar.css had: it extends Card and must come after card.css */
61
+ @import './app-bar.css';
60
62
  @import './toolbar.css';
61
63
  @import './tooltip.css';
62
64
  @import './user-card.css';