@oxy.so/bloom 5.3.0 → 5.4.1

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 (143) hide show
  1. package/docs/adoption-matrix.mdx +7 -7
  2. package/docs/ai-chat.mdx +4 -0
  3. package/docs/app-shell.mdx +17 -0
  4. package/docs/mail-list.mdx +22 -22
  5. package/docs/settings-modal.mdx +1 -1
  6. package/lib/commonjs/ai-chat/AiChatShell.js +230 -206
  7. package/lib/commonjs/ai-chat/AiChatShell.js.map +1 -1
  8. package/lib/commonjs/app-shell/AppShellSplit.js +48 -22
  9. package/lib/commonjs/app-shell/AppShellSplit.js.map +1 -1
  10. package/lib/commonjs/app-shell/index.js +7 -0
  11. package/lib/commonjs/app-shell/index.js.map +1 -1
  12. package/lib/commonjs/app-shell/index.web.js +7 -0
  13. package/lib/commonjs/app-shell/index.web.js.map +1 -1
  14. package/lib/commonjs/icons/remix/RiFormatClear.js +12 -0
  15. package/lib/commonjs/icons/remix/RiFormatClear.js.map +1 -0
  16. package/lib/commonjs/icons/remix/RiListOrdered.js +12 -0
  17. package/lib/commonjs/icons/remix/RiListOrdered.js.map +1 -0
  18. package/lib/commonjs/icons/remix/RiStrikethrough.js +12 -0
  19. package/lib/commonjs/icons/remix/RiStrikethrough.js.map +1 -0
  20. package/lib/commonjs/icons/remix/RiUnderline.js +12 -0
  21. package/lib/commonjs/icons/remix/RiUnderline.js.map +1 -0
  22. package/lib/commonjs/icons/remix/index.js +28 -0
  23. package/lib/commonjs/icons/remix/index.js.map +1 -1
  24. package/lib/commonjs/mail-list/MailList.js +2 -0
  25. package/lib/commonjs/mail-list/MailList.js.map +1 -1
  26. package/lib/commonjs/mail-list/MailRow.js +26 -29
  27. package/lib/commonjs/mail-list/MailRow.js.map +1 -1
  28. package/lib/commonjs/mail-list/shared.js +26 -10
  29. package/lib/commonjs/mail-list/shared.js.map +1 -1
  30. package/lib/commonjs/provider/BloomProvider.js +13 -10
  31. package/lib/commonjs/provider/BloomProvider.js.map +1 -1
  32. package/lib/commonjs/settings-modal/SettingsModal.js +12 -18
  33. package/lib/commonjs/settings-modal/SettingsModal.js.map +1 -1
  34. package/lib/module/ai-chat/AiChatShell.js +231 -207
  35. package/lib/module/ai-chat/AiChatShell.js.map +1 -1
  36. package/lib/module/app-shell/AppShellSplit.js +49 -21
  37. package/lib/module/app-shell/AppShellSplit.js.map +1 -1
  38. package/lib/module/app-shell/index.js +1 -0
  39. package/lib/module/app-shell/index.js.map +1 -1
  40. package/lib/module/app-shell/index.web.js +1 -0
  41. package/lib/module/app-shell/index.web.js.map +1 -1
  42. package/lib/module/icons/remix/RiFormatClear.js +9 -0
  43. package/lib/module/icons/remix/RiFormatClear.js.map +1 -0
  44. package/lib/module/icons/remix/RiListOrdered.js +9 -0
  45. package/lib/module/icons/remix/RiListOrdered.js.map +1 -0
  46. package/lib/module/icons/remix/RiStrikethrough.js +9 -0
  47. package/lib/module/icons/remix/RiStrikethrough.js.map +1 -0
  48. package/lib/module/icons/remix/RiUnderline.js +9 -0
  49. package/lib/module/icons/remix/RiUnderline.js.map +1 -0
  50. package/lib/module/icons/remix/index.js +4 -0
  51. package/lib/module/icons/remix/index.js.map +1 -1
  52. package/lib/module/mail-list/MailList.js +2 -0
  53. package/lib/module/mail-list/MailList.js.map +1 -1
  54. package/lib/module/mail-list/MailRow.js +26 -29
  55. package/lib/module/mail-list/MailRow.js.map +1 -1
  56. package/lib/module/mail-list/shared.js +26 -10
  57. package/lib/module/mail-list/shared.js.map +1 -1
  58. package/lib/module/provider/BloomProvider.js +13 -10
  59. package/lib/module/provider/BloomProvider.js.map +1 -1
  60. package/lib/module/settings-modal/SettingsModal.js +12 -18
  61. package/lib/module/settings-modal/SettingsModal.js.map +1 -1
  62. package/lib/typescript/commonjs/ai-chat/AiChatShell.d.ts +1 -1
  63. package/lib/typescript/commonjs/ai-chat/AiChatShell.d.ts.map +1 -1
  64. package/lib/typescript/commonjs/ai-chat/types.d.ts +7 -0
  65. package/lib/typescript/commonjs/ai-chat/types.d.ts.map +1 -1
  66. package/lib/typescript/commonjs/app-shell/AppShellSplit.d.ts +1 -20
  67. package/lib/typescript/commonjs/app-shell/AppShellSplit.d.ts.map +1 -1
  68. package/lib/typescript/commonjs/app-shell/index.d.ts +2 -0
  69. package/lib/typescript/commonjs/app-shell/index.d.ts.map +1 -1
  70. package/lib/typescript/commonjs/app-shell/index.web.d.ts +2 -0
  71. package/lib/typescript/commonjs/app-shell/index.web.d.ts.map +1 -1
  72. package/lib/typescript/commonjs/app-shell/types.d.ts +28 -0
  73. package/lib/typescript/commonjs/app-shell/types.d.ts.map +1 -1
  74. package/lib/typescript/commonjs/icons/remix/RiFormatClear.d.ts +8 -0
  75. package/lib/typescript/commonjs/icons/remix/RiFormatClear.d.ts.map +1 -0
  76. package/lib/typescript/commonjs/icons/remix/RiListOrdered.d.ts +8 -0
  77. package/lib/typescript/commonjs/icons/remix/RiListOrdered.d.ts.map +1 -0
  78. package/lib/typescript/commonjs/icons/remix/RiStrikethrough.d.ts +8 -0
  79. package/lib/typescript/commonjs/icons/remix/RiStrikethrough.d.ts.map +1 -0
  80. package/lib/typescript/commonjs/icons/remix/RiUnderline.d.ts +8 -0
  81. package/lib/typescript/commonjs/icons/remix/RiUnderline.d.ts.map +1 -0
  82. package/lib/typescript/commonjs/icons/remix/index.d.ts +4 -0
  83. package/lib/typescript/commonjs/icons/remix/index.d.ts.map +1 -1
  84. package/lib/typescript/commonjs/mail-list/MailList.d.ts +1 -1
  85. package/lib/typescript/commonjs/mail-list/MailList.d.ts.map +1 -1
  86. package/lib/typescript/commonjs/mail-list/MailRow.d.ts +5 -20
  87. package/lib/typescript/commonjs/mail-list/MailRow.d.ts.map +1 -1
  88. package/lib/typescript/commonjs/mail-list/shared.d.ts +2 -9
  89. package/lib/typescript/commonjs/mail-list/shared.d.ts.map +1 -1
  90. package/lib/typescript/commonjs/mail-list/types.d.ts +8 -6
  91. package/lib/typescript/commonjs/mail-list/types.d.ts.map +1 -1
  92. package/lib/typescript/commonjs/provider/BloomProvider.d.ts.map +1 -1
  93. package/lib/typescript/commonjs/settings-modal/SettingsModal.d.ts.map +1 -1
  94. package/lib/typescript/module/ai-chat/AiChatShell.d.ts +1 -1
  95. package/lib/typescript/module/ai-chat/AiChatShell.d.ts.map +1 -1
  96. package/lib/typescript/module/ai-chat/types.d.ts +7 -0
  97. package/lib/typescript/module/ai-chat/types.d.ts.map +1 -1
  98. package/lib/typescript/module/app-shell/AppShellSplit.d.ts +1 -20
  99. package/lib/typescript/module/app-shell/AppShellSplit.d.ts.map +1 -1
  100. package/lib/typescript/module/app-shell/index.d.ts +2 -0
  101. package/lib/typescript/module/app-shell/index.d.ts.map +1 -1
  102. package/lib/typescript/module/app-shell/index.web.d.ts +2 -0
  103. package/lib/typescript/module/app-shell/index.web.d.ts.map +1 -1
  104. package/lib/typescript/module/app-shell/types.d.ts +28 -0
  105. package/lib/typescript/module/app-shell/types.d.ts.map +1 -1
  106. package/lib/typescript/module/icons/remix/RiFormatClear.d.ts +8 -0
  107. package/lib/typescript/module/icons/remix/RiFormatClear.d.ts.map +1 -0
  108. package/lib/typescript/module/icons/remix/RiListOrdered.d.ts +8 -0
  109. package/lib/typescript/module/icons/remix/RiListOrdered.d.ts.map +1 -0
  110. package/lib/typescript/module/icons/remix/RiStrikethrough.d.ts +8 -0
  111. package/lib/typescript/module/icons/remix/RiStrikethrough.d.ts.map +1 -0
  112. package/lib/typescript/module/icons/remix/RiUnderline.d.ts +8 -0
  113. package/lib/typescript/module/icons/remix/RiUnderline.d.ts.map +1 -0
  114. package/lib/typescript/module/icons/remix/index.d.ts +4 -0
  115. package/lib/typescript/module/icons/remix/index.d.ts.map +1 -1
  116. package/lib/typescript/module/mail-list/MailList.d.ts +1 -1
  117. package/lib/typescript/module/mail-list/MailList.d.ts.map +1 -1
  118. package/lib/typescript/module/mail-list/MailRow.d.ts +5 -20
  119. package/lib/typescript/module/mail-list/MailRow.d.ts.map +1 -1
  120. package/lib/typescript/module/mail-list/shared.d.ts +2 -9
  121. package/lib/typescript/module/mail-list/shared.d.ts.map +1 -1
  122. package/lib/typescript/module/mail-list/types.d.ts +8 -6
  123. package/lib/typescript/module/mail-list/types.d.ts.map +1 -1
  124. package/lib/typescript/module/provider/BloomProvider.d.ts.map +1 -1
  125. package/lib/typescript/module/settings-modal/SettingsModal.d.ts.map +1 -1
  126. package/package.json +1 -1
  127. package/src/ai-chat/AiChatShell.tsx +23 -4
  128. package/src/ai-chat/types.ts +7 -0
  129. package/src/app-shell/AppShellSplit.tsx +37 -41
  130. package/src/app-shell/index.ts +3 -0
  131. package/src/app-shell/index.web.ts +3 -0
  132. package/src/app-shell/types.ts +30 -0
  133. package/src/icons/remix/RiFormatClear.tsx +6 -0
  134. package/src/icons/remix/RiListOrdered.tsx +6 -0
  135. package/src/icons/remix/RiStrikethrough.tsx +6 -0
  136. package/src/icons/remix/RiUnderline.tsx +6 -0
  137. package/src/icons/remix/index.ts +4 -0
  138. package/src/mail-list/MailList.tsx +2 -0
  139. package/src/mail-list/MailRow.tsx +24 -33
  140. package/src/mail-list/shared.ts +27 -10
  141. package/src/mail-list/types.ts +8 -6
  142. package/src/provider/BloomProvider.tsx +41 -36
  143. package/src/settings-modal/SettingsModal.tsx +12 -15
