@remit/ui 0.0.79 → 0.0.81
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/package.json +1 -1
- package/src/components/app-shell-types.ts +31 -0
- package/src/components/app-shell.tsx +6 -2
- package/src/components/brief-sections.tsx +38 -8
- package/src/components/filter-sheet.tsx +36 -8
- package/src/components/keyboard-hint-bar.render.test.ts +29 -0
- package/src/components/keyboard-hint-bar.tsx +2 -14
- package/src/components/message-list-pane.brief-filter.test.ts +185 -0
- package/src/components/message-list-pane.render.test.ts +0 -1
- package/src/components/message-list-pane.stories.tsx +48 -36
- package/src/components/message-list-pane.tsx +93 -11
- package/src/components/message-row.tsx +22 -2
- package/src/index.ts +36 -2
- package/src/lib/keymap-dispatch.test.ts +255 -0
- package/src/lib/keymap-dispatch.ts +244 -0
- package/src/lib/keymap.test.ts +68 -0
- package/src/lib/keymap.ts +241 -0
- package/src/lib/use-list-cursor.test.ts +273 -0
- package/src/lib/use-list-cursor.ts +263 -0
- package/src/lib/use-list-keyboard.test.ts +172 -0
- package/src/lib/use-list-keyboard.ts +101 -0
- package/src/lib/use-triage-keyboard.test.ts +153 -0
- package/src/lib/use-triage-keyboard.ts +119 -0
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
import type { Decorator, Meta, StoryObj } from "@storybook/react";
|
|
2
2
|
import { useState } from "react";
|
|
3
3
|
import { inboxFilterConfig } from "../filter-presets.js";
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
type SelectionModifiers,
|
|
7
|
-
useSelection,
|
|
8
|
-
} from "../lib/use-selection.js";
|
|
9
|
-
import type { ThreadSection } from "./app-shell-types.js";
|
|
4
|
+
import { useListKeyboard } from "../lib/use-list-keyboard.js";
|
|
5
|
+
import type { MessageListKeyboard, ThreadSection } from "./app-shell-types.js";
|
|
10
6
|
import { FilterSheet } from "./filter-sheet.js";
|
|
11
7
|
import { MailHeader } from "./mail-header.js";
|
|
12
8
|
import { MessageListPane } from "./message-list-pane.js";
|
|
@@ -68,7 +64,6 @@ const meta: Meta<typeof MessageListPane> = {
|
|
|
68
64
|
listMeta: "3 conversations",
|
|
69
65
|
sections,
|
|
70
66
|
onSelectThread: () => undefined,
|
|
71
|
-
onSelectBriefCategory: () => undefined,
|
|
72
67
|
},
|
|
73
68
|
};
|
|
74
69
|
export default meta;
|
|
@@ -90,8 +85,41 @@ const narrowFrame: Decorator = (Story) => (
|
|
|
90
85
|
</div>
|
|
91
86
|
);
|
|
92
87
|
|
|
88
|
+
/**
|
|
89
|
+
* A pane with no keyboard over it — a story whose rows come from elsewhere, or
|
|
90
|
+
* that has no rows at all. It answers nothing and so offers nothing.
|
|
91
|
+
*/
|
|
92
|
+
const noKeyboard: MessageListKeyboard = {
|
|
93
|
+
focusedId: undefined,
|
|
94
|
+
handlers: {},
|
|
95
|
+
ref: () => undefined,
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The list under the triage keyboard: j/k walk the rows, x and Space tick the
|
|
100
|
+
* one under the cursor, Shift+j/k build a range and ⌘A takes them all. The
|
|
101
|
+
* footer offers what is wired.
|
|
102
|
+
*/
|
|
103
|
+
function LiveList({ briefFilters = false }: { briefFilters?: boolean }) {
|
|
104
|
+
const orderedIds = sections.flatMap((s) => s.threads).map((t) => t.id);
|
|
105
|
+
const list = useListKeyboard({ orderedIds, isDesktop: true });
|
|
106
|
+
return (
|
|
107
|
+
<MessageListPane
|
|
108
|
+
listTitle="Inbox"
|
|
109
|
+
listMeta="3 conversations"
|
|
110
|
+
sections={sections}
|
|
111
|
+
flatList={!briefFilters}
|
|
112
|
+
briefFilters={briefFilters}
|
|
113
|
+
isDesktop
|
|
114
|
+
onSelectThread={() => undefined}
|
|
115
|
+
selection={list.selection}
|
|
116
|
+
keyboard={list.keyboard}
|
|
117
|
+
/>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
93
121
|
export const DesktopList: Story = {
|
|
94
|
-
|
|
122
|
+
render: () => <LiveList />,
|
|
95
123
|
decorators: [desktopFrame],
|
|
96
124
|
};
|
|
97
125
|
|
|
@@ -101,7 +129,7 @@ export const NarrowTouchList: Story = {
|
|
|
101
129
|
};
|
|
102
130
|
|
|
103
131
|
export const Brief: Story = {
|
|
104
|
-
|
|
132
|
+
render: () => <LiveList briefFilters />,
|
|
105
133
|
decorators: [desktopFrame],
|
|
106
134
|
};
|
|
107
135
|
|
|
@@ -188,13 +216,14 @@ export const InboxWithFilterExpanded: Story = {
|
|
|
188
216
|
decorators: [narrowFrame],
|
|
189
217
|
};
|
|
190
218
|
|
|
191
|
-
/** Consumer-supplied `listBody` slot — the pane renders the chrome
|
|
192
|
-
*
|
|
193
|
-
* the web-client's virtualized inbox path. */
|
|
219
|
+
/** Consumer-supplied `listBody` slot — the pane renders the chrome while the
|
|
220
|
+
* caller owns the scrollable rows, and the keys over those rows with them.
|
|
221
|
+
* This models the web-client's virtualized inbox path. */
|
|
194
222
|
export const CustomListBody: Story = {
|
|
195
223
|
args: {
|
|
196
224
|
isDesktop: true,
|
|
197
225
|
flatList: true,
|
|
226
|
+
keyboard: noKeyboard,
|
|
198
227
|
listBody: (
|
|
199
228
|
<div className="flex-1 overflow-y-auto divide-y divide-line">
|
|
200
229
|
{sections.flatMap((s) =>
|
|
@@ -218,7 +247,6 @@ export const CustomListBody: Story = {
|
|
|
218
247
|
};
|
|
219
248
|
|
|
220
249
|
function SelectableList({ isDesktop }: { isDesktop: boolean }) {
|
|
221
|
-
const selection = useSelection();
|
|
222
250
|
const [trashedIds, setTrashedIds] = useState<ReadonlySet<string>>(new Set());
|
|
223
251
|
const [readIds, setReadIds] = useState<ReadonlySet<string>>(new Set());
|
|
224
252
|
|
|
@@ -231,6 +259,8 @@ function SelectableList({ isDesktop }: { isDesktop: boolean }) {
|
|
|
231
259
|
),
|
|
232
260
|
}));
|
|
233
261
|
const orderedIds = visible.flatMap((s) => s.threads).map((t) => t.id);
|
|
262
|
+
const list = useListKeyboard({ orderedIds, isDesktop });
|
|
263
|
+
const { selection } = list.cursor;
|
|
234
264
|
const allSelected =
|
|
235
265
|
orderedIds.length > 0 &&
|
|
236
266
|
orderedIds.every((id) => selection.selectedIds.has(id));
|
|
@@ -240,24 +270,6 @@ function SelectableList({ isDesktop }: { isDesktop: boolean }) {
|
|
|
240
270
|
selection.clearSelection();
|
|
241
271
|
};
|
|
242
272
|
|
|
243
|
-
// Shift and cmd/ctrl come off a mouse, which the touch list has no path for.
|
|
244
|
-
const onRowSelect = isDesktop
|
|
245
|
-
? (id: string, modifiers: SelectionModifiers) => {
|
|
246
|
-
const intent = rowSelectIntent(modifiers);
|
|
247
|
-
if (intent === "range") {
|
|
248
|
-
selection.selectRange(orderedIds, id);
|
|
249
|
-
return true;
|
|
250
|
-
}
|
|
251
|
-
if (intent === "toggle") {
|
|
252
|
-
selection.toggle(id);
|
|
253
|
-
return true;
|
|
254
|
-
}
|
|
255
|
-
selection.clearSelection();
|
|
256
|
-
selection.setAnchor(id);
|
|
257
|
-
return false;
|
|
258
|
-
}
|
|
259
|
-
: undefined;
|
|
260
|
-
|
|
261
273
|
return (
|
|
262
274
|
<MessageListPane
|
|
263
275
|
listTitle="Inbox"
|
|
@@ -266,11 +278,8 @@ function SelectableList({ isDesktop }: { isDesktop: boolean }) {
|
|
|
266
278
|
flatList
|
|
267
279
|
isDesktop={isDesktop}
|
|
268
280
|
onSelectThread={() => undefined}
|
|
269
|
-
selection={
|
|
270
|
-
|
|
271
|
-
onToggle: selection.toggle,
|
|
272
|
-
onRowSelect,
|
|
273
|
-
}}
|
|
281
|
+
selection={list.selection}
|
|
282
|
+
keyboard={list.keyboard}
|
|
274
283
|
selectionBar={
|
|
275
284
|
<SelectionTopBar
|
|
276
285
|
title="Inbox"
|
|
@@ -329,6 +338,7 @@ export const EmptyState: Story = {
|
|
|
329
338
|
isDesktop: true,
|
|
330
339
|
flatList: true,
|
|
331
340
|
listState: "empty",
|
|
341
|
+
keyboard: noKeyboard,
|
|
332
342
|
},
|
|
333
343
|
decorators: [desktopFrame],
|
|
334
344
|
};
|
|
@@ -343,6 +353,7 @@ export const FilteredEmptyState: Story = {
|
|
|
343
353
|
isDesktop: true,
|
|
344
354
|
flatList: true,
|
|
345
355
|
listState: "empty",
|
|
356
|
+
keyboard: noKeyboard,
|
|
346
357
|
listFilter: {
|
|
347
358
|
label: "Personal",
|
|
348
359
|
reach: "whole-folder",
|
|
@@ -361,6 +372,7 @@ export const ErrorState: Story = {
|
|
|
361
372
|
isDesktop: true,
|
|
362
373
|
flatList: true,
|
|
363
374
|
listState: "error",
|
|
375
|
+
keyboard: noKeyboard,
|
|
364
376
|
errorMessage: "Request timed out while loading this mailbox.",
|
|
365
377
|
onRetry: () => undefined,
|
|
366
378
|
onReportError: () => undefined,
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { Menu } from "lucide-react";
|
|
2
2
|
import type { MouseEvent, ReactNode } from "react";
|
|
3
|
-
import { useCallback, useRef, useState } from "react";
|
|
3
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
4
|
+
import { defaultKeyboardHints, keyboardHintsFor } from "../lib/keymap.js";
|
|
4
5
|
import { LIST_ROW_SELECTOR, useRovingFocus } from "../lib/roving-focus.js";
|
|
5
6
|
import { deriveIsMultiSelectMode, modifiersOf } from "../lib/use-selection.js";
|
|
6
7
|
import type {
|
|
7
8
|
AppShellProps,
|
|
9
|
+
MessageListKeyboard,
|
|
8
10
|
MessageListSelection,
|
|
9
11
|
TouchSeed,
|
|
10
12
|
} from "./app-shell-types.js";
|
|
11
|
-
import { BriefSections } from "./brief-sections.js";
|
|
13
|
+
import { type BriefFilterSurface, BriefSections } from "./brief-sections.js";
|
|
12
14
|
import { Button } from "./button.js";
|
|
13
15
|
import { KeyboardHintBar } from "./keyboard-hint-bar.js";
|
|
14
16
|
import {
|
|
@@ -46,15 +48,15 @@ export function MessageListPane({
|
|
|
46
48
|
errorMessage,
|
|
47
49
|
onRetry,
|
|
48
50
|
onReportError,
|
|
49
|
-
|
|
51
|
+
briefFilter,
|
|
50
52
|
selectedThreadId,
|
|
51
53
|
density = "comfortable",
|
|
52
54
|
onSelectThread,
|
|
53
|
-
onSelectBriefCategory,
|
|
54
55
|
onOpenNav,
|
|
55
56
|
isDesktop,
|
|
56
57
|
initialTouchState,
|
|
57
58
|
selection,
|
|
59
|
+
keyboard,
|
|
58
60
|
selectionBar,
|
|
59
61
|
paneOverlay,
|
|
60
62
|
listBody,
|
|
@@ -71,11 +73,9 @@ export function MessageListPane({
|
|
|
71
73
|
| "errorMessage"
|
|
72
74
|
| "onRetry"
|
|
73
75
|
| "onReportError"
|
|
74
|
-
| "briefCategory"
|
|
75
76
|
| "selectedThreadId"
|
|
76
77
|
| "density"
|
|
77
78
|
| "onSelectThread"
|
|
78
|
-
| "onSelectBriefCategory"
|
|
79
79
|
> & {
|
|
80
80
|
/**
|
|
81
81
|
* The active category filter, when the caller has one. Without it the empty
|
|
@@ -84,6 +84,14 @@ export function MessageListPane({
|
|
|
84
84
|
* unfiltered, so a surface that filters must pass this.
|
|
85
85
|
*/
|
|
86
86
|
listFilter?: MessageListFilter;
|
|
87
|
+
/**
|
|
88
|
+
* The brief's category scope, account pills and attribute chips, held by the
|
|
89
|
+
* caller. The cross-account brief is segmented from this panel, and a caller
|
|
90
|
+
* narrowing the same rows on a second surface hands both the one set it
|
|
91
|
+
* holds. Absent, the chips are the brief's own, no source row is offered and
|
|
92
|
+
* every category is in scope.
|
|
93
|
+
*/
|
|
94
|
+
briefFilter?: BriefFilterSurface;
|
|
87
95
|
/** Name of the collection, e.g. "Inbox". Passed to the empty state. */
|
|
88
96
|
listScopeLabel?: string;
|
|
89
97
|
/** When set, the list header shows a folders/menu button that opens the nav
|
|
@@ -98,6 +106,13 @@ export function MessageListPane({
|
|
|
98
106
|
* none of it. Absent means the list does not offer multi-select.
|
|
99
107
|
*/
|
|
100
108
|
selection?: MessageListSelection;
|
|
109
|
+
/**
|
|
110
|
+
* The keyboard layer driving the list, when the caller mounts one. The pane
|
|
111
|
+
* binds it to its own element, draws the cursor it moves and offers only the
|
|
112
|
+
* keys its handlers answer; absent, the footer offers the app's own set and
|
|
113
|
+
* the rows keep their arrow-key traversal.
|
|
114
|
+
*/
|
|
115
|
+
keyboard?: MessageListKeyboard;
|
|
101
116
|
/**
|
|
102
117
|
* The pane header. The caller mounts it for every state of the list: a
|
|
103
118
|
* `SelectionTopBar` names the view while nothing is ticked and carries the
|
|
@@ -129,11 +144,60 @@ export function MessageListPane({
|
|
|
129
144
|
}) {
|
|
130
145
|
const Row = density === "compact" ? CompactRow : ComfortableRow;
|
|
131
146
|
const flatListRef = useRef<HTMLDivElement>(null);
|
|
147
|
+
const paneRef = useRef<HTMLElement | null>(null);
|
|
148
|
+
|
|
149
|
+
// The layer answers the arrows only if it registered them. Anything else it
|
|
150
|
+
// hands over — a layer with no cursor keys, or no layer at all — leaves the
|
|
151
|
+
// rows their own traversal and their own single tab stop.
|
|
152
|
+
const walksRows =
|
|
153
|
+
keyboard !== undefined &&
|
|
154
|
+
keyboard.handlers.focusNext !== undefined &&
|
|
155
|
+
keyboard.handlers.focusPrevious !== undefined;
|
|
132
156
|
useRovingFocus({
|
|
133
157
|
containerRef: flatListRef,
|
|
134
158
|
itemSelector: LIST_ROW_SELECTOR,
|
|
159
|
+
enabled: !walksRows,
|
|
135
160
|
});
|
|
136
161
|
|
|
162
|
+
const cursorId = walksRows ? keyboard.focusedId : undefined;
|
|
163
|
+
// One tab stop into the list, as the roving group gives it: the row the
|
|
164
|
+
// cursor is on, or the first row before it has moved.
|
|
165
|
+
const tabStopId = walksRows
|
|
166
|
+
? (cursorId ?? sections[0]?.threads[0]?.id)
|
|
167
|
+
: undefined;
|
|
168
|
+
const rowTabIndex = useCallback(
|
|
169
|
+
(id: string): number | undefined =>
|
|
170
|
+
tabStopId === undefined ? undefined : id === tabStopId ? 0 : -1,
|
|
171
|
+
[tabStopId],
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
// Real browser focus follows the cursor, so Tab, Shift+Tab and the focus
|
|
175
|
+
// ring agree with the row the list highlights — and so the keys keep
|
|
176
|
+
// reaching the layer, which listens on the pane rather than the window.
|
|
177
|
+
useEffect(() => {
|
|
178
|
+
if (cursorId === undefined) return;
|
|
179
|
+
const pane = paneRef.current;
|
|
180
|
+
if (!pane) return;
|
|
181
|
+
const row = pane.querySelector<HTMLElement>(
|
|
182
|
+
`${LIST_ROW_SELECTOR}[data-message-id="${cursorId}"]`,
|
|
183
|
+
);
|
|
184
|
+
if (!row || row === pane.ownerDocument.activeElement) return;
|
|
185
|
+
row.focus({ preventScroll: false });
|
|
186
|
+
}, [cursorId]);
|
|
187
|
+
|
|
188
|
+
// A layer bound to this pane hears nothing until focus is inside it. Taking
|
|
189
|
+
// focus on mount is what keeps j working without a click first — and only
|
|
190
|
+
// from a document where nothing else has claimed it, so a page of stories
|
|
191
|
+
// does not fight over the caret.
|
|
192
|
+
useEffect(() => {
|
|
193
|
+
if (!walksRows) return;
|
|
194
|
+
const pane = paneRef.current;
|
|
195
|
+
if (!pane) return;
|
|
196
|
+
const active = pane.ownerDocument.activeElement;
|
|
197
|
+
if (active !== null && active !== pane.ownerDocument.body) return;
|
|
198
|
+
pane.focus({ preventScroll: true });
|
|
199
|
+
}, [walksRows]);
|
|
200
|
+
|
|
137
201
|
const touchTriage = !isDesktop && !briefFilters && listState === "ready";
|
|
138
202
|
const [refreshing, setRefreshing] = useState(false);
|
|
139
203
|
const initialPeek: SwipePeek | undefined =
|
|
@@ -193,6 +257,8 @@ export function MessageListPane({
|
|
|
193
257
|
<Row
|
|
194
258
|
thread={thread}
|
|
195
259
|
active={active}
|
|
260
|
+
focused={thread.id === cursorId}
|
|
261
|
+
tabIndex={rowTabIndex(thread.id)}
|
|
196
262
|
selection={rowSelection(thread.id)}
|
|
197
263
|
onClick={(event) => {
|
|
198
264
|
if (takeRowSelect(thread.id, event)) return;
|
|
@@ -200,11 +266,18 @@ export function MessageListPane({
|
|
|
200
266
|
}}
|
|
201
267
|
/>
|
|
202
268
|
),
|
|
203
|
-
[Row, rowSelection, takeRowSelect],
|
|
269
|
+
[Row, rowSelection, takeRowSelect, cursorId, rowTabIndex],
|
|
204
270
|
);
|
|
205
271
|
|
|
206
272
|
return (
|
|
207
|
-
<section
|
|
273
|
+
<section
|
|
274
|
+
ref={(element) => {
|
|
275
|
+
paneRef.current = element;
|
|
276
|
+
keyboard?.ref(element);
|
|
277
|
+
}}
|
|
278
|
+
tabIndex={walksRows ? -1 : undefined}
|
|
279
|
+
className="relative flex h-full w-full flex-col bg-surface outline-none"
|
|
280
|
+
>
|
|
208
281
|
{selectionBar ??
|
|
209
282
|
(hideHeader ? null : (
|
|
210
283
|
<header className="flex h-pane-header shrink-0 items-center gap-2 border-b border-line px-row-inset">
|
|
@@ -250,12 +323,11 @@ export function MessageListPane({
|
|
|
250
323
|
/>
|
|
251
324
|
) : briefFilters ? (
|
|
252
325
|
<BriefSections
|
|
326
|
+
{...(briefFilter ?? {})}
|
|
253
327
|
sections={sections}
|
|
254
|
-
briefCategory={briefCategory}
|
|
255
328
|
selectedThreadId={selectedThreadId}
|
|
256
329
|
Row={BriefRow}
|
|
257
330
|
onSelectThread={onSelectThread}
|
|
258
|
-
onSelectBriefCategory={onSelectBriefCategory}
|
|
259
331
|
/>
|
|
260
332
|
) : listBody != null ? (
|
|
261
333
|
/* Consumer-provided body wins on every width — it owns the rows
|
|
@@ -298,6 +370,8 @@ export function MessageListPane({
|
|
|
298
370
|
key={thread.id}
|
|
299
371
|
thread={thread}
|
|
300
372
|
active={thread.id === selectedThreadId}
|
|
373
|
+
focused={thread.id === cursorId}
|
|
374
|
+
tabIndex={rowTabIndex(thread.id)}
|
|
301
375
|
selection={rowSelection(thread.id)}
|
|
302
376
|
onClick={(event) => {
|
|
303
377
|
if (takeRowSelect(thread.id, event)) return;
|
|
@@ -311,7 +385,15 @@ export function MessageListPane({
|
|
|
311
385
|
</div>
|
|
312
386
|
)}
|
|
313
387
|
|
|
314
|
-
{isDesktop &&
|
|
388
|
+
{isDesktop && (
|
|
389
|
+
<KeyboardHintBar
|
|
390
|
+
hints={
|
|
391
|
+
keyboard
|
|
392
|
+
? keyboardHintsFor(keyboard.handlers)
|
|
393
|
+
: defaultKeyboardHints
|
|
394
|
+
}
|
|
395
|
+
/>
|
|
396
|
+
)}
|
|
315
397
|
{paneOverlay}
|
|
316
398
|
</section>
|
|
317
399
|
);
|
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
SyntheticEvent,
|
|
7
7
|
} from "react";
|
|
8
8
|
import { cn } from "../lib/cn.js";
|
|
9
|
+
import { ROW_ATTRIBUTE } from "../lib/keymap-dispatch.js";
|
|
9
10
|
import { LIST_ROW_ATTRIBUTE } from "../lib/roving-focus.js";
|
|
10
11
|
import { categoryTone, type ThreadRowData } from "./app-shell-types.js";
|
|
11
12
|
import { Avatar } from "./avatar.js";
|
|
@@ -324,21 +325,37 @@ export function ComfortableRowBody({
|
|
|
324
325
|
);
|
|
325
326
|
}
|
|
326
327
|
|
|
328
|
+
/**
|
|
329
|
+
* What marks a row as a row: the roving-focus marker, the message-list marker
|
|
330
|
+
* the keyboard layer reads to leave Enter and Space with the list rather than
|
|
331
|
+
* with the button under focus, and the id the cursor finds it by. The app's own
|
|
332
|
+
* row carries the same three (`MessageRow`), so a key pressed on a kit row does
|
|
333
|
+
* what it does on an app row.
|
|
334
|
+
*/
|
|
335
|
+
const rowMarkers = (thread: ThreadRowData) => ({
|
|
336
|
+
...LIST_ROW_ATTRIBUTE,
|
|
337
|
+
[ROW_ATTRIBUTE]: "",
|
|
338
|
+
"data-message-id": thread.id,
|
|
339
|
+
});
|
|
340
|
+
|
|
327
341
|
export function CompactRow({
|
|
328
342
|
thread,
|
|
329
343
|
active,
|
|
330
344
|
focused,
|
|
345
|
+
tabIndex,
|
|
331
346
|
onClick,
|
|
332
347
|
}: {
|
|
333
348
|
thread: ThreadRowData;
|
|
334
349
|
active?: boolean;
|
|
335
350
|
focused?: boolean;
|
|
351
|
+
tabIndex?: number;
|
|
336
352
|
onClick?: (event: MouseEvent<HTMLButtonElement>) => void;
|
|
337
353
|
}) {
|
|
338
354
|
return (
|
|
339
355
|
<button
|
|
340
356
|
type="button"
|
|
341
|
-
{...
|
|
357
|
+
{...rowMarkers(thread)}
|
|
358
|
+
tabIndex={tabIndex}
|
|
342
359
|
onClick={onClick}
|
|
343
360
|
className={compactRowClass({ active, focused })}
|
|
344
361
|
>
|
|
@@ -351,19 +368,22 @@ export function ComfortableRow({
|
|
|
351
368
|
thread,
|
|
352
369
|
active,
|
|
353
370
|
focused,
|
|
371
|
+
tabIndex,
|
|
354
372
|
selection,
|
|
355
373
|
onClick,
|
|
356
374
|
}: {
|
|
357
375
|
thread: ThreadRowData;
|
|
358
376
|
active?: boolean;
|
|
359
377
|
focused?: boolean;
|
|
378
|
+
tabIndex?: number;
|
|
360
379
|
selection?: RowSelection;
|
|
361
380
|
onClick?: (event: MouseEvent<HTMLButtonElement>) => void;
|
|
362
381
|
}) {
|
|
363
382
|
return (
|
|
364
383
|
<button
|
|
365
384
|
type="button"
|
|
366
|
-
{...
|
|
385
|
+
{...rowMarkers(thread)}
|
|
386
|
+
tabIndex={tabIndex}
|
|
367
387
|
onClick={onClick}
|
|
368
388
|
className={cn("group", comfortableRowClass({ active, focused }))}
|
|
369
389
|
>
|
package/src/index.ts
CHANGED
|
@@ -29,6 +29,8 @@ export {
|
|
|
29
29
|
categoryTone,
|
|
30
30
|
type Density,
|
|
31
31
|
INTELLIGENCE_MIN_WIDTH,
|
|
32
|
+
isBriefCategory,
|
|
33
|
+
type MessageListKeyboard,
|
|
32
34
|
type MessageListSelection,
|
|
33
35
|
type NarrowView,
|
|
34
36
|
type NavAccount,
|
|
@@ -87,9 +89,14 @@ export {
|
|
|
87
89
|
SECTION_ROW_CAP,
|
|
88
90
|
} from "./components/brief-section.js";
|
|
89
91
|
export {
|
|
92
|
+
type BriefCategoryControl,
|
|
93
|
+
type BriefFilterControl,
|
|
90
94
|
type BriefFilterId,
|
|
95
|
+
type BriefFilterSurface,
|
|
91
96
|
BriefSections,
|
|
92
97
|
type BriefSectionsProps,
|
|
98
|
+
type BriefSourceControl,
|
|
99
|
+
isBriefFilterId,
|
|
93
100
|
matchesBriefFilters,
|
|
94
101
|
} from "./components/brief-sections.js";
|
|
95
102
|
export {
|
|
@@ -264,8 +271,6 @@ export {
|
|
|
264
271
|
} from "./components/isolated-email-frame.js";
|
|
265
272
|
export { Kbd, type KbdProps } from "./components/kbd.js";
|
|
266
273
|
export {
|
|
267
|
-
defaultKeyboardHints,
|
|
268
|
-
type KeyboardHint,
|
|
269
274
|
KeyboardHintBar,
|
|
270
275
|
type KeyboardHintBarProps,
|
|
271
276
|
} from "./components/keyboard-hint-bar.js";
|
|
@@ -662,6 +667,28 @@ export {
|
|
|
662
667
|
isFocusable,
|
|
663
668
|
isSelectable,
|
|
664
669
|
} from "./lib/folder-tree-focus.js";
|
|
670
|
+
export {
|
|
671
|
+
defaultKeyboardHints,
|
|
672
|
+
KEY_HINT_GROUPS,
|
|
673
|
+
type KeyboardHint,
|
|
674
|
+
type KeyHint,
|
|
675
|
+
type KeyHintGroup,
|
|
676
|
+
keyboardHintsFor,
|
|
677
|
+
keysForAction,
|
|
678
|
+
shortcutHintForAction,
|
|
679
|
+
type TriageAction,
|
|
680
|
+
type TriageHandlers,
|
|
681
|
+
tooltipForAction,
|
|
682
|
+
} from "./lib/keymap.js";
|
|
683
|
+
export {
|
|
684
|
+
type DispatchResult,
|
|
685
|
+
dispatchKey,
|
|
686
|
+
isControlTarget,
|
|
687
|
+
isEditableTarget,
|
|
688
|
+
type KeyStroke,
|
|
689
|
+
ROW_ATTRIBUTE,
|
|
690
|
+
type SequencePrefix,
|
|
691
|
+
} from "./lib/keymap-dispatch.js";
|
|
665
692
|
export {
|
|
666
693
|
isLabelColorValue,
|
|
667
694
|
type LabelColorValue,
|
|
@@ -702,6 +729,12 @@ export {
|
|
|
702
729
|
type SuggestKeyState,
|
|
703
730
|
suggestKeyAction,
|
|
704
731
|
} from "./lib/suggest-keys.js";
|
|
732
|
+
export { type ListCursor, useListCursor } from "./lib/use-list-cursor.js";
|
|
733
|
+
export {
|
|
734
|
+
type ListKeyboard,
|
|
735
|
+
type UseListKeyboardOptions,
|
|
736
|
+
useListKeyboard,
|
|
737
|
+
} from "./lib/use-list-keyboard.js";
|
|
705
738
|
export {
|
|
706
739
|
type UseLongPressOptions,
|
|
707
740
|
type UseLongPressResult,
|
|
@@ -727,6 +760,7 @@ export {
|
|
|
727
760
|
type UseSuggestListInput,
|
|
728
761
|
useSuggestList,
|
|
729
762
|
} from "./lib/use-suggest-list.js";
|
|
763
|
+
export { useTriageKeyboard } from "./lib/use-triage-keyboard.js";
|
|
730
764
|
export {
|
|
731
765
|
backExits,
|
|
732
766
|
clauseSentence,
|