@eifi1/ui-kit 0.7.1 → 0.8.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 +32 -17
- package/dist/chart.d.ts +3 -2
- package/dist/components/alert-banner.d.ts +59 -10
- package/dist/components/alert-banner.js +114 -8
- package/dist/components/alert-banner.js.map +1 -1
- package/dist/components/amount-input.d.ts +3 -0
- package/dist/components/button-group.d.ts +26 -0
- package/dist/components/button-group.js +49 -0
- package/dist/components/button-group.js.map +1 -0
- package/dist/components/calculator.d.ts +3 -0
- package/dist/components/chart-zoom.d.ts +63 -14
- package/dist/components/chart-zoom.js +82 -16
- package/dist/components/chart-zoom.js.map +1 -1
- package/dist/components/chip.d.ts +67 -2
- package/dist/components/chip.js +73 -8
- package/dist/components/chip.js.map +1 -1
- package/dist/components/confirm-dialog.d.ts +120 -0
- package/dist/components/confirm-dialog.js +96 -0
- package/dist/components/confirm-dialog.js.map +1 -0
- package/dist/components/copy-button.d.ts +62 -0
- package/dist/components/copy-button.js +95 -0
- package/dist/components/copy-button.js.map +1 -0
- 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 +5 -1
- package/dist/components/data-table-pagination.js +71 -58
- package/dist/components/data-table-pagination.js.map +1 -1
- package/dist/components/data-table-sort.d.ts +28 -5
- package/dist/components/data-table-sort.js +11 -7
- package/dist/components/data-table-sort.js.map +1 -1
- package/dist/components/data-table.d.ts +1 -1
- package/dist/components/data-table.js +544 -423
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-picker.d.ts +51 -5
- package/dist/components/date-picker.js +152 -40
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/description-list.d.ts +60 -0
- package/dist/components/description-list.js +112 -0
- package/dist/components/description-list.js.map +1 -0
- package/dist/components/disclosure.d.ts +37 -2
- package/dist/components/disclosure.js +14 -5
- package/dist/components/disclosure.js.map +1 -1
- package/dist/components/facing-pair.d.ts +2 -0
- package/dist/components/file-button.d.ts +17 -1
- package/dist/components/file-button.js +8 -1
- package/dist/components/file-button.js.map +1 -1
- package/dist/components/file-dropzone.d.ts +54 -9
- package/dist/components/file-dropzone.js +100 -70
- package/dist/components/file-dropzone.js.map +1 -1
- package/dist/components/floating-panel.d.ts +107 -0
- package/dist/components/floating-panel.js +206 -0
- package/dist/components/floating-panel.js.map +1 -0
- package/dist/components/full-bleed-dialog.d.ts +12 -1
- package/dist/components/full-bleed-dialog.js +26 -5
- package/dist/components/full-bleed-dialog.js.map +1 -1
- package/dist/components/modal.d.ts +6 -2
- package/dist/components/modal.js +2 -1
- package/dist/components/modal.js.map +1 -1
- package/dist/components/number-field.d.ts +3 -0
- package/dist/components/number-input.d.ts +3 -0
- package/dist/components/numpad-sheet.d.ts +3 -0
- package/dist/components/progress-bar.d.ts +54 -0
- package/dist/components/progress-bar.js +94 -0
- package/dist/components/progress-bar.js.map +1 -0
- package/dist/components/scroll-area.d.ts +53 -0
- package/dist/components/scroll-area.js +86 -0
- package/dist/components/scroll-area.js.map +1 -0
- package/dist/components/separator.d.ts +23 -0
- package/dist/components/separator.js +24 -0
- package/dist/components/separator.js.map +1 -0
- package/dist/components/series-chart-ticks.d.ts +34 -1
- package/dist/components/series-chart-ticks.js +83 -1
- package/dist/components/series-chart-ticks.js.map +1 -1
- package/dist/components/series-chart.d.ts +314 -19
- package/dist/components/series-chart.js +510 -120
- package/dist/components/series-chart.js.map +1 -1
- package/dist/components/skeleton.d.ts +36 -0
- package/dist/components/skeleton.js +35 -0
- package/dist/components/skeleton.js.map +1 -0
- package/dist/components/stat-tile.d.ts +60 -6
- package/dist/components/stat-tile.js +60 -26
- package/dist/components/stat-tile.js.map +1 -1
- package/dist/components/table.d.ts +83 -0
- package/dist/components/table.js +166 -0
- package/dist/components/table.js.map +1 -0
- package/dist/components/toggle-legend.d.ts +3 -2
- package/dist/components/toggle-legend.js +1 -1
- package/dist/components/toggle-legend.js.map +1 -1
- package/dist/components/tree-view.d.ts +129 -0
- package/dist/components/tree-view.js +376 -0
- package/dist/components/tree-view.js.map +1 -0
- package/dist/components/treemap.d.ts +9 -2
- package/dist/components/treemap.js +2 -1
- package/dist/components/treemap.js.map +1 -1
- package/dist/components/ui.d.ts +87 -13
- package/dist/components/ui.js +111 -29
- package/dist/components/ui.js.map +1 -1
- package/dist/components/use-table-state.d.ts +1 -1
- package/dist/{data-table-filters-noy0Abvi.d.ts → data-table-filters-CF1PXqjQ.d.ts} +73 -3
- package/dist/data-table.d.ts +2 -2
- package/dist/data-table.js.map +1 -1
- package/dist/hooks/use-copy-to-clipboard.d.ts +28 -0
- package/dist/hooks/use-copy-to-clipboard.js +81 -0
- package/dist/hooks/use-copy-to-clipboard.js.map +1 -0
- package/dist/hooks/use-debounce.d.ts +43 -0
- package/dist/hooks/use-debounce.js +56 -0
- package/dist/hooks/use-debounce.js.map +1 -0
- package/dist/hooks/use-file-drop.d.ts +72 -0
- package/dist/hooks/use-file-drop.js +57 -0
- package/dist/hooks/use-file-drop.js.map +1 -0
- package/dist/i18n/defaults.d.ts +3 -0
- package/dist/i18n/defaults.js +7 -1
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +15 -0
- package/dist/i18n/kit-labels.js +6 -2
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/de-CH-informal.d.ts +43 -0
- package/dist/i18n/locales/de-CH-informal.js +8 -0
- package/dist/i18n/locales/de-CH-informal.js.map +1 -0
- package/dist/i18n/locales/de-CH.d.ts +3 -0
- package/dist/i18n/locales/de-CH.js +1 -14
- package/dist/i18n/locales/de-CH.js.map +1 -1
- package/dist/i18n/locales/de-informal.d.ts +64 -0
- package/dist/i18n/locales/de-informal.js +37 -0
- package/dist/i18n/locales/de-informal.js.map +1 -0
- package/dist/i18n/locales/de.d.ts +3 -0
- package/dist/i18n/locales/de.js +25 -0
- package/dist/i18n/locales/de.js.map +1 -1
- package/dist/i18n/locales/es.d.ts +3 -0
- package/dist/i18n/locales/es.js +25 -0
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +3 -0
- package/dist/i18n/locales/fr.js +25 -0
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +3 -0
- package/dist/i18n/locales/hu.js +25 -0
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +3 -0
- package/dist/i18n/locales/it.js +25 -0
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +3 -0
- package/dist/i18n/locales/zh.js +25 -0
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/swiss.d.ts +13 -0
- package/dist/i18n/swiss.js +19 -0
- package/dist/i18n/swiss.js.map +1 -0
- package/dist/index.d.ts +25 -10
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/dates.d.ts +37 -1
- package/dist/lib/dates.js +19 -0
- package/dist/lib/dates.js.map +1 -1
- package/dist/search/command-palette.d.ts +15 -1
- package/dist/search/command-palette.js +10 -2
- package/dist/search/command-palette.js.map +1 -1
- package/dist/wizard/stepper-nav.d.ts +35 -2
- package/dist/wizard/stepper-nav.js +54 -22
- package/dist/wizard/stepper-nav.js.map +1 -1
- package/dist/wizard/types.d.ts +107 -5
- package/dist/wizard/types.js +2 -1
- package/dist/wizard/types.js.map +1 -1
- package/dist/wizard/use-wizard.js +141 -21
- package/dist/wizard/use-wizard.js.map +1 -1
- package/dist/wizard/wizard-summary.d.ts +10 -2
- package/dist/wizard/wizard-summary.js +3 -1
- package/dist/wizard/wizard-summary.js.map +1 -1
- package/dist/wizard.d.ts +4 -1
- package/dist/wizard.js.map +1 -1
- package/package.json +1 -1
- package/src/components/alert-banner.tsx +211 -20
- package/src/components/button-group.tsx +75 -0
- package/src/components/chart-zoom.tsx +158 -20
- package/src/components/chip.tsx +160 -9
- package/src/components/confirm-dialog.tsx +242 -0
- package/src/components/copy-button.tsx +158 -0
- package/src/components/data-table-pagination.tsx +19 -6
- package/src/components/data-table-sort.ts +49 -10
- package/src/components/data-table.tsx +293 -24
- package/src/components/date-picker.tsx +262 -57
- package/src/components/description-list.tsx +174 -0
- package/src/components/disclosure.tsx +58 -5
- package/src/components/file-button.tsx +22 -2
- package/src/components/file-dropzone.tsx +198 -100
- package/src/components/floating-panel.tsx +341 -0
- package/src/components/full-bleed-dialog.tsx +48 -5
- package/src/components/modal.tsx +9 -2
- package/src/components/progress-bar.tsx +163 -0
- package/src/components/scroll-area.tsx +129 -0
- package/src/components/separator.tsx +37 -0
- package/src/components/series-chart-ticks.ts +135 -0
- package/src/components/series-chart.tsx +934 -67
- package/src/components/skeleton.tsx +65 -0
- package/src/components/stat-tile.tsx +160 -27
- package/src/components/table.tsx +263 -0
- package/src/components/toggle-legend.tsx +4 -3
- package/src/components/tree-view.tsx +589 -0
- package/src/components/treemap.tsx +10 -2
- package/src/components/ui.tsx +263 -39
- package/src/data-table.ts +2 -0
- package/src/hooks/use-copy-to-clipboard.ts +122 -0
- package/src/hooks/use-debounce.ts +105 -0
- package/src/hooks/use-file-drop.ts +123 -0
- package/src/i18n/defaults.ts +6 -0
- package/src/i18n/kit-labels.tsx +19 -0
- package/src/i18n/locales/de-CH-informal.ts +10 -0
- package/src/i18n/locales/de-CH.ts +1 -16
- package/src/i18n/locales/de-informal.ts +61 -0
- package/src/i18n/locales/de.ts +25 -0
- package/src/i18n/locales/es.ts +25 -0
- package/src/i18n/locales/fr.ts +25 -0
- package/src/i18n/locales/hu.ts +25 -0
- package/src/i18n/locales/it.ts +25 -0
- package/src/i18n/locales/zh.ts +25 -0
- package/src/i18n/swiss.ts +25 -0
- package/src/index.ts +29 -0
- package/src/lib/dates.ts +48 -0
- package/src/search/command-palette.tsx +29 -3
- package/src/wizard/stepper-nav.tsx +102 -28
- package/src/wizard/types.ts +106 -4
- package/src/wizard/use-wizard.ts +211 -31
- package/src/wizard/wizard-summary.tsx +24 -12
- package/src/wizard.ts +3 -2
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
} from "../i18n/kit-labels";
|
|
13
13
|
import { splitTriggerAria } from "./trigger-aria";
|
|
14
14
|
import type { TriggerAria } from "./trigger-aria";
|
|
15
|
-
import { FieldLabel, FIELD_BASE, FIELD_TRIGGER, FIELD_FLOATING_PAD, FIELD_INVALID } from "./ui";
|
|
15
|
+
import { Button, FieldLabel, FIELD_BASE, FIELD_TRIGGER, FIELD_FLOATING_PAD, FIELD_INVALID } from "./ui";
|
|
16
16
|
import { MiniCalendar, type MiniCalendarProps } from "./mini-calendar";
|
|
17
17
|
import { Popover } from "./popover";
|
|
18
18
|
import { Tooltip } from "./tooltip";
|
|
@@ -553,23 +553,252 @@ export function DatePicker({
|
|
|
553
553
|
}
|
|
554
554
|
|
|
555
555
|
export interface DateRangePickerPreset {
|
|
556
|
+
/**
|
|
557
|
+
* The preset's identity, independent of its dates — `"last_3_months"`, not two ISO
|
|
558
|
+
* strings. Optional for the 0.x callers that pass anonymous pairs.
|
|
559
|
+
*
|
|
560
|
+
* keksdose's report range field (report-range-field.tsx, "Why this is app-level")
|
|
561
|
+
* is why it exists: its URL stores `?preset=last_3_months` and recomputes the dates
|
|
562
|
+
* from today on every read, so a preset that is only its dates froze "last 3 months"
|
|
563
|
+
* at whatever day the panel rendered, and two presets that happen to cover the same
|
|
564
|
+
* days (this week and today, on a Monday) could not be told apart. With an `id` the
|
|
565
|
+
* choice is reported to `onChange` and shown as selected for as long as the range
|
|
566
|
+
* still matches it. Recompute `from`/`to` on every render (`calendarMonthPresets()`,
|
|
567
|
+
* `dateRangePresets()`) and pass the key as the id.
|
|
568
|
+
*/
|
|
569
|
+
id?: string;
|
|
556
570
|
label: ReactNode;
|
|
557
571
|
from: string;
|
|
558
572
|
to: string;
|
|
559
573
|
}
|
|
560
574
|
|
|
575
|
+
/**
|
|
576
|
+
* When the picker hands its range to `onChange`.
|
|
577
|
+
*
|
|
578
|
+
* - `"immediate"` (the default, and all of 0.7): a preset click commits and closes,
|
|
579
|
+
* and each calendar click is reported as it happens — the half-made `(from, "")`
|
|
580
|
+
* included. The right shape for quick ranges on a filter, where one click is one
|
|
581
|
+
* answer.
|
|
582
|
+
* - `"apply"`: the panel works on a DRAFT. A preset ARMS the calendar instead of
|
|
583
|
+
* committing, days can then be nudged ("last 3 months, but end it last Friday"),
|
|
584
|
+
* and nothing reaches `onChange` until Apply — which stays disabled until both ends
|
|
585
|
+
* exist. Cancel, Escape and an outside click discard the draft, and every open
|
|
586
|
+
* starts again from the committed value. keksdose's report range asked for exactly
|
|
587
|
+
* this: each commit there re-runs a whole page of charts, so an intermediate state
|
|
588
|
+
* must never escape the panel.
|
|
589
|
+
*/
|
|
590
|
+
export type DateRangeCommit = "immediate" | "apply";
|
|
591
|
+
|
|
561
592
|
export interface DateRangePickerProps extends DatePickerBaseProps {
|
|
562
593
|
from: string;
|
|
563
594
|
to: string;
|
|
564
|
-
|
|
595
|
+
/** The committed range. `presetId` is the chosen preset's `id` when a preset
|
|
596
|
+
* produced it (and the range was not nudged since), `undefined` for days picked by
|
|
597
|
+
* hand. A 0.x two-argument handler simply ignores it. */
|
|
598
|
+
onChange: (from: string, to: string, presetId?: string) => void;
|
|
565
599
|
/** Separator between the two formatted dates in the trigger (default " – "). */
|
|
566
600
|
separator?: string;
|
|
567
|
-
/** Optional named ranges rendered as a
|
|
601
|
+
/** Optional named ranges rendered as a start-side column (last month / YTD / …). */
|
|
568
602
|
presets?: DateRangePickerPreset[];
|
|
603
|
+
/**
|
|
604
|
+
* The active preset's `id`, controlled — e.g. straight from keksdose's `?preset=`.
|
|
605
|
+
* `null` (or an id no preset has, such as `"custom"`) says "no preset": nothing in
|
|
606
|
+
* the column is marked even if a preset happens to cover the same days. A preset is
|
|
607
|
+
* marked only while its dates still equal `from`/`to`.
|
|
608
|
+
*
|
|
609
|
+
* Leave it `undefined` and the picker remembers the last preset it committed
|
|
610
|
+
* itself; with no memory at all it falls back to the first preset whose dates match.
|
|
611
|
+
*/
|
|
612
|
+
preset?: string | null;
|
|
613
|
+
/** See {@link DateRangeCommit}. Default `"immediate"`. */
|
|
614
|
+
commit?: DateRangeCommit;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
interface RangeDraft {
|
|
618
|
+
from: string;
|
|
619
|
+
to: string;
|
|
620
|
+
presetId: string | undefined;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/** Which preset (by index) the column marks, or -1. See `DateRangePickerProps.preset`. */
|
|
624
|
+
function markedPreset(
|
|
625
|
+
presets: readonly DateRangePickerPreset[],
|
|
626
|
+
from: string,
|
|
627
|
+
to: string,
|
|
628
|
+
activeId: string | null | undefined,
|
|
629
|
+
controlled: boolean,
|
|
630
|
+
): number {
|
|
631
|
+
const matches = (p: DateRangePickerPreset) => p.from === from && p.to === to;
|
|
632
|
+
if (activeId != null) {
|
|
633
|
+
const byId = presets.findIndex((p) => p.id === activeId && matches(p));
|
|
634
|
+
if (byId !== -1 || controlled) return byId;
|
|
635
|
+
} else if (controlled) {
|
|
636
|
+
return -1;
|
|
637
|
+
}
|
|
638
|
+
return presets.findIndex(matches);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* The preset column: a named group of toggle buttons, `aria-pressed` on the marked one.
|
|
643
|
+
* Pressed-state rather than colour alone, because "which range is this" is the one
|
|
644
|
+
* thing a screen-reader user opening the panel needs to hear.
|
|
645
|
+
*/
|
|
646
|
+
function PresetColumn({
|
|
647
|
+
presets,
|
|
648
|
+
marked,
|
|
649
|
+
label,
|
|
650
|
+
onPick,
|
|
651
|
+
}: {
|
|
652
|
+
presets: readonly DateRangePickerPreset[];
|
|
653
|
+
marked: number;
|
|
654
|
+
label: string;
|
|
655
|
+
onPick: (p: DateRangePickerPreset) => void;
|
|
656
|
+
}) {
|
|
657
|
+
return (
|
|
658
|
+
// `border-e`/`pe`, not `-r`: the preset column is on the START side, and the rule
|
|
659
|
+
// between it and the calendar has to follow it in a right-to-left UI.
|
|
660
|
+
<div
|
|
661
|
+
role="group"
|
|
662
|
+
aria-label={label}
|
|
663
|
+
className="flex w-32 shrink-0 flex-col gap-0.5 border-e border-[var(--border)] pe-2"
|
|
664
|
+
>
|
|
665
|
+
{presets.map((p, i) => {
|
|
666
|
+
const selected = i === marked;
|
|
667
|
+
return (
|
|
668
|
+
<button
|
|
669
|
+
key={p.id ?? i}
|
|
670
|
+
type="button"
|
|
671
|
+
aria-pressed={selected}
|
|
672
|
+
data-preset={p.id}
|
|
673
|
+
onClick={() => onPick(p)}
|
|
674
|
+
className={cn(
|
|
675
|
+
"rounded px-2 py-1.5 text-start text-xs",
|
|
676
|
+
selected
|
|
677
|
+
? "bg-[var(--bg-active)] font-medium text-[var(--text-primary)]"
|
|
678
|
+
: "text-[var(--text-secondary)] hover:bg-[var(--bg-hover)]",
|
|
679
|
+
)}
|
|
680
|
+
>
|
|
681
|
+
{p.label}
|
|
682
|
+
</button>
|
|
683
|
+
);
|
|
684
|
+
})}
|
|
685
|
+
</div>
|
|
686
|
+
);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* The panel's body. A component of its own so `commit="apply"` can keep its draft in
|
|
691
|
+
* state that is born when the panel opens: `Popover` unmounts its children on close,
|
|
692
|
+
* so the draft is re-seeded from the committed value on every open, and an abandoned
|
|
693
|
+
* half-selection cannot survive into the next one.
|
|
694
|
+
*/
|
|
695
|
+
function RangePanel({
|
|
696
|
+
from,
|
|
697
|
+
to,
|
|
698
|
+
presets,
|
|
699
|
+
activeId,
|
|
700
|
+
controlled,
|
|
701
|
+
commit,
|
|
702
|
+
close,
|
|
703
|
+
commitRange,
|
|
704
|
+
calendarProps,
|
|
705
|
+
labels,
|
|
706
|
+
}: {
|
|
707
|
+
from: string;
|
|
708
|
+
to: string;
|
|
709
|
+
presets: readonly DateRangePickerPreset[] | undefined;
|
|
710
|
+
activeId: string | null | undefined;
|
|
711
|
+
controlled: boolean;
|
|
712
|
+
commit: DateRangeCommit;
|
|
713
|
+
close: () => void;
|
|
714
|
+
commitRange: (from: string, to: string, presetId: string | undefined) => void;
|
|
715
|
+
calendarProps: Pick<MiniCalendarProps, "locale" | "min" | "max" | "labels">;
|
|
716
|
+
labels: DatePickerLabels;
|
|
717
|
+
}) {
|
|
718
|
+
const drafting = commit === "apply";
|
|
719
|
+
const [draft, setDraft] = useState<RangeDraft>({
|
|
720
|
+
from,
|
|
721
|
+
to,
|
|
722
|
+
presetId: activeId ?? undefined,
|
|
723
|
+
});
|
|
724
|
+
// What the panel SHOWS: the draft while drafting, the committed value otherwise.
|
|
725
|
+
const shown = drafting ? draft : { from, to, presetId: activeId ?? undefined };
|
|
726
|
+
const marked = presets
|
|
727
|
+
? markedPreset(presets, shown.from, shown.to, drafting ? draft.presetId : activeId, drafting || controlled)
|
|
728
|
+
: -1;
|
|
729
|
+
const complete = Boolean(draft.from && draft.to);
|
|
730
|
+
|
|
731
|
+
const calendar = (
|
|
732
|
+
<MiniCalendar
|
|
733
|
+
{...calendarProps}
|
|
734
|
+
focusOnOpen
|
|
735
|
+
from={shown.from}
|
|
736
|
+
to={shown.to}
|
|
737
|
+
onSelect={(f, t) => {
|
|
738
|
+
if (drafting) {
|
|
739
|
+
setDraft({ from: f, to: t, presetId: undefined });
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
commitRange(f, t, undefined);
|
|
743
|
+
// Two-click range: only dismiss once both ends are chosen.
|
|
744
|
+
if (f && t) close();
|
|
745
|
+
}}
|
|
746
|
+
/>
|
|
747
|
+
);
|
|
748
|
+
|
|
749
|
+
const body =
|
|
750
|
+
presets && presets.length > 0 ? (
|
|
751
|
+
<div className="flex gap-3">
|
|
752
|
+
<PresetColumn
|
|
753
|
+
presets={presets}
|
|
754
|
+
marked={marked}
|
|
755
|
+
label={labels.presets}
|
|
756
|
+
onPick={(p) => {
|
|
757
|
+
if (drafting) {
|
|
758
|
+
setDraft({ from: p.from, to: p.to, presetId: p.id });
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
commitRange(p.from, p.to, p.id);
|
|
762
|
+
close();
|
|
763
|
+
}}
|
|
764
|
+
/>
|
|
765
|
+
{/* `flex-1`: the calendar takes the rest of the panel. Content-sized it was
|
|
766
|
+
seven tiny cells beside a wide preset column. */}
|
|
767
|
+
<div className="min-w-0 flex-1">{calendar}</div>
|
|
768
|
+
</div>
|
|
769
|
+
) : (
|
|
770
|
+
calendar
|
|
771
|
+
);
|
|
772
|
+
|
|
773
|
+
if (!drafting) return body;
|
|
774
|
+
return (
|
|
775
|
+
<div className="flex flex-col gap-2">
|
|
776
|
+
{body}
|
|
777
|
+
<div className="flex items-center justify-end gap-2 border-t border-[var(--border)] pt-2">
|
|
778
|
+
<Button type="button" variant="ghost" onClick={close}>
|
|
779
|
+
{labels.cancel}
|
|
780
|
+
</Button>
|
|
781
|
+
<Button
|
|
782
|
+
type="button"
|
|
783
|
+
// A half-made `(from, "")` would commit a window with no end — a blank
|
|
784
|
+
// report, one click into a two-click gesture.
|
|
785
|
+
disabled={!complete}
|
|
786
|
+
onClick={() => {
|
|
787
|
+
if (!complete) return;
|
|
788
|
+
commitRange(draft.from, draft.to, draft.presetId);
|
|
789
|
+
close();
|
|
790
|
+
}}
|
|
791
|
+
>
|
|
792
|
+
{labels.apply}
|
|
793
|
+
</Button>
|
|
794
|
+
</div>
|
|
795
|
+
</div>
|
|
796
|
+
);
|
|
569
797
|
}
|
|
570
798
|
|
|
571
799
|
/** Two-date range picker: click a start then an end; closes once both are set.
|
|
572
|
-
* With `presets`, a
|
|
800
|
+
* With `presets`, a column of named ranges is shown beside the calendar; with
|
|
801
|
+
* `commit="apply"` the panel drafts and commits on Apply (see {@link DateRangeCommit}). */
|
|
573
802
|
export function DateRangePicker({
|
|
574
803
|
from,
|
|
575
804
|
to,
|
|
@@ -582,6 +811,8 @@ export function DateRangePicker({
|
|
|
582
811
|
formatValue,
|
|
583
812
|
separator = " – ",
|
|
584
813
|
presets,
|
|
814
|
+
preset,
|
|
815
|
+
commit = "immediate",
|
|
585
816
|
calendarLabels,
|
|
586
817
|
label,
|
|
587
818
|
clearable,
|
|
@@ -593,6 +824,11 @@ export function DateRangePicker({
|
|
|
593
824
|
const locale = useKitLocale(localeProp);
|
|
594
825
|
const text = useDatePickerLabels(clearLabel, undefined, undefined);
|
|
595
826
|
const render = formatValue ?? ((iso: string) => formatDate(iso, locale, formatOptions));
|
|
827
|
+
// The preset this picker committed itself, for a caller that does not control
|
|
828
|
+
// `preset`. Cleared by a hand-picked range or a clear.
|
|
829
|
+
const [ownPreset, setOwnPreset] = useState<string | undefined>(undefined);
|
|
830
|
+
const controlled = preset !== undefined;
|
|
831
|
+
const activeId = controlled ? preset : ownPreset;
|
|
596
832
|
const a = from ? render(from) : "";
|
|
597
833
|
const b = to ? render(to) : "";
|
|
598
834
|
const triggerText = from
|
|
@@ -601,22 +837,13 @@ export function DateRangePicker({
|
|
|
601
837
|
: `${a}${separator}…`
|
|
602
838
|
: (placeholder ?? "");
|
|
603
839
|
const hasPresets = Boolean(presets && presets.length > 0);
|
|
604
|
-
const
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
max={max}
|
|
612
|
-
labels={calendarLabels}
|
|
613
|
-
onSelect={(f, t) => {
|
|
614
|
-
onChange(f, t);
|
|
615
|
-
// Two-click range: only dismiss once both ends are chosen.
|
|
616
|
-
if (f && t) close();
|
|
617
|
-
}}
|
|
618
|
-
/>
|
|
619
|
-
);
|
|
840
|
+
const commitRange = (f: string, t: string, presetId: string | undefined) => {
|
|
841
|
+
setOwnPreset(presetId);
|
|
842
|
+
// Two arguments when no preset is involved — exactly the 0.7 call, so a caller's
|
|
843
|
+
// `toHaveBeenCalledWith(from, to)` and a variadic handler see no difference.
|
|
844
|
+
if (presetId === undefined) onChange(f, t);
|
|
845
|
+
else onChange(f, t, presetId);
|
|
846
|
+
};
|
|
620
847
|
return (
|
|
621
848
|
<DateField
|
|
622
849
|
{...rest}
|
|
@@ -630,44 +857,22 @@ export function DateRangePicker({
|
|
|
630
857
|
width={hasPresets ? 440 : undefined}
|
|
631
858
|
hasValue={Boolean(from || to)}
|
|
632
859
|
triggerText={triggerText}
|
|
633
|
-
onClear={() =>
|
|
860
|
+
onClear={() => commitRange("", "", undefined)}
|
|
634
861
|
>
|
|
635
|
-
{(close) =>
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
close();
|
|
650
|
-
}}
|
|
651
|
-
className={cn(
|
|
652
|
-
"rounded px-2 py-1.5 text-start text-xs",
|
|
653
|
-
selected
|
|
654
|
-
? "bg-[var(--bg-active)] font-medium text-[var(--text-primary)]"
|
|
655
|
-
: "text-[var(--text-secondary)] hover:bg-[var(--bg-hover)]",
|
|
656
|
-
)}
|
|
657
|
-
>
|
|
658
|
-
{p.label}
|
|
659
|
-
</button>
|
|
660
|
-
);
|
|
661
|
-
})}
|
|
662
|
-
</div>
|
|
663
|
-
{/* `flex-1`: the calendar takes the rest of the panel. Content-sized it was
|
|
664
|
-
seven tiny cells beside a wide preset column. */}
|
|
665
|
-
<div className="min-w-0 flex-1">{calendar(close)}</div>
|
|
666
|
-
</div>
|
|
667
|
-
) : (
|
|
668
|
-
calendar(close)
|
|
669
|
-
)
|
|
670
|
-
}
|
|
862
|
+
{(close) => (
|
|
863
|
+
<RangePanel
|
|
864
|
+
from={from}
|
|
865
|
+
to={to}
|
|
866
|
+
presets={presets}
|
|
867
|
+
activeId={activeId}
|
|
868
|
+
controlled={controlled}
|
|
869
|
+
commit={commit}
|
|
870
|
+
close={close}
|
|
871
|
+
commitRange={commitRange}
|
|
872
|
+
calendarProps={{ locale, min, max, labels: calendarLabels }}
|
|
873
|
+
labels={text}
|
|
874
|
+
/>
|
|
875
|
+
)}
|
|
671
876
|
</DateField>
|
|
672
877
|
);
|
|
673
878
|
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { createContext, useContext } from "react";
|
|
2
|
+
import type { ComponentPropsWithoutRef, CSSProperties, ReactNode } from "react";
|
|
3
|
+
import { cn } from "../lib/cn";
|
|
4
|
+
import { FieldHint } from "./ui";
|
|
5
|
+
|
|
6
|
+
export type DescriptionListLayout = "rows" | "cards";
|
|
7
|
+
export type DescriptionListDensity = "comfortable" | "compact";
|
|
8
|
+
|
|
9
|
+
interface ListContextValue {
|
|
10
|
+
layout: DescriptionListLayout;
|
|
11
|
+
numeric: boolean;
|
|
12
|
+
density: DescriptionListDensity;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const ListContext = createContext<ListContextValue>({ layout: "rows", numeric: false, density: "comfortable" });
|
|
16
|
+
|
|
17
|
+
export interface DescriptionListProps extends ComponentPropsWithoutRef<"dl"> {
|
|
18
|
+
/**
|
|
19
|
+
* `rows` (default): term beside detail, a two-column list that stacks the pair
|
|
20
|
+
* when the LIST is narrower than 24rem — a container query, so a detail panel
|
|
21
|
+
* beside a sidebar stacks where it is actually cramped, not where the viewport is.
|
|
22
|
+
* `cards`: a grid of small bordered cards, term above body — keksdose's overview
|
|
23
|
+
* figures, lenkbank's account facts.
|
|
24
|
+
*/
|
|
25
|
+
layout?: DescriptionListLayout;
|
|
26
|
+
/** Every detail is a figure: tabular digits, end-aligned. A `numeric` list of
|
|
27
|
+
* rows does not stack on a phone — a figure is short, and putting it under its
|
|
28
|
+
* term would double the list's height to gain no width. Per item: see
|
|
29
|
+
* {@link DescriptionItemProps.numeric}. */
|
|
30
|
+
numeric?: boolean;
|
|
31
|
+
/** `compact` tightens the rhythm for a dense side panel. */
|
|
32
|
+
density?: DescriptionListDensity;
|
|
33
|
+
/** `cards` only: the narrowest a card may get before the grid drops a column. */
|
|
34
|
+
minCardWidth?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A real `<dl>` — terms and their details, which is what keksdose's eight and
|
|
39
|
+
* lenkbank's four hand-built "label: value" blocks were, and none of them said so.
|
|
40
|
+
* As a `<dl>` a reader announces "list, 6 items" and pairs each term with its detail;
|
|
41
|
+
* as a stack of `<div>`s it read one undifferentiated run of text.
|
|
42
|
+
*
|
|
43
|
+
* Composed from {@link DescriptionItem} children rather than taking an `items`
|
|
44
|
+
* array. The call sites this replaces are mostly CONDITIONAL — "IBAN, if there is
|
|
45
|
+
* one", "closing date, once closed" — and `{iban && <DescriptionItem …>}` reads as
|
|
46
|
+
* what it means, where an array wants `.filter(Boolean)` and a cast. Children also
|
|
47
|
+
* give each item its own props (a numeric figure in an otherwise textual list, a
|
|
48
|
+
* `data-private` detail) with no parallel config object. A data-driven list loses
|
|
49
|
+
* nothing: `items.map((i) => <DescriptionItem key={i.term} {...i} />)`.
|
|
50
|
+
*
|
|
51
|
+
* Each pair is wrapped in a `<div>`, which HTML permits inside a `<dl>` for exactly
|
|
52
|
+
* this: styling a pair as a unit (a row, a card) without breaking the list.
|
|
53
|
+
*/
|
|
54
|
+
export function DescriptionList({
|
|
55
|
+
layout = "rows",
|
|
56
|
+
numeric = false,
|
|
57
|
+
density = "comfortable",
|
|
58
|
+
minCardWidth = "10rem",
|
|
59
|
+
className,
|
|
60
|
+
style,
|
|
61
|
+
...rest
|
|
62
|
+
}: DescriptionListProps) {
|
|
63
|
+
const cards = layout === "cards";
|
|
64
|
+
const grid: CSSProperties | undefined = cards
|
|
65
|
+
? { gridTemplateColumns: `repeat(auto-fill, minmax(min(100%, ${minCardWidth}), 1fr))` }
|
|
66
|
+
: undefined;
|
|
67
|
+
return (
|
|
68
|
+
<ListContext.Provider value={{ layout, numeric, density }}>
|
|
69
|
+
<dl
|
|
70
|
+
{...rest}
|
|
71
|
+
data-layout={layout}
|
|
72
|
+
style={grid ? { ...grid, ...style } : style}
|
|
73
|
+
className={cn(
|
|
74
|
+
cards
|
|
75
|
+
? cn("grid", density === "compact" ? "gap-2" : "gap-3")
|
|
76
|
+
: "@container divide-y divide-[var(--border)]",
|
|
77
|
+
density === "compact" ? "text-xs" : "text-sm",
|
|
78
|
+
className,
|
|
79
|
+
)}
|
|
80
|
+
/>
|
|
81
|
+
</ListContext.Provider>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface DescriptionItemProps extends Omit<ComponentPropsWithoutRef<"div">, "children"> {
|
|
86
|
+
/** What is described: "Opened", "IBAN", "Net rent". */
|
|
87
|
+
term: ReactNode;
|
|
88
|
+
/** The description itself. */
|
|
89
|
+
children?: ReactNode;
|
|
90
|
+
/** The explanation behind a "?" beside the term (a {@link FieldHint}) — the same
|
|
91
|
+
* meaning `hint` has on `StatTile` and the labelled fields. */
|
|
92
|
+
hint?: string;
|
|
93
|
+
/** This detail is a figure (see {@link DescriptionListProps.numeric}); overrides
|
|
94
|
+
* the list's setting either way. */
|
|
95
|
+
numeric?: boolean;
|
|
96
|
+
/** Classes for the `<dd>`. */
|
|
97
|
+
detailClassName?: string;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const ROW_PAD: Record<DescriptionListDensity, string> = { comfortable: "py-2.5", compact: "py-1.5" };
|
|
101
|
+
const CARD_PAD: Record<DescriptionListDensity, string> = { comfortable: "px-3 py-2.5", compact: "px-2.5 py-1.5" };
|
|
102
|
+
|
|
103
|
+
/** One term and its detail inside a {@link DescriptionList}. */
|
|
104
|
+
export function DescriptionItem({
|
|
105
|
+
term,
|
|
106
|
+
children,
|
|
107
|
+
hint,
|
|
108
|
+
numeric: numericProp,
|
|
109
|
+
detailClassName,
|
|
110
|
+
className,
|
|
111
|
+
...rest
|
|
112
|
+
}: DescriptionItemProps) {
|
|
113
|
+
const { layout, numeric: listNumeric, density } = useContext(ListContext);
|
|
114
|
+
const numeric = numericProp ?? listNumeric;
|
|
115
|
+
const cards = layout === "cards";
|
|
116
|
+
|
|
117
|
+
const termNode = (
|
|
118
|
+
<dt
|
|
119
|
+
className={cn(
|
|
120
|
+
"flex min-w-0 items-center gap-1 text-[var(--text-muted)]",
|
|
121
|
+
cards && "text-xs uppercase tracking-wide",
|
|
122
|
+
)}
|
|
123
|
+
>
|
|
124
|
+
<span className="min-w-0 break-words">{term}</span>
|
|
125
|
+
{hint && <FieldHint label={hint} side="top" className="shrink-0" />}
|
|
126
|
+
</dt>
|
|
127
|
+
);
|
|
128
|
+
const detail = (
|
|
129
|
+
<dd
|
|
130
|
+
className={cn(
|
|
131
|
+
"min-w-0 break-words text-[var(--text-primary)]",
|
|
132
|
+
numeric && "tabular-nums text-end",
|
|
133
|
+
cards && "mt-0.5 font-medium",
|
|
134
|
+
detailClassName,
|
|
135
|
+
)}
|
|
136
|
+
>
|
|
137
|
+
{children}
|
|
138
|
+
</dd>
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
if (cards) {
|
|
142
|
+
return (
|
|
143
|
+
<div
|
|
144
|
+
{...rest}
|
|
145
|
+
className={cn(
|
|
146
|
+
"min-w-0 rounded-md border border-[var(--border)] bg-[var(--bg-surface)]",
|
|
147
|
+
CARD_PAD[density],
|
|
148
|
+
className,
|
|
149
|
+
)}
|
|
150
|
+
>
|
|
151
|
+
{termNode}
|
|
152
|
+
{detail}
|
|
153
|
+
</div>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
<div
|
|
159
|
+
{...rest}
|
|
160
|
+
className={cn(
|
|
161
|
+
"grid gap-x-4 gap-y-0.5",
|
|
162
|
+
numeric
|
|
163
|
+
? // Never stacks: a figure is short (see DescriptionListProps.numeric).
|
|
164
|
+
"grid-cols-[minmax(0,1fr)_auto] items-baseline"
|
|
165
|
+
: "grid-cols-1 @sm:grid-cols-[minmax(0,1fr)_minmax(0,2fr)] @sm:items-baseline",
|
|
166
|
+
ROW_PAD[density],
|
|
167
|
+
className,
|
|
168
|
+
)}
|
|
169
|
+
>
|
|
170
|
+
{termNode}
|
|
171
|
+
{detail}
|
|
172
|
+
</div>
|
|
173
|
+
);
|
|
174
|
+
}
|
|
@@ -104,6 +104,17 @@ export function Collapse({ open, keepMounted = false, children, className, style
|
|
|
104
104
|
);
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
/**
|
|
108
|
+
* What {@link DisclosureProps.triggerProps} may put on the header button: its own
|
|
109
|
+
* attributes and handlers, and any `data-*`. Not the ones the disclosure owns —
|
|
110
|
+
* `aria-expanded`, `aria-controls`, `disabled`, `onClick`, `type` and the content — nor
|
|
111
|
+
* `className`, which is {@link DisclosureProps.headerClassName}.
|
|
112
|
+
*/
|
|
113
|
+
export type DisclosureTriggerProps = Omit<
|
|
114
|
+
ComponentPropsWithoutRef<"button">,
|
|
115
|
+
"type" | "aria-expanded" | "aria-controls" | "disabled" | "onClick" | "children" | "className"
|
|
116
|
+
> & { [data: `data-${string}`]: string | number | boolean | undefined };
|
|
117
|
+
|
|
107
118
|
/**
|
|
108
119
|
* `title` is omitted from the div's own props because this component already owns the
|
|
109
120
|
* name: here it is the header's content (and a ReactNode), not the browser's tooltip.
|
|
@@ -135,6 +146,16 @@ export interface DisclosureProps extends Omit<ComponentPropsWithoutRef<"div">, "
|
|
|
135
146
|
* its own surface.
|
|
136
147
|
*/
|
|
137
148
|
variant?: "card" | "bare";
|
|
149
|
+
/**
|
|
150
|
+
* Where a `bare` disclosure draws its chevron. `start` (default) is the leading
|
|
151
|
+
* chevron that turns from the reading direction to down. `end` puts it at the far
|
|
152
|
+
* end of the row, pointing down and turning up as the card's does — keksdose's
|
|
153
|
+
* account menu, whose language sub-list reads "Language 🇩🇪 ⌄" with the chevron
|
|
154
|
+
* after the flag, like every other menu row with a sub-list. With `trailing`, the
|
|
155
|
+
* chevron follows it, as on a card. A `card` always has it at the end, so this is
|
|
156
|
+
* ignored there.
|
|
157
|
+
*/
|
|
158
|
+
chevronPosition?: "start" | "end";
|
|
138
159
|
/**
|
|
139
160
|
* Wrap the header button in a heading of this level. The WAI-ARIA disclosure pattern
|
|
140
161
|
* puts the button INSIDE the heading when the disclosure titles a section, so the
|
|
@@ -147,6 +168,22 @@ export interface DisclosureProps extends Omit<ComponentPropsWithoutRef<"div">, "
|
|
|
147
168
|
disabled?: boolean;
|
|
148
169
|
/** Extra classes for the header button. */
|
|
149
170
|
headerClassName?: string;
|
|
171
|
+
/**
|
|
172
|
+
* Attributes for the header BUTTON — the outer props go to the wrapping div — such as
|
|
173
|
+
* an `aria-label`, an `id` or a `data-*` test hook. A function of the open state
|
|
174
|
+
* when they depend on it: keksdose's budget table names each group's toggle
|
|
175
|
+
* "Expand group Food" / "Collapse group Food", because the visible title is only the
|
|
176
|
+
* group's name:
|
|
177
|
+
*
|
|
178
|
+
* ```tsx
|
|
179
|
+
* triggerProps={(open) => ({ "aria-label": t(open ? "collapseGroup" : "expandGroup", { name }) })}
|
|
180
|
+
* ```
|
|
181
|
+
*
|
|
182
|
+
* An `aria-label` REPLACES the title as the button's name, so it must still contain
|
|
183
|
+
* the title's words (WCAG 2.5.3, label in name). See {@link DisclosureTriggerProps}
|
|
184
|
+
* for what the disclosure keeps for itself.
|
|
185
|
+
*/
|
|
186
|
+
triggerProps?: DisclosureTriggerProps | ((open: boolean) => DisclosureTriggerProps);
|
|
150
187
|
/** Extra classes for the body's wrapper — where its padding and spacing live. */
|
|
151
188
|
bodyClassName?: string;
|
|
152
189
|
/**
|
|
@@ -193,10 +230,12 @@ export function Disclosure({
|
|
|
193
230
|
defaultOpen = false,
|
|
194
231
|
onOpenChange,
|
|
195
232
|
variant = "card",
|
|
233
|
+
chevronPosition = "start",
|
|
196
234
|
headingAs: Heading,
|
|
197
235
|
keepMounted,
|
|
198
236
|
disabled,
|
|
199
237
|
headerClassName,
|
|
238
|
+
triggerProps,
|
|
200
239
|
bodyClassName,
|
|
201
240
|
trailing,
|
|
202
241
|
controls,
|
|
@@ -208,6 +247,8 @@ export function Disclosure({
|
|
|
208
247
|
const open = controlled ?? own;
|
|
209
248
|
const bodyId = useId();
|
|
210
249
|
const card = variant === "card";
|
|
250
|
+
// The card's chevron trails always; a bare one only when asked to.
|
|
251
|
+
const chevronAtEnd = card || chevronPosition === "end";
|
|
211
252
|
const triggerOnly = controls !== undefined;
|
|
212
253
|
// The card's header squares its lower corners only when a body opens under it.
|
|
213
254
|
const joined = open && !triggerOnly;
|
|
@@ -219,8 +260,11 @@ export function Disclosure({
|
|
|
219
260
|
onOpenChange?.(next);
|
|
220
261
|
};
|
|
221
262
|
|
|
263
|
+
const extraTriggerProps = typeof triggerProps === "function" ? triggerProps(open) : triggerProps;
|
|
264
|
+
|
|
222
265
|
const button = (
|
|
223
266
|
<button
|
|
267
|
+
{...extraTriggerProps}
|
|
224
268
|
type="button"
|
|
225
269
|
aria-expanded={open}
|
|
226
270
|
aria-controls={triggerOnly ? controls : bodyId}
|
|
@@ -249,7 +293,7 @@ export function Disclosure({
|
|
|
249
293
|
headerClassName,
|
|
250
294
|
)}
|
|
251
295
|
>
|
|
252
|
-
{!
|
|
296
|
+
{!chevronAtEnd && <Chevron open={open} leading />}
|
|
253
297
|
{/* `flex-1`: the title takes the row, so whatever a caller puts at its end sits
|
|
254
298
|
at the header's far edge. */}
|
|
255
299
|
<span className="min-w-0 flex-1">
|
|
@@ -258,7 +302,7 @@ export function Disclosure({
|
|
|
258
302
|
<span className="mt-0.5 block text-xs font-normal text-[var(--text-muted)]">{hint}</span>
|
|
259
303
|
)}
|
|
260
304
|
</span>
|
|
261
|
-
{
|
|
305
|
+
{chevronAtEnd && !hasTrailing && <Chevron open={open} small={!card} />}
|
|
262
306
|
</button>
|
|
263
307
|
);
|
|
264
308
|
|
|
@@ -286,7 +330,7 @@ export function Disclosure({
|
|
|
286
330
|
{/* Positioned, so it paints over the stretched button and takes its own
|
|
287
331
|
clicks; the chevron is not, so a click on it lands on the button. */}
|
|
288
332
|
<div className="relative flex shrink-0 items-center gap-2">{trailing}</div>
|
|
289
|
-
{
|
|
333
|
+
{chevronAtEnd && <Chevron open={open} small={!card} />}
|
|
290
334
|
</div>
|
|
291
335
|
) : (
|
|
292
336
|
header
|
|
@@ -307,13 +351,22 @@ export function Disclosure({
|
|
|
307
351
|
* header's convention. Rotation, not a mirrored glyph, so RTL needs one opposite angle
|
|
308
352
|
* and no `scale` composing with the turn.
|
|
309
353
|
*/
|
|
310
|
-
function Chevron({
|
|
354
|
+
function Chevron({
|
|
355
|
+
open,
|
|
356
|
+
leading = false,
|
|
357
|
+
small = leading,
|
|
358
|
+
}: {
|
|
359
|
+
open: boolean;
|
|
360
|
+
leading?: boolean;
|
|
361
|
+
/** The bare variant's size, which a bare chevron keeps at either end of its row. */
|
|
362
|
+
small?: boolean;
|
|
363
|
+
}) {
|
|
311
364
|
return (
|
|
312
365
|
<ChevronDown
|
|
313
366
|
aria-hidden
|
|
314
367
|
className={cn(
|
|
315
368
|
"shrink-0 text-[var(--text-muted)] transition-transform duration-200 ease-out motion-reduce:transition-none",
|
|
316
|
-
|
|
369
|
+
small ? "size-3.5" : "size-4",
|
|
317
370
|
leading ? !open && "-rotate-90 rtl:rotate-90" : open && "rotate-180",
|
|
318
371
|
)}
|
|
319
372
|
/>
|