@oxy.so/bloom 4.19.1 → 4.21.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.
- package/docs/adoption-matrix.mdx +6 -6
- package/docs/ai-chat.mdx +32 -1
- package/docs/button.mdx +1 -1
- package/docs/composer-panel.mdx +4 -0
- package/docs/design-tokens.mdx +22 -0
- package/docs/dialog.mdx +5 -0
- package/lib/commonjs/accordion/Accordion.js +7 -3
- package/lib/commonjs/accordion/Accordion.js.map +1 -1
- package/lib/commonjs/ai-chat/AiChat.js +2 -1
- package/lib/commonjs/ai-chat/AiChat.js.map +1 -1
- package/lib/commonjs/ai-chat/AiChat.web.js +2 -1
- package/lib/commonjs/ai-chat/AiChat.web.js.map +1 -1
- package/lib/commonjs/ai-chat/AiChatControls.js +60 -4
- package/lib/commonjs/ai-chat/AiChatControls.js.map +1 -1
- package/lib/commonjs/ai-chat/AiChatFeedbackRowBase.js +40 -12
- package/lib/commonjs/ai-chat/AiChatFeedbackRowBase.js.map +1 -1
- package/lib/commonjs/ai-chat/AiChatMessages.js +31 -5
- package/lib/commonjs/ai-chat/AiChatMessages.js.map +1 -1
- package/lib/commonjs/ai-chat/bindAiChat.js +10 -0
- package/lib/commonjs/ai-chat/bindAiChat.js.map +1 -1
- package/lib/commonjs/ai-chat/index.js +1 -1
- package/lib/commonjs/ai-chat/index.js.map +1 -1
- package/lib/commonjs/ai-chat/index.web.js +1 -1
- package/lib/commonjs/ai-chat/index.web.js.map +1 -1
- package/lib/commonjs/ai-chat/shared.js +11 -2
- package/lib/commonjs/ai-chat/shared.js.map +1 -1
- package/lib/commonjs/button/Button.js +37 -1
- package/lib/commonjs/button/Button.js.map +1 -1
- package/lib/commonjs/composer-panel/AttachmentStrip.js +108 -8
- package/lib/commonjs/composer-panel/AttachmentStrip.js.map +1 -1
- package/lib/commonjs/composer-panel/ComposerPanelBase.js +6 -2
- package/lib/commonjs/composer-panel/ComposerPanelBase.js.map +1 -1
- package/lib/commonjs/composer-panel/ComposerPillBase.js +2 -1
- package/lib/commonjs/composer-panel/ComposerPillBase.js.map +1 -1
- package/lib/commonjs/composer-panel/shared.js +2 -0
- package/lib/commonjs/composer-panel/shared.js.map +1 -1
- package/lib/commonjs/composer-panel/use-attachment-queue.js +5 -2
- package/lib/commonjs/composer-panel/use-attachment-queue.js.map +1 -1
- package/lib/commonjs/design-tokens/theme-css.js +24 -2
- package/lib/commonjs/design-tokens/theme-css.js.map +1 -1
- package/lib/commonjs/design-tokens/theme.css +8 -8
- package/lib/commonjs/dialog/DialogBottomSheet.js +19 -3
- package/lib/commonjs/dialog/DialogBottomSheet.js.map +1 -1
- package/lib/commonjs/dialog/DialogHeader.js +78 -6
- package/lib/commonjs/dialog/DialogHeader.js.map +1 -1
- package/lib/module/accordion/Accordion.js +7 -3
- package/lib/module/accordion/Accordion.js.map +1 -1
- package/lib/module/ai-chat/AiChat.js +1 -0
- package/lib/module/ai-chat/AiChat.js.map +1 -1
- package/lib/module/ai-chat/AiChat.web.js +1 -0
- package/lib/module/ai-chat/AiChat.web.js.map +1 -1
- package/lib/module/ai-chat/AiChatControls.js +58 -4
- package/lib/module/ai-chat/AiChatControls.js.map +1 -1
- package/lib/module/ai-chat/AiChatFeedbackRowBase.js +41 -13
- package/lib/module/ai-chat/AiChatFeedbackRowBase.js.map +1 -1
- package/lib/module/ai-chat/AiChatMessages.js +30 -4
- package/lib/module/ai-chat/AiChatMessages.js.map +1 -1
- package/lib/module/ai-chat/bindAiChat.js +11 -1
- package/lib/module/ai-chat/bindAiChat.js.map +1 -1
- package/lib/module/ai-chat/index.js +2 -2
- package/lib/module/ai-chat/index.js.map +1 -1
- package/lib/module/ai-chat/index.web.js +2 -2
- package/lib/module/ai-chat/index.web.js.map +1 -1
- package/lib/module/ai-chat/shared.js +11 -2
- package/lib/module/ai-chat/shared.js.map +1 -1
- package/lib/module/button/Button.js +38 -2
- package/lib/module/button/Button.js.map +1 -1
- package/lib/module/composer-panel/AttachmentStrip.js +108 -8
- package/lib/module/composer-panel/AttachmentStrip.js.map +1 -1
- package/lib/module/composer-panel/ComposerPanelBase.js +6 -2
- package/lib/module/composer-panel/ComposerPanelBase.js.map +1 -1
- package/lib/module/composer-panel/ComposerPillBase.js +2 -1
- package/lib/module/composer-panel/ComposerPillBase.js.map +1 -1
- package/lib/module/composer-panel/shared.js +2 -0
- package/lib/module/composer-panel/shared.js.map +1 -1
- package/lib/module/composer-panel/use-attachment-queue.js +5 -2
- package/lib/module/composer-panel/use-attachment-queue.js.map +1 -1
- package/lib/module/design-tokens/theme-css.js +23 -2
- package/lib/module/design-tokens/theme-css.js.map +1 -1
- package/lib/module/design-tokens/theme.css +8 -8
- package/lib/module/dialog/DialogBottomSheet.js +19 -3
- package/lib/module/dialog/DialogBottomSheet.js.map +1 -1
- package/lib/module/dialog/DialogHeader.js +76 -5
- package/lib/module/dialog/DialogHeader.js.map +1 -1
- package/lib/typescript/commonjs/accordion/Accordion.d.ts.map +1 -1
- package/lib/typescript/commonjs/ai-chat/AiChat.d.ts +4 -0
- package/lib/typescript/commonjs/ai-chat/AiChat.d.ts.map +1 -1
- package/lib/typescript/commonjs/ai-chat/AiChat.web.d.ts +4 -0
- package/lib/typescript/commonjs/ai-chat/AiChat.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/ai-chat/AiChatControls.d.ts +22 -1
- package/lib/typescript/commonjs/ai-chat/AiChatControls.d.ts.map +1 -1
- package/lib/typescript/commonjs/ai-chat/AiChatFeedbackRowBase.d.ts +5 -2
- package/lib/typescript/commonjs/ai-chat/AiChatFeedbackRowBase.d.ts.map +1 -1
- package/lib/typescript/commonjs/ai-chat/AiChatMessages.d.ts +1 -1
- package/lib/typescript/commonjs/ai-chat/AiChatMessages.d.ts.map +1 -1
- package/lib/typescript/commonjs/ai-chat/bindAiChat.d.ts +5 -1
- package/lib/typescript/commonjs/ai-chat/bindAiChat.d.ts.map +1 -1
- package/lib/typescript/commonjs/ai-chat/index.d.ts +3 -3
- package/lib/typescript/commonjs/ai-chat/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/ai-chat/index.web.d.ts +3 -3
- package/lib/typescript/commonjs/ai-chat/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/ai-chat/shared.d.ts +2 -2
- package/lib/typescript/commonjs/ai-chat/shared.d.ts.map +1 -1
- package/lib/typescript/commonjs/ai-chat/types.d.ts +43 -5
- package/lib/typescript/commonjs/ai-chat/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/button/Button.d.ts.map +1 -1
- package/lib/typescript/commonjs/composer-panel/AttachmentStrip.d.ts +10 -1
- package/lib/typescript/commonjs/composer-panel/AttachmentStrip.d.ts.map +1 -1
- package/lib/typescript/commonjs/composer-panel/ComposerPanelBase.d.ts +1 -1
- package/lib/typescript/commonjs/composer-panel/ComposerPanelBase.d.ts.map +1 -1
- package/lib/typescript/commonjs/composer-panel/shared.d.ts +3 -0
- package/lib/typescript/commonjs/composer-panel/shared.d.ts.map +1 -1
- package/lib/typescript/commonjs/composer-panel/types.d.ts +11 -0
- package/lib/typescript/commonjs/composer-panel/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/composer-panel/use-attachment-queue.d.ts.map +1 -1
- package/lib/typescript/commonjs/design-tokens/theme-css.d.ts +9 -1
- package/lib/typescript/commonjs/design-tokens/theme-css.d.ts.map +1 -1
- package/lib/typescript/commonjs/dialog/DialogBottomSheet.d.ts.map +1 -1
- package/lib/typescript/commonjs/dialog/DialogHeader.d.ts +24 -0
- package/lib/typescript/commonjs/dialog/DialogHeader.d.ts.map +1 -1
- package/lib/typescript/module/accordion/Accordion.d.ts.map +1 -1
- package/lib/typescript/module/ai-chat/AiChat.d.ts +4 -0
- package/lib/typescript/module/ai-chat/AiChat.d.ts.map +1 -1
- package/lib/typescript/module/ai-chat/AiChat.web.d.ts +4 -0
- package/lib/typescript/module/ai-chat/AiChat.web.d.ts.map +1 -1
- package/lib/typescript/module/ai-chat/AiChatControls.d.ts +22 -1
- package/lib/typescript/module/ai-chat/AiChatControls.d.ts.map +1 -1
- package/lib/typescript/module/ai-chat/AiChatFeedbackRowBase.d.ts +5 -2
- package/lib/typescript/module/ai-chat/AiChatFeedbackRowBase.d.ts.map +1 -1
- package/lib/typescript/module/ai-chat/AiChatMessages.d.ts +1 -1
- package/lib/typescript/module/ai-chat/AiChatMessages.d.ts.map +1 -1
- package/lib/typescript/module/ai-chat/bindAiChat.d.ts +5 -1
- package/lib/typescript/module/ai-chat/bindAiChat.d.ts.map +1 -1
- package/lib/typescript/module/ai-chat/index.d.ts +3 -3
- package/lib/typescript/module/ai-chat/index.d.ts.map +1 -1
- package/lib/typescript/module/ai-chat/index.web.d.ts +3 -3
- package/lib/typescript/module/ai-chat/index.web.d.ts.map +1 -1
- package/lib/typescript/module/ai-chat/shared.d.ts +2 -2
- package/lib/typescript/module/ai-chat/shared.d.ts.map +1 -1
- package/lib/typescript/module/ai-chat/types.d.ts +43 -5
- package/lib/typescript/module/ai-chat/types.d.ts.map +1 -1
- package/lib/typescript/module/button/Button.d.ts.map +1 -1
- package/lib/typescript/module/composer-panel/AttachmentStrip.d.ts +10 -1
- package/lib/typescript/module/composer-panel/AttachmentStrip.d.ts.map +1 -1
- package/lib/typescript/module/composer-panel/ComposerPanelBase.d.ts +1 -1
- package/lib/typescript/module/composer-panel/ComposerPanelBase.d.ts.map +1 -1
- package/lib/typescript/module/composer-panel/shared.d.ts +3 -0
- package/lib/typescript/module/composer-panel/shared.d.ts.map +1 -1
- package/lib/typescript/module/composer-panel/types.d.ts +11 -0
- package/lib/typescript/module/composer-panel/types.d.ts.map +1 -1
- package/lib/typescript/module/composer-panel/use-attachment-queue.d.ts.map +1 -1
- package/lib/typescript/module/design-tokens/theme-css.d.ts +9 -1
- package/lib/typescript/module/design-tokens/theme-css.d.ts.map +1 -1
- package/lib/typescript/module/dialog/DialogBottomSheet.d.ts.map +1 -1
- package/lib/typescript/module/dialog/DialogHeader.d.ts +24 -0
- package/lib/typescript/module/dialog/DialogHeader.d.ts.map +1 -1
- package/package.json +5 -2
- package/src/accordion/Accordion.tsx +9 -1
- package/src/ai-chat/AiChat.tsx +1 -0
- package/src/ai-chat/AiChat.web.tsx +1 -0
- package/src/ai-chat/AiChatControls.tsx +58 -3
- package/src/ai-chat/AiChatFeedbackRowBase.tsx +45 -17
- package/src/ai-chat/AiChatMessages.tsx +24 -1
- package/src/ai-chat/bindAiChat.tsx +19 -2
- package/src/ai-chat/index.ts +3 -1
- package/src/ai-chat/index.web.ts +3 -1
- package/src/ai-chat/shared.ts +11 -2
- package/src/ai-chat/types.ts +45 -5
- package/src/button/Button.tsx +41 -3
- package/src/composer-panel/AttachmentStrip.tsx +98 -6
- package/src/composer-panel/ComposerPanelBase.tsx +4 -0
- package/src/composer-panel/ComposerPillBase.tsx +1 -0
- package/src/composer-panel/shared.ts +5 -0
- package/src/composer-panel/types.ts +11 -0
- package/src/composer-panel/use-attachment-queue.ts +5 -2
- package/src/design-tokens/theme-css.ts +23 -2
- package/src/design-tokens/theme.css +8 -8
- package/src/dialog/DialogBottomSheet.tsx +21 -0
- package/src/dialog/DialogHeader.tsx +94 -11
|
@@ -10,6 +10,8 @@ import Animated, {
|
|
|
10
10
|
import Svg, { Path } from 'react-native-svg';
|
|
11
11
|
|
|
12
12
|
import { mixColor } from '../button/shared';
|
|
13
|
+
import { RiErrorWarningLine } from '../icons/remix/RiErrorWarningLine';
|
|
14
|
+
import { RiRefreshLine } from '../icons/remix/RiRefreshLine';
|
|
13
15
|
import { Text } from '../typography';
|
|
14
16
|
import type { WebCssStyle } from '../styles/web-view-style';
|
|
15
17
|
import {
|
|
@@ -101,6 +103,41 @@ function TileDismiss({
|
|
|
101
103
|
);
|
|
102
104
|
}
|
|
103
105
|
|
|
106
|
+
/**
|
|
107
|
+
* A failed tile's retry: a 24px disc on the card surface where the kind glyph
|
|
108
|
+
* sits (3, 4), a 14px refresh glyph in the error foreground, text-primary on
|
|
109
|
+
* hover.
|
|
110
|
+
*/
|
|
111
|
+
function TileRetry({ label, onPress, palette }: { label: string; onPress: () => void; palette: ComposerPalette }) {
|
|
112
|
+
const [hovered, setHovered] = useState(false);
|
|
113
|
+
const style: WebCssStyle = {
|
|
114
|
+
position: 'absolute',
|
|
115
|
+
top: 3,
|
|
116
|
+
left: 3,
|
|
117
|
+
width: 24,
|
|
118
|
+
height: 24,
|
|
119
|
+
borderRadius: 12,
|
|
120
|
+
alignItems: 'center',
|
|
121
|
+
justifyContent: 'center',
|
|
122
|
+
backgroundColor: palette.surface,
|
|
123
|
+
cursor: 'pointer',
|
|
124
|
+
'--bloom-composer-ring': palette.focusRing,
|
|
125
|
+
};
|
|
126
|
+
return (
|
|
127
|
+
<Pressable
|
|
128
|
+
{...dataHook('bloomComposerControl', 'offset')}
|
|
129
|
+
accessibilityRole="button"
|
|
130
|
+
accessibilityLabel={label}
|
|
131
|
+
onPress={onPress}
|
|
132
|
+
onHoverIn={() => setHovered(true)}
|
|
133
|
+
onHoverOut={() => setHovered(false)}
|
|
134
|
+
hitSlop={4}
|
|
135
|
+
style={style}>
|
|
136
|
+
<RiRefreshLine width={14} height={14} fill={hovered ? palette.text : palette.errorText} />
|
|
137
|
+
</Pressable>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
104
141
|
/** The upload percentage's box: top 3 / right 3, blurring out as it fades. */
|
|
105
142
|
function percentStyle(inFlight: boolean): WebCssStyle {
|
|
106
143
|
return {
|
|
@@ -123,7 +160,9 @@ interface TileProps {
|
|
|
123
160
|
attachment: ComposerPanelAttachment;
|
|
124
161
|
palette: ComposerPalette;
|
|
125
162
|
onRemove?: () => void;
|
|
163
|
+
onRetry?: () => void;
|
|
126
164
|
removeLabel: string;
|
|
165
|
+
retryLabel: string;
|
|
127
166
|
}
|
|
128
167
|
|
|
129
168
|
/**
|
|
@@ -134,9 +173,23 @@ interface TileProps {
|
|
|
134
173
|
* percentage (9/16 medium, tabular; white over a photo, accent-500 otherwise)
|
|
135
174
|
* sits at top 3 / right 3; once landed it blurs out as the dismiss blurs in on
|
|
136
175
|
* the same spot.
|
|
176
|
+
*
|
|
177
|
+
* A FAILED tile (`error`) drops the ring and percentage and fills with the
|
|
178
|
+
* error subtle surface under an error-foreground border: the warning glyph (or
|
|
179
|
+
* the retry button) at (3, 4) and the message in 9/11 medium, two lines at
|
|
180
|
+
* most, from (5, 30). The dismiss shows as on a landed tile.
|
|
137
181
|
*/
|
|
138
|
-
export const AttachmentTile = memo(function AttachmentTile({
|
|
139
|
-
|
|
182
|
+
export const AttachmentTile = memo(function AttachmentTile({
|
|
183
|
+
attachment,
|
|
184
|
+
palette,
|
|
185
|
+
onRemove,
|
|
186
|
+
onRetry,
|
|
187
|
+
removeLabel,
|
|
188
|
+
retryLabel,
|
|
189
|
+
}: TileProps) {
|
|
190
|
+
const { error } = attachment;
|
|
191
|
+
const failed = !!error;
|
|
192
|
+
const progress = failed ? undefined : attachment.progress;
|
|
140
193
|
const inFlight = progress !== undefined;
|
|
141
194
|
const image = attachment.kind === 'image' ? attachment.src : undefined;
|
|
142
195
|
const dash = inFlight && progress < 100 ? (Math.max(0, progress) / 100) * TILE_RING_LENGTH : TILE_RING_LENGTH + 2;
|
|
@@ -144,8 +197,8 @@ export const AttachmentTile = memo(function AttachmentTile({ attachment, palette
|
|
|
144
197
|
|
|
145
198
|
return (
|
|
146
199
|
<View
|
|
147
|
-
accessibilityLabel={attachment.name}
|
|
148
|
-
{...(IS_WEB ? { title: attachment.name } : {})}
|
|
200
|
+
accessibilityLabel={failed ? `${attachment.name}: ${error}` : attachment.name}
|
|
201
|
+
{...(IS_WEB ? { title: failed ? `${attachment.name}: ${error}` : attachment.name } : {})}
|
|
149
202
|
style={{
|
|
150
203
|
position: 'relative',
|
|
151
204
|
width: TILE,
|
|
@@ -153,7 +206,7 @@ export const AttachmentTile = memo(function AttachmentTile({ attachment, palette
|
|
|
153
206
|
flexShrink: 0,
|
|
154
207
|
borderRadius: TILE_RADIUS,
|
|
155
208
|
borderWidth: 1,
|
|
156
|
-
borderColor: palette.tileBorder,
|
|
209
|
+
borderColor: failed ? palette.errorText : palette.tileBorder,
|
|
157
210
|
}}>
|
|
158
211
|
{image ? (
|
|
159
212
|
<View
|
|
@@ -196,6 +249,41 @@ export const AttachmentTile = memo(function AttachmentTile({ attachment, palette
|
|
|
196
249
|
</View>
|
|
197
250
|
)}
|
|
198
251
|
|
|
252
|
+
{failed ? (
|
|
253
|
+
<View
|
|
254
|
+
style={{
|
|
255
|
+
position: 'absolute',
|
|
256
|
+
top: 0,
|
|
257
|
+
left: 0,
|
|
258
|
+
right: 0,
|
|
259
|
+
bottom: 0,
|
|
260
|
+
borderRadius: TILE_RADIUS - 1,
|
|
261
|
+
backgroundColor: palette.errorSurface,
|
|
262
|
+
}}>
|
|
263
|
+
{onRetry ? null : (
|
|
264
|
+
<View style={{ position: 'absolute', top: 4, left: 3, width: 24, height: 24, alignItems: 'center', justifyContent: 'center' }}>
|
|
265
|
+
<RiErrorWarningLine width={20} height={20} fill={palette.errorText} />
|
|
266
|
+
</View>
|
|
267
|
+
)}
|
|
268
|
+
<Text
|
|
269
|
+
numberOfLines={2}
|
|
270
|
+
style={{
|
|
271
|
+
position: 'absolute',
|
|
272
|
+
top: 30,
|
|
273
|
+
left: 5,
|
|
274
|
+
right: 5,
|
|
275
|
+
// 9px has no type-ramp step; spelled out literally.
|
|
276
|
+
fontSize: 9,
|
|
277
|
+
lineHeight: 11,
|
|
278
|
+
fontWeight: '500',
|
|
279
|
+
color: palette.errorText,
|
|
280
|
+
}}>
|
|
281
|
+
{error}
|
|
282
|
+
</Text>
|
|
283
|
+
{onRetry ? <TileRetry label={`${retryLabel} ${attachment.name}`} onPress={onRetry} palette={palette} /> : null}
|
|
284
|
+
</View>
|
|
285
|
+
) : null}
|
|
286
|
+
|
|
199
287
|
{/* Upload ring over the border: the svg spans the border box. */}
|
|
200
288
|
<View
|
|
201
289
|
pointerEvents="none"
|
|
@@ -288,7 +376,9 @@ interface StripProps {
|
|
|
288
376
|
attachments: ReadonlyArray<ComposerPanelAttachment>;
|
|
289
377
|
palette: ComposerPalette;
|
|
290
378
|
onRemove?: (id: string) => void;
|
|
379
|
+
onRetry?: (id: string) => void;
|
|
291
380
|
removeLabel: string;
|
|
381
|
+
retryLabel: string;
|
|
292
382
|
}
|
|
293
383
|
|
|
294
384
|
/**
|
|
@@ -296,7 +386,7 @@ interface StripProps {
|
|
|
296
386
|
* scale 0.8 → 1, fade and un-blur on arrival over 280ms `cubic-bezier(0.22, 1,
|
|
297
387
|
* 0.36, 1)`, and play it backwards on dismissal before leaving the row.
|
|
298
388
|
*/
|
|
299
|
-
export function AttachmentStrip({ attachments, palette, onRemove, removeLabel }: StripProps) {
|
|
389
|
+
export function AttachmentStrip({ attachments, palette, onRemove, onRetry, removeLabel, retryLabel }: StripProps) {
|
|
300
390
|
const [leaving, setLeaving] = useState<ReadonlyArray<ComposerPanelAttachment>>([]);
|
|
301
391
|
const previous = useRef(attachments);
|
|
302
392
|
|
|
@@ -330,7 +420,9 @@ export function AttachmentStrip({ attachments, palette, onRemove, removeLabel }:
|
|
|
330
420
|
attachment={attachment}
|
|
331
421
|
palette={palette}
|
|
332
422
|
removeLabel={removeLabel}
|
|
423
|
+
retryLabel={retryLabel}
|
|
333
424
|
onRemove={onRemove && !isLeaving ? () => onRemove(attachment.id) : undefined}
|
|
425
|
+
onRetry={onRetry && !isLeaving ? () => onRetry(attachment.id) : undefined}
|
|
334
426
|
/>
|
|
335
427
|
</PresenceTile>
|
|
336
428
|
))}
|
|
@@ -50,6 +50,7 @@ const DEFAULT_LABELS: Required<ComposerPanelLabels> = {
|
|
|
50
50
|
send: 'Send message',
|
|
51
51
|
stop: 'Stop generating',
|
|
52
52
|
remove: 'Remove',
|
|
53
|
+
retry: 'Retry',
|
|
53
54
|
};
|
|
54
55
|
|
|
55
56
|
/**
|
|
@@ -143,6 +144,7 @@ export function ComposerPanelBase({
|
|
|
143
144
|
onListeningChange,
|
|
144
145
|
attachments,
|
|
145
146
|
onRemoveAttachment,
|
|
147
|
+
onAttachmentRetry,
|
|
146
148
|
status,
|
|
147
149
|
emptyAction,
|
|
148
150
|
onKeyPress: onKeyPressProp,
|
|
@@ -235,7 +237,9 @@ export function ComposerPanelBase({
|
|
|
235
237
|
attachments={attachments ?? []}
|
|
236
238
|
palette={palette}
|
|
237
239
|
onRemove={onRemoveAttachment}
|
|
240
|
+
onRetry={onAttachmentRetry}
|
|
238
241
|
removeLabel={labels.remove}
|
|
242
|
+
retryLabel={labels.retry}
|
|
239
243
|
/>
|
|
240
244
|
</View>
|
|
241
245
|
</Collapse>
|
|
@@ -74,6 +74,9 @@ export interface ComposerPalette {
|
|
|
74
74
|
accent500: string;
|
|
75
75
|
/** `border-focus-ring`. */
|
|
76
76
|
focusRing: string;
|
|
77
|
+
/** A failed tile: the error subtle surface and its paired foreground. */
|
|
78
|
+
errorSurface: string;
|
|
79
|
+
errorText: string;
|
|
77
80
|
/** `shadow-xs`, `shadow-dropdown` and the card's raw Figma shadow. */
|
|
78
81
|
shadowXs: string;
|
|
79
82
|
shadowDropdown: string;
|
|
@@ -121,6 +124,8 @@ export function resolveComposerPalette(theme: Theme): ComposerPalette {
|
|
|
121
124
|
accent400: c.primary,
|
|
122
125
|
accent500: c.primarySubtleForeground,
|
|
123
126
|
focusRing: c.primary,
|
|
127
|
+
errorSurface: c.errorSubtle,
|
|
128
|
+
errorText: c.errorSubtleForeground,
|
|
124
129
|
shadowXs: dark ? BUTTON_SHADOW.dark : BUTTON_SHADOW.light,
|
|
125
130
|
shadowDropdown: dark ? MENU_SHADOW.dark : MENU_SHADOW.light,
|
|
126
131
|
shadowCard: '0 1px 0.5px 0 rgba(0, 0, 0, 0.02), 0 4px 2px 0 rgba(0, 0, 0, 0.02)',
|
|
@@ -80,6 +80,13 @@ export interface ComposerPanelAttachment {
|
|
|
80
80
|
* once the file has landed — that also reveals the dismiss.
|
|
81
81
|
*/
|
|
82
82
|
progress?: number;
|
|
83
|
+
/**
|
|
84
|
+
* The upload failed, and why ("Too large", "Network error"). The tile turns
|
|
85
|
+
* to the error surface and shows this in place of the name; the ring and the
|
|
86
|
+
* percentage go (any `progress` is ignored) and the dismiss comes back. With
|
|
87
|
+
* `ComposerPanel`'s `onAttachmentRetry` it also gets a retry button.
|
|
88
|
+
*/
|
|
89
|
+
error?: string;
|
|
83
90
|
}
|
|
84
91
|
|
|
85
92
|
// ---------------------------------------------------------------------------
|
|
@@ -195,6 +202,8 @@ export interface ComposerPanelLabels {
|
|
|
195
202
|
stop?: string;
|
|
196
203
|
/** Default `'Remove'` — prefixed to the file name on a tile's dismiss. */
|
|
197
204
|
remove?: string;
|
|
205
|
+
/** Default `'Retry'` — prefixed to the file name on a failed tile's retry. */
|
|
206
|
+
retry?: string;
|
|
198
207
|
}
|
|
199
208
|
|
|
200
209
|
export interface ComposerPanelProps {
|
|
@@ -256,6 +265,8 @@ export interface ComposerPanelProps {
|
|
|
256
265
|
/** Tiles above the prompt. Each one's `progress` drives its upload ring. */
|
|
257
266
|
attachments?: ReadonlyArray<ComposerPanelAttachment>;
|
|
258
267
|
onRemoveAttachment?: (id: string) => void;
|
|
268
|
+
/** Draws a retry button on every tile with an `error`; called with its id. */
|
|
269
|
+
onAttachmentRetry?: (id: string) => void;
|
|
259
270
|
|
|
260
271
|
/** The tab on the card's top edge — typically `<ComposerPanelStatusTab />`. */
|
|
261
272
|
status?: ReactNode;
|
|
@@ -4,8 +4,11 @@ import type { ComposerAttachmentsProps, ComposerPanelAttachment } from './types'
|
|
|
4
4
|
|
|
5
5
|
const TICK_MS = 50;
|
|
6
6
|
|
|
7
|
-
/**
|
|
8
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Anything carrying a progress value — the closed ring at 100 included — is
|
|
9
|
+
* still the queue's, unless it failed: a failed file is done with.
|
|
10
|
+
*/
|
|
11
|
+
const inFlight = (attachment: ComposerPanelAttachment) => attachment.progress !== undefined && !attachment.error;
|
|
9
12
|
|
|
10
13
|
/** Keep the parent's order; keep our progress for files we already know. */
|
|
11
14
|
function reconcile(
|
|
@@ -35,6 +35,17 @@ import {
|
|
|
35
35
|
} from './scales';
|
|
36
36
|
import { SHADOW_BOX } from './shadows';
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* A px line-height as the unitless ratio of its font-size, spelled the way
|
|
40
|
+
* Tailwind spells its own type scale: `lineHeightRatio(22, 15)` → `calc(22 / 15)`.
|
|
41
|
+
* Exact on both platforms (no rounded decimal), and the only form
|
|
42
|
+
* react-native-css resolves correctly through `var()` — it multiplies any
|
|
43
|
+
* number by the font-size.
|
|
44
|
+
*/
|
|
45
|
+
export function lineHeightRatio(lineHeightPx: number, fontSizePx: number): string {
|
|
46
|
+
return `calc(${lineHeightPx} / ${fontSizePx})`;
|
|
47
|
+
}
|
|
48
|
+
|
|
38
49
|
/**
|
|
39
50
|
* Produce the BODY of a Tailwind v4 `@theme` block (no surrounding braces) that
|
|
40
51
|
* registers the full Bloom semantic vocabulary.
|
|
@@ -42,7 +53,7 @@ import { SHADOW_BOX } from './shadows';
|
|
|
42
53
|
* - Colors → `--color-<role>: var(--canonical);`
|
|
43
54
|
* - Spacing → `--spacing-<key>: <px>;`
|
|
44
55
|
* - Radius → `--radius-<key>: <px>;`
|
|
45
|
-
* - Type → `--text-<role>: <size>; --text-<role>--line-height: <lh
|
|
56
|
+
* - Type → `--text-<role>: <size>; --text-<role>--line-height: calc(<lh> / <size>);`
|
|
46
57
|
* `--font-<role>: var(--bloom-font-*);` (+ weight via utility doc)
|
|
47
58
|
* - Shadow → `--shadow-<s|m>: <box-shadow>;`
|
|
48
59
|
*/
|
|
@@ -94,10 +105,20 @@ export function bloomThemeCss(): string {
|
|
|
94
105
|
}
|
|
95
106
|
|
|
96
107
|
// Typography: size + line-height + family per role.
|
|
108
|
+
//
|
|
109
|
+
// The line-height is a UNITLESS ratio (`calc(22 / 15)`), the form Tailwind's
|
|
110
|
+
// own `--text-sm--line-height: calc(1.25 / 0.875)` takes — never px. On the
|
|
111
|
+
// web the two are identical (a unitless line-height is a multiple of the
|
|
112
|
+
// font-size: 15px × 22/15 = 22px). On native they are not: Tailwind v4 emits
|
|
113
|
+
// `line-height: var(--tw-leading, var(--text-body--line-height))`,
|
|
114
|
+
// react-native-css compiles that to `lineHeight(var(…, 22))`, and its runtime
|
|
115
|
+
// `lineHeight` resolver treats EVERY number as an em multiplier — so a px
|
|
116
|
+
// token rendered `text-body` at 22 × 15 = 330dp line-height on Android and
|
|
117
|
+
// iOS. See `lineHeightRatio` and `docs/typography.mdx`.
|
|
97
118
|
for (const name of Object.keys(TYPOGRAPHY) as TypeRoleName[]) {
|
|
98
119
|
const role = TYPOGRAPHY[name];
|
|
99
120
|
lines.push(` --text-${name}: ${role.size}px;`);
|
|
100
|
-
lines.push(` --text-${name}--line-height: ${role.lineHeight}
|
|
121
|
+
lines.push(` --text-${name}--line-height: ${lineHeightRatio(role.lineHeight, role.size)};`);
|
|
101
122
|
lines.push(` --text-${name}--font-weight: ${role.weight};`);
|
|
102
123
|
lines.push(` --font-${name}: ${FONT_FAMILY_VARS[role.family]};`);
|
|
103
124
|
}
|
|
@@ -124,35 +124,35 @@
|
|
|
124
124
|
--radius-radius-max: 9999px;
|
|
125
125
|
--border-width-hairline: 0.5px;
|
|
126
126
|
--text-caption: 11px;
|
|
127
|
-
--text-caption--line-height:
|
|
127
|
+
--text-caption--line-height: calc(14 / 11);
|
|
128
128
|
--text-caption--font-weight: 400;
|
|
129
129
|
--font-caption: var(--bloom-font-sans);
|
|
130
130
|
--text-bodySmall: 13px;
|
|
131
|
-
--text-bodySmall--line-height:
|
|
131
|
+
--text-bodySmall--line-height: calc(18 / 13);
|
|
132
132
|
--text-bodySmall--font-weight: 400;
|
|
133
133
|
--font-bodySmall: var(--bloom-font-sans);
|
|
134
134
|
--text-bodyTitleSmall: 13px;
|
|
135
|
-
--text-bodyTitleSmall--line-height:
|
|
135
|
+
--text-bodyTitleSmall--line-height: calc(18 / 13);
|
|
136
136
|
--text-bodyTitleSmall--font-weight: 600;
|
|
137
137
|
--font-bodyTitleSmall: var(--bloom-font-sans);
|
|
138
138
|
--text-body: 15px;
|
|
139
|
-
--text-body--line-height:
|
|
139
|
+
--text-body--line-height: calc(22 / 15);
|
|
140
140
|
--text-body--font-weight: 400;
|
|
141
141
|
--font-body: var(--bloom-font-sans);
|
|
142
142
|
--text-subtitle: 17px;
|
|
143
|
-
--text-subtitle--line-height:
|
|
143
|
+
--text-subtitle--line-height: calc(24 / 17);
|
|
144
144
|
--text-subtitle--font-weight: 500;
|
|
145
145
|
--font-subtitle: var(--bloom-font-sans);
|
|
146
146
|
--text-sectionTitle: 20px;
|
|
147
|
-
--text-sectionTitle--line-height:
|
|
147
|
+
--text-sectionTitle--line-height: calc(26 / 20);
|
|
148
148
|
--text-sectionTitle--font-weight: 600;
|
|
149
149
|
--font-sectionTitle: var(--bloom-font-sans);
|
|
150
150
|
--text-headerBold: 28px;
|
|
151
|
-
--text-headerBold--line-height:
|
|
151
|
+
--text-headerBold--line-height: calc(34 / 28);
|
|
152
152
|
--text-headerBold--font-weight: 700;
|
|
153
153
|
--font-headerBold: var(--bloom-font-display);
|
|
154
154
|
--text-buttonLarge: 17px;
|
|
155
|
-
--text-buttonLarge--line-height:
|
|
155
|
+
--text-buttonLarge--line-height: calc(22 / 17);
|
|
156
156
|
--text-buttonLarge--font-weight: 600;
|
|
157
157
|
--font-buttonLarge: var(--bloom-font-sans);
|
|
158
158
|
--font-bloom-sans: var(--bloom-font-sans);
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
type ViewStyle,
|
|
14
14
|
} from 'react-native';
|
|
15
15
|
import { type AnimatedStyle } from 'react-native-reanimated';
|
|
16
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
16
17
|
|
|
17
18
|
import { BottomSheet, type BottomSheetRef } from '../bottom-sheet';
|
|
18
19
|
import { useTheme } from '../theme/use-theme';
|
|
@@ -86,6 +87,11 @@ export function DialogBottomSheet({
|
|
|
86
87
|
const titleId = useId();
|
|
87
88
|
const descriptionId = useId();
|
|
88
89
|
const headerController = useDialogHeaderController();
|
|
90
|
+
// The sheet is flush with the bottom of the window (a `<Modal>` drawn
|
|
91
|
+
// edge-to-edge on Android, and under the home indicator on iOS), and
|
|
92
|
+
// `BottomSheet` deliberately leaves the safe area to its content. This Dialog
|
|
93
|
+
// IS the content's chrome, so it owns the inset (the spacer after the body).
|
|
94
|
+
const { bottom: bottomInset } = useSafeAreaInsets();
|
|
89
95
|
|
|
90
96
|
// Read the latest controlled flag / `onClose` inside stable callbacks without
|
|
91
97
|
// re-binding them (the context + imperative handle depend on `close` staying
|
|
@@ -325,12 +331,27 @@ export function DialogBottomSheet({
|
|
|
325
331
|
<DialogMorphContent morph={morphState} style={scrollableResolved ? undefined : FILL_BOUNDED}>
|
|
326
332
|
{header ? headerBody : dialogBody}
|
|
327
333
|
</DialogMorphContent>
|
|
334
|
+
{/* The bottom safe area, INSIDE the scroll content. Without it the
|
|
335
|
+
last row of every bottom-placement Dialog was painted under the
|
|
336
|
+
Android gesture bar / iOS home indicator (a 2400px Pixel put a
|
|
337
|
+
"Having trouble?" link at 2297–2356px), and a body that outgrew
|
|
338
|
+
the sheet could not scroll its last rows out from under the bar —
|
|
339
|
+
the scroll range ended exactly where the bar began. A spacer
|
|
340
|
+
rather than a padding: the content box keeps the caller's
|
|
341
|
+
`padding`/`contentPadding` untouched on both platforms, and the
|
|
342
|
+
morph layer above still measures the content alone. */}
|
|
343
|
+
{bottomInset > 0 ? (
|
|
344
|
+
<View testID={SAFE_AREA_SPACER_TESTID} style={{ height: bottomInset }} />
|
|
345
|
+
) : null}
|
|
328
346
|
</StyledView>
|
|
329
347
|
</Context.Provider>
|
|
330
348
|
</BottomSheet>
|
|
331
349
|
);
|
|
332
350
|
}
|
|
333
351
|
|
|
352
|
+
/** Test handle for the bottom safe-area spacer. */
|
|
353
|
+
const SAFE_AREA_SPACER_TESTID = 'dialog-sheet-safe-area';
|
|
354
|
+
|
|
334
355
|
/** Passes a bounded height down to a `scrollable={false}` body. */
|
|
335
356
|
const FILL_BOUNDED = { flex: 1, minHeight: 0 } as const;
|
|
336
357
|
|
|
@@ -83,6 +83,52 @@ const HEADER_H_PADDING = 20;
|
|
|
83
83
|
/** Circular icon-button diameter for the default back / close affordances. */
|
|
84
84
|
const NAV_BUTTON_SIZE = 36;
|
|
85
85
|
|
|
86
|
+
/** The large title's line box (px) — `styles.largeTitle.lineHeight`. */
|
|
87
|
+
const LARGE_TITLE_LINE_HEIGHT = 38;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Content top inset (px) for an INLINE-CLOSE header (see {@link isInlineCloseHeader}):
|
|
91
|
+
* the large title's first line is centred on the nav row, level with the ✕,
|
|
92
|
+
* instead of starting below the whole bar.
|
|
93
|
+
*/
|
|
94
|
+
export const DIALOG_HEADER_INLINE_CONTENT_TOP = (DIALOG_NAV_BAR_HEIGHT - LARGE_TITLE_LINE_HEIGHT) / 2;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Space (px) the large title's first line keeps clear at its trailing edge for
|
|
98
|
+
* the ✕ it shares a row with: the nav row's gutter + the button + an 8px gap,
|
|
99
|
+
* less the title block's own gutter.
|
|
100
|
+
*/
|
|
101
|
+
const INLINE_CLOSE_TITLE_RESERVE = HEADER_H_PADDING - 4 + NAV_BUTTON_SIZE + 8 - HEADER_H_PADDING;
|
|
102
|
+
|
|
103
|
+
/** Scroll distance (px) over which the bar's scrim fades in on an inline-close header. */
|
|
104
|
+
const INLINE_SCRIM_FADE_DISTANCE = 16;
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Whether a large-title header puts its title IN the nav row, beside the ✕,
|
|
108
|
+
* rather than in a band below the bar.
|
|
109
|
+
*
|
|
110
|
+
* The nav row exists for its controls. When it would hold nothing but the
|
|
111
|
+
* default close — no back, no leading slot, no trailing actions, no custom
|
|
112
|
+
* trailing node — a full 72px row above the title is an empty band on every
|
|
113
|
+
* entry view of a flow (an account dialog's first screen, a one-page sheet).
|
|
114
|
+
* The title then starts level with the ✕ and wraps before it; the bar's scrim
|
|
115
|
+
* and the small collapsed title still arrive once the title scrolls under it.
|
|
116
|
+
*
|
|
117
|
+
* Anything that gives the row content of its own keeps the stacked layout: a
|
|
118
|
+
* back button (the title would sit between two buttons), rich trailing actions
|
|
119
|
+
* or a custom `right` (unknown width), a branded `titleContent` (no large title
|
|
120
|
+
* at all), and `tone: 'onImage'` (the scrim is what makes the chrome legible
|
|
121
|
+
* over media, so it cannot start hidden).
|
|
122
|
+
*/
|
|
123
|
+
export function isInlineCloseHeader(config: DialogHeaderConfig): boolean {
|
|
124
|
+
const hasLargeTitle = !config.titleContent && (config.largeTitle ?? true) && !!config.title;
|
|
125
|
+
if (!hasLargeTitle) return false;
|
|
126
|
+
if (config.tone === 'onImage') return false;
|
|
127
|
+
if (config.left || config.onBack || config.right) return false;
|
|
128
|
+
if (config.actions?.length || config.primaryAction) return false;
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
|
|
86
132
|
/**
|
|
87
133
|
* Trailing icon `actions` shown inline before they collapse into a "more"
|
|
88
134
|
* overflow menu (Material overflow pattern). Kept small so the nav row never
|
|
@@ -473,6 +519,17 @@ export const DialogNavHeader = memo(function DialogNavHeader({
|
|
|
473
519
|
// so there is nothing to collapse and no large in-content title to collapse under.
|
|
474
520
|
const hasLargeTitle =
|
|
475
521
|
collapse && !config.titleContent && (config.largeTitle ?? true) && !!config.title;
|
|
522
|
+
// The large title shares the nav row with the ✕ at rest (`isInlineCloseHeader`),
|
|
523
|
+
// so the scrim — opaque at the top — must not paint over it until content
|
|
524
|
+
// actually scrolls under the bar. Only a collapsing bar has an in-content title.
|
|
525
|
+
const inlineClose = collapse && isInlineCloseHeader(config);
|
|
526
|
+
|
|
527
|
+
const scrimStyle = useAnimatedStyle(() => {
|
|
528
|
+
if (!inlineClose) return { opacity: 1 };
|
|
529
|
+
return {
|
|
530
|
+
opacity: interpolate(scrollY.value, [0, INLINE_SCRIM_FADE_DISTANCE], [0, 1], Extrapolation.CLAMP),
|
|
531
|
+
};
|
|
532
|
+
}, [scrollY, inlineClose]);
|
|
476
533
|
|
|
477
534
|
const titleStyle = useAnimatedStyle(() => {
|
|
478
535
|
// When there is no large title to collapse under, the bar title is always
|
|
@@ -548,14 +605,23 @@ export const DialogNavHeader = memo(function DialogNavHeader({
|
|
|
548
605
|
{/* Opaque (surface bg) at the top → transparent at the bottom, so scrolled
|
|
549
606
|
content fades out under the bar. `onImage` swaps to a dark scrim so the
|
|
550
607
|
chrome reads over media. Pure NativeWind — no SVG dependency. */}
|
|
551
|
-
<View
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
608
|
+
<Animated.View
|
|
609
|
+
testID="dialog-nav-scrim"
|
|
610
|
+
style={[
|
|
611
|
+
StyleSheet.absoluteFill,
|
|
612
|
+
{ height: DIALOG_HEADER_OVERLAY_HEIGHT, pointerEvents: 'none' },
|
|
613
|
+
scrimStyle,
|
|
614
|
+
]}
|
|
615
|
+
>
|
|
616
|
+
<View
|
|
617
|
+
className={
|
|
618
|
+
onImage
|
|
619
|
+
? 'bg-gradient-to-b from-black/60 to-transparent'
|
|
620
|
+
: 'bg-gradient-to-b from-bg to-transparent'
|
|
621
|
+
}
|
|
622
|
+
style={StyleSheet.absoluteFill}
|
|
623
|
+
/>
|
|
624
|
+
</Animated.View>
|
|
559
625
|
<View pointerEvents="box-none" style={styles.navRow}>
|
|
560
626
|
<View style={styles.side}>{left}</View>
|
|
561
627
|
<Animated.View
|
|
@@ -625,14 +691,31 @@ export const DialogLargeTitle = memo(function DialogLargeTitle({
|
|
|
625
691
|
return onImage ? null : <View style={{ height: DIALOG_NAV_BAR_HEIGHT }} />;
|
|
626
692
|
}
|
|
627
693
|
|
|
694
|
+
// Title level with the ✕ when the nav row would hold nothing else.
|
|
695
|
+
const inlineClose = isInlineCloseHeader(config);
|
|
696
|
+
|
|
628
697
|
return (
|
|
629
698
|
<>
|
|
630
|
-
{/* Clear the whole gradient overlay so the block starts below the bar
|
|
631
|
-
|
|
699
|
+
{/* Clear the whole gradient overlay so the block starts below the bar —
|
|
700
|
+
or, inline with the ✕, just centre the first line on the nav row. */}
|
|
701
|
+
<View
|
|
702
|
+
testID="dialog-large-title-inset"
|
|
703
|
+
style={{ height: inlineClose ? DIALOG_HEADER_INLINE_CONTENT_TOP : DIALOG_HEADER_CONTENT_TOP }}
|
|
704
|
+
/>
|
|
632
705
|
<View onLayout={onLayout} testID="dialog-large-title" style={styles.largeTitleBlock}>
|
|
633
706
|
{hasLargeTitle ? (
|
|
634
707
|
<>
|
|
635
|
-
<H1
|
|
708
|
+
<H1
|
|
709
|
+
style={[
|
|
710
|
+
styles.largeTitle,
|
|
711
|
+
{ color: onImage ? ON_IMAGE_TEXT : theme.colors.text },
|
|
712
|
+
// Wrap before the ✕ it shares the row with. A logical inset, so
|
|
713
|
+
// it follows the ✕ to the left edge in a right-to-left layout.
|
|
714
|
+
inlineClose && config.showClose !== false
|
|
715
|
+
? { marginInlineEnd: INLINE_CLOSE_TITLE_RESERVE }
|
|
716
|
+
: null,
|
|
717
|
+
]}
|
|
718
|
+
>
|
|
636
719
|
{config.title}
|
|
637
720
|
</H1>
|
|
638
721
|
{config.subtitle ? (
|