@@ -19,28 +19,13 @@ import { ScrollView, View } from 'react-native';
19
19
 
20
20
  import { AiChatResizeHandle } from '../ai-chat/AiChatResizeHandle';
21
21
  import { Z_INDEX } from '../styles/z-index';
22
+ import { space } from '../styles/tokens';
22
23
  import { useTheme } from '../theme/use-theme';
24
+ import { useMessages } from '../locale/messages';
25
+ import { APP_SHELL_MESSAGES } from './messages';
23
26
 
24
- export interface AppShellSplitPanesProps {
25
- list?: React.ReactNode;
26
- detail?: React.ReactNode;
27
- info?: React.ReactNode;
28
- /** Which panes have room right now. */
29
- showList: boolean;
30
- showDetail: boolean;
31
- showInfo: boolean;
32
- /** The list pane's resting width; a drag moves it within min/max. */
33
- listWidth: number;
34
- listMinWidth: number;
35
- listMaxWidth: number;
36
- onListWidthChange?: (width: number) => void;
37
- infoWidth: number;
38
- resizable: boolean;
39
- /** The shell wraps each pane in its own `ScrollView` unless the page owns one. */
40
- paneScroll: boolean;
41
- resizeLabel: string;
42
- testID?: string;
43
- }
27
+ import type { AppShellSplitPanesProps } from './types';
28
+ import { APP_SHELL_DEFAULTS as D } from './constants';
44
29
 
