@remit/ui 0.0.12 → 0.0.14
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 +7 -0
- package/src/components/app-shell.render.test.ts +18 -3
- package/src/components/app-shell.tsx +5 -0
- package/src/components/folder-role.test.ts +72 -0
- package/src/components/folder-role.tsx +59 -0
- package/src/components/message-list-pane.stories.tsx +30 -2
- package/src/components/mobile-search-view.render.test.ts +78 -0
- package/src/components/mobile-search-view.stories.tsx +117 -2
- package/src/components/mobile-search-view.tsx +20 -1
- package/src/components/search-result-row.tsx +23 -0
- package/src/components/search-results.render.test.ts +181 -0
- package/src/components/search-results.stories.tsx +165 -28
- package/src/components/search-results.tsx +101 -5
- package/src/components/selection-top-bar.render.test.ts +89 -0
- package/src/components/selection-top-bar.stories.tsx +66 -0
- package/src/components/selection-top-bar.tsx +49 -1
- package/src/components/spam-results-offer.render.test.ts +26 -0
- package/src/components/spam-results-offer.stories.tsx +41 -0
- package/src/components/spam-results-offer.tsx +52 -0
- package/src/components/swipeable-row.stories.tsx +14 -0
- package/src/components/touch-list-body.stories.tsx +19 -0
- package/src/index.ts +9 -0
|
@@ -64,4 +64,93 @@ describe("SelectionTopBar", () => {
|
|
|
64
64
|
);
|
|
65
65
|
assert.match(html, /Cross-account moves are not supported/);
|
|
66
66
|
});
|
|
67
|
+
|
|
68
|
+
it("renders statusLabel in place of the count copy when provided", () => {
|
|
69
|
+
const html = renderToString(
|
|
70
|
+
createElement(SelectionTopBar, {
|
|
71
|
+
...handlers,
|
|
72
|
+
count: 3412,
|
|
73
|
+
statusLabel: "Deleting 1,200 of 3,412…",
|
|
74
|
+
}),
|
|
75
|
+
);
|
|
76
|
+
assert.match(text(html), /Deleting 1,200 of 3,412…/);
|
|
77
|
+
assert.doesNotMatch(text(html), /3412 messages selected/);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("falls back to the count copy when statusLabel is absent", () => {
|
|
81
|
+
const html = renderToString(
|
|
82
|
+
createElement(SelectionTopBar, { ...handlers, count: 2 }),
|
|
83
|
+
);
|
|
84
|
+
assert.match(text(html), /2 messages selected/);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("renders failureHint when provided", () => {
|
|
88
|
+
const html = renderToString(
|
|
89
|
+
createElement(SelectionTopBar, {
|
|
90
|
+
...handlers,
|
|
91
|
+
count: 2,
|
|
92
|
+
failureHint: "340 failed to delete — retry?",
|
|
93
|
+
}),
|
|
94
|
+
);
|
|
95
|
+
assert.match(html, /340 failed to delete — retry\?/);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("omits the select-all control when selectAll is absent", () => {
|
|
99
|
+
const html = renderToString(
|
|
100
|
+
createElement(SelectionTopBar, { ...handlers, count: 2 }),
|
|
101
|
+
);
|
|
102
|
+
assert.doesNotMatch(html, /aria-label="Select all"/);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("renders the select-all control, unchecked, in the some-selected state", () => {
|
|
106
|
+
const html = renderToString(
|
|
107
|
+
createElement(SelectionTopBar, {
|
|
108
|
+
...handlers,
|
|
109
|
+
count: 2,
|
|
110
|
+
selectAll: {
|
|
111
|
+
checked: false,
|
|
112
|
+
indeterminate: true,
|
|
113
|
+
onChange: () => undefined,
|
|
114
|
+
},
|
|
115
|
+
}),
|
|
116
|
+
);
|
|
117
|
+
assert.match(html, /aria-label="Select all"/);
|
|
118
|
+
assert.doesNotMatch(
|
|
119
|
+
html,
|
|
120
|
+
/aria-label="Select all"[^>]*checked=""/,
|
|
121
|
+
"some-selected is not the checked state",
|
|
122
|
+
);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("renders the select-all control checked in the all-selected state", () => {
|
|
126
|
+
const html = renderToString(
|
|
127
|
+
createElement(SelectionTopBar, {
|
|
128
|
+
...handlers,
|
|
129
|
+
count: 12,
|
|
130
|
+
selectAll: {
|
|
131
|
+
checked: true,
|
|
132
|
+
indeterminate: false,
|
|
133
|
+
onChange: () => undefined,
|
|
134
|
+
},
|
|
135
|
+
}),
|
|
136
|
+
);
|
|
137
|
+
assert.match(
|
|
138
|
+
html,
|
|
139
|
+
/aria-label="Select all"[^>]*checked=""/,
|
|
140
|
+
"all-selected renders the checkbox checked",
|
|
141
|
+
);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("omitting every new prop renders exactly the pre-existing bar", () => {
|
|
145
|
+
const html = renderToString(
|
|
146
|
+
createElement(SelectionTopBar, { ...handlers, count: 2 }),
|
|
147
|
+
);
|
|
148
|
+
assert.doesNotMatch(
|
|
149
|
+
html,
|
|
150
|
+
/aria-label="Select all"/,
|
|
151
|
+
"no select-all control",
|
|
152
|
+
);
|
|
153
|
+
assert.match(text(html), /2 messages selected/, "default count copy");
|
|
154
|
+
assert.doesNotMatch(html, /role="status"/, "no status line rendered");
|
|
155
|
+
});
|
|
67
156
|
});
|
|
@@ -37,3 +37,69 @@ export const CrossAccountHint: Story = {
|
|
|
37
37
|
"Move only works within one account — clear selection or pick messages from a single account",
|
|
38
38
|
},
|
|
39
39
|
};
|
|
40
|
+
|
|
41
|
+
/** Some but not all rows checked: the select-all control renders the
|
|
42
|
+
* `Checkbox` tri-state dash, not the box or the tick. */
|
|
43
|
+
export const SelectAll: Story = {
|
|
44
|
+
args: {
|
|
45
|
+
count: 3,
|
|
46
|
+
selectAll: {
|
|
47
|
+
checked: false,
|
|
48
|
+
indeterminate: true,
|
|
49
|
+
onChange: () => undefined,
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/** Every row checked: the select-all control renders as a plain checked box. */
|
|
55
|
+
export const AllSelected: Story = {
|
|
56
|
+
args: {
|
|
57
|
+
count: 12,
|
|
58
|
+
selectAll: {
|
|
59
|
+
checked: true,
|
|
60
|
+
indeterminate: false,
|
|
61
|
+
onChange: () => undefined,
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/** While a search result set is still paging, the exact count isn't known yet —
|
|
67
|
+
* `statusLabel` replaces the "{count} selected" text with a counting message. */
|
|
68
|
+
export const Counting: Story = {
|
|
69
|
+
args: {
|
|
70
|
+
count: 0,
|
|
71
|
+
statusLabel: "Counting matching messages…",
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/** A bulk delete in progress reports a running total via `statusLabel`, and
|
|
76
|
+
* the delete button shows its busy spinner (never disables). */
|
|
77
|
+
export const DeletingWithProgress: Story = {
|
|
78
|
+
args: {
|
|
79
|
+
count: 3412,
|
|
80
|
+
statusLabel: "Deleting 1,200 of 3,412…",
|
|
81
|
+
isBusy: true,
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/** After a bulk delete finishes with some batches failed, `failureHint`
|
|
86
|
+
* surfaces the shortfall in danger tone — independent of `moveDisabledHint`,
|
|
87
|
+
* which is muted and cross-account-specific. */
|
|
88
|
+
export const PartialFailure: Story = {
|
|
89
|
+
args: {
|
|
90
|
+
count: 3412,
|
|
91
|
+
failureHint: "3,072 deleted, 340 failed to delete — retry?",
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* count === 0 is unreachable in production: both the kit
|
|
97
|
+
* (`message-list-pane.tsx`'s `toggleCheck`) and the web-client
|
|
98
|
+
* (`MessageList.tsx`'s multi-select effect) auto-exit selection mode the
|
|
99
|
+
* instant the last checked row is unchecked. `SelectionTopBar` itself has no
|
|
100
|
+
* floor on `count` — this story pins the contract that no caller should ever
|
|
101
|
+
* leave it mounted here.
|
|
102
|
+
*/
|
|
103
|
+
export const ZeroSelected: Story = {
|
|
104
|
+
args: { count: 0 },
|
|
105
|
+
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Loader2, MailOpen, Trash2, X } from "lucide-react";
|
|
2
2
|
import type { ReactNode } from "react";
|
|
3
3
|
import { Button } from "./button.js";
|
|
4
|
+
import { Checkbox } from "./checkbox.js";
|
|
4
5
|
|
|
5
6
|
export interface SelectionTopBarProps {
|
|
6
7
|
count: number;
|
|
@@ -23,6 +24,30 @@ export interface SelectionTopBarProps {
|
|
|
23
24
|
* shows a spinner; other actions no-op. Never disables controls.
|
|
24
25
|
*/
|
|
25
26
|
isBusy?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Select-all control rendered between cancel and the count label. Presence
|
|
29
|
+
* of this prop is what renders the checkbox — omit it for a bar with no
|
|
30
|
+
* select-all affordance. `indeterminate` renders the some-selected tri-state
|
|
31
|
+
* (`Checkbox`'s dash), `checked` is the all-selected state.
|
|
32
|
+
*/
|
|
33
|
+
selectAll?: {
|
|
34
|
+
checked: boolean;
|
|
35
|
+
indeterminate?: boolean;
|
|
36
|
+
onChange: () => void;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Overrides the "{count} messages selected" text. For states where the
|
|
40
|
+
* count itself isn't the useful thing to show yet — "Counting…" while a
|
|
41
|
+
* search result set is still paging, or "Deleting 1,200 of 3,412…" progress
|
|
42
|
+
* during a bulk delete.
|
|
43
|
+
*/
|
|
44
|
+
statusLabel?: string;
|
|
45
|
+
/**
|
|
46
|
+
* Danger-toned status line below the action row, for a partial failure
|
|
47
|
+
* after a bulk operation (e.g. some batches failed to delete). Independent
|
|
48
|
+
* of `moveDisabledHint` — a caller shows one or the other, never both.
|
|
49
|
+
*/
|
|
50
|
+
failureHint?: string;
|
|
26
51
|
}
|
|
27
52
|
|
|
28
53
|
/**
|
|
@@ -37,6 +62,9 @@ export function SelectionTopBar({
|
|
|
37
62
|
moveSlot,
|
|
38
63
|
moveDisabledHint,
|
|
39
64
|
isBusy = false,
|
|
65
|
+
selectAll,
|
|
66
|
+
statusLabel,
|
|
67
|
+
failureHint,
|
|
40
68
|
}: SelectionTopBarProps) {
|
|
41
69
|
return (
|
|
42
70
|
<header className="flex shrink-0 flex-col border-b border-line bg-surface-sunken">
|
|
@@ -49,8 +77,18 @@ export function SelectionTopBar({
|
|
|
49
77
|
aria-label="Cancel selection"
|
|
50
78
|
className="-ml-1 shrink-0"
|
|
51
79
|
/>
|
|
80
|
+
{selectAll && (
|
|
81
|
+
<Checkbox
|
|
82
|
+
aria-label="Select all"
|
|
83
|
+
checked={selectAll.checked}
|
|
84
|
+
indeterminate={selectAll.indeterminate}
|
|
85
|
+
onChange={selectAll.onChange}
|
|
86
|
+
className="shrink-0"
|
|
87
|
+
/>
|
|
88
|
+
)}
|
|
52
89
|
<span className="min-w-0 flex-1 truncate text-sm font-medium text-fg">
|
|
53
|
-
{
|
|
90
|
+
{statusLabel ??
|
|
91
|
+
`${count} ${count === 1 ? "message" : "messages"} selected`}
|
|
54
92
|
</span>
|
|
55
93
|
{onMarkRead && (
|
|
56
94
|
<Button
|
|
@@ -90,6 +128,16 @@ export function SelectionTopBar({
|
|
|
90
128
|
{moveDisabledHint}
|
|
91
129
|
</p>
|
|
92
130
|
)}
|
|
131
|
+
{failureHint && (
|
|
132
|
+
// biome-ignore lint/a11y/useSemanticElements: <p> with role="status" preserves block layout; <output> is inline
|
|
133
|
+
<p
|
|
134
|
+
className="px-row-inset pb-2 text-xs text-danger"
|
|
135
|
+
role="status"
|
|
136
|
+
aria-live="polite"
|
|
137
|
+
>
|
|
138
|
+
{failureHint}
|
|
139
|
+
</p>
|
|
140
|
+
)}
|
|
93
141
|
</header>
|
|
94
142
|
);
|
|
95
143
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import { createElement } from "react";
|
|
4
|
+
import { renderToString } from "react-dom/server";
|
|
5
|
+
import { SpamResultsOffer } from "./spam-results-offer.js";
|
|
6
|
+
|
|
7
|
+
const noop = () => {};
|
|
8
|
+
|
|
9
|
+
describe("SpamResultsOffer", () => {
|
|
10
|
+
it("states the count and offers a way into Spam", () => {
|
|
11
|
+
const html = renderToString(
|
|
12
|
+
createElement(SpamResultsOffer, { count: 3, onScopeToSpam: noop }),
|
|
13
|
+
);
|
|
14
|
+
assert.match(html, />3</);
|
|
15
|
+
assert.match(html, /results from Spam/);
|
|
16
|
+
assert.match(html, /View them/);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("reads in the singular for one match", () => {
|
|
20
|
+
const html = renderToString(
|
|
21
|
+
createElement(SpamResultsOffer, { count: 1, onScopeToSpam: noop }),
|
|
22
|
+
);
|
|
23
|
+
assert.match(html, /result from Spam/);
|
|
24
|
+
assert.doesNotMatch(html, /results from Spam/);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Decorator, Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { SpamResultsOffer } from "./spam-results-offer.js";
|
|
3
|
+
|
|
4
|
+
/** Frames the offer at the width of the list pane it sits at the top of. */
|
|
5
|
+
const listPaneFrame: Decorator = (Story) => (
|
|
6
|
+
<div
|
|
7
|
+
className="overflow-hidden rounded-lg border border-line bg-canvas"
|
|
8
|
+
style={{ width: 360 }}
|
|
9
|
+
>
|
|
10
|
+
<Story />
|
|
11
|
+
</div>
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
const meta: Meta<typeof SpamResultsOffer> = {
|
|
15
|
+
title: "Components/SpamResultsOffer",
|
|
16
|
+
component: SpamResultsOffer,
|
|
17
|
+
parameters: { layout: "centered" },
|
|
18
|
+
decorators: [listPaneFrame],
|
|
19
|
+
};
|
|
20
|
+
export default meta;
|
|
21
|
+
|
|
22
|
+
type Story = StoryObj<typeof SpamResultsOffer>;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Spam matches a global search held out of its results. Deliberately quiet —
|
|
26
|
+
* an offer, not a warning — and the action scopes the search to Spam rather
|
|
27
|
+
* than opening a view of its own.
|
|
28
|
+
*/
|
|
29
|
+
export const Default: Story = {
|
|
30
|
+
args: { count: 3, onScopeToSpam: () => {} },
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/** One match reads in the singular. */
|
|
34
|
+
export const SingleResult: Story = {
|
|
35
|
+
args: { count: 1, onScopeToSpam: () => {} },
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** Large counts stay on one line; the figure is tabular so it does not jitter. */
|
|
39
|
+
export const ManyResults: Story = {
|
|
40
|
+
args: { count: 128, onScopeToSpam: () => {} },
|
|
41
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Banner } from "./banner.js";
|
|
2
|
+
import { Button } from "./button.js";
|
|
3
|
+
|
|
4
|
+
export interface SpamResultsOfferProps {
|
|
5
|
+
/** How many matches the search found in Spam. */
|
|
6
|
+
count: number;
|
|
7
|
+
/**
|
|
8
|
+
* Scope the search to Spam. This is a shortcut to the state reached by
|
|
9
|
+
* navigating to Spam with the query carried over — the same scoped search,
|
|
10
|
+
* with the same `in:spam` chip — not a separate result mode.
|
|
11
|
+
*/
|
|
12
|
+
onScopeToSpam: () => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const plural = (n: number): string => (n === 1 ? "result" : "results");
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Spam matches held out of a global search, offered rather than mixed in. Spam
|
|
19
|
+
* is the one folder a search that reaches everywhere does not inline, because
|
|
20
|
+
* the whole point of the folder is that its contents are unwanted until asked
|
|
21
|
+
* for. Taking the offer scopes the search to Spam.
|
|
22
|
+
*
|
|
23
|
+
* Quiet on purpose: this is an offer, not a warning. Presentational — the
|
|
24
|
+
* caller owns what "scope to spam" does.
|
|
25
|
+
*/
|
|
26
|
+
export function SpamResultsOffer({
|
|
27
|
+
count,
|
|
28
|
+
onScopeToSpam,
|
|
29
|
+
}: SpamResultsOfferProps) {
|
|
30
|
+
return (
|
|
31
|
+
<Banner
|
|
32
|
+
tone="info"
|
|
33
|
+
variant="soft"
|
|
34
|
+
className="items-center justify-between gap-3 rounded-none border-b border-line"
|
|
35
|
+
>
|
|
36
|
+
<div className="flex items-center justify-between gap-3">
|
|
37
|
+
<p className="min-w-0 text-xs text-fg-muted">
|
|
38
|
+
<span className="font-semibold text-fg tabular-nums">{count}</span>{" "}
|
|
39
|
+
{`${plural(count)} from Spam`}
|
|
40
|
+
</p>
|
|
41
|
+
<Button
|
|
42
|
+
variant="ghost"
|
|
43
|
+
size="sm"
|
|
44
|
+
onClick={onScopeToSpam}
|
|
45
|
+
className="shrink-0 text-accent"
|
|
46
|
+
>
|
|
47
|
+
View them
|
|
48
|
+
</Button>
|
|
49
|
+
</div>
|
|
50
|
+
</Banner>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
@@ -55,6 +55,20 @@ export const PeekedLeading: Story = { args: { peek: "leading" } };
|
|
|
55
55
|
|
|
56
56
|
export const PeekedTrailing: Story = { args: { peek: "trailing" } };
|
|
57
57
|
|
|
58
|
+
/**
|
|
59
|
+
* In selection mode the leading avatar is REPLACED by a checkbox affordance
|
|
60
|
+
* — unchecked below, checked in the next story. `baseArgs` never flips
|
|
61
|
+
* `selectionMode`/`checked`, so this row-level toggle had zero coverage.
|
|
62
|
+
*/
|
|
63
|
+
export const SelectionUnchecked: Story = {
|
|
64
|
+
args: { peek: "none", selectionMode: true, checked: false },
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/** Selection mode, row checked: the circle fills accent and shows a tick. */
|
|
68
|
+
export const SelectionChecked: Story = {
|
|
69
|
+
args: { peek: "none", selectionMode: true, checked: true },
|
|
70
|
+
};
|
|
71
|
+
|
|
58
72
|
/**
|
|
59
73
|
* The open affordance is rendered as a real `<a href>` via `linkComponent`,
|
|
60
74
|
* so deep-link, middle-click and open-in-new-tab work. Consumers pass their
|
|
@@ -74,3 +74,22 @@ export const Refreshing: Story = { args: { refreshing: true } };
|
|
|
74
74
|
export const SelectionMode: Story = {
|
|
75
75
|
args: { selectionMode: true, checkedIds: new Set(["t1", "t3"]) },
|
|
76
76
|
};
|
|
77
|
+
|
|
78
|
+
/** Every row checked — the ceiling a select-all control drives toward. */
|
|
79
|
+
export const SelectionModeAllChecked: Story = {
|
|
80
|
+
args: {
|
|
81
|
+
selectionMode: true,
|
|
82
|
+
checkedIds: new Set(
|
|
83
|
+
sections.flatMap((section) => section.threads.map((t) => t.id)),
|
|
84
|
+
),
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Selection mode with nothing checked. `TouchListBody` itself has no floor —
|
|
90
|
+
* the auto-exit-at-zero contract belongs to the caller (`MessageListPane`,
|
|
91
|
+
* production `MessageList.tsx`), which this component doesn't own.
|
|
92
|
+
*/
|
|
93
|
+
export const SelectionModeNoneChecked: Story = {
|
|
94
|
+
args: { selectionMode: true, checkedIds: new Set<string>() },
|
|
95
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -121,7 +121,10 @@ export {
|
|
|
121
121
|
export {
|
|
122
122
|
canonicalRoleLabel,
|
|
123
123
|
type FolderRole,
|
|
124
|
+
isVirtualFolderRole,
|
|
125
|
+
provenanceFolderLabel,
|
|
124
126
|
providerLeaf,
|
|
127
|
+
type ResultFolder,
|
|
125
128
|
roleIcon,
|
|
126
129
|
} from "./components/folder-role.js";
|
|
127
130
|
export {
|
|
@@ -304,9 +307,11 @@ export {
|
|
|
304
307
|
type SearchResultTone,
|
|
305
308
|
} from "./components/search-result-row.js";
|
|
306
309
|
export {
|
|
310
|
+
partitionSpamResults,
|
|
307
311
|
type SearchResultSection,
|
|
308
312
|
SearchResults,
|
|
309
313
|
type SearchResultsProps,
|
|
314
|
+
type SearchScope,
|
|
310
315
|
} from "./components/search-results.js";
|
|
311
316
|
export {
|
|
312
317
|
SearchChipRow,
|
|
@@ -358,6 +363,10 @@ export {
|
|
|
358
363
|
SlidePanel,
|
|
359
364
|
type SlidePanelProps,
|
|
360
365
|
} from "./components/slide-panel.js";
|
|
366
|
+
export {
|
|
367
|
+
SpamResultsOffer,
|
|
368
|
+
type SpamResultsOfferProps,
|
|
369
|
+
} from "./components/spam-results-offer.js";
|
|
361
370
|
export {
|
|
362
371
|
commitPeek,
|
|
363
372
|
SwipeableRow,
|