@eifi1/ui-kit 0.13.1 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/dist/components/error-boundary.d.ts +8 -1
- package/dist/components/error-boundary.js +4 -1
- package/dist/components/error-boundary.js.map +1 -1
- package/dist/components/page-header.d.ts +55 -2
- package/dist/components/page-header.js +34 -3
- package/dist/components/page-header.js.map +1 -1
- package/dist/components/tooltip.d.ts +31 -0
- package/dist/components/tooltip.js +33 -0
- package/dist/components/tooltip.js.map +1 -1
- package/dist/feedback/feedback-thread.d.ts +87 -6
- package/dist/feedback/feedback-thread.js +134 -58
- package/dist/feedback/feedback-thread.js.map +1 -1
- package/dist/feedback.d.ts +1 -1
- package/dist/i18n/locales/de.js +3 -1
- package/dist/i18n/locales/de.js.map +1 -1
- package/dist/i18n/locales/es.js +2 -1
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.js +2 -1
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.js +2 -1
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.js +2 -1
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.js +2 -1
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/package.json +1 -1
- package/src/components/error-boundary.tsx +16 -2
- package/src/components/page-header.tsx +98 -3
- package/src/components/tooltip.tsx +109 -2
- package/src/feedback/feedback-thread.tsx +229 -37
- package/src/i18n/locales/de.ts +2 -0
- package/src/i18n/locales/es.ts +1 -0
- package/src/i18n/locales/fr.ts +1 -0
- package/src/i18n/locales/hu.ts +1 -0
- package/src/i18n/locales/it.ts +1 -0
- package/src/i18n/locales/zh.ts +1 -0
|
@@ -2,7 +2,7 @@ import { useEffect, useImperativeHandle, useLayoutEffect, useRef, useState, useS
|
|
|
2
2
|
import type { ReactNode, Ref } from "react";
|
|
3
3
|
import { Paperclip, Send } from "lucide-react";
|
|
4
4
|
import { cn } from "../lib/cn";
|
|
5
|
-
import { formatRelativeTime } from "../lib/format";
|
|
5
|
+
import { formatDate, formatRelativeTime } from "../lib/format";
|
|
6
6
|
import { Button, EmptyState, Spinner, Textarea } from "../components/ui";
|
|
7
7
|
import { Skeleton } from "../components/skeleton";
|
|
8
8
|
import { Tooltip } from "../components/tooltip";
|
|
@@ -35,6 +35,13 @@ import type { FeedbackAttachmentLabels } from "./feedback-dialog";
|
|
|
35
35
|
* **RTL.** Sides are logical — `justify-end` is the reading end, and the bubble's
|
|
36
36
|
* tucked corner is `rounded-ee` / `rounded-es` — and each body is `dir="auto"`, so a
|
|
37
37
|
* Hebrew reply in an English thread (or the reverse) lays out as its own language.
|
|
38
|
+
*
|
|
39
|
+
* **A chat, too** (keksdose G4/G5). keksdose's support chat is the same conversation
|
|
40
|
+
* read the chat way: status lines between the messages ("Closed automatically",
|
|
41
|
+
* "Reopened"), a day line where the date changes, clock times under it, Enter to send
|
|
42
|
+
* and several files uploaded ahead of the send. Each of those is an opt-in here
|
|
43
|
+
* (`kind: "event"`, `daySeparators`, `timeFormat="clock"`, `sendOn="enter"`,
|
|
44
|
+
* `attachmentSlot` + `canSend`), so the feedback thread's defaults stay as they were.
|
|
38
45
|
*/
|
|
39
46
|
|
|
40
47
|
/* ── Labels ───────────────────────────────────────────────────────────────── */
|
|
@@ -71,6 +78,9 @@ export interface FeedbackComposerLabels {
|
|
|
71
78
|
send: string;
|
|
72
79
|
/** The shortcut line, given the platform's modifier ("Ctrl" or "⌘"). */
|
|
73
80
|
sendHint: (modifier: string) => string;
|
|
81
|
+
/** The shortcut line under `sendOn="enter"` (keksdose G5): Enter sends, so say how a
|
|
82
|
+
* new line is made. */
|
|
83
|
+
sendHintEnter: string;
|
|
74
84
|
}
|
|
75
85
|
|
|
76
86
|
export const DEFAULT_FEEDBACK_COMPOSER_LABELS: FeedbackComposerLabels = {
|
|
@@ -78,6 +88,7 @@ export const DEFAULT_FEEDBACK_COMPOSER_LABELS: FeedbackComposerLabels = {
|
|
|
78
88
|
placeholder: "Write a comment…",
|
|
79
89
|
send: "Send",
|
|
80
90
|
sendHint: (modifier) => `${modifier} + Enter to send`,
|
|
91
|
+
sendHintEnter: "Enter to send, Shift + Enter for a new line",
|
|
81
92
|
};
|
|
82
93
|
|
|
83
94
|
/* ── Thread ───────────────────────────────────────────────────────────────── */
|
|
@@ -94,6 +105,8 @@ export interface FeedbackThreadAttachment {
|
|
|
94
105
|
}
|
|
95
106
|
|
|
96
107
|
export interface FeedbackThreadMessage {
|
|
108
|
+
/** Leave out, or `"message"`. `"event"` is a {@link FeedbackThreadEvent}. */
|
|
109
|
+
kind?: "message";
|
|
97
110
|
id: string | number;
|
|
98
111
|
/** Who wrote it. Left out on an `own` message, it reads "You". */
|
|
99
112
|
author?: ReactNode;
|
|
@@ -107,8 +120,34 @@ export interface FeedbackThreadMessage {
|
|
|
107
120
|
staff?: boolean;
|
|
108
121
|
}
|
|
109
122
|
|
|
123
|
+
/**
|
|
124
|
+
* A line in the thread that nobody wrote — "Closed automatically", "Marked as
|
|
125
|
+
* resolved", "Reopened" — drawn centred and muted with its time, no author, no side,
|
|
126
|
+
* no bubble.
|
|
127
|
+
*
|
|
128
|
+
* keksdose G4: its support chat interleaves these with the messages, and as a message
|
|
129
|
+
* they came out as a bubble from nobody at the start side, which reads as the member
|
|
130
|
+
* having said "resolved". Word it for the READER, as with `own`: the app knows whether
|
|
131
|
+
* "you reopened it" or "Anna reopened it" is true for the person looking.
|
|
132
|
+
*
|
|
133
|
+
* The text is the app's, not somebody's words about their data, so it is NOT tagged
|
|
134
|
+
* `data-private`; one that names a person can wrap the name in its own
|
|
135
|
+
* `<span data-private>` (`body` is a node for that reason).
|
|
136
|
+
*/
|
|
137
|
+
export interface FeedbackThreadEvent {
|
|
138
|
+
kind: "event";
|
|
139
|
+
id: string | number;
|
|
140
|
+
/** A `Date`, an ISO string or epoch ms. */
|
|
141
|
+
createdAt: Date | string | number;
|
|
142
|
+
body: ReactNode;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** One row of a {@link FeedbackThread}: a message, or an event line between them. */
|
|
146
|
+
export type FeedbackThreadItem = FeedbackThreadMessage | FeedbackThreadEvent;
|
|
147
|
+
|
|
110
148
|
export interface FeedbackThreadProps {
|
|
111
|
-
messages
|
|
149
|
+
/** Oldest first. Event lines ({@link FeedbackThreadEvent}) sit among the messages. */
|
|
150
|
+
messages: readonly FeedbackThreadItem[];
|
|
112
151
|
loading?: boolean;
|
|
113
152
|
/** Replaces the default "No comments yet". */
|
|
114
153
|
empty?: ReactNode;
|
|
@@ -120,7 +159,23 @@ export interface FeedbackThreadProps {
|
|
|
120
159
|
renderAttachment?: (attachment: FeedbackThreadAttachment, message: FeedbackThreadMessage) => ReactNode;
|
|
121
160
|
/** Tag bodies, authors and attachments `data-private`. Default true. */
|
|
122
161
|
redact?: boolean;
|
|
123
|
-
/**
|
|
162
|
+
/**
|
|
163
|
+
* How a row's time reads. `"relative"` (default): "5 minutes ago", a date past six
|
|
164
|
+
* days — right for a report's comments, read days apart. `"clock"`: the time of day
|
|
165
|
+
* in the locale ("14:05", "2:05 PM") — the chat look, keksdose G4; pair it with
|
|
166
|
+
* `daySeparators`, which say which day that clock time is on. Either way the full
|
|
167
|
+
* date and time is in the Tooltip.
|
|
168
|
+
*/
|
|
169
|
+
timeFormat?: "relative" | "clock";
|
|
170
|
+
/**
|
|
171
|
+
* A centred day line before the first row and wherever the next row is on another
|
|
172
|
+
* calendar day (the device's zone): "Today", "Yesterday", else the date — keksdose
|
|
173
|
+
* G4. "Today" / "Yesterday" are `Intl.RelativeTimeFormat`'s own words in the kit
|
|
174
|
+
* locale, so there is no label to translate. Default false.
|
|
175
|
+
*/
|
|
176
|
+
daySeparators?: boolean;
|
|
177
|
+
/** "Now" for the relative times and the day lines — pin it in tests and stories.
|
|
178
|
+
* Default: the clock. */
|
|
124
179
|
now?: Date | number;
|
|
125
180
|
/** Overrides the provider's locale for the times. */
|
|
126
181
|
locale?: string;
|
|
@@ -136,6 +191,8 @@ export function FeedbackThread({
|
|
|
136
191
|
redact = true,
|
|
137
192
|
now,
|
|
138
193
|
locale: localeProp,
|
|
194
|
+
timeFormat = "relative",
|
|
195
|
+
daySeparators = false,
|
|
139
196
|
labels: labelsProp,
|
|
140
197
|
className,
|
|
141
198
|
}: FeedbackThreadProps) {
|
|
@@ -160,13 +217,60 @@ export function FeedbackThread({
|
|
|
160
217
|
);
|
|
161
218
|
}
|
|
162
219
|
|
|
220
|
+
// Per row, the calendar day of the last readable date up to it — what a day line
|
|
221
|
+
// compares against.
|
|
222
|
+
const days: (number | null)[] = [];
|
|
223
|
+
for (const message of messages) {
|
|
224
|
+
const date = toDate(message.createdAt);
|
|
225
|
+
days.push(date ? dayNumber(date) : (days.at(-1) ?? null));
|
|
226
|
+
}
|
|
227
|
+
|
|
163
228
|
return (
|
|
164
229
|
<ol aria-label={labels.thread} aria-busy={loading || undefined} className={cn("space-y-3", className)}>
|
|
165
|
-
{messages.
|
|
230
|
+
{messages.flatMap((message, index) => {
|
|
166
231
|
const date = toDate(message.createdAt);
|
|
232
|
+
const time = date && (
|
|
233
|
+
// The kit's Tooltip, not a native title: one tooltip look app-wide
|
|
234
|
+
// (keksdose F0 / dev#523).
|
|
235
|
+
<Tooltip label={new Intl.DateTimeFormat(locale, { dateStyle: "full", timeStyle: "short" }).format(date)}>
|
|
236
|
+
<time dateTime={date.toISOString()} className="tabular-nums">
|
|
237
|
+
{timeFormat === "clock"
|
|
238
|
+
? formatDate(date, "time", { locale })
|
|
239
|
+
: formatRelativeTime(date, {
|
|
240
|
+
now: reference,
|
|
241
|
+
locale,
|
|
242
|
+
// Past the kit's day steps it is a date: "2 weeks ago" makes
|
|
243
|
+
// the reader do arithmetic the date already did.
|
|
244
|
+
absoluteAfterDays: 6.5,
|
|
245
|
+
})}
|
|
246
|
+
</time>
|
|
247
|
+
</Tooltip>
|
|
248
|
+
);
|
|
249
|
+
const rows: ReactNode[] = [];
|
|
250
|
+
const day = date && dayNumber(date);
|
|
251
|
+
// Against the last row WITH a readable date: an unparseable one neither starts
|
|
252
|
+
// a day nor hides the next change.
|
|
253
|
+
if (daySeparators && date && day !== days[index - 1]) {
|
|
254
|
+
rows.push(<DaySeparator key={`day-${message.id}`} date={date} now={reference} locale={locale} />);
|
|
255
|
+
}
|
|
256
|
+
if (message.kind === "event") {
|
|
257
|
+
// A list item like the rest, so "Reopened" is read in its place, but not an
|
|
258
|
+
// article: an article is somebody's post, and this has no author.
|
|
259
|
+
rows.push(
|
|
260
|
+
<li
|
|
261
|
+
key={message.id}
|
|
262
|
+
data-event=""
|
|
263
|
+
className="flex flex-wrap items-baseline justify-center gap-x-2 px-4 text-center text-xs text-[var(--text-muted)]"
|
|
264
|
+
>
|
|
265
|
+
<span>{message.body}</span>
|
|
266
|
+
{time && <span className="text-[11px]">{time}</span>}
|
|
267
|
+
</li>,
|
|
268
|
+
);
|
|
269
|
+
return rows;
|
|
270
|
+
}
|
|
167
271
|
const author = message.author ?? (message.own ? labels.you : null);
|
|
168
272
|
const attachments = message.attachments ?? [];
|
|
169
|
-
|
|
273
|
+
rows.push(
|
|
170
274
|
<li
|
|
171
275
|
key={message.id}
|
|
172
276
|
className={cn("flex", message.own ? "justify-end" : "justify-start")}
|
|
@@ -191,21 +295,7 @@ export function FeedbackThread({
|
|
|
191
295
|
{labels.staff}
|
|
192
296
|
</span>
|
|
193
297
|
)}
|
|
194
|
-
{
|
|
195
|
-
// The kit's Tooltip, not a native title: one tooltip look app-wide
|
|
196
|
-
// (keksdose F0 / dev#523).
|
|
197
|
-
<Tooltip label={new Intl.DateTimeFormat(locale, { dateStyle: "full", timeStyle: "short" }).format(date)}>
|
|
198
|
-
<time dateTime={date.toISOString()} className="tabular-nums">
|
|
199
|
-
{formatRelativeTime(date, {
|
|
200
|
-
now: reference,
|
|
201
|
-
locale,
|
|
202
|
-
// Past the kit's day steps it is a date: "2 weeks ago" makes
|
|
203
|
-
// the reader do arithmetic the date already did.
|
|
204
|
-
absoluteAfterDays: 6.5,
|
|
205
|
-
})}
|
|
206
|
-
</time>
|
|
207
|
-
</Tooltip>
|
|
208
|
-
)}
|
|
298
|
+
{time}
|
|
209
299
|
</header>
|
|
210
300
|
{message.body && (
|
|
211
301
|
<p
|
|
@@ -230,8 +320,9 @@ export function FeedbackThread({
|
|
|
230
320
|
</ul>
|
|
231
321
|
)}
|
|
232
322
|
</article>
|
|
233
|
-
</li
|
|
323
|
+
</li>,
|
|
234
324
|
);
|
|
325
|
+
return rows;
|
|
235
326
|
})}
|
|
236
327
|
</ol>
|
|
237
328
|
);
|
|
@@ -290,6 +381,57 @@ function useMinuteClock(active: boolean): number {
|
|
|
290
381
|
return time;
|
|
291
382
|
}
|
|
292
383
|
|
|
384
|
+
/** The local calendar day as one number (20260926), for "same day?" and keys. */
|
|
385
|
+
function dayNumber(date: Date): number {
|
|
386
|
+
return date.getFullYear() * 10_000 + (date.getMonth() + 1) * 100 + date.getDate();
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* The day line of `daySeparators` (keksdose G4): "Today" / "Yesterday" from
|
|
391
|
+
* `Intl.RelativeTimeFormat` (`numeric: "auto"`) — the locale's own words, capitalised
|
|
392
|
+
* for a line that stands alone ("heute" → "Heute"; a script without case is left as
|
|
393
|
+
* is) — else the weekday and date, with the year only when it is not this one.
|
|
394
|
+
*
|
|
395
|
+
* Counted in calendar days, not 24-hour spans: 23:50 yesterday is "Yesterday" at
|
|
396
|
+
* 00:10 today.
|
|
397
|
+
*/
|
|
398
|
+
function DaySeparator({ date, now, locale }: { date: Date; now: number; locale: string | undefined }) {
|
|
399
|
+
const today = new Date(now);
|
|
400
|
+
const days = Math.round(
|
|
401
|
+
(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()) -
|
|
402
|
+
Date.UTC(today.getFullYear(), today.getMonth(), today.getDate())) /
|
|
403
|
+
86_400_000,
|
|
404
|
+
);
|
|
405
|
+
let label: string;
|
|
406
|
+
if (days === 0 || days === -1) {
|
|
407
|
+
const word = new Intl.RelativeTimeFormat(locale, { numeric: "auto" }).format(days + 0, "day");
|
|
408
|
+
label = word.charAt(0).toLocaleUpperCase(locale) + word.slice(1);
|
|
409
|
+
} else {
|
|
410
|
+
label = formatDate(
|
|
411
|
+
date,
|
|
412
|
+
{
|
|
413
|
+
weekday: "long",
|
|
414
|
+
day: "numeric",
|
|
415
|
+
month: "long",
|
|
416
|
+
...(date.getFullYear() === today.getFullYear() ? null : { year: "numeric" }),
|
|
417
|
+
},
|
|
418
|
+
{ locale },
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
const y = String(date.getFullYear()).padStart(4, "0");
|
|
422
|
+
const m = String(date.getMonth() + 1).padStart(2, "0");
|
|
423
|
+
const d = String(date.getDate()).padStart(2, "0");
|
|
424
|
+
return (
|
|
425
|
+
<li data-day-separator="" className="flex items-center gap-3 py-1 text-xs text-[var(--text-muted)]">
|
|
426
|
+
<span aria-hidden className="h-px flex-1 bg-[var(--border)]" />
|
|
427
|
+
<time dateTime={`${y}-${m}-${d}`} className="font-medium">
|
|
428
|
+
{label}
|
|
429
|
+
</time>
|
|
430
|
+
<span aria-hidden className="h-px flex-1 bg-[var(--border)]" />
|
|
431
|
+
</li>
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
|
|
293
435
|
function toDate(value: Date | string | number): Date | null {
|
|
294
436
|
const date = value instanceof Date ? value : new Date(value);
|
|
295
437
|
return Number.isNaN(date.getTime()) ? null : date;
|
|
@@ -308,6 +450,10 @@ export interface FeedbackComposerProps {
|
|
|
308
450
|
* Send the comment. Return the promise: the box clears when it RESOLVES, and only of
|
|
309
451
|
* what was sent — text typed while it was in flight stays. A rejection keeps
|
|
310
452
|
* everything for a retry (surface the error yourself; the composer stays quiet).
|
|
453
|
+
*
|
|
454
|
+
* `attachment` is the built-in field's one picture. Files the host uploaded ahead
|
|
455
|
+
* (`attachmentSlot`) are the host's state, read by the host here — and `body` may be
|
|
456
|
+
* `""` when `canSend` let an attachments-only message go.
|
|
311
457
|
*/
|
|
312
458
|
onSend: (body: string, attachment: File | null) => void | Promise<unknown>;
|
|
313
459
|
/** The send is in flight: a spinner on the button and no second send. */
|
|
@@ -315,6 +461,31 @@ export interface FeedbackComposerProps {
|
|
|
315
461
|
/** Offer one picture with the comment (file, paste or `onCaptureScreenshot`).
|
|
316
462
|
* `true` for the defaults. Left out, the composer is text only. */
|
|
317
463
|
attachment?: boolean | FeedbackComposerAttachment;
|
|
464
|
+
/**
|
|
465
|
+
* The host's own attach control, in the row with Send where the built-in field sits
|
|
466
|
+
* (after it, if both are on) — keksdose G5: its support chat uploads up to N files
|
|
467
|
+
* AHEAD of the send and sends their refs, which one `File` in `onSend` cannot carry.
|
|
468
|
+
* The slot is the host's picker plus the chips of what is uploaded; the upload, the
|
|
469
|
+
* refs and clearing them once `onSend` resolves stay with the host. Pair it with
|
|
470
|
+
* `canSend`.
|
|
471
|
+
*/
|
|
472
|
+
attachmentSlot?: ReactNode;
|
|
473
|
+
/**
|
|
474
|
+
* Whether Send is enabled, over the composer's own rule (some text in the box) —
|
|
475
|
+
* for state the composer cannot see, i.e. `attachmentSlot`'s (keksdose G5): `true`
|
|
476
|
+
* with files ready makes an empty draft sendable (`onSend("", null)`), `false` while
|
|
477
|
+
* an upload is still running holds even a written reply back. Left out, the text
|
|
478
|
+
* decides. `pending` blocks a send either way.
|
|
479
|
+
*/
|
|
480
|
+
canSend?: boolean;
|
|
481
|
+
/**
|
|
482
|
+
* Which key sends. `"mod-enter"` (default): Ctrl/⌘+Enter, the report dialog's and
|
|
483
|
+
* note editor's shortcut, with plain Enter a new line — feedback is written in
|
|
484
|
+
* paragraphs. `"enter"`: the chat way (keksdose G5) — Enter sends, Shift+Enter is a
|
|
485
|
+
* new line, Ctrl/⌘+Enter still sends; an Enter that confirms an IME composition
|
|
486
|
+
* (Japanese, Chinese input) is the IME's, never a send.
|
|
487
|
+
*/
|
|
488
|
+
sendOn?: "mod-enter" | "enter";
|
|
318
489
|
/** Rendered INSTEAD of the composer — the thread is closed, or read-only for this
|
|
319
490
|
* reader. */
|
|
320
491
|
disabledReason?: ReactNode;
|
|
@@ -372,12 +543,15 @@ const isApple = () =>
|
|
|
372
543
|
* The answer box under a {@link FeedbackThread}: a textarea, an optional picture,
|
|
373
544
|
* and Send — Ctrl/⌘+Enter sends, the shortcut the report dialog and the note editor
|
|
374
545
|
* use, so the habit carries across the feature. Plain Enter is a new line: feedback is
|
|
375
|
-
* written in paragraphs, unlike a chat.
|
|
546
|
+
* written in paragraphs, unlike a chat — which `sendOn="enter"` is for (keksdose G5).
|
|
376
547
|
*/
|
|
377
548
|
export function FeedbackComposer({
|
|
378
549
|
onSend,
|
|
379
550
|
pending = false,
|
|
380
551
|
attachment,
|
|
552
|
+
attachmentSlot,
|
|
553
|
+
canSend: canSendProp,
|
|
554
|
+
sendOn = "mod-enter",
|
|
381
555
|
disabledReason,
|
|
382
556
|
rows = 3,
|
|
383
557
|
placeholder,
|
|
@@ -445,7 +619,7 @@ export function FeedbackComposer({
|
|
|
445
619
|
return <p className={cn("text-xs text-[var(--text-muted)]", className)}>{disabledReason}</p>;
|
|
446
620
|
}
|
|
447
621
|
|
|
448
|
-
const canSend = draft.trim().length > 0 && !pending;
|
|
622
|
+
const canSend = (canSendProp ?? draft.trim().length > 0) && !pending;
|
|
449
623
|
const send = () => {
|
|
450
624
|
if (!canSend) return;
|
|
451
625
|
const sentDraft = draft;
|
|
@@ -471,30 +645,48 @@ export function FeedbackComposer({
|
|
|
471
645
|
value={draft}
|
|
472
646
|
onChange={(event) => setDraft(event.target.value)}
|
|
473
647
|
onKeyDown={(event) => {
|
|
474
|
-
if (
|
|
648
|
+
if (event.key !== "Enter") return;
|
|
649
|
+
const mod = event.ctrlKey || event.metaKey;
|
|
650
|
+
// Under "enter": plain Enter sends, Shift+Enter falls through to the
|
|
651
|
+
// textarea's new line, and an Enter that ends an IME composition is the
|
|
652
|
+
// IME's (keyCode 229 is how Safari reports it after the composition ended).
|
|
653
|
+
const chat =
|
|
654
|
+
sendOn === "enter" &&
|
|
655
|
+
!event.shiftKey &&
|
|
656
|
+
!event.altKey &&
|
|
657
|
+
!event.nativeEvent.isComposing &&
|
|
658
|
+
event.keyCode !== 229;
|
|
659
|
+
if (mod || chat) {
|
|
475
660
|
event.preventDefault();
|
|
476
661
|
send();
|
|
477
662
|
}
|
|
478
663
|
}}
|
|
479
664
|
/>
|
|
480
665
|
<div className="flex flex-wrap items-end justify-between gap-2">
|
|
481
|
-
{config ? (
|
|
482
|
-
<
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
666
|
+
{config || attachmentSlot != null ? (
|
|
667
|
+
<div className="flex min-w-0 flex-wrap items-end gap-2">
|
|
668
|
+
{config && (
|
|
669
|
+
<FeedbackAttachmentField
|
|
670
|
+
value={file}
|
|
671
|
+
onChange={setFile}
|
|
672
|
+
labels={config.labels}
|
|
673
|
+
accept={config.accept}
|
|
674
|
+
maxBytes={config.maxBytes}
|
|
675
|
+
onError={config.onError}
|
|
676
|
+
onCaptureScreenshot={config.onCaptureScreenshot}
|
|
677
|
+
// The box is this field's sibling; a paste in it bubbles to `root`.
|
|
678
|
+
pasteFrom={root}
|
|
679
|
+
/>
|
|
680
|
+
)}
|
|
681
|
+
{attachmentSlot}
|
|
682
|
+
</div>
|
|
493
683
|
) : (
|
|
494
684
|
<span />
|
|
495
685
|
)}
|
|
496
686
|
<div className="ms-auto flex items-center gap-2">
|
|
497
|
-
<span className="text-xs text-[var(--text-placeholder)]">
|
|
687
|
+
<span className="text-xs text-[var(--text-placeholder)]">
|
|
688
|
+
{sendOn === "enter" ? labels.sendHintEnter : labels.sendHint(modifier)}
|
|
689
|
+
</span>
|
|
498
690
|
<Button onClick={send} disabled={!canSend} aria-busy={pending || undefined}>
|
|
499
691
|
{pending ? (
|
|
500
692
|
<Spinner label={null} className="size-4" />
|
package/src/i18n/locales/de.ts
CHANGED
|
@@ -47,6 +47,8 @@ export function uiKitLabelsDe(numberLocale = "de-DE"): UiKitLabels {
|
|
|
47
47
|
send: "Senden",
|
|
48
48
|
// German keyboards label the key "Strg".
|
|
49
49
|
sendHint: (modifier) => `${modifier === "Ctrl" ? "Strg" : modifier} + Enter zum Senden`,
|
|
50
|
+
// "Umschalt", the German key label, as in the table's sort hint.
|
|
51
|
+
sendHintEnter: "Enter zum Senden, Umschalt + Enter für neue Zeile",
|
|
50
52
|
},
|
|
51
53
|
accountSettings: {
|
|
52
54
|
profile: {
|
package/src/i18n/locales/es.ts
CHANGED
|
@@ -45,6 +45,7 @@ export function uiKitLabelsEs(numberLocale = "es-ES"): UiKitLabels {
|
|
|
45
45
|
placeholder: "Escribe un comentario…",
|
|
46
46
|
send: "Enviar",
|
|
47
47
|
sendHint: (modifier) => `${modifier} + Intro para enviar`,
|
|
48
|
+
sendHintEnter: "Intro para enviar, Mayús + Intro para una nueva línea",
|
|
48
49
|
},
|
|
49
50
|
accountSettings: {
|
|
50
51
|
profile: {
|
package/src/i18n/locales/fr.ts
CHANGED
|
@@ -45,6 +45,7 @@ export function uiKitLabelsFr(numberLocale = "fr-FR"): UiKitLabels {
|
|
|
45
45
|
placeholder: "Écrire un commentaire…",
|
|
46
46
|
send: "Envoyer",
|
|
47
47
|
sendHint: (modifier) => `${modifier} + Entrée pour envoyer`,
|
|
48
|
+
sendHintEnter: "Entrée pour envoyer, Maj + Entrée pour aller à la ligne",
|
|
48
49
|
},
|
|
49
50
|
accountSettings: {
|
|
50
51
|
profile: {
|
package/src/i18n/locales/hu.ts
CHANGED
|
@@ -44,6 +44,7 @@ export function uiKitLabelsHu(numberLocale = "hu-HU"): UiKitLabels {
|
|
|
44
44
|
placeholder: "Hozzászólás írása…",
|
|
45
45
|
send: "Küldés",
|
|
46
46
|
sendHint: (modifier) => `${modifier} + Enter a küldéshez`,
|
|
47
|
+
sendHintEnter: "Enter: küldés, Shift + Enter: új sor",
|
|
47
48
|
},
|
|
48
49
|
accountSettings: {
|
|
49
50
|
profile: {
|
package/src/i18n/locales/it.ts
CHANGED
|
@@ -45,6 +45,7 @@ export function uiKitLabelsIt(numberLocale = "it-IT"): UiKitLabels {
|
|
|
45
45
|
placeholder: "Scrivi un commento…",
|
|
46
46
|
send: "Invia",
|
|
47
47
|
sendHint: (modifier) => `${modifier} + Invio per inviare`,
|
|
48
|
+
sendHintEnter: "Invio per inviare, Maiusc + Invio per andare a capo",
|
|
48
49
|
},
|
|
49
50
|
accountSettings: {
|
|
50
51
|
profile: {
|
package/src/i18n/locales/zh.ts
CHANGED