@eifi1/ui-kit 0.6.1 → 0.7.1
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 +69 -33
- package/dist/chart.d.ts +1 -0
- package/dist/components/account-settings.js +3 -2
- package/dist/components/account-settings.js.map +1 -1
- package/dist/components/amount-input.d.ts +5 -0
- package/dist/components/amount-input.js +6 -4
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/autocomplete.d.ts +33 -6
- package/dist/components/autocomplete.js +62 -16
- package/dist/components/autocomplete.js.map +1 -1
- package/dist/components/calculator.d.ts +5 -0
- package/dist/components/calculator.js +2 -1
- package/dist/components/calculator.js.map +1 -1
- package/dist/components/chart-zoom.js +2 -1
- package/dist/components/chart-zoom.js.map +1 -1
- package/dist/components/chart.d.ts +33 -2
- package/dist/components/chart.js +20 -8
- package/dist/components/chart.js.map +1 -1
- package/dist/components/chip.d.ts +34 -14
- package/dist/components/chip.js +63 -28
- package/dist/components/chip.js.map +1 -1
- package/dist/components/choice-card.d.ts +18 -1
- package/dist/components/choice-card.js +21 -8
- package/dist/components/choice-card.js.map +1 -1
- package/dist/components/combobox-core.d.ts +61 -2
- package/dist/components/combobox-core.js +68 -17
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/combobox.d.ts +11 -7
- package/dist/components/combobox.js +51 -28
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/currency-select.js +3 -3
- package/dist/components/currency-select.js.map +1 -1
- package/dist/components/danger-confirm.d.ts +30 -6
- package/dist/components/danger-confirm.js +51 -32
- package/dist/components/danger-confirm.js.map +1 -1
- package/dist/components/data-table-filter-popover.d.ts +1 -1
- package/dist/components/data-table-filters.d.ts +1 -1
- package/dist/components/data-table-pagination.d.ts +6 -3
- package/dist/components/data-table-pagination.js +6 -5
- package/dist/components/data-table-pagination.js.map +1 -1
- package/dist/components/data-table.d.ts +1 -1
- package/dist/components/data-table.js +332 -248
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-picker.js +17 -7
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/dialog-frame.d.ts +28 -3
- package/dist/components/dialog-frame.js +47 -11
- package/dist/components/dialog-frame.js.map +1 -1
- package/dist/components/disclosure.d.ts +22 -2
- package/dist/components/disclosure.js +41 -8
- package/dist/components/disclosure.js.map +1 -1
- package/dist/components/dropdown.d.ts +7 -3
- package/dist/components/dropdown.js +6 -2
- package/dist/components/dropdown.js.map +1 -1
- package/dist/components/entity-combobox.d.ts +17 -8
- package/dist/components/entity-combobox.js +7 -5
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/file-button.d.ts +38 -5
- package/dist/components/file-button.js +23 -2
- package/dist/components/file-button.js.map +1 -1
- package/dist/components/file-dropzone.d.ts +11 -5
- package/dist/components/file-dropzone.js +14 -19
- package/dist/components/file-dropzone.js.map +1 -1
- package/dist/components/grouped-picker.js +2 -2
- package/dist/components/grouped-picker.js.map +1 -1
- package/dist/components/hover-menu.d.ts +8 -3
- package/dist/components/hover-menu.js +88 -4
- package/dist/components/hover-menu.js.map +1 -1
- package/dist/components/icon-picker.d.ts +10 -1
- package/dist/components/icon-picker.js +5 -1
- package/dist/components/icon-picker.js.map +1 -1
- package/dist/components/measured-grid.d.ts +165 -0
- package/dist/components/measured-grid.js +559 -0
- package/dist/components/measured-grid.js.map +1 -0
- package/dist/components/mini-calendar.js +7 -8
- package/dist/components/mini-calendar.js.map +1 -1
- package/dist/components/month-picker.js +13 -9
- package/dist/components/month-picker.js.map +1 -1
- package/dist/components/multi-entity-combobox.js +2 -2
- package/dist/components/multi-entity-combobox.js.map +1 -1
- package/dist/components/multi-select.d.ts +9 -1
- package/dist/components/multi-select.js +22 -8
- package/dist/components/multi-select.js.map +1 -1
- package/dist/components/number-field.d.ts +5 -0
- package/dist/components/number-input.d.ts +5 -0
- package/dist/components/number-input.js +3 -3
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.d.ts +5 -0
- package/dist/components/numpad-sheet.js +3 -3
- package/dist/components/numpad-sheet.js.map +1 -1
- package/dist/components/picker-sheet.js +1 -1
- package/dist/components/picker-sheet.js.map +1 -1
- package/dist/components/popover.js +11 -2
- package/dist/components/popover.js.map +1 -1
- package/dist/components/series-chart-ticks.d.ts +13 -0
- package/dist/components/series-chart-ticks.js +29 -0
- package/dist/components/series-chart-ticks.js.map +1 -0
- package/dist/components/series-chart.js +33 -27
- package/dist/components/series-chart.js.map +1 -1
- package/dist/components/settings-fields.d.ts +2 -0
- package/dist/components/swatch-picker.d.ts +10 -1
- package/dist/components/swatch-picker.js +2 -0
- package/dist/components/swatch-picker.js.map +1 -1
- package/dist/components/swipeable-row.d.ts +11 -2
- package/dist/components/swipeable-row.js +5 -3
- package/dist/components/swipeable-row.js.map +1 -1
- package/dist/components/tile-radio.js +5 -7
- package/dist/components/tile-radio.js.map +1 -1
- package/dist/components/time-input.d.ts +2 -0
- package/dist/components/toggle-group.d.ts +13 -3
- package/dist/components/toggle-group.js +21 -1
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/toggle-legend.js +1 -2
- package/dist/components/toggle-legend.js.map +1 -1
- package/dist/components/tooltip.d.ts +10 -4
- package/dist/components/tooltip.js +23 -6
- package/dist/components/tooltip.js.map +1 -1
- package/dist/components/treemap.d.ts +6 -1
- package/dist/components/treemap.js +16 -6
- package/dist/components/treemap.js.map +1 -1
- package/dist/components/ui.d.ts +36 -8
- package/dist/components/ui.js +27 -19
- package/dist/components/ui.js.map +1 -1
- package/dist/components/use-anchor-dir.d.ts +15 -0
- package/dist/components/use-anchor-dir.js +14 -0
- package/dist/components/use-anchor-dir.js.map +1 -0
- package/dist/components/use-mobile-reveal.d.ts +8 -1
- package/dist/components/use-mobile-reveal.js +3 -2
- package/dist/components/use-mobile-reveal.js.map +1 -1
- package/dist/components/use-table-state.d.ts +4 -4
- package/dist/components/use-table-state.js +12 -5
- package/dist/components/use-table-state.js.map +1 -1
- package/dist/{data-table-filters-DM1auUfH.d.ts → data-table-filters-noy0Abvi.d.ts} +40 -7
- package/dist/data-table.d.ts +1 -1
- package/dist/data-table.js.map +1 -1
- package/dist/feedback/feedback-attachment.d.ts +22 -3
- package/dist/feedback/feedback-attachment.js +15 -7
- package/dist/feedback/feedback-attachment.js.map +1 -1
- package/dist/feedback/feedback-dialog.d.ts +3 -2
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback.d.ts +1 -1
- package/dist/hooks/use-windowed-rows.d.ts +29 -0
- package/dist/hooks/use-windowed-rows.js +31 -0
- package/dist/hooks/use-windowed-rows.js.map +1 -0
- package/dist/i18n/defaults.d.ts +5 -0
- package/dist/i18n/defaults.js +5 -1
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +7 -0
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/de-CH.d.ts +45 -0
- package/dist/i18n/locales/de-CH.js +21 -0
- package/dist/i18n/locales/de-CH.js.map +1 -0
- package/dist/i18n/locales/de.d.ts +45 -0
- package/dist/i18n/locales/de.js +344 -0
- package/dist/i18n/locales/de.js.map +1 -0
- package/dist/i18n/locales/es.d.ts +45 -0
- package/dist/i18n/locales/es.js +339 -0
- package/dist/i18n/locales/es.js.map +1 -0
- package/dist/i18n/locales/fr.d.ts +45 -0
- package/dist/i18n/locales/fr.js +344 -0
- package/dist/i18n/locales/fr.js.map +1 -0
- package/dist/i18n/locales/hu.d.ts +45 -0
- package/dist/i18n/locales/hu.js +343 -0
- package/dist/i18n/locales/hu.js.map +1 -0
- package/dist/i18n/locales/it.d.ts +45 -0
- package/dist/i18n/locales/it.js +342 -0
- package/dist/i18n/locales/it.js.map +1 -0
- package/dist/i18n/locales/zh.d.ts +45 -0
- package/dist/i18n/locales/zh.js +338 -0
- package/dist/i18n/locales/zh.js.map +1 -0
- package/dist/index.d.ts +7 -4
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/direction.d.ts +24 -0
- package/dist/lib/direction.js +19 -0
- package/dist/lib/direction.js.map +1 -0
- package/dist/rhf/form.d.ts +2 -0
- package/dist/rhf/form.js.map +1 -1
- package/dist/rhf.d.ts +2 -0
- package/dist/search/command-palette.d.ts +5 -0
- package/dist/search/command-palette.js +80 -50
- package/dist/search/command-palette.js.map +1 -1
- package/dist/shell/app-shell.d.ts +13 -1
- package/dist/shell/app-shell.js +134 -85
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/shell/topbar-action-menu.js +1 -1
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell/topbar-controls.d.ts +7 -3
- package/dist/shell/topbar-controls.js +38 -31
- package/dist/shell/topbar-controls.js.map +1 -1
- package/dist/theme/chart-palette.d.ts +7 -1
- package/dist/theme/chart-palette.js +2 -1
- package/dist/theme/chart-palette.js.map +1 -1
- package/dist/tour/tour.d.ts +11 -5
- package/dist/tour/tour.js +124 -58
- package/dist/tour/tour.js.map +1 -1
- package/dist/wizard/use-wizard.js +30 -13
- package/dist/wizard/use-wizard.js.map +1 -1
- package/dist/wizard/wizard-summary.js +1 -1
- package/dist/wizard/wizard-summary.js.map +1 -1
- package/package.json +12 -2
- package/src/components/account-settings.tsx +6 -2
- package/src/components/amount-input.tsx +10 -8
- package/src/components/autocomplete.tsx +110 -24
- package/src/components/calculator.tsx +5 -1
- package/src/components/chart-zoom.tsx +8 -1
- package/src/components/chart.tsx +68 -7
- package/src/components/chip.tsx +131 -49
- package/src/components/choice-card.tsx +77 -36
- package/src/components/combobox-core.tsx +147 -20
- package/src/components/combobox.tsx +100 -44
- package/src/components/currency-select.tsx +6 -6
- package/src/components/danger-confirm.tsx +90 -36
- package/src/components/data-table-pagination.tsx +32 -19
- package/src/components/data-table.tsx +198 -36
- package/src/components/date-picker.tsx +24 -7
- package/src/components/dialog-frame.tsx +97 -22
- package/src/components/disclosure.tsx +75 -13
- package/src/components/dropdown.tsx +17 -5
- package/src/components/entity-combobox.tsx +26 -13
- package/src/components/file-button.tsx +74 -5
- package/src/components/file-dropzone.tsx +44 -35
- package/src/components/grouped-picker.tsx +2 -2
- package/src/components/hover-menu.tsx +139 -10
- package/src/components/icon-picker.tsx +14 -1
- package/src/components/measured-grid.tsx +852 -0
- package/src/components/mini-calendar.tsx +11 -8
- package/src/components/month-picker.tsx +18 -11
- package/src/components/multi-entity-combobox.tsx +2 -2
- package/src/components/multi-select.tsx +34 -7
- package/src/components/number-input.tsx +4 -4
- package/src/components/numpad-sheet.tsx +5 -3
- package/src/components/picker-sheet.tsx +1 -1
- package/src/components/popover.tsx +18 -3
- package/src/components/series-chart-ticks.ts +52 -0
- package/src/components/series-chart.tsx +39 -31
- package/src/components/swatch-picker.tsx +11 -0
- package/src/components/swipeable-row.tsx +21 -6
- package/src/components/tile-radio.tsx +5 -7
- package/src/components/toggle-group.tsx +39 -3
- package/src/components/toggle-legend.tsx +5 -2
- package/src/components/tooltip.tsx +40 -12
- package/src/components/treemap.tsx +28 -5
- package/src/components/ui.tsx +79 -33
- package/src/components/use-anchor-dir.ts +20 -0
- package/src/components/use-mobile-reveal.ts +10 -2
- package/src/components/use-table-state.ts +44 -7
- package/src/data-table.ts +1 -0
- package/src/feedback/feedback-attachment.tsx +36 -9
- package/src/feedback/feedback-dialog.tsx +3 -2
- package/src/hooks/use-windowed-rows.ts +69 -0
- package/src/i18n/defaults.ts +4 -0
- package/src/i18n/kit-labels.tsx +4 -0
- package/src/i18n/locales/de-CH.ts +30 -0
- package/src/i18n/locales/de.ts +365 -0
- package/src/i18n/locales/es.ts +358 -0
- package/src/i18n/locales/fr.ts +371 -0
- package/src/i18n/locales/hu.ts +359 -0
- package/src/i18n/locales/it.ts +360 -0
- package/src/i18n/locales/zh.ts +348 -0
- package/src/index.ts +10 -1
- package/src/lib/direction.ts +37 -0
- package/src/rhf/form.tsx +21 -0
- package/src/search/command-palette.tsx +77 -19
- package/src/shell/app-shell.tsx +201 -130
- package/src/shell/topbar-action-menu.tsx +1 -1
- package/src/shell/topbar-controls.tsx +19 -8
- package/src/theme/chart-palette.ts +9 -2
- package/src/tour/tour.tsx +119 -18
- package/src/wizard/use-wizard.ts +55 -17
- package/src/wizard/wizard-summary.tsx +1 -1
|
@@ -2,6 +2,7 @@ import { useEffect, useId, useMemo, useRef, useState } from "react";
|
|
|
2
2
|
import type { ComponentPropsWithoutRef, KeyboardEvent } from "react";
|
|
3
3
|
import { ChevronLeft, ChevronRight } from "lucide-react";
|
|
4
4
|
import { cn } from "../lib/cn";
|
|
5
|
+
import { horizontalStep } from "../lib/direction";
|
|
5
6
|
import { useAnnounce } from "../hooks/use-announce";
|
|
6
7
|
import { parseIsoDate, sameYmd, toLocalIso } from "../lib/dates";
|
|
7
8
|
import { useKitLabels, useKitLocale, useKitWeekStart } from "../i18n/kit-labels";
|
|
@@ -322,12 +323,13 @@ export function MiniCalendar({
|
|
|
322
323
|
*/
|
|
323
324
|
const onDayKeyDown = (e: KeyboardEvent<HTMLButtonElement>) => {
|
|
324
325
|
const next = new Date(activeDate);
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
326
|
+
// ←/→ are "previous/next day" along the READING direction: the grid's columns run
|
|
327
|
+
// right-to-left in RTL (it is a CSS grid, so the browser mirrors it), and ArrowLeft
|
|
328
|
+
// there moves to the day that sits to the left — the next one.
|
|
329
|
+
const step = horizontalStep(e.key, e.currentTarget);
|
|
330
|
+
switch (step ? "horizontal" : e.key) {
|
|
331
|
+
case "horizontal":
|
|
332
|
+
next.setDate(next.getDate() + step);
|
|
331
333
|
break;
|
|
332
334
|
case "ArrowUp":
|
|
333
335
|
next.setDate(next.getDate() - 7);
|
|
@@ -419,7 +421,8 @@ export function MiniCalendar({
|
|
|
419
421
|
aria-label={labels.previousMonth}
|
|
420
422
|
className="rounded p-1 text-[var(--text-muted)] hover:bg-[var(--bg-hover)]"
|
|
421
423
|
>
|
|
422
|
-
|
|
424
|
+
{/* Mirrored in RTL: "previous" points to the reading START, which is right. */}
|
|
425
|
+
<ChevronLeft className="size-4 rtl:-scale-x-100" aria-hidden />
|
|
423
426
|
</button>
|
|
424
427
|
{/* Named by this element rather than by a string of its own, and `aria-live`
|
|
425
428
|
so paging months says which month you have landed on — the caption is the
|
|
@@ -438,7 +441,7 @@ export function MiniCalendar({
|
|
|
438
441
|
aria-label={labels.nextMonth}
|
|
439
442
|
className="rounded p-1 text-[var(--text-muted)] hover:bg-[var(--bg-hover)]"
|
|
440
443
|
>
|
|
441
|
-
<ChevronRight className="size-4" />
|
|
444
|
+
<ChevronRight className="size-4 rtl:-scale-x-100" aria-hidden />
|
|
442
445
|
</button>
|
|
443
446
|
</div>
|
|
444
447
|
<div
|
|
@@ -2,6 +2,7 @@ import { useEffect, useId, useMemo, useRef, useState } from "react";
|
|
|
2
2
|
import type { ComponentPropsWithoutRef, KeyboardEvent, ReactNode, RefObject } from "react";
|
|
3
3
|
import { Calendar, ChevronLeft, ChevronRight } from "lucide-react";
|
|
4
4
|
import { cn } from "../lib/cn";
|
|
5
|
+
import { dirOf, horizontalStep, type Direction } from "../lib/direction";
|
|
5
6
|
import { monthKey, pad } from "../lib/dates";
|
|
6
7
|
import { FieldLabel, FIELD_FLOATING_PAD, FIELD_INVALID, FIELD_TRIGGER } from "./ui";
|
|
7
8
|
import { Popover } from "./popover";
|
|
@@ -309,17 +310,14 @@ function MonthGrid({
|
|
|
309
310
|
|
|
310
311
|
const onCellKeyDown = (e: KeyboardEvent<HTMLButtonElement>) => {
|
|
311
312
|
// Left and right are VISUAL directions: in a right-to-left page the grid runs the
|
|
312
|
-
// other way, so "right" is the previous month there.
|
|
313
|
-
//
|
|
314
|
-
const
|
|
313
|
+
// other way, so "right" is the previous month there. The panel is portalled, but
|
|
314
|
+
// it carries the field's `dir` (see MonthPicker), so the nearest `dir` is right.
|
|
315
|
+
const step = horizontalStep(e.key, e.currentTarget);
|
|
315
316
|
const col = (active.month - 1) % COLUMNS;
|
|
316
317
|
let delta: number;
|
|
317
|
-
switch (e.key) {
|
|
318
|
-
case "
|
|
319
|
-
delta =
|
|
320
|
-
break;
|
|
321
|
-
case "ArrowRight":
|
|
322
|
-
delta = rtl ? -1 : 1;
|
|
318
|
+
switch (step ? "horizontal" : e.key) {
|
|
319
|
+
case "horizontal":
|
|
320
|
+
delta = step;
|
|
323
321
|
break;
|
|
324
322
|
case "ArrowUp":
|
|
325
323
|
delta = -COLUMNS;
|
|
@@ -502,9 +500,14 @@ export function MonthPicker({
|
|
|
502
500
|
const panelId = `${id}-panel`;
|
|
503
501
|
// Only a STRING label can double as a name; a ReactNode may hold its own controls.
|
|
504
502
|
const named = typeof label === "string";
|
|
503
|
+
// The panel is portalled to <body>, out of the subtree whose `dir` it inherited — an
|
|
504
|
+
// RTL form got an LTR grid. Read when opening (`toggle` is the popover's only way
|
|
505
|
+
// open) and put back on the panel. Also what the arrow keys above read.
|
|
506
|
+
const rootRef = useRef<HTMLDivElement>(null);
|
|
507
|
+
const [dir, setDir] = useState<Direction>("ltr");
|
|
505
508
|
|
|
506
509
|
return (
|
|
507
|
-
<div {...wrapperRest} className={cn("relative", className)}>
|
|
510
|
+
<div {...wrapperRest} ref={rootRef} className={cn("relative", className)}>
|
|
508
511
|
{label !== undefined && <FieldLabel>{label}</FieldLabel>}
|
|
509
512
|
{/* The hidden twin the trigger is named by — see DateField. `sr-only-fixed`,
|
|
510
513
|
and inside this `relative` root either way. */}
|
|
@@ -517,10 +520,14 @@ export function MonthPicker({
|
|
|
517
520
|
width={256}
|
|
518
521
|
panelId={panelId}
|
|
519
522
|
labels={{ panel: labels.panel }}
|
|
523
|
+
dir={dir}
|
|
520
524
|
trigger={({ open, toggle, ref }) => (
|
|
521
525
|
<MonthFieldTrigger
|
|
522
526
|
open={open}
|
|
523
|
-
toggle={
|
|
527
|
+
toggle={() => {
|
|
528
|
+
setDir(dirOf(rootRef.current));
|
|
529
|
+
toggle();
|
|
530
|
+
}}
|
|
524
531
|
triggerRef={ref}
|
|
525
532
|
triggerText={triggerText}
|
|
526
533
|
hasValue={selected != null}
|
|
@@ -200,7 +200,7 @@ export function MultiEntityCombobox<V extends string | number>({
|
|
|
200
200
|
}}
|
|
201
201
|
className={cn(
|
|
202
202
|
FIELD_TRIGGER,
|
|
203
|
-
"
|
|
203
|
+
"pe-9",
|
|
204
204
|
label !== undefined && FIELD_FLOATING_PAD,
|
|
205
205
|
disabled && "cursor-not-allowed opacity-50",
|
|
206
206
|
field.isInvalid && FIELD_INVALID,
|
|
@@ -226,7 +226,7 @@ export function MultiEntityCombobox<V extends string | number>({
|
|
|
226
226
|
e.stopPropagation();
|
|
227
227
|
onChange([]);
|
|
228
228
|
}}
|
|
229
|
-
className="absolute
|
|
229
|
+
className="absolute end-2 top-1/2 -translate-y-1/2 rounded p-0.5 text-[var(--text-placeholder)] hover:text-[var(--text-secondary)]"
|
|
230
230
|
>
|
|
231
231
|
<X className="size-4" />
|
|
232
232
|
</span>
|
|
@@ -8,7 +8,7 @@ import { Check } from "lucide-react";
|
|
|
8
8
|
import { FieldChevron, FieldLabel, FIELD_TRIGGER, FIELD_INVALID, FIELD_FLOATING_PAD } from "./ui";
|
|
9
9
|
import { cn } from "../lib/cn";
|
|
10
10
|
import { DropdownPanel, DropdownSearchHeader, useDropdownSearch } from "./dropdown";
|
|
11
|
-
import { useActiveOptionScroll } from "./combobox-core";
|
|
11
|
+
import { useActiveOptionScroll, useComboboxFieldError } from "./combobox-core";
|
|
12
12
|
import {
|
|
13
13
|
DEFAULT_COMMON_LABELS,
|
|
14
14
|
DEFAULT_MULTI_SELECT_LABELS,
|
|
@@ -50,6 +50,14 @@ export interface MultiSelectProps extends Omit<ComponentPropsWithoutRef<"div">,
|
|
|
50
50
|
* this package carries it now, so a form can mark any of its fields rather than
|
|
51
51
|
* only the one that happened to have it first. */
|
|
52
52
|
invalid?: boolean;
|
|
53
|
+
/** What is wrong with the selection, in the caller's words. Rendered under the
|
|
54
|
+
* field, pointed at by the trigger's `aria-describedby` (merged with any the
|
|
55
|
+
* caller passed) and implies `invalid` — {@link Input}'s `error`, on the same
|
|
56
|
+
* rules. */
|
|
57
|
+
error?: ReactNode;
|
|
58
|
+
/** The trigger cannot be opened: FIELD_BASE's settled grey, no chevron (as a
|
|
59
|
+
* disabled {@link Select} drops its own), and a dimmed label. */
|
|
60
|
+
disabled?: boolean;
|
|
53
61
|
}
|
|
54
62
|
|
|
55
63
|
export function MultiSelect({
|
|
@@ -66,7 +74,10 @@ export function MultiSelect({
|
|
|
66
74
|
className,
|
|
67
75
|
panelClassName,
|
|
68
76
|
invalid,
|
|
77
|
+
error,
|
|
78
|
+
disabled,
|
|
69
79
|
"aria-label": ariaLabel,
|
|
80
|
+
"aria-describedby": ariaDescribedBy,
|
|
70
81
|
...rest
|
|
71
82
|
}: MultiSelectProps) {
|
|
72
83
|
const { open, setOpen, wrapperRef, triggerRef, closeToTrigger, query, setQuery, inputRef } =
|
|
@@ -81,6 +92,7 @@ export function MultiSelect({
|
|
|
81
92
|
selectedCount: itemLabel,
|
|
82
93
|
});
|
|
83
94
|
const common = useKitLabels("common", DEFAULT_COMMON_LABELS);
|
|
95
|
+
const field = useComboboxFieldError(error, invalid, ariaDescribedBy);
|
|
84
96
|
// One id per instance: `aria-controls` on the trigger names the list while the
|
|
85
97
|
// list is still closed.
|
|
86
98
|
const uid = useId();
|
|
@@ -151,10 +163,11 @@ export function MultiSelect({
|
|
|
151
163
|
// the thing with a role. Spread FIRST so the ARIA the trigger composes, and the
|
|
152
164
|
// handlers that open the list, cannot be replaced by accident from outside.
|
|
153
165
|
<div {...rest} ref={wrapperRef} className={cn("relative", className)}>
|
|
154
|
-
{label !== undefined && <FieldLabel>{label}</FieldLabel>}
|
|
166
|
+
{label !== undefined && <FieldLabel className={disabled ? "opacity-50" : undefined}>{label}</FieldLabel>}
|
|
155
167
|
<button
|
|
156
168
|
ref={triggerRef}
|
|
157
169
|
type="button"
|
|
170
|
+
disabled={disabled}
|
|
158
171
|
// A combobox, not a bare button: this trigger carried `aria-invalid`, which
|
|
159
172
|
// `button` does not support, so a required-and-empty filter painted its rose
|
|
160
173
|
// border and announced nothing (ESLint's `role-supports-aria-props`, the
|
|
@@ -187,15 +200,25 @@ export function MultiSelect({
|
|
|
187
200
|
setActive(0);
|
|
188
201
|
}
|
|
189
202
|
}}
|
|
190
|
-
aria-invalid={
|
|
191
|
-
|
|
203
|
+
aria-invalid={field.isInvalid || undefined}
|
|
204
|
+
// On the TRIGGER, where focus lands — the `aria-describedby` a caller put on
|
|
205
|
+
// the component is taken off the wrapper for the same reason.
|
|
206
|
+
aria-describedby={field.describedBy}
|
|
207
|
+
className={cn(
|
|
208
|
+
FIELD_TRIGGER,
|
|
209
|
+
"pe-9",
|
|
210
|
+
label !== undefined && FIELD_FLOATING_PAD,
|
|
211
|
+
// FIELD_TRIGGER's hover would still light a trigger nothing can open.
|
|
212
|
+
"disabled:hover:bg-[var(--bg-surface-2)]",
|
|
213
|
+
field.isInvalid && FIELD_INVALID,
|
|
214
|
+
)}
|
|
192
215
|
>
|
|
193
216
|
{/* The summary IS the field's value, so it inherits FIELD_BASE's ink rather
|
|
194
217
|
than restating a lighter one (Keksdose dev#477). */}
|
|
195
218
|
<span className="truncate">{selectedSummary}</span>
|
|
196
|
-
<FieldChevron />
|
|
219
|
+
{!disabled && <FieldChevron />}
|
|
197
220
|
</button>
|
|
198
|
-
{open && (
|
|
221
|
+
{open && !disabled && (
|
|
199
222
|
<DropdownPanel
|
|
200
223
|
// 16rem by default; `panelClassName` is how a caller widens it (Keksdose
|
|
201
224
|
// feedback #147: the market picker's rows carry a postcode, a town, a
|
|
@@ -266,7 +289,7 @@ export function MultiSelect({
|
|
|
266
289
|
onClick={() => toggle(o.value)}
|
|
267
290
|
onMouseEnter={() => setActive(i)}
|
|
268
291
|
className={cn(
|
|
269
|
-
"flex w-full items-center justify-between gap-2 px-3 py-1.5 text-sm text-
|
|
292
|
+
"flex w-full items-center justify-between gap-2 px-3 py-1.5 text-sm text-start hover:bg-[var(--bg-hover)]",
|
|
270
293
|
checked && "bg-[var(--bg-active)]",
|
|
271
294
|
i === active && "bg-[var(--bg-hover)]",
|
|
272
295
|
)}
|
|
@@ -298,6 +321,10 @@ export function MultiSelect({
|
|
|
298
321
|
})}
|
|
299
322
|
</DropdownPanel>
|
|
300
323
|
)}
|
|
324
|
+
{/* After the panel, not before it: the panel is `absolute` with no `top`, so it
|
|
325
|
+
opens at its place in the flow — under the message, had the message come
|
|
326
|
+
first. */}
|
|
327
|
+
{field.errorEl}
|
|
301
328
|
</div>
|
|
302
329
|
);
|
|
303
330
|
}
|
|
@@ -234,7 +234,7 @@ export function NumberInput({
|
|
|
234
234
|
}}
|
|
235
235
|
onKeyDown={onKeyDown}
|
|
236
236
|
aria-invalid={invalid || undefined}
|
|
237
|
-
// The
|
|
237
|
+
// The end padding sits last so it always wins over an inputClassName that
|
|
238
238
|
// sets its own px. Both trailing controls can be on at once, so the room
|
|
239
239
|
// they need is reserved together rather than by whichever happens to render:
|
|
240
240
|
// the calculator is ~36px and a short unit ~24px, and a field that reserved
|
|
@@ -246,7 +246,7 @@ export function NumberInput({
|
|
|
246
246
|
? FLOATING_INPUT_CLASS
|
|
247
247
|
: FIELD_BASE,
|
|
248
248
|
inputClassName,
|
|
249
|
-
showCalc && suffix !== undefined ? "
|
|
249
|
+
showCalc && suffix !== undefined ? "pe-16" : showCalc ? "pe-9" : suffix !== undefined ? "pe-8" : undefined,
|
|
250
250
|
invalid && FIELD_INVALID,
|
|
251
251
|
)}
|
|
252
252
|
/>
|
|
@@ -254,7 +254,7 @@ export function NumberInput({
|
|
|
254
254
|
// One flex track for both, so the unit and the calculator sit side by side
|
|
255
255
|
// instead of stacking on the same corner — AmountInput's arrangement, which
|
|
256
256
|
// has carried a chip and a calculator together since #430.
|
|
257
|
-
<div className="absolute inset-y-0
|
|
257
|
+
<div className="absolute inset-y-0 end-0 flex items-center">
|
|
258
258
|
{showCalc && (
|
|
259
259
|
<CalculatorButton
|
|
260
260
|
value={value}
|
|
@@ -267,7 +267,7 @@ export function NumberInput({
|
|
|
267
267
|
{suffix !== undefined && (
|
|
268
268
|
<span
|
|
269
269
|
aria-hidden
|
|
270
|
-
className="pointer-events-none
|
|
270
|
+
className="pointer-events-none pe-3 text-xs font-medium text-[var(--text-muted)]"
|
|
271
271
|
>
|
|
272
272
|
{suffix}
|
|
273
273
|
</span>
|
|
@@ -203,9 +203,11 @@ export function NumberPadSheet({
|
|
|
203
203
|
{label != null && (
|
|
204
204
|
<span className="truncate pb-1 text-xs font-medium text-[var(--money-neutral)]">{label}</span>
|
|
205
205
|
)}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
206
|
+
{/* At the logical end, opposite the label; the figures themselves stay
|
|
207
|
+
left-to-right and right-aligned, like the desktop calculator's. */}
|
|
208
|
+
<div className="ms-auto min-w-0">
|
|
209
|
+
<div dir="ltr" className="truncate text-right font-mono text-lg leading-tight text-[var(--text-primary)]">{value || "0"}</div>
|
|
210
|
+
<div dir="ltr" className="h-4 text-right font-mono text-xs text-[var(--money-neutral)]">{preview}</div>
|
|
209
211
|
</div>
|
|
210
212
|
</div>
|
|
211
213
|
|
|
@@ -246,6 +246,6 @@ export function PickerSheet({
|
|
|
246
246
|
/** A row inside a {@link PickerSheet} — the phone-sized version of a dropdown row,
|
|
247
247
|
* so a list is comfortable to hit with a thumb rather than merely legible. */
|
|
248
248
|
export const SHEET_ROW_CLASS = cn(
|
|
249
|
-
"flex min-h-11 w-full items-center gap-2 px-4 py-2 text-
|
|
249
|
+
"flex min-h-11 w-full items-center gap-2 px-4 py-2 text-start text-base",
|
|
250
250
|
"text-[var(--text-primary)] hover:bg-[var(--bg-hover)]",
|
|
251
251
|
);
|
|
@@ -5,6 +5,8 @@ import { useAnchoredPanel } from "../hooks/use-anchored-panel";
|
|
|
5
5
|
import { useEscapeKey, useOutsideClick } from "../hooks/use-dismiss";
|
|
6
6
|
import { useFocusTrap } from "../hooks/use-focus-trap";
|
|
7
7
|
import { cn } from "../lib/cn";
|
|
8
|
+
import { dirOf } from "../lib/direction";
|
|
9
|
+
import type { Direction } from "../lib/direction";
|
|
8
10
|
import { useKitLabels } from "../i18n/kit-labels";
|
|
9
11
|
|
|
10
12
|
/** The panel's accessible name. A `role="dialog"` with no name announces as "dialog"
|
|
@@ -101,13 +103,22 @@ export function Popover({
|
|
|
101
103
|
const triggerRef = useRef<HTMLButtonElement>(null);
|
|
102
104
|
const panelRef = useRef<HTMLDivElement>(null);
|
|
103
105
|
const [open, setOpen] = useState(false);
|
|
106
|
+
// The trigger's reading direction, read when it is pressed. The panel is portalled to
|
|
107
|
+
// <body>, out of any `dir="rtl"` subtree the trigger sits in, so without carrying it
|
|
108
|
+
// across a right-to-left calendar or filter rendered left-to-right.
|
|
109
|
+
const [dir, setDir] = useState<Direction>("ltr");
|
|
104
110
|
const close = () => setOpen(false);
|
|
105
|
-
const toggle = () =>
|
|
111
|
+
const toggle = () => {
|
|
112
|
+
setDir(dirOf(triggerRef.current));
|
|
113
|
+
setOpen((v) => !v);
|
|
114
|
+
};
|
|
106
115
|
// prop > `<UiKitProvider labels={{ popover }}>` > English. The provider's
|
|
107
116
|
// `popover.panel` is only the last-resort name for a panel no caller named.
|
|
108
117
|
const text = useKitLabels("popover", DEFAULT_POPOVER_LABELS, labels);
|
|
109
118
|
|
|
110
|
-
// Align the panel's
|
|
119
|
+
// Align the panel's END edge to the trigger's — right in LTR, left in RTL; it was
|
|
120
|
+
// always `rect.right`, so an RTL panel hung off the wrong side — and clamp it
|
|
121
|
+
// horizontally; the
|
|
111
122
|
// hook owns the vertical half — below by default, flipped above (and height-capped)
|
|
112
123
|
// when that is where the room is, e.g. once a mobile keyboard has eaten the bottom
|
|
113
124
|
// of the screen (feedback #135). It re-measures on scroll/resize.
|
|
@@ -116,7 +127,10 @@ export function Popover({
|
|
|
116
127
|
const pos = rect
|
|
117
128
|
? {
|
|
118
129
|
top: placement.top,
|
|
119
|
-
left: Math.min(
|
|
130
|
+
left: Math.min(
|
|
131
|
+
Math.max(8, dir === "rtl" ? rect.left : rect.right - width),
|
|
132
|
+
window.innerWidth - width - 8,
|
|
133
|
+
),
|
|
120
134
|
}
|
|
121
135
|
: null;
|
|
122
136
|
|
|
@@ -169,6 +183,7 @@ export function Popover({
|
|
|
169
183
|
// element; the DOM spelling is the one being standardised on, so where both
|
|
170
184
|
// are present it is the one that survives.
|
|
171
185
|
aria-label={ariaLabel ?? text.panel}
|
|
186
|
+
dir={rest.dir ?? dir}
|
|
172
187
|
// Without it the panel cannot take focus at all, and the trap above would
|
|
173
188
|
// silently do nothing on a panel whose children are not yet tabbable.
|
|
174
189
|
tabIndex={-1}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Round tick values for `SeriesChart` — internal, not re-exported by `@eifi1/ui-kit/chart`.
|
|
2
|
+
//
|
|
3
|
+
// Handed an explicit numeric domain, recharts divides it into equal parts and prints
|
|
4
|
+
// wherever those land: a fitted band of [-11, 811] comes out as -11, 189, 389 … and a
|
|
5
|
+
// padded [89, 1011] as 89, 289, 489 — numbers nobody can put a ruler against. These
|
|
6
|
+
// are the ticks a person would draw instead: every multiple of a 1, 2 or 5 × 10^n step
|
|
7
|
+
// that lies INSIDE the domain. The domain itself is left alone, so the 4 % of air the
|
|
8
|
+
// fit leaves and a zoom window's exact edges both survive; the frame simply does not
|
|
9
|
+
// get a number of its own.
|
|
10
|
+
|
|
11
|
+
/** Roughly how many ticks to aim for. The 1/2/5 ladder lands anywhere from about half
|
|
12
|
+
* to about twice this, which is the price of every tick being a round number. */
|
|
13
|
+
const TARGET_TICKS = 5;
|
|
14
|
+
|
|
15
|
+
/** The round step closest to `span / target`: 1, 2 or 5 times a power of ten. */
|
|
16
|
+
export function niceStep(span: number, target: number = TARGET_TICKS): number {
|
|
17
|
+
const rough = span / Math.max(1, target);
|
|
18
|
+
const magnitude = 10 ** Math.floor(Math.log10(rough));
|
|
19
|
+
const norm = rough / magnitude;
|
|
20
|
+
const factor = norm < 1.5 ? 1 : norm < 3 ? 2 : norm < 7 ? 5 : 10;
|
|
21
|
+
return factor * magnitude;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The round ticks inside `[low, high]`, ascending — or `undefined` when there is no
|
|
26
|
+
* span to divide (no domain, a zero-height one, anything non-finite), which leaves the
|
|
27
|
+
* choice to recharts.
|
|
28
|
+
*
|
|
29
|
+
* Each tick is computed as `k × step` and then rounded to the step's own decimals:
|
|
30
|
+
* accumulating `+= 0.1` would print 0.30000000000000004 on the third tick.
|
|
31
|
+
*/
|
|
32
|
+
export function niceTicks(
|
|
33
|
+
domain: readonly [number, number] | undefined,
|
|
34
|
+
target: number = TARGET_TICKS,
|
|
35
|
+
): number[] | undefined {
|
|
36
|
+
if (!domain) return undefined;
|
|
37
|
+
const [low, high] = domain;
|
|
38
|
+
if (!Number.isFinite(low) || !Number.isFinite(high) || !(high > low)) return undefined;
|
|
39
|
+
const step = niceStep(high - low, target);
|
|
40
|
+
if (!(step > 0) || !Number.isFinite(step)) return undefined;
|
|
41
|
+
const decimals = Math.max(0, -Math.floor(Math.log10(step)));
|
|
42
|
+
const ticks: number[] = [];
|
|
43
|
+
// Half a step's tolerance per end would admit ticks outside the frame; a millionth
|
|
44
|
+
// of one only forgives the float error in `low / step`.
|
|
45
|
+
const first = Math.ceil(low / step - 1e-6);
|
|
46
|
+
const last = Math.floor(high / step + 1e-6);
|
|
47
|
+
for (let k = first; k <= last; k++) {
|
|
48
|
+
// `+ 0` turns the -0 that `Number("-0.00")` gives back into a plain 0.
|
|
49
|
+
ticks.push(Number((k * step).toFixed(decimals)) + 0);
|
|
50
|
+
}
|
|
51
|
+
return ticks.length ? ticks : undefined;
|
|
52
|
+
}
|
|
@@ -24,6 +24,7 @@ import { ChartContainer, ChartTooltip, ChartTooltipContent, type ChartConfig } f
|
|
|
24
24
|
import { DEFAULT_Y_AXIS, withChartZoom, type ZoomBinding } from "./chart-zoom";
|
|
25
25
|
import { STEP_DASH, strokeDash } from "./toggle-legend";
|
|
26
26
|
import { DEFAULT_SERIES_CHART_LABELS, type SeriesChartLabels } from "./series-chart-labels";
|
|
27
|
+
import { niceTicks } from "./series-chart-ticks";
|
|
27
28
|
import { paletteFor } from "../theme/chart-palette";
|
|
28
29
|
import { useKitLabels, useKitLocale } from "../i18n/kit-labels";
|
|
29
30
|
import { cn } from "../lib/cn";
|
|
@@ -353,6 +354,8 @@ export function StaticSeriesChart({
|
|
|
353
354
|
};
|
|
354
355
|
|
|
355
356
|
// Fitted with air around it, unless the reader has zoomed or the caller pinned it.
|
|
357
|
+
// Ticks are the round values INSIDE whichever domain that is (see
|
|
358
|
+
// `series-chart-ticks.ts`), so a zoom window still gets round numbers, just finer.
|
|
356
359
|
const fittedX = zoom?.xDomain ?? paddedDomain(rows.map((row) => row[xKey]));
|
|
357
360
|
const fittedY = (axis: SeriesChartAxis) =>
|
|
358
361
|
zoom?.yDomains[axis.id] ??
|
|
@@ -375,6 +378,7 @@ export function StaticSeriesChart({
|
|
|
375
378
|
// category axis would straighten exactly the curvature the chart is for.
|
|
376
379
|
type="number"
|
|
377
380
|
domain={fittedX ?? ["dataMin", "dataMax"]}
|
|
381
|
+
ticks={niceTicks(fittedX)}
|
|
378
382
|
// Clip the lines to a zoom window instead of recharts widening it back out.
|
|
379
383
|
allowDataOverflow={zoom?.xDomain !== undefined}
|
|
380
384
|
tickLine={false}
|
|
@@ -395,37 +399,41 @@ export function StaticSeriesChart({
|
|
|
395
399
|
/>
|
|
396
400
|
)}
|
|
397
401
|
</XAxis>
|
|
398
|
-
{axes.map((axis) =>
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
402
|
+
{axes.map((axis) => {
|
|
403
|
+
const domain = fittedY(axis);
|
|
404
|
+
return (
|
|
405
|
+
<YAxis
|
|
406
|
+
key={axis.id}
|
|
407
|
+
yAxisId={axis.id}
|
|
408
|
+
hide={axis.hide}
|
|
409
|
+
domain={domain}
|
|
410
|
+
ticks={niceTicks(domain)}
|
|
411
|
+
allowDataOverflow={zoom?.yDomains[axis.id] !== undefined}
|
|
412
|
+
orientation={axis.orientation ?? "left"}
|
|
413
|
+
width={axisBandWidth(axis.width, Boolean(axis.title) && !axis.hide)}
|
|
414
|
+
tickLine={false}
|
|
415
|
+
axisLine={false}
|
|
416
|
+
tickFormatter={axis.format ?? number}
|
|
417
|
+
// With an axis per channel the tint is the only thing saying which
|
|
418
|
+
// numbers belong to which line.
|
|
419
|
+
tick={axis.color ? { fill: axis.color } : undefined}
|
|
420
|
+
>
|
|
421
|
+
{axis.title && !axis.hide && (
|
|
422
|
+
<Label
|
|
423
|
+
value={axis.title}
|
|
424
|
+
// Rotated so both sides read upward from the bottom of their axis.
|
|
425
|
+
angle={axis.orientation === "right" ? 90 : -90}
|
|
426
|
+
position={axis.orientation === "right" ? "insideRight" : "insideLeft"}
|
|
427
|
+
// Inline, so a sole series' colour beats the class's neutral fill —
|
|
428
|
+
// and absent for a shared axis, so the class draws it.
|
|
429
|
+
style={{ textAnchor: "middle", fill: soleSeriesColor(axis, series) }}
|
|
430
|
+
offset={AXIS_TITLE_OFFSET}
|
|
431
|
+
className="fill-[var(--text-muted)] text-[11px]"
|
|
432
|
+
/>
|
|
433
|
+
)}
|
|
434
|
+
</YAxis>
|
|
435
|
+
);
|
|
436
|
+
})}
|
|
429
437
|
<ChartTooltip
|
|
430
438
|
content={
|
|
431
439
|
<ChartTooltipContent
|
|
@@ -59,6 +59,15 @@ export interface SwatchPickerProps<T extends string>
|
|
|
59
59
|
activation?: "automatic" | "manual";
|
|
60
60
|
/** 28 / 32 / 44px tiles. Default "md"; "lg" is the touch-target size. */
|
|
61
61
|
size?: TileSize;
|
|
62
|
+
/**
|
|
63
|
+
* Classes for every tile's frame, merged after the kit's own — so a caller's
|
|
64
|
+
* `size-*` beats `size`'s. For a tile that has to follow a container query or a
|
|
65
|
+
* breakpoint (keksdose grows its tiles to 40px in a narrow card:
|
|
66
|
+
* `tileClassName="@max-md:size-10"`) without reaching into the kit's markup with an
|
|
67
|
+
* `[&_[role=radio]]` selector. The swatch dot / glyph inside keeps `size`'s
|
|
68
|
+
* dimensions; the tile grows round it.
|
|
69
|
+
*/
|
|
70
|
+
tileClassName?: string;
|
|
62
71
|
disabled?: boolean;
|
|
63
72
|
labels?: Partial<SwatchPickerLabels>;
|
|
64
73
|
}
|
|
@@ -83,6 +92,7 @@ export function SwatchPicker<T extends string>({
|
|
|
83
92
|
mixed = false,
|
|
84
93
|
activation = "automatic",
|
|
85
94
|
size = "md",
|
|
95
|
+
tileClassName,
|
|
86
96
|
disabled = false,
|
|
87
97
|
labels,
|
|
88
98
|
className,
|
|
@@ -112,6 +122,7 @@ export function SwatchPicker<T extends string>({
|
|
|
112
122
|
activation={activation}
|
|
113
123
|
disabled={disabled}
|
|
114
124
|
size={size}
|
|
125
|
+
tileClassName={tileClassName ? () => tileClassName : undefined}
|
|
115
126
|
renderTile={(item) => {
|
|
116
127
|
const opt = item.key === null ? undefined : byValue.get(item.key);
|
|
117
128
|
return (
|
|
@@ -24,10 +24,19 @@ export interface SwipeAction {
|
|
|
24
24
|
armedClassName: string;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* `right`/`left` are PHYSICAL drag directions in every writing direction, and stay so on
|
|
29
|
+
* purpose: a swipe is a movement of the finger across the glass, the gesture hook
|
|
30
|
+
* measures it in screen pixels, and "which way is destructive" is an app decision that
|
|
31
|
+
* RTL guidelines do not agree on. An app that wants mirrored actions in RTL swaps the
|
|
32
|
+
* two arrays itself. What the component does mirror is everything it lays out: the
|
|
33
|
+
* reveal panel's contents hug the physical edge being uncovered, and the keyboard
|
|
34
|
+
* buttons sit at the row's logical END.
|
|
35
|
+
*/
|
|
27
36
|
export interface SwipeableRowProps {
|
|
28
|
-
/** Actions committed by dragging RIGHT, nearest threshold first. */
|
|
37
|
+
/** Actions committed by dragging (physically) RIGHT, nearest threshold first. */
|
|
29
38
|
right?: SwipeAction[];
|
|
30
|
-
/** Actions committed by dragging LEFT, nearest threshold first. */
|
|
39
|
+
/** Actions committed by dragging (physically) LEFT, nearest threshold first. */
|
|
31
40
|
left?: SwipeAction[];
|
|
32
41
|
/** Turn the gesture off — an open editor, a pending mutation, a locked row. This
|
|
33
42
|
* also withdraws the keyboard buttons below: a row that must not be acted on must
|
|
@@ -180,8 +189,10 @@ export function SwipeableRow({
|
|
|
180
189
|
armed ? "opacity-100" : "opacity-60",
|
|
181
190
|
armed ? shown.armedClassName : shown.className,
|
|
182
191
|
// The panel is anchored to the edge the row is uncovering, so the label
|
|
183
|
-
// sits where the eye already is rather than across the screen.
|
|
184
|
-
|
|
192
|
+
// sits where the eye already is rather than across the screen. That edge is
|
|
193
|
+
// physical, while a flex row starts at the INLINE start — so in RTL the two
|
|
194
|
+
// directions swap, or the label sat under the row on the covered side.
|
|
195
|
+
draggingLeft ? "flex-row-reverse rtl:flex-row" : "rtl:flex-row-reverse",
|
|
185
196
|
)}
|
|
186
197
|
>
|
|
187
198
|
{shown.icon && (
|
|
@@ -236,10 +247,14 @@ export function SwipeableRow({
|
|
|
236
247
|
//
|
|
237
248
|
// `pointer-events-none` here, restored on a button only once it HAS focus: a
|
|
238
249
|
// sr-only button is clipped to nothing but is still a hit target, and this
|
|
239
|
-
// strip sits at the row's
|
|
250
|
+
// strip sits at the row's trailing edge — exactly where a drag toward the
|
|
251
|
+
// start begins.
|
|
252
|
+
//
|
|
253
|
+
// `end-0`/`pe-2`, not `right-0`/`pr-2`: in RTL the right edge is where the
|
|
254
|
+
// row's title starts, and a focused button there covered it.
|
|
240
255
|
// A gesture that silently failed to start because it landed on an invisible
|
|
241
256
|
// button is not a bug anyone would find by looking at the screen.
|
|
242
|
-
className="pointer-events-none absolute inset-y-0
|
|
257
|
+
className="pointer-events-none absolute inset-y-0 end-0 z-10 flex items-center gap-1 pe-2"
|
|
243
258
|
>
|
|
244
259
|
{/* Both sides, each still nearest-threshold first, so the tab order reads in
|
|
245
260
|
the order the gesture arms them. */}
|
|
@@ -2,6 +2,7 @@ import { useId, useRef } from "react";
|
|
|
2
2
|
import type { KeyboardEvent, ReactNode } from "react";
|
|
3
3
|
import { Ban, Check } from "lucide-react";
|
|
4
4
|
import { cn } from "../lib/cn";
|
|
5
|
+
import { horizontalStep } from "../lib/direction";
|
|
5
6
|
import { Tooltip } from "./tooltip";
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -97,14 +98,11 @@ export function TileRadioGroup<T extends string>({
|
|
|
97
98
|
// Left and right are VISUAL directions: in a right-to-left page the row runs the
|
|
98
99
|
// other way. Up and down follow reading order either way — the APG's radio group
|
|
99
100
|
// treats them as previous/next, and the tiles wrap, so there is no column to keep.
|
|
100
|
-
const
|
|
101
|
+
const horizontal = horizontalStep(e.key, e.currentTarget);
|
|
101
102
|
let step: number | "first" | "last";
|
|
102
|
-
switch (e.key) {
|
|
103
|
-
case "
|
|
104
|
-
step =
|
|
105
|
-
break;
|
|
106
|
-
case "ArrowLeft":
|
|
107
|
-
step = rtl ? 1 : -1;
|
|
103
|
+
switch (horizontal ? "horizontal" : e.key) {
|
|
104
|
+
case "horizontal":
|
|
105
|
+
step = horizontal;
|
|
108
106
|
break;
|
|
109
107
|
case "ArrowDown":
|
|
110
108
|
step = 1;
|