@eifi1/ui-kit 0.13.0 → 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.
Files changed (54) hide show
  1. package/README.md +5 -5
  2. package/dist/components/amount-input.d.ts +6 -0
  3. package/dist/components/amount-input.js +35 -5
  4. package/dist/components/amount-input.js.map +1 -1
  5. package/dist/components/error-boundary.d.ts +8 -1
  6. package/dist/components/error-boundary.js +18 -2
  7. package/dist/components/error-boundary.js.map +1 -1
  8. package/dist/components/numpad-sheet.d.ts +7 -3
  9. package/dist/components/numpad-sheet.js +4 -3
  10. package/dist/components/numpad-sheet.js.map +1 -1
  11. package/dist/components/page-header.d.ts +55 -2
  12. package/dist/components/page-header.js +34 -3
  13. package/dist/components/page-header.js.map +1 -1
  14. package/dist/components/text.js.map +1 -1
  15. package/dist/components/tooltip.d.ts +31 -0
  16. package/dist/components/tooltip.js +33 -0
  17. package/dist/components/tooltip.js.map +1 -1
  18. package/dist/feedback/feedback-thread.d.ts +87 -6
  19. package/dist/feedback/feedback-thread.js +134 -58
  20. package/dist/feedback/feedback-thread.js.map +1 -1
  21. package/dist/feedback.d.ts +1 -1
  22. package/dist/i18n/locales/de.js +3 -1
  23. package/dist/i18n/locales/de.js.map +1 -1
  24. package/dist/i18n/locales/es.js +2 -1
  25. package/dist/i18n/locales/es.js.map +1 -1
  26. package/dist/i18n/locales/fr.js +2 -1
  27. package/dist/i18n/locales/fr.js.map +1 -1
  28. package/dist/i18n/locales/hu.js +2 -1
  29. package/dist/i18n/locales/hu.js.map +1 -1
  30. package/dist/i18n/locales/it.js +2 -1
  31. package/dist/i18n/locales/it.js.map +1 -1
  32. package/dist/i18n/locales/zh.js +2 -1
  33. package/dist/i18n/locales/zh.js.map +1 -1
  34. package/dist/index.d.ts +2 -2
  35. package/dist/lib/format.js +1 -1
  36. package/dist/lib/format.js.map +1 -1
  37. package/dist/rhf/fields.d.ts +3 -1
  38. package/dist/rhf/fields.js.map +1 -1
  39. package/package.json +1 -1
  40. package/src/components/amount-input.tsx +73 -7
  41. package/src/components/error-boundary.tsx +33 -3
  42. package/src/components/numpad-sheet.tsx +13 -4
  43. package/src/components/page-header.tsx +98 -3
  44. package/src/components/text.tsx +2 -1
  45. package/src/components/tooltip.tsx +109 -2
  46. package/src/feedback/feedback-thread.tsx +229 -37
  47. package/src/i18n/locales/de.ts +2 -0
  48. package/src/i18n/locales/es.ts +1 -0
  49. package/src/i18n/locales/fr.ts +1 -0
  50. package/src/i18n/locales/hu.ts +1 -0
  51. package/src/i18n/locales/it.ts +1 -0
  52. package/src/i18n/locales/zh.ts +1 -0
  53. package/src/lib/format.ts +3 -1
  54. package/src/rhf/fields.tsx +3 -1
@@ -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: readonly FeedbackThreadMessage[];
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
- /** "Now" for the relative times — pin it in tests and stories. Default: the clock. */
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.map((message) => {
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
- return (
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
- {date && (
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 ((event.ctrlKey || event.metaKey) && event.key === "Enter") {
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
- <FeedbackAttachmentField
483
- value={file}
484
- onChange={setFile}
485
- labels={config.labels}
486
- accept={config.accept}
487
- maxBytes={config.maxBytes}
488
- onError={config.onError}
489
- onCaptureScreenshot={config.onCaptureScreenshot}
490
- // The box is this field's sibling; a paste in it bubbles to `root`.
491
- pasteFrom={root}
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)]">{labels.sendHint(modifier)}</span>
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" />
@@ -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: {
@@ -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: {
@@ -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: {
@@ -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: {
@@ -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: {
@@ -44,6 +44,7 @@ export function uiKitLabelsZh(numberLocale = "zh-CN"): UiKitLabels {
44
44
  placeholder: "撰写评论…",
45
45
  send: "发送",
46
46
  sendHint: (modifier) => `${modifier} + Enter 发送`,
47
+ sendHintEnter: "Enter 发送,Shift + Enter 换行",
47
48
  },
48
49
  accountSettings: {
49
50
  profile: {
package/src/lib/format.ts CHANGED
@@ -115,7 +115,9 @@ function numberOptions(o: FormatNumberBaseOptions): Intl.NumberFormatOptions {
115
115
  }
116
116
 
117
117
  function isMissing(value: number | null | undefined): value is null | undefined {
118
- return value === null || value === undefined || Number.isNaN(value);
118
+ // ±Infinity too (lenkbank, adopting 0.13): a division by zero upstream is a missing
119
+ // figure, and "∞" printed in a money or measurement column reads like data.
120
+ return value === null || value === undefined || !Number.isFinite(value);
119
121
  }
120
122
 
121
123
  /** A number in `locale`: "1,234.5", "1.234,5", "1’234.5" — with `unit`, "3.400 N". */
@@ -545,7 +545,9 @@ export type RhfMoneyFieldProps<
545
545
  /** What an emptied field stores when `valueAs` is `"number"`. Default `null`. */
546
546
  emptyValue?: null | "";
547
547
  /** Decimals the amount settles to on blur, Enter or a calculator result. Default:
548
- * the currency's minor unit (CHF 2, JPY 0). See {@link AmountInput}'s `digits`. */
548
+ * the currency's minor unit (CHF 2, JPY 0). ⚠️ A unit price or a rate needs its
549
+ * column's scale here (`digits={4}` for 1.789 €/l), or it is stored rounded to
550
+ * cents without a word (keksdose G2). See {@link AmountInput}'s `digits`. */
549
551
  digits?: number;
550
552
  /** Clamp the settled amount (a release capped at the deposit's balance). Validate
551
553
  * with `rules` as well if a clamp needs explaining. */