45
30
  /** One pane: a bounded column that scrolls its own overflow. */
46
31
  function Pane({
@@ -68,23 +53,26 @@ function Pane({
68
53
  }
69
54
 
70
55
  const AppShellSplitPanesComponent: React.FC<AppShellSplitPanesProps> = ({
56
+ variant = 'joined',
71
57
  list,
72
58
  detail,
73
59
  info,
74
- showList,
75
- showDetail,
76
- showInfo,
77
- listWidth,
78
- listMinWidth,
79
- listMaxWidth,
60
+ showList = list != null,
61
+ showDetail = true,
62
+ showInfo = info != null,
63
+ listWidth = D.listWidth,
64
+ listMinWidth = D.listMinWidth,
65
+ listMaxWidth = D.listMaxWidth,
80
66
  onListWidthChange,
81
- infoWidth,
82
- resizable,
83
- paneScroll,
84
- resizeLabel,
67
+ infoWidth = D.infoWidth,
68
+ resizable = true,
69
+ paneScroll = true,
70
+ resizeLabel: resizeLabelProp,
85
71
  testID,
86
72
  }) => {
87
73
  const theme = useTheme();
74
+ const { messages } = useMessages(APP_SHELL_MESSAGES);
75
+ const resizeLabel = resizeLabelProp ?? messages.resizePanes;
88
76
  const [width, setWidth] = useState(listWidth);
89
77
  const widthAtDragStart = useRef(listWidth);
90
78
 
@@ -116,11 +104,13 @@ const AppShellSplitPanesComponent: React.FC<AppShellSplitPanesProps> = ({
116
104
  * hit test at the divider's centre returned the pane, and the drag never
117
105
  * started. Raising the DIVIDER lifts the whole context over both panes.
118
106
  */
107
+ const separated = variant === 'separated';
119
108
  const divider = {
120
- width: 1,
109
+ width: separated ? space.md : 1,
110
+ flexShrink: 0,
121
111
  alignSelf: 'stretch' as const,
122
112
  zIndex: Z_INDEX.floating,
123
- backgroundColor: theme.colors.border,
113
+ backgroundColor: separated ? 'transparent' : theme.colors.border,
124
114
  };
125
115
  // Only the panes that are actually drawn count: alone, a pane fills the row.
126
116
  const solo = [showList, showDetail, showInfo].filter(Boolean).length === 1;
@@ -137,15 +127,21 @@ const AppShellSplitPanesComponent: React.FC<AppShellSplitPanesProps> = ({
137
127
  </Pane>
138
128
  ) : null}
139
129
  {showList && !solo ? (
140
- <View style={divider}>
130
+ <View style={divider} testID={testID ? `${testID}-list-gap` : undefined}>
141
131
  {resizable ? (
142
- <AiChatResizeHandle
143
- label={resizeLabel}
144
- onResizeStart={onResizeStart}
145
- onResize={onResize}
146
- onNudge={onNudge}
147
- testID={testID ? `${testID}-divider` : undefined}
148
- />
132
+ <View
133
+ pointerEvents="box-none"
134
+ testID={testID ? `${testID}-resize-anchor` : undefined}
135
+ style={separated ? { position: 'absolute', top: 0, bottom: 0, left: '50%', right: '50%', width: 0 } : { flex: 1 }}
136
+ >
137
+ <AiChatResizeHandle
138
+ label={resizeLabel}
139
+ onResizeStart={onResizeStart}
140
+ onResize={onResize}
141
+ onNudge={onNudge}
142
+ testID={testID ? `${testID}-divider` : undefined}
143
+ />
144
+ </View>
149
145
  ) : null}
150
146
  </View>
151
147
  ) : null}
@@ -158,7 +154,7 @@ const AppShellSplitPanesComponent: React.FC<AppShellSplitPanesProps> = ({
158
154
  {detail}
159
155
  </Pane>
160
156
  ) : null}
161
- {showInfo && !solo ? <View style={divider} /> : null}
157
+ {showInfo && showDetail && !solo ? <View style={divider} testID={testID ? `${testID}-info-gap` : undefined} /> : null}
162
158
  {showInfo ? (
163
159
  <Pane
164
160
  testID={testID ? `${testID}-pane-info` : undefined}
@@ -27,3 +27,6 @@ export type {
27
27
  NotificationBellProps,
28
28
  ProOfferCardProps,
29
29
  } from './types';
30
+
31
+ export { AppShellSplitPanes } from './AppShellSplit';
32
+ export type { AppShellSplitPanesProps } from './types';
@@ -32,3 +32,6 @@ export type {
32
32
  NotificationBellProps,
33
33
  ProOfferCardProps,
34
34
  } from './types';
35
+
36
+ export { AppShellSplitPanes } from './AppShellSplit';
37
+ export type { AppShellSplitPanesProps } from './types';
@@ -375,3 +375,33 @@ export interface AppShellNavigationProps {
375
375
  export type AppShellProps = Omit<AppShellEngineProps, 'scroll'> & AppShellNavigationProps & {
376
376
  scroll?: AppShellScroll | 'auto' | 'external';
377
377
  };
378
+
379
+
380
+ /** A bounded list/detail/info layout, independent of navigation or surfaces. */
381
+ export interface AppShellSplitPanesProps {
382
+ /** Joined uses hairline dividers (default); separated leaves a 12px gutter for independent panel surfaces. */
383
+ variant?: 'joined' | 'separated';
384
+ list?: ReactNode;
385
+ detail?: ReactNode;
386
+ info?: ReactNode;
387
+ /** Defaults to whether list content was supplied. Hidden panes unmount. */
388
+ showList?: boolean;
389
+ /** Defaults to true. Keep true across breakpoints to retain routed detail state. */
390
+ showDetail?: boolean;
391
+ /** Defaults to whether info content was supplied. */
392
+ showInfo?: boolean;
393
+ /** Initial list width (360), constrained by listMinWidth/listMaxWidth (280/520). */
394
+ listWidth?: number;
395
+ listMinWidth?: number;
396
+ listMaxWidth?: number;
397
+ onListWidthChange?: (width: number) => void;
398
+ /** Info column width, defaults to 320. */
399
+ infoWidth?: number;
400
+ /** Show the shared draggable/keyboard divider, defaults to true. */
401
+ resizable?: boolean;
402
+ /** Wrap each pane in a ScrollView (default true); false for a navigator or virtual list. */
403
+ paneScroll?: boolean;
404
+ /** Accessible divider name, defaults to "Resize panes". */
405
+ resizeLabel?: string;
406
+ testID?: string;
407
+ }
@@ -0,0 +1,6 @@
1
+ import { createSinglePathSVG } from '../TEMPLATE';
2
+
3
+ /** Remix Icon `format-clear` (Editor). */
4
+ export const RiFormatClear = createSinglePathSVG({
5
+ path: "M12.6512 14.0654L11.6047 20H9.57389L10.9247 12.339L3.51465 4.92892L4.92886 3.51471L20.4852 19.0711L19.071 20.4853L12.6512 14.0654ZM11.7727 7.53009L12.0425 5.99999H10.2426L8.24257 3.99999H19.9999V5.99999H14.0733L13.4991 9.25652L11.7727 7.53009Z",
6
+ });
@@ -0,0 +1,6 @@
1
+ import { createSinglePathSVG } from '../TEMPLATE';
2
+
3
+ /** Remix Icon `list-ordered` (Editor). */
4
+ export const RiListOrdered = createSinglePathSVG({
5
+ path: "M8 4H21V6H8V4ZM5 3V6H6V7H3V6H4V4H3V3H5ZM3 14V11.5H5V11H3V10H6V12.5H4V13H6V14H3ZM5 19.5H3V18.5H5V18H3V17H6V21H3V20H5V19.5ZM8 11H21V13H8V11ZM8 18H21V20H8V18Z",
6
+ });
@@ -0,0 +1,6 @@
1
+ import { createSinglePathSVG } from '../TEMPLATE';
2
+
3
+ /** Remix Icon `strikethrough` (Editor). */
4
+ export const RiStrikethrough = createSinglePathSVG({
5
+ path: "M17.1538 14C17.3846 14.5161 17.5 15.0893 17.5 15.7196C17.5 17.0625 16.9762 18.1116 15.9286 18.867C14.8809 19.6223 13.4335 20 11.5862 20C9.94674 20 8.32335 19.6185 6.71592 18.8555V16.6009C8.23538 17.4783 9.7908 17.917 11.3822 17.917C13.9333 17.917 15.2128 17.1846 15.2208 15.7196C15.2208 15.0939 15.0049 14.5598 14.5731 14.1173C14.5339 14.0772 14.4939 14.0381 14.4531 14H3V12H21V14H17.1538ZM13.076 11H7.62908C7.4566 10.8433 7.29616 10.6692 7.14776 10.4778C6.71592 9.92084 6.5 9.24559 6.5 8.45207C6.5 7.21602 6.96583 6.165 7.89749 5.299C8.82916 4.43299 10.2706 4 12.2219 4C13.6934 4 15.1009 4.32808 16.4444 4.98426V7.13591C15.2448 6.44921 13.9293 6.10587 12.4978 6.10587C10.0187 6.10587 8.77917 6.88793 8.77917 8.45207C8.77917 8.87172 8.99709 9.23796 9.43293 9.55079C9.86878 9.86362 10.4066 10.1135 11.0463 10.3004C11.6665 10.4816 12.3431 10.7148 13.076 11H13.076Z",
6
+ });
@@ -0,0 +1,6 @@
1
+ import { createSinglePathSVG } from '../TEMPLATE';
2
+
3
+ /** Remix Icon `underline` (Editor). */
4
+ export const RiUnderline = createSinglePathSVG({
5
+ path: "M8 3V12C8 14.2091 9.79086 16 12 16C14.2091 16 16 14.2091 16 12V3H18V12C18 15.3137 15.3137 18 12 18C8.68629 18 6 15.3137 6 12V3H8ZM4 20H20V22H4V20Z",
6
+ });
@@ -197,6 +197,7 @@ export { RiFolderOpenLine } from './RiFolderOpenLine';
197
197
  export { RiFontSize } from './RiFontSize';
198
198
  export { RiFontSize2 } from './RiFontSize2';
199
199
  export { RiForbidLine } from './RiForbidLine';
200
+ export { RiFormatClear } from './RiFormatClear';
200
201
  export { RiForward10Line } from './RiForward10Line';
201
202
  export { RiForward15Line } from './RiForward15Line';
202
203
  export { RiForward30Line } from './RiForward30Line';
@@ -259,6 +260,7 @@ export { RiLink } from './RiLink';
259
260
  export { RiLinkM } from './RiLinkM';
260
261
  export { RiLinkedinBoxFill } from './RiLinkedinBoxFill';
261
262
  export { RiListCheck3 } from './RiListCheck3';
263
+ export { RiListOrdered } from './RiListOrdered';
262
264
  export { RiListUnordered } from './RiListUnordered';
263
265
  export { RiListView } from './RiListView';
264
266
  export { RiLiveLine } from './RiLiveLine';
@@ -417,6 +419,7 @@ export { RiStethoscopeLine } from './RiStethoscopeLine';
417
419
  export { RiStockLine } from './RiStockLine';
418
420
  export { RiStopFill } from './RiStopFill';
419
421
  export { RiStore2Line } from './RiStore2Line';
422
+ export { RiStrikethrough } from './RiStrikethrough';
420
423
  export { RiSubtractLine } from './RiSubtractLine';
421
424
  export { RiSubwayLine } from './RiSubwayLine';
422
425
  export { RiSuitcaseLine } from './RiSuitcaseLine';
@@ -446,6 +449,7 @@ export { RiTreeLine } from './RiTreeLine';
446
449
  export { RiTv2Line } from './RiTv2Line';
447
450
  export { RiTvLine } from './RiTvLine';
448
451
  export { RiTwitterXFill } from './RiTwitterXFill';
452
+ export { RiUnderline } from './RiUnderline';
449
453
  export { RiUnpinLine } from './RiUnpinLine';
450
454
  export { RiUpload2Line } from './RiUpload2Line';
451
455
  export { RiUploadCloud2Line } from './RiUploadCloud2Line';
@@ -54,6 +54,7 @@ function MailListComponent({
54
54
  sections,
55
55
  mails,
56
56
  density = 'comfortable',
57
+ showAvatar = true,
57
58
  selectedId,
58
59
  checkedIds,
59
60
  onCheckedIdsChange,
@@ -166,6 +167,7 @@ function MailListComponent({
166
167
  actionsPlacement={mail.actionsPlacement ?? actionsPlacement}
167
168
  swipeEnabled={mail.swipeEnabled ?? swipeEnabled}
168
169
  density={density}
170
+ showAvatar={mail.showAvatar ?? showAvatar}
169
171
  strings={mail.strings ?? strings}
170
172
  selected={selectedId === id}
171
173
  checked={checked.has(id)}
@@ -41,26 +41,11 @@ import type { MailAction, MailRowProps } from './types';
41
41
  /**
42
42
  * One message thread in the inbox.
43
43
  *
44
- * comfortable 72 tall · 16 padding · 48 avatar · sender and time on the
45
- * first line, subject and snippet on the second, the star on
46
- * the right where it is a control
47
- * compact 40 tall · 12 padding · 24 avatar · sender, then subject and
48
- * snippet on ONE baseline, then the states and the time
44
+ * Compact uses one baseline; comfortable and cozy stack sender above subject
45
+ * and snippet. All three share a geometry table, content, paint and actions.
49
46
  *
50
- * ONE component, two rungs of a geometry table. There is no second row
51
- * component for the desktop: everything a compact row draws, a comfortable row
52
- * draws too, and a density that forked into two files is a density that drifts.
53
- *
54
- * TWO LINES, NOT THREE, AND NOTHING DRAWN AT REST THAT IS NOT INFORMATION.
55
- * The phone row is the same 72/48 as a conversation row, so an inbox and a
56
- * messages screen read as one library. What paid for the third line: the
57
- * subject and the snippet share line two, the labels collapse to a chip and
58
- * dots, and the star is drawn only where it is a control.
59
- *
60
- * THE SUBJECT AND THE SNIPPET ARE ONE `Text`, at both densities. Two
61
- * neighbouring flex children each want to be as wide as their content, and the
62
- * result of a long one is a row that pushes sideways or a subject clipped to
63
- * nothing. One string with a nested span truncates where the line ends.
47
+ * The subject and snippet share ONE Text at every density, so a long snippet
48
+ * cannot claim its own flex width and push the subject out of the row.
64
49
  *
65
50
  * UNREAD IS THE WHOLE ROW, NOT A DOT. The sender and the subject go semibold
66
51
  * and every rung of text moves up to the primary colour; a read row sits on the
@@ -97,6 +82,7 @@ function MailRowComponent({
97
82
  checked = false,
98
83
  onCheckedChange,
99
84
  density = 'comfortable',
85
+ showAvatar = true,
100
86
  onPress,
101
87
  onLongPress,
102
88
  href,
@@ -210,6 +196,7 @@ function MailRowComponent({
210
196
 
211
197
  const paperclip = hasAttachment ? (
212
198
  <View
199
+ pointerEvents="none"
213
200
  aria-hidden
214
201
  importantForAccessibility="no-hide-descendants"
215
202
  style={{ flexShrink: 0 }}
@@ -222,6 +209,7 @@ function MailRowComponent({
222
209
  const threadBadge =
223
210
  (threadCount ?? 0) > 1 ? (
224
211
  <View
212
+ pointerEvents="none"
225
213
  aria-hidden
226
214
  importantForAccessibility="no-hide-descendants"
227
215
  style={{ flexShrink: 0 }}
@@ -233,20 +221,22 @@ function MailRowComponent({
233
221
 
234
222
  const timeText =
235
223
  time === undefined ? null : (
236
- <Text
237
- variant={geo.timeVariant}
238
- numberOfLines={1}
239
- style={{ color: timeColor, flexShrink: 0 }}
240
- testID={testID ? `${testID}-time` : undefined}
241
- >
242
- {time}
243
- </Text>
224
+ <View pointerEvents="none" style={{ flexShrink: 0 }}>
225
+ <Text
226
+ variant={geo.timeVariant}
227
+ numberOfLines={1}
228
+ style={{ color: timeColor, flexShrink: 0 }}
229
+ testID={testID ? `${testID}-time` : undefined}
230
+ >
231
+ {time}
232
+ </Text>
233
+ </View>
244
234
  );
245
235
 
246
236
  // --- the leading slot: a checkbox when the list can multi-select ----------
247
237
  const leading =
248
238
  onCheckedChange !== undefined ? (
249
- <View style={{ width: geo.avatar, alignItems: 'center', flexShrink: 0 }}>
239
+ <View pointerEvents="box-none" style={{ width: geo.avatar, alignItems: 'center', flexShrink: 0 }}>
250
240
  <Checkbox
251
241
  checked={checked}
252
242
  onCheckedChange={onCheckedChange}
@@ -255,7 +245,7 @@ function MailRowComponent({
255
245
  testID={testID ? `${testID}-checkbox` : undefined}
256
246
  />
257
247
  </View>
258
- ) : (
248
+ ) : showAvatar ? (
259
249
  <View
260
250
  pointerEvents="none"
261
251
  aria-hidden
@@ -269,7 +259,7 @@ function MailRowComponent({
269
259
  testID={testID ? `${testID}-avatar` : undefined}
270
260
  />
271
261
  </View>
272
- );
262
+ ) : null;
273
263
 
274
264
  // --- the text column -----------------------------------------------------
275
265
  const column = compact ? (
@@ -283,7 +273,7 @@ function MailRowComponent({
283
273
  flexDirection: 'row',
284
274
  alignItems: 'center',
285
275
  gap: geo.gap,
286
- marginLeft: geo.gap,
276
+ marginLeft: leading === null ? 0 : geo.gap,
287
277
  }}
288
278
  >
289
279
  <Text
@@ -318,7 +308,7 @@ function MailRowComponent({
318
308
  pointerEvents="none"
319
309
  aria-hidden
320
310
  importantForAccessibility="no-hide-descendants"
321
- style={{ flex: 1, minWidth: 0, marginLeft: geo.gap, gap: geo.lineGap }}
311
+ style={{ flex: 1, minWidth: 0, marginLeft: leading === null ? 0 : geo.gap, gap: geo.lineGap }}
322
312
  >
323
313
  <View style={{ flexDirection: 'row', alignItems: 'center', gap: 6, minWidth: 0 }}>
324
314
  <Text
@@ -374,7 +364,7 @@ function MailRowComponent({
374
364
  // and every star press would open the thread. Everything else in here is
375
365
  // hidden from assistive technology; the row's composed name already says it.
376
366
  const states = paperclip || threadBadge || onStarredChange !== undefined ? (
377
- <View style={{ flexDirection: 'row', alignItems: 'center', gap: 4 }}>
367
+ <View pointerEvents="box-none" style={{ flexDirection: 'row', alignItems: 'center', gap: 4 }}>
378
368
  {paperclip}
379
369
  {threadBadge}
380
370
  {onStarredChange === undefined ? (starred ? star : null) : star}
@@ -447,6 +437,7 @@ function MailRowComponent({
447
437
  testID={testID ? `${testID}-link` : undefined}
448
438
  />
449
439
  <View
440
+ pointerEvents="box-none"
450
441
  style={{
451
442
  flexDirection: 'row',
452
443
  alignItems: 'center',
@@ -1,5 +1,7 @@
1
1
  import { Platform } from 'react-native';
2
2
 
3
+ import { space } from '../styles/tokens';
4
+
3
5
  import { RiInbox2Line } from '../icons/remix/RiInbox2Line';
4
6
  import { formatGregorian } from '../locale/format-date';
5
7
  import { pickMessages } from '../locale/messages';
@@ -31,7 +33,7 @@ export const IS_WEB = Platform.OS === 'web';
31
33
  export const MAIL_EMPTY_ICON = RiInbox2Line;
32
34
 
33
35
  // ---------------------------------------------------------------------------
34
- // Geometry — the whole difference between the two densities
36
+ // Geometry — the whole difference between the densities
35
37
  // ---------------------------------------------------------------------------
36
38
 
37
39
  export interface MailRowGeometry {
@@ -85,15 +87,8 @@ export interface MailRowGeometry {
85
87
  }
86
88
 
87
89
  /**
88
- * 64 tall with two lines on a phone, 40 tall with one on a desktop. The avatar
89
- * is centred in both, so a row with no snippet is the same height as one with a
90
- * snippet and a label.
91
- *
92
- * The phone rung is the same 72/48 as a `chat-list` row on purpose: a mail row
93
- * and a conversation row are the same kind of object, and an inbox that stands
94
- * 16px taller per row than the messages screen reads as a different library.
95
- * Two lines is what pays for it — the sender and the time share the first, the
96
- * subject and the snippet the second.
90
+ * Two stacked lines at comfortable/cozy, one baseline at compact. Stacked rows
91
+ * grow with their content; cozy gives the same facts more breathing room.
97
92
  */
98
93
  export const MAIL_ROW_GEOMETRY: Record<MailDensity, MailRowGeometry> = {
99
94
  comfortable: {
@@ -121,6 +116,27 @@ export const MAIL_ROW_GEOMETRY: Record<MailDensity, MailRowGeometry> = {
121
116
  actionGlyph: 18,
122
117
  starControl: 24,
123
118
  },
119
+ cozy: {
120
+ minHeight: 80,
121
+ paddingVertical: space.lg,
122
+ paddingHorizontal: space.lg,
123
+ avatar: 48,
124
+ gap: space.md,
125
+ lineGap: space._2xs,
126
+ glyph: space.lg,
127
+ senderWidth: 0,
128
+ senderMinWidth: 0,
129
+ senderMaxWidth: '100%',
130
+ senderVariant: 'headline-regular',
131
+ senderUnreadVariant: 'headline-semibold',
132
+ subjectVariant: 'body-regular',
133
+ subjectUnreadVariant: 'body-semibold',
134
+ snippetVariant: 'body-regular',
135
+ timeVariant: 'caption-1-regular',
136
+ action: 44,
137
+ actionGlyph: 18,
138
+ starControl: space._2xl,
139
+ },
124
140
  compact: {
125
141
  minHeight: 40,
126
142
  height: 40,
@@ -164,6 +180,7 @@ export const MAIL_LABEL_DOT = 8;
164
180
  /** The selection bar's height, per density. */
165
181
  export const MAIL_SELECTION_BAR_HEIGHT: Record<MailDensity, number> = {
166
182
  comfortable: 56,
183
+ cozy: 56,
167
184
  compact: 44,
168
185
  };
169
186
 
@@ -5,13 +5,11 @@ import type { BloomIconComponent } from '../icons/icon-component';
5
5
  import type { AccentTone } from '../theme/accent-colors';
6
6
 
7
7
  /**
8
- * The two rungs of ONE row, not two components.
9
- *
10
- * `comfortable` is the phone inbox: avatar, three lines, 88 tall. `compact` is
11
- * the desktop list: one line, 40 tall, the same facts on a single baseline.
12
- * Nothing but the geometry table changes between them.
8
+ * One row at three densities: compact is a single 40px baseline, comfortable
9
+ * stacks two lines at a 64px floor, cozy keeps those lines with an 80px floor
10
+ * and a larger avatar and insets. All share the same content and actions.
13
11
  */
14
- export type MailDensity = 'comfortable' | 'compact';
12
+ export type MailDensity = 'comfortable' | 'cozy' | 'compact';
15
13
 
16
14
  /** Who a message is from, as a row draws them. */
17
15
  export interface MailSender {
@@ -154,6 +152,8 @@ export interface MailRowProps {
154
152
  */
155
153
  checked?: boolean;
156
154
  onCheckedChange?: (checked: boolean) => void;
155
+ /** Show sender avatars. Selection checkboxes remain visible when enabled. */
156
+ showAvatar?: boolean;
157
157
  density?: MailDensity;
158
158
  onPress?: () => void;
159
159
  onLongPress?: () => void;
@@ -203,6 +203,8 @@ export interface MailListProps {
203
203
  sections?: readonly MailListSection[];
204
204
  /** One unlabelled run, for a list that is not bucketed. */
205
205
  mails?: readonly MailSummary[];
206
+ /** Show sender avatars. Selection checkboxes remain visible when enabled. */
207
+ showAvatar?: boolean;
206
208
  density?: MailDensity;
207
209
  /** The row the reading pane is showing. */
208
210
  selectedId?: string;
@@ -34,47 +34,52 @@ export function BloomProvider({
34
34
  // `value` is passed unconditionally (null when unset) so toggling a resolver
35
35
  // never changes the tree shape and remounts everything below it.
36
36
  <ImageResolverProvider value={imageResolver ?? null}>
37
- {/* Holds the first paint on the same visual as font loading until a
38
- non-English locale's strings have loaded (English never waits). */}
39
- <LocaleProvider locale={locale} fallback={themeProps.onFontsLoading ?? null}>
37
+ {/* The locale for everything below, the font-loading screen included. */}
38
+ <LocaleProvider locale={locale}>
40
39
  <BloomThemeProvider {...themeProps}>
41
- <ScrollRestorationProvider adapter={scrollAdapter}>
42
- <BloomHapticsProvider enabled={haptics}>
43
- {/*
44
- A PROVIDER, not an outlet, and the distinction is the one this
45
- component's exclusion list encodes. Outlets stay out because a
46
- second mount duplicates every surface they render; this renders NO
47
- surface — a transparent container on Android and nothing at all
48
- elsewhere — so a second mount duplicates nothing, and the nearest
49
- one simply becomes the target for its subtree. Nor is its position
50
- an app decision the way an outlet's is: it has no z-order and no
51
- insets, and it must WRAP the content, which is exactly what this
52
- provider already does with `children`.
53
- */}
54
- <GlassBlurTargetProvider>
40
+ {/* Holds the first paint on the same screen as font loading until a
41
+ non-English locale's strings have loaded (English never waits).
42
+ Inside the theme, like the font wait, because that screen is built
43
+ from Bloom components that read it. */}
44
+ <LocaleProvider locale={locale} fallback={themeProps.onFontsLoading ?? null}>
45
+ <ScrollRestorationProvider adapter={scrollAdapter}>
46
+ <BloomHapticsProvider enabled={haptics}>
55
47
  {/*
56
- The bottom edge's claim registry. Also a provider, not an outlet:
57
- it renders nothing and holds only the set of surfaces currently
58
- parked at the bottom edge, so a tab bar can publish its footprint
59
- and a FAB or a toast stack can read it without either importing
60
- the other.
48
+ A PROVIDER, not an outlet, and the distinction is the one this
49
+ component's exclusion list encodes. Outlets stay out because a
50
+ second mount duplicates every surface they render; this renders NO
51
+ surface — a transparent container on Android and nothing at all
52
+ elsewhere — so a second mount duplicates nothing, and the nearest
53
+ one simply becomes the target for its subtree. Nor is its position
54
+ an app decision the way an outlet's is: it has no z-order and no
55
+ insets, and it must WRAP the content, which is exactly what this
56
+ provider already does with `children`.
61
57
  */}
62
- <BottomEdgeProvider>
58
+ <GlassBlurTargetProvider>
63
59
  {/*
64
- And the top edge's, for the same reason one edge further up: a
65
- floating `PageHeader` overlays the content rather than sitting
66
- above it, so the content has to be told how much of the edge is
67
- gone. Two registries rather than one keyed by edge, because a
68
- reader of either only ever wants one number and must not
69
- re-render when the other moves.
60
+ The bottom edge's claim registry. Also a provider, not an outlet:
61
+ it renders nothing and holds only the set of surfaces currently
62
+ parked at the bottom edge, so a tab bar can publish its footprint
63
+ and a FAB or a toast stack can read it without either importing
64
+ the other.
70
65
  */}
71
- <TopEdgeProvider>
72
- <TabBarMinimizeProvider>{children}</TabBarMinimizeProvider>
73
- </TopEdgeProvider>
74
- </BottomEdgeProvider>
75
- </GlassBlurTargetProvider>
76
- </BloomHapticsProvider>
77
- </ScrollRestorationProvider>
66
+ <BottomEdgeProvider>
67
+ {/*
68
+ And the top edge's, for the same reason one edge further up: a
69
+ floating `PageHeader` overlays the content rather than sitting
70
+ above it, so the content has to be told how much of the edge is
71
+ gone. Two registries rather than one keyed by edge, because a
72
+ reader of either only ever wants one number and must not
73
+ re-render when the other moves.
74
+ */}
75
+ <TopEdgeProvider>
76
+ <TabBarMinimizeProvider>{children}</TabBarMinimizeProvider>
77
+ </TopEdgeProvider>
78
+ </BottomEdgeProvider>
79
+ </GlassBlurTargetProvider>
80
+ </BloomHapticsProvider>
81
+ </ScrollRestorationProvider>
82
+ </LocaleProvider>
78
83
  </BloomThemeProvider>
79
84
  </LocaleProvider>
80
85
  </ImageResolverProvider>
@@ -25,6 +25,7 @@ import { PageHeader } from '../page-header';
25
25
  import { ButtonGroup, ButtonGroupItem } from '../button-group';
26
26
  import { RiCloseLine } from '../icons/remix/RiCloseLine';
27
27
  import { Backdrop, OverlayRoot } from '../overlay';
28
+ import { ModalKeyboard } from '../overlay/ModalKeyboard';
28
29
  import type { WebCssStyle } from '../styles/web-view-style';
29
30
  import { webDataSet } from '../styles/web-data';
30
31
  import { Text } from '../typography';
@@ -139,6 +140,7 @@ export function SettingsModal({
139
140
  labels,
140
141
  testID,
141
142
  }: SettingsModalProps) {
143
+ const panelRef = useRef<View>(null);
142
144
  const common = useCommonMessages();
143
145
  const { messages } = useMessages(SETTINGS_MODAL_MESSAGES);
144
146
  useSettingsWebCss();
@@ -253,21 +255,6 @@ export function SettingsModal({
253
255
  if (mayLeave('navigation')) setCompactPageOpen(false);
254
256
  }, [mayLeave]);
255
257
 
256
- // Escape closes (web). On `window`, not `document`: a menu or popover opened
257
- // inside the modal handles Escape on `document` and stops propagation there,
258
- // so the innermost surface closes first and the modal stays open.
259
- useEffect(() => {
260
- if (!mounted || !IS_WEB || typeof window === 'undefined') return;
261
- const handler = (event: KeyboardEvent) => {
262
- if (event.key === 'Escape') {
263
- event.stopPropagation();
264
- requestClose();
265
- }
266
- };
267
- window.addEventListener('keydown', handler);
268
- return () => window.removeEventListener('keydown', handler);
269
- }, [mounted, requestClose]);
270
-
271
258
  const compactPageOpenRef = useRef(false);
272
259
  compactPageOpenRef.current = compactPageOpen && layoutRef.current === 'compact';
273
260
 
@@ -355,6 +342,14 @@ export function SettingsModal({
355
342
  <ModalPortal>
356
343
  <SettingsModalContext.Provider value={context}>
357
344
  <OverlayRoot onRequestClose={onHardwareBack} modal>
345
+ {IS_WEB ? (
346
+ <ModalKeyboard
347
+ panelRef={panelRef}
348
+ closing={!visible && wasVisible.current}
349
+ dismissible
350
+ dismiss={requestClose}
351
+ />
352
+ ) : null}
358
353
  <Backdrop
359
354
  onPress={requestClose}
360
355
  progress={progress}
@@ -377,6 +372,8 @@ export function SettingsModal({
377
372
  ]}
378
373
  >
379
374
  <View
375
+ ref={panelRef}
376
+ tabIndex={IS_WEB ? -1 : undefined}
380
377
  role="dialog"
381
378
  aria-modal
382
379
  aria-label={labels?.dialog ?? messages.dialog}