@remit/ui 0.0.113 → 0.0.115
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 +3 -3
- package/src/components/app-shell-slotted.render.test.ts +83 -0
- package/src/components/app-shell-slotted.tsx +31 -3
- package/src/components/attendee-row.render.test.ts +73 -0
- package/src/components/attendee-row.stories.tsx +68 -0
- package/src/components/attendee-row.tsx +96 -0
- package/src/components/calendar-event-chip.render.test.ts +67 -0
- package/src/components/calendar-event-chip.stories.tsx +128 -0
- package/src/components/calendar-event-chip.tsx +116 -0
- package/src/components/calendar-list.render.test.ts +77 -0
- package/src/components/calendar-list.stories.tsx +130 -0
- package/src/components/calendar-list.tsx +225 -0
- package/src/components/calendar-toolbar.render.test.ts +69 -0
- package/src/components/calendar-toolbar.stories.tsx +55 -0
- package/src/components/calendar-toolbar.tsx +178 -0
- package/src/components/calendar-types.ts +135 -0
- package/src/components/custom-recurrence.stories.tsx +106 -0
- package/src/components/custom-recurrence.tsx +411 -0
- package/src/components/event-detail.render.test.ts +104 -0
- package/src/components/event-detail.stories.tsx +178 -0
- package/src/components/event-detail.tsx +188 -0
- package/src/components/event-editor-pane.tsx +54 -0
- package/src/components/event-editor.render.test.ts +83 -0
- package/src/components/event-editor.stories.tsx +99 -0
- package/src/components/event-editor.tsx +480 -0
- package/src/components/event-quick-entry.render.test.ts +40 -0
- package/src/components/event-quick-entry.stories.tsx +56 -0
- package/src/components/event-quick-entry.tsx +159 -0
- package/src/components/event-suggestion-card.render.test.ts +65 -0
- package/src/components/event-suggestion-card.stories.tsx +93 -0
- package/src/components/event-suggestion-card.tsx +116 -0
- package/src/components/flow-screen.tsx +169 -0
- package/src/components/intelligence-panel.stories.tsx +5 -1
- package/src/components/intelligence-panel.tsx +4 -0
- package/src/components/isolated-email-frame.tsx +1 -18
- package/src/components/message-list-pane.render.test.ts +2 -2
- package/src/components/message-list-pane.stories.tsx +1 -1
- package/src/components/nav-sidebar.tsx +20 -0
- package/src/components/popover-menu.tsx +230 -27
- package/src/components/pull-to-refresh.tsx +1 -19
- package/src/components/recurrence-scope-prompt.render.test.ts +36 -0
- package/src/components/recurrence-scope-prompt.stories.tsx +46 -0
- package/src/components/recurrence-scope-prompt.tsx +84 -0
- package/src/components/rich-text-correction-menu.tsx +220 -0
- package/src/components/rich-text-editor.stories.tsx +507 -5
- package/src/components/rich-text-editor.tsx +482 -83
- package/src/components/rich-text-spellcheck-menu.test.ts +865 -0
- package/src/components/rich-text-spellcheck-provider.test.ts +114 -1
- package/src/components/rich-text-spellcheck-provider.ts +68 -3
- package/src/components/rich-text-spellcheck-words.ts +168 -4
- package/src/components/rich-text-spellcheck-worker.ts +11 -0
- package/src/components/rich-text-spellcheck.test.ts +7 -0
- package/src/components/rich-text-spellcheck.ts +28 -2
- package/src/components/selection-wizard.tsx +19 -69
- package/src/index.ts +116 -0
- package/src/lib/calendar-color.ts +71 -0
- package/src/lib/event-phrase.test.ts +89 -0
- package/src/lib/event-phrase.ts +228 -0
- package/src/lib/recurrence.test.ts +141 -0
- package/src/lib/recurrence.ts +266 -0
- package/src/lib/use-match-media.ts +25 -0
- package/src/rich-text.ts +12 -0
- package/src/tokens.css +63 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { BookPlus, EyeOff } from "lucide-react";
|
|
2
|
+
import { useEffect, useRef } from "react";
|
|
3
|
+
import { DESKTOP_MEDIA_QUERY } from "../lib/layout-breakpoints.js";
|
|
4
|
+
import { useRovingFocus } from "../lib/roving-focus.js";
|
|
5
|
+
import { useMatchMedia } from "../lib/use-match-media.js";
|
|
6
|
+
import { BottomSheet } from "./bottom-sheet.js";
|
|
7
|
+
import {
|
|
8
|
+
type PopoverMenuAnchor,
|
|
9
|
+
PopoverMenuPanel,
|
|
10
|
+
PopoverMenuPortal,
|
|
11
|
+
PopoverMenuRow,
|
|
12
|
+
} from "./popover-menu.js";
|
|
13
|
+
|
|
14
|
+
const SKELETON_WIDTHS = ["w-28", "w-20", "w-24"];
|
|
15
|
+
|
|
16
|
+
export type CorrectionMenuAnchor = PopoverMenuAnchor;
|
|
17
|
+
|
|
18
|
+
export interface RichTextCorrectionMenuProps {
|
|
19
|
+
word: string;
|
|
20
|
+
/** Null while the checker is still answering. */
|
|
21
|
+
suggestions: readonly string[] | null;
|
|
22
|
+
/** What the checker said when it could not answer. */
|
|
23
|
+
failure: string | null;
|
|
24
|
+
anchor: CorrectionMenuAnchor;
|
|
25
|
+
language: string;
|
|
26
|
+
onReplace: (suggestion: string) => void;
|
|
27
|
+
onIgnore: () => void;
|
|
28
|
+
/** Rendered only when the mount carries somewhere to put the word. */
|
|
29
|
+
onAddWord?: () => void;
|
|
30
|
+
/**
|
|
31
|
+
* `returnFocus` is false when the writer's own click is already on its way
|
|
32
|
+
* somewhere else, and taking the caret back would fight it.
|
|
33
|
+
*/
|
|
34
|
+
onDismiss: (returnFocus: boolean) => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const CorrectionRows = ({
|
|
38
|
+
word,
|
|
39
|
+
suggestions,
|
|
40
|
+
failure,
|
|
41
|
+
language,
|
|
42
|
+
onReplace,
|
|
43
|
+
onIgnore,
|
|
44
|
+
onAddWord,
|
|
45
|
+
}: Omit<RichTextCorrectionMenuProps, "anchor" | "onDismiss">) => (
|
|
46
|
+
<>
|
|
47
|
+
<p
|
|
48
|
+
lang={language}
|
|
49
|
+
data-testid="spell-word"
|
|
50
|
+
className="px-4 pb-1 pt-1.5 text-xs font-medium text-fg-subtle"
|
|
51
|
+
>
|
|
52
|
+
{word}
|
|
53
|
+
</p>
|
|
54
|
+
{suggestions === null && failure === null
|
|
55
|
+
? SKELETON_WIDTHS.map((width) => (
|
|
56
|
+
<div
|
|
57
|
+
key={width}
|
|
58
|
+
aria-hidden="true"
|
|
59
|
+
data-testid="spell-suggestion-skeleton"
|
|
60
|
+
className="flex min-h-11 items-center px-4 py-2.5"
|
|
61
|
+
>
|
|
62
|
+
<span
|
|
63
|
+
className={`h-3 animate-pulse rounded bg-surface-sunken ${width}`}
|
|
64
|
+
/>
|
|
65
|
+
</div>
|
|
66
|
+
))
|
|
67
|
+
: null}
|
|
68
|
+
{failure !== null && (
|
|
69
|
+
<p
|
|
70
|
+
role="alert"
|
|
71
|
+
data-testid="spell-suggestions-failed"
|
|
72
|
+
className="px-4 py-2 text-sm text-danger"
|
|
73
|
+
>
|
|
74
|
+
No suggestions came back: {failure}
|
|
75
|
+
</p>
|
|
76
|
+
)}
|
|
77
|
+
{suggestions?.length === 0 && (
|
|
78
|
+
<p
|
|
79
|
+
data-testid="spell-no-suggestions"
|
|
80
|
+
className="px-4 py-2 text-sm text-fg-muted"
|
|
81
|
+
>
|
|
82
|
+
No suggestions for this word.
|
|
83
|
+
</p>
|
|
84
|
+
)}
|
|
85
|
+
{suggestions?.map((suggestion) => (
|
|
86
|
+
<PopoverMenuRow
|
|
87
|
+
key={suggestion}
|
|
88
|
+
label={suggestion}
|
|
89
|
+
lang={language}
|
|
90
|
+
data-testid="spell-suggestion"
|
|
91
|
+
className="font-medium"
|
|
92
|
+
onSelect={() => onReplace(suggestion)}
|
|
93
|
+
/>
|
|
94
|
+
))}
|
|
95
|
+
<hr className="my-1 border-line" />
|
|
96
|
+
<PopoverMenuRow
|
|
97
|
+
label="Ignore for now"
|
|
98
|
+
icon={<EyeOff className="size-4" />}
|
|
99
|
+
data-testid="spell-ignore"
|
|
100
|
+
onSelect={onIgnore}
|
|
101
|
+
/>
|
|
102
|
+
{onAddWord && (
|
|
103
|
+
<PopoverMenuRow
|
|
104
|
+
label="Add to dictionary"
|
|
105
|
+
icon={<BookPlus className="size-4" />}
|
|
106
|
+
data-testid="spell-add-word"
|
|
107
|
+
onSelect={onAddWord}
|
|
108
|
+
/>
|
|
109
|
+
)}
|
|
110
|
+
</>
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The corrections offered for one misspelt word. The menu is up before the
|
|
115
|
+
* checker has answered — skeleton rows stand where the suggestions will be —
|
|
116
|
+
* and it never goes quiet: a request that failed says so where the suggestions
|
|
117
|
+
* would have been, and ignoring the word stays reachable either way.
|
|
118
|
+
*
|
|
119
|
+
* Below the desktop gate the same rows are a sheet, because a popover anchored
|
|
120
|
+
* to a word sits under the thumb that opened it.
|
|
121
|
+
*/
|
|
122
|
+
export const RichTextCorrectionMenu = ({
|
|
123
|
+
anchor,
|
|
124
|
+
onDismiss,
|
|
125
|
+
...rows
|
|
126
|
+
}: RichTextCorrectionMenuProps) => {
|
|
127
|
+
const desktop = useMatchMedia(DESKTOP_MEDIA_QUERY);
|
|
128
|
+
const panelRef = useRef<HTMLDivElement>(null);
|
|
129
|
+
|
|
130
|
+
useRovingFocus({ containerRef: panelRef, itemSelector: '[role="menuitem"]' });
|
|
131
|
+
|
|
132
|
+
// The menu takes focus as it opens, so Escape and the arrow keys have
|
|
133
|
+
// somewhere to land rather than staying behind in the message.
|
|
134
|
+
useEffect(() => {
|
|
135
|
+
panelRef.current?.focus();
|
|
136
|
+
}, []);
|
|
137
|
+
|
|
138
|
+
// A press rather than a click, and a pointer press rather than a mouse one:
|
|
139
|
+
// the tap that opened this menu is followed by the browser's compatibility
|
|
140
|
+
// mouse events, which a `mousedown` listener reads as a press somewhere else
|
|
141
|
+
// and closes the menu on the way up.
|
|
142
|
+
//
|
|
143
|
+
// Escape is caught here too, at the document rather than the panel: the
|
|
144
|
+
// panel takes focus on mount so the panel's own `onKeyDown` ordinarily
|
|
145
|
+
// gets there first and this one never fires (its `stopPropagation` keeps
|
|
146
|
+
// the keydown from reaching here), but a focus call that lands a beat
|
|
147
|
+
// later than the browser is ready for is not something to depend on —
|
|
148
|
+
// this is what actually closes the menu when that happens.
|
|
149
|
+
useEffect(() => {
|
|
150
|
+
if (!desktop) return;
|
|
151
|
+
const onPointer = (event: Event) => {
|
|
152
|
+
if (panelRef.current?.contains(event.target as Node)) return;
|
|
153
|
+
onDismiss(false);
|
|
154
|
+
};
|
|
155
|
+
const onDocumentKeyDown = (event: KeyboardEvent) => {
|
|
156
|
+
if (event.key !== "Escape") return;
|
|
157
|
+
onDismiss(true);
|
|
158
|
+
};
|
|
159
|
+
document.addEventListener("pointerdown", onPointer);
|
|
160
|
+
document.addEventListener("keydown", onDocumentKeyDown);
|
|
161
|
+
return () => {
|
|
162
|
+
document.removeEventListener("pointerdown", onPointer);
|
|
163
|
+
document.removeEventListener("keydown", onDocumentKeyDown);
|
|
164
|
+
};
|
|
165
|
+
}, [desktop, onDismiss]);
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Escape closes it, and so does Tab: a menu is not a dialog, and the repo
|
|
169
|
+
* traps focus nowhere. Letting Tab walk on would put the caret behind an
|
|
170
|
+
* open sheet, so the menu goes first and the message has the focus back
|
|
171
|
+
* before the next stop.
|
|
172
|
+
*/
|
|
173
|
+
const onKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
174
|
+
if (event.key !== "Escape" && event.key !== "Tab") return;
|
|
175
|
+
event.preventDefault();
|
|
176
|
+
event.stopPropagation();
|
|
177
|
+
onDismiss(true);
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
if (!desktop) {
|
|
181
|
+
return (
|
|
182
|
+
<BottomSheet
|
|
183
|
+
open
|
|
184
|
+
onClose={() => onDismiss(true)}
|
|
185
|
+
dismissLabel="Close corrections"
|
|
186
|
+
>
|
|
187
|
+
<div
|
|
188
|
+
ref={panelRef}
|
|
189
|
+
role="menu"
|
|
190
|
+
aria-label={`Corrections for ${rows.word}`}
|
|
191
|
+
tabIndex={-1}
|
|
192
|
+
onKeyDown={onKeyDown}
|
|
193
|
+
data-testid="spell-menu"
|
|
194
|
+
className="flex flex-col overflow-y-auto pb-4 outline-none"
|
|
195
|
+
>
|
|
196
|
+
<CorrectionRows {...rows} />
|
|
197
|
+
</div>
|
|
198
|
+
</BottomSheet>
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return (
|
|
203
|
+
<PopoverMenuPortal
|
|
204
|
+
open
|
|
205
|
+
align="start"
|
|
206
|
+
panelRef={panelRef}
|
|
207
|
+
getAnchor={() => anchor}
|
|
208
|
+
>
|
|
209
|
+
<PopoverMenuPanel
|
|
210
|
+
ref={panelRef}
|
|
211
|
+
label={`Corrections for ${rows.word}`}
|
|
212
|
+
onKeyDown={onKeyDown}
|
|
213
|
+
data-testid="spell-menu"
|
|
214
|
+
className="max-w-64"
|
|
215
|
+
>
|
|
216
|
+
<CorrectionRows {...rows} />
|
|
217
|
+
</PopoverMenuPanel>
|
|
218
|
+
</PopoverMenuPortal>
|
|
219
|
+
);
|
|
220
|
+
};
|