@stll/ui 0.44.0 → 0.45.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/dist/components/accordion.d.ts +6 -2
- package/dist/components/accordion.js +4 -4
- package/dist/components/button.d.ts +1 -1
- package/dist/components/court-badge.d.ts +7 -1
- package/dist/components/court-badge.js +13 -3
- package/dist/components/date-picker-popover.d.ts +12 -2
- package/dist/components/date-picker-popover.js +105 -9
- package/dist/components/date-range-filter.d.ts +14 -0
- package/dist/components/date-range-filter.js +74 -0
- package/dist/components/document-identity-badge.d.ts +12 -0
- package/dist/components/document-identity-badge.js +94 -0
- package/dist/components/document-identity-badge.logic.d.ts +25 -0
- package/dist/components/document-identity-badge.logic.js +25 -0
- package/dist/components/landing.d.ts +1 -1
- package/dist/components/landing.js +1 -1
- package/dist/components/matter-icon.d.ts +16 -0
- package/dist/components/matter-icon.js +56 -0
- package/dist/components/menu.d.ts +9 -1
- package/dist/components/menu.js +6 -6
- package/dist/components/outline-rail.d.ts +8 -1
- package/dist/components/outline-rail.js +95 -48
- package/dist/components/search-field.js +30 -34
- package/dist/components/sheet.js +3 -2
- package/dist/components/switch.d.ts +5 -0
- package/dist/components/switch.js +16 -0
- package/dist/components/toast.js +39 -23
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/inspector/entity-tab.js +1 -2
- package/dist/lib/matter-colors.d.ts +38 -0
- package/dist/lib/matter-colors.js +65 -0
- package/dist/lib/panel-inset.d.ts +11 -0
- package/dist/lib/panel-inset.js +33 -0
- package/dist/review/review-word-diff.d.ts +13 -0
- package/dist/review/review-word-diff.js +78 -0
- package/dist/styles/theme.css +9 -0
- package/package.json +37 -5
|
@@ -2,7 +2,11 @@ import { Accordion as Accordion$1 } from "@base-ui/react/accordion";
|
|
|
2
2
|
//#region src/components/accordion.d.ts
|
|
3
3
|
declare const Accordion: <Value = unknown>(props: Accordion$1.Root.Props<Value>) => import("react").JSX.Element;
|
|
4
4
|
declare const AccordionItem: ({ className, ...props }: Accordion$1.Item.Props) => import("react").JSX.Element;
|
|
5
|
-
declare const AccordionTrigger: ({ className, children, ...props }: Accordion$1.Trigger.Props
|
|
6
|
-
|
|
5
|
+
declare const AccordionTrigger: ({ className, children, size, ...props }: Accordion$1.Trigger.Props & {
|
|
6
|
+
size?: "default" | "compact";
|
|
7
|
+
}) => import("react").JSX.Element;
|
|
8
|
+
declare const AccordionPanel: ({ className, children, size, ...props }: Accordion$1.Panel.Props & {
|
|
9
|
+
size?: "default" | "compact";
|
|
10
|
+
}) => import("react").JSX.Element;
|
|
7
11
|
//#endregion
|
|
8
12
|
export { Accordion, AccordionPanel as AccordionContent, AccordionPanel, AccordionItem, AccordionTrigger };
|
|
@@ -13,10 +13,10 @@ const AccordionItem = ({ className, ...props }) => /* @__PURE__ */ jsx(Accordion
|
|
|
13
13
|
"data-slot": "accordion-item",
|
|
14
14
|
...props
|
|
15
15
|
});
|
|
16
|
-
const AccordionTrigger = ({ className, children, ...props }) => /* @__PURE__ */ jsx(Accordion$1.Header, {
|
|
16
|
+
const AccordionTrigger = ({ className, children, size = "default", ...props }) => /* @__PURE__ */ jsx(Accordion$1.Header, {
|
|
17
17
|
className: "flex",
|
|
18
18
|
children: /* @__PURE__ */ jsxs(Accordion$1.Trigger, {
|
|
19
|
-
className: cn("focus-visible:ring-ring flex flex-1 cursor-pointer items-start justify-between gap-4 rounded-md
|
|
19
|
+
className: cn("focus-visible:ring-ring flex flex-1 cursor-pointer items-start justify-between gap-4 rounded-md text-start font-medium outline-none focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-64 data-panel-open:*:data-[slot=accordion-indicator]:rotate-180", size === "compact" ? "py-2 text-xs" : "py-4 text-sm", className),
|
|
20
20
|
"data-slot": "accordion-trigger",
|
|
21
21
|
...props,
|
|
22
22
|
children: [children, /* @__PURE__ */ jsx(ChevronDownIcon, {
|
|
@@ -25,12 +25,12 @@ const AccordionTrigger = ({ className, children, ...props }) => /* @__PURE__ */
|
|
|
25
25
|
})]
|
|
26
26
|
})
|
|
27
27
|
});
|
|
28
|
-
const AccordionPanel = ({ className, children, ...props }) => /* @__PURE__ */ jsx(Accordion$1.Panel, {
|
|
28
|
+
const AccordionPanel = ({ className, children, size = "default", ...props }) => /* @__PURE__ */ jsx(Accordion$1.Panel, {
|
|
29
29
|
className: "text-muted-foreground h-(--accordion-panel-height) overflow-hidden text-sm transition-[height] duration-200 ease-in-out data-ending-style:h-0 data-starting-style:h-0",
|
|
30
30
|
"data-slot": "accordion-panel",
|
|
31
31
|
...props,
|
|
32
32
|
children: /* @__PURE__ */ jsx("div", {
|
|
33
|
-
className: cn("pt-0 pb-4", className),
|
|
33
|
+
className: cn("pt-0", size === "compact" ? "pb-3" : "pb-4", className),
|
|
34
34
|
children
|
|
35
35
|
})
|
|
36
36
|
});
|
|
@@ -13,7 +13,7 @@ import { VariantProps } from "class-variance-authority";
|
|
|
13
13
|
*/
|
|
14
14
|
declare const buttonAccessibleDisabledClass = "cursor-not-allowed opacity-64";
|
|
15
15
|
declare const buttonVariants: (props?: ({
|
|
16
|
-
size?: "default" | "
|
|
16
|
+
size?: "default" | "chip" | "icon" | "icon-lg" | "icon-sm" | "icon-xl" | "icon-xs" | "lg" | "row" | "sm" | "xl" | "xs" | null | undefined;
|
|
17
17
|
variant?: "link" | "default" | "destructive" | "destructive-outline" | "destructive-ghost" | "ghost" | "muted" | "outline" | "secondary" | null | undefined;
|
|
18
18
|
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
19
19
|
type ButtonProps = {
|
|
@@ -9,10 +9,16 @@
|
|
|
9
9
|
* carries meaning on its own anyway: the court's name stands beside it.
|
|
10
10
|
*/
|
|
11
11
|
export type CourtBadgeWeight = "solid" | "tinted" | "outline" | "dashed";
|
|
12
|
+
/**
|
|
13
|
+
* `default` stands on its own beside a court's name; `inline` sits inside a
|
|
14
|
+
* line of small text (a list row's meta line) without making it taller.
|
|
15
|
+
*/
|
|
16
|
+
export type CourtBadgeSize = "default" | "inline";
|
|
12
17
|
type CourtBadgeProps = {
|
|
13
18
|
/** The court's own abbreviation, in the language its name is in. */
|
|
14
19
|
abbreviation: string;
|
|
15
20
|
weight: CourtBadgeWeight;
|
|
21
|
+
size?: CourtBadgeSize | undefined;
|
|
16
22
|
className?: string;
|
|
17
23
|
};
|
|
18
24
|
/**
|
|
@@ -23,5 +29,5 @@ type CourtBadgeProps = {
|
|
|
23
29
|
* abbreviation is actually known. There is no placeholder: a court nobody
|
|
24
30
|
* abbreviates gets no chip.
|
|
25
31
|
*/
|
|
26
|
-
export declare const CourtBadge: ({ abbreviation, className, weight }: CourtBadgeProps) => import("react").JSX.Element;
|
|
32
|
+
export declare const CourtBadge: ({ abbreviation, className, size, weight }: CourtBadgeProps) => import("react").JSX.Element;
|
|
27
33
|
//#endregion
|
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
import { cn } from "../lib/utils.js";
|
|
2
2
|
import { jsx } from "react/jsx-runtime";
|
|
3
3
|
//#region src/components/court-badge.tsx
|
|
4
|
-
const COURT_BADGE_BASE_CLASS = "inline-flex shrink-0 items-center rounded-sm border
|
|
4
|
+
const COURT_BADGE_BASE_CLASS = "inline-flex shrink-0 items-center rounded-sm border font-mono font-semibold tracking-wide whitespace-nowrap";
|
|
5
|
+
/**
|
|
6
|
+
* The line box runs a third taller than the type, because these capitals
|
|
7
|
+
* carry diacritics (Ú, Ř, Š) whose ink rises above cap height; a tighter box
|
|
8
|
+
* sets the acute against the pill's border. `inline` keeps that ratio at a
|
|
9
|
+
* smaller size and drops the padding, so the chip fits a 16px text line.
|
|
10
|
+
*/
|
|
11
|
+
const SIZE_CLASS = {
|
|
12
|
+
default: "px-1 py-0.5 text-xs leading-4",
|
|
13
|
+
inline: "px-0.5 text-3xs leading-3.5"
|
|
14
|
+
};
|
|
5
15
|
/**
|
|
6
16
|
* Every weight keeps a border, because `--muted` is a 4% tint: a borderless
|
|
7
17
|
* tinted chip on a sidebar or a muted row is a pill whose edge nobody can
|
|
@@ -31,8 +41,8 @@ const WEIGHT_CLASS = {
|
|
|
31
41
|
* abbreviation is actually known. There is no placeholder: a court nobody
|
|
32
42
|
* abbreviates gets no chip.
|
|
33
43
|
*/
|
|
34
|
-
const CourtBadge = ({ abbreviation, className, weight }) => /* @__PURE__ */ jsx("span", {
|
|
35
|
-
className: cn(COURT_BADGE_BASE_CLASS, WEIGHT_CLASS[weight], className),
|
|
44
|
+
const CourtBadge = ({ abbreviation, className, size = "default", weight }) => /* @__PURE__ */ jsx("span", {
|
|
45
|
+
className: cn(COURT_BADGE_BASE_CLASS, SIZE_CLASS[size], WEIGHT_CLASS[weight], className),
|
|
36
46
|
"data-slot": "court-badge",
|
|
37
47
|
dir: "ltr",
|
|
38
48
|
children: abbreviation
|
|
@@ -41,6 +41,16 @@ type DatePickerPopoverProps = {
|
|
|
41
41
|
placeholderLabel?: string;
|
|
42
42
|
clearLabel?: string;
|
|
43
43
|
defaultOpen?: boolean;
|
|
44
|
+
open?: boolean;
|
|
45
|
+
triggerRef?: React.Ref<HTMLButtonElement> | undefined;
|
|
46
|
+
/** Keep the trigger focused when opening a following range bound. */
|
|
47
|
+
focusTriggerOnOpen?: boolean;
|
|
48
|
+
/** Enables direct date entry, with host-localized validation. */
|
|
49
|
+
dateInputLabel?: string;
|
|
50
|
+
outOfRangeLabel?: string;
|
|
51
|
+
noFuture?: boolean;
|
|
52
|
+
/** Host-owned calendar date; defaults to the browser timezone. */
|
|
53
|
+
getToday?: () => string;
|
|
44
54
|
onOpenChange?: (open: boolean) => void;
|
|
45
55
|
/** Label for the "go to today" button. Auto-localized from the locale when omitted. */
|
|
46
56
|
todayLabel?: string;
|
|
@@ -57,8 +67,8 @@ type DatePickerPopoverProps = {
|
|
|
57
67
|
/** Label of the time input in `date-time` mode. */
|
|
58
68
|
timeLabel?: string;
|
|
59
69
|
/** Inclusive `YYYY-MM-DD` bounds; in `date-time` mode they bound the day. */
|
|
60
|
-
minDate?: string;
|
|
61
|
-
maxDate?: string;
|
|
70
|
+
minDate?: string | undefined;
|
|
71
|
+
maxDate?: string | undefined;
|
|
62
72
|
isDateDisabled?: (date: string) => boolean;
|
|
63
73
|
layer?: OverlayLayer;
|
|
64
74
|
};
|
|
@@ -3,6 +3,7 @@ import { CalendarIcon, ChevronLeftIcon, ChevronRightIcon } from "../icons.js";
|
|
|
3
3
|
import { cn } from "../lib/utils.js";
|
|
4
4
|
import { Button } from "./button.js";
|
|
5
5
|
import { DirectionalIcon } from "./directional-icon.js";
|
|
6
|
+
import { Input } from "./input.js";
|
|
6
7
|
import { FIELD_TRIGGER_CLASS_NAME } from "../lib/field-trigger.js";
|
|
7
8
|
import { getLocaleWeekInfo, getWeekendDays } from "../lib/week.js";
|
|
8
9
|
import { DATE_PICKER_MODE, formatDateTimeValue, getHourOptions, getMinuteOptions, getTimeFieldNames, joinPickerTime, localDateFromTimestamp, millisecondsUntilNextLocalDate, parseDateTimeValue, resolveCalendarViewMonth, shiftCalendarDate, splitPickerTime } from "./date-picker-popover.logic.js";
|
|
@@ -10,7 +11,7 @@ import { Popover, PopoverContent as PopoverPopup, PopoverTrigger } from "./popov
|
|
|
10
11
|
import { Select, SelectContent as SelectPopup, SelectItem, SelectTrigger, SelectValue } from "./select.js";
|
|
11
12
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
12
13
|
import { useCallback, useId, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
13
|
-
import { panic } from "better-result";
|
|
14
|
+
import { Result, panic } from "better-result";
|
|
14
15
|
import { Temporal } from "temporal-polyfill/full";
|
|
15
16
|
//#region src/components/date-picker-popover.tsx
|
|
16
17
|
const toISODate = (date) => date.toString();
|
|
@@ -57,7 +58,7 @@ const subscribeToLocalDate = (onStoreChange) => {
|
|
|
57
58
|
if (localDateListeners.size === 0) stopLocalDateSubscription();
|
|
58
59
|
};
|
|
59
60
|
};
|
|
60
|
-
const useHydrationSafeToday = () => useSyncExternalStore(subscribeToLocalDate,
|
|
61
|
+
const useHydrationSafeToday = (getToday) => useSyncExternalStore(subscribeToLocalDate, getToday, () => HYDRATION_DATE);
|
|
61
62
|
const useHydrationSafeBrowserLocale = () => useSyncExternalStore(noopSubscribe, () => navigator.language || HYDRATION_LOCALE, () => HYDRATION_LOCALE);
|
|
62
63
|
const getFirstDayOfWeek = (locale) => {
|
|
63
64
|
const info = getLocaleWeekInfo(locale);
|
|
@@ -252,7 +253,7 @@ const PICKER_SIZE_CLASS_NAMES = {
|
|
|
252
253
|
}
|
|
253
254
|
};
|
|
254
255
|
const DatePickerPopoverContent = (props) => {
|
|
255
|
-
const { id, labelledBy, className, disabled, hideClear, size = "compact", variant = "inline", mode = DATE_PICKER_MODE.date, value: rawValue, onChange, locale, isOverdue = false, showIcon = true, placeholderLabel, clearLabel = "Clear date", defaultOpen = false, onOpenChange, todayLabel: todayLabelProp, overdueLabel, minDate, maxDate, isDateDisabled, layer = "default", today } = props;
|
|
256
|
+
const { id, labelledBy, className, disabled, hideClear, size = "compact", variant = "inline", mode = DATE_PICKER_MODE.date, value: rawValue, onChange, locale, isOverdue = false, showIcon = true, placeholderLabel, clearLabel = "Clear date", defaultOpen = false, open: controlledOpen, triggerRef, focusTriggerOnOpen = false, dateInputLabel, outOfRangeLabel, onOpenChange, todayLabel: todayLabelProp, overdueLabel, minDate, maxDate, isDateDisabled, layer = "default", today } = props;
|
|
256
257
|
const selection = resolveSelection(mode, rawValue);
|
|
257
258
|
const value = selection.date;
|
|
258
259
|
const labels = resolvePopupLabels(props);
|
|
@@ -294,14 +295,19 @@ const DatePickerPopoverContent = (props) => {
|
|
|
294
295
|
maxDate,
|
|
295
296
|
isDateDisabled
|
|
296
297
|
]);
|
|
298
|
+
const dateDraft = useDatePickerDraft({
|
|
299
|
+
value,
|
|
300
|
+
isDayDisabled,
|
|
301
|
+
onChange
|
|
302
|
+
});
|
|
297
303
|
const displayLabel = formatSelection(locale, selection) ?? placeholderLabel ?? "—";
|
|
298
304
|
const selectDay = (date) => {
|
|
299
305
|
switch (selection.mode) {
|
|
300
306
|
case DATE_PICKER_MODE.date:
|
|
301
|
-
|
|
307
|
+
dateDraft.apply(date);
|
|
302
308
|
return;
|
|
303
309
|
case DATE_PICKER_MODE.dateTime:
|
|
304
|
-
|
|
310
|
+
dateDraft.apply(formatDateTimeValue({
|
|
305
311
|
date,
|
|
306
312
|
time: selection.time ?? "00:00"
|
|
307
313
|
}));
|
|
@@ -311,7 +317,7 @@ const DatePickerPopoverContent = (props) => {
|
|
|
311
317
|
};
|
|
312
318
|
const handleTimeChange = (clock) => {
|
|
313
319
|
if (value === "") return;
|
|
314
|
-
|
|
320
|
+
dateDraft.apply(formatDateTimeValue({
|
|
315
321
|
date: value,
|
|
316
322
|
time: joinPickerTime(clock)
|
|
317
323
|
}));
|
|
@@ -443,6 +449,7 @@ const DatePickerPopoverContent = (props) => {
|
|
|
443
449
|
})();
|
|
444
450
|
return /* @__PURE__ */ jsxs(Popover, {
|
|
445
451
|
defaultOpen,
|
|
452
|
+
open: controlledOpen,
|
|
446
453
|
onOpenChange: handleOpenChange,
|
|
447
454
|
children: [/* @__PURE__ */ jsxs(PopoverTrigger, {
|
|
448
455
|
disabled,
|
|
@@ -451,6 +458,7 @@ const DatePickerPopoverContent = (props) => {
|
|
|
451
458
|
"aria-labelledby": labelledBy ? `${labelledBy} ${displayValueId}` : void 0,
|
|
452
459
|
className: cn(TRIGGER_VARIANT_CLASS_NAMES[variant], triggerToneClassName, className),
|
|
453
460
|
id,
|
|
461
|
+
ref: triggerRef,
|
|
454
462
|
type: "button"
|
|
455
463
|
}),
|
|
456
464
|
children: [
|
|
@@ -466,7 +474,8 @@ const DatePickerPopoverContent = (props) => {
|
|
|
466
474
|
})
|
|
467
475
|
]
|
|
468
476
|
}), /* @__PURE__ */ jsx(PopoverPopup, {
|
|
469
|
-
|
|
477
|
+
onBlur: dateDraft.handleBlur,
|
|
478
|
+
initialFocus: focusTriggerOnOpen ? false : () => gridRef.current?.querySelector(`[data-date="${value || today}"]`) ?? gridRef.current,
|
|
470
479
|
className: "data-starting-style:scale-100",
|
|
471
480
|
layer,
|
|
472
481
|
padding: "sm",
|
|
@@ -516,6 +525,7 @@ const DatePickerPopoverContent = (props) => {
|
|
|
516
525
|
onClick: () => {
|
|
517
526
|
if (!isUnavailable) selectDay(day.date);
|
|
518
527
|
},
|
|
528
|
+
onFocus: () => setFocusedDate(day.date),
|
|
519
529
|
role: "gridcell",
|
|
520
530
|
tabIndex: isFocused ? 0 : -1,
|
|
521
531
|
type: "button",
|
|
@@ -546,11 +556,21 @@ const DatePickerPopoverContent = (props) => {
|
|
|
546
556
|
onChange: handleTimeChange,
|
|
547
557
|
time: selection.time
|
|
548
558
|
}),
|
|
559
|
+
/* @__PURE__ */ jsx(TypedDateField, {
|
|
560
|
+
mode: selection.mode,
|
|
561
|
+
label: dateInputLabel,
|
|
562
|
+
value: dateDraft.value,
|
|
563
|
+
invalid: dateDraft.invalid,
|
|
564
|
+
errorLabel: outOfRangeLabel,
|
|
565
|
+
onDraftChange: dateDraft.edit,
|
|
566
|
+
onCommit: dateDraft.submit
|
|
567
|
+
}),
|
|
549
568
|
/* @__PURE__ */ jsxs("div", {
|
|
550
569
|
className: "mt-1 flex items-center gap-1 border-t pt-1",
|
|
551
570
|
children: [/* @__PURE__ */ jsx(Button, {
|
|
552
571
|
className: "flex-1",
|
|
553
572
|
onClick: () => {
|
|
573
|
+
dateDraft.discard();
|
|
554
574
|
const todayDate = Temporal.PlainDate.from(today);
|
|
555
575
|
setViewMonthOverride({
|
|
556
576
|
month: todayDate.month - 1,
|
|
@@ -563,7 +583,7 @@ const DatePickerPopoverContent = (props) => {
|
|
|
563
583
|
children: todayLabel
|
|
564
584
|
}), value && !hideClear && /* @__PURE__ */ jsx(Button, {
|
|
565
585
|
className: "flex-1",
|
|
566
|
-
onClick: () =>
|
|
586
|
+
onClick: () => dateDraft.apply(null),
|
|
567
587
|
size: "xs",
|
|
568
588
|
variant: "ghost",
|
|
569
589
|
children: clearLabel
|
|
@@ -576,10 +596,12 @@ const DatePickerPopoverContent = (props) => {
|
|
|
576
596
|
};
|
|
577
597
|
const DatePickerPopover = (props) => {
|
|
578
598
|
const browserLocale = useHydrationSafeBrowserLocale();
|
|
579
|
-
const today = useHydrationSafeToday();
|
|
599
|
+
const today = useHydrationSafeToday(props.getToday ?? getLocalToday);
|
|
580
600
|
const locale = props.locale ?? browserLocale;
|
|
601
|
+
const maxDate = props.noFuture && (!props.maxDate || props.maxDate > today) ? today : props.maxDate;
|
|
581
602
|
return /* @__PURE__ */ jsx(DatePickerPopoverContent, {
|
|
582
603
|
...props,
|
|
604
|
+
maxDate,
|
|
583
605
|
locale,
|
|
584
606
|
today
|
|
585
607
|
});
|
|
@@ -738,5 +760,79 @@ const YearGrid = ({ decadeBase, currentYear, cellClassName, onSelect, today }) =
|
|
|
738
760
|
}, row[0]))
|
|
739
761
|
});
|
|
740
762
|
};
|
|
763
|
+
const useDatePickerDraft = ({ value, isDayDisabled, onChange }) => {
|
|
764
|
+
const [draft, setDraft] = useState(null);
|
|
765
|
+
const commitStatus = useRef("consumed");
|
|
766
|
+
const hasDraft = draft?.baseValue === value;
|
|
767
|
+
const inputValue = hasDraft ? draft.value : value;
|
|
768
|
+
const parsed = Result.try(() => Temporal.PlainDate.from(inputValue));
|
|
769
|
+
const invalid = hasDraft && inputValue !== "" && (parsed.isErr() || parsed.value.toString() !== inputValue || isDayDisabled(inputValue));
|
|
770
|
+
const discard = () => {
|
|
771
|
+
commitStatus.current = "consumed";
|
|
772
|
+
setDraft(null);
|
|
773
|
+
};
|
|
774
|
+
const apply = (next) => {
|
|
775
|
+
discard();
|
|
776
|
+
onChange(next);
|
|
777
|
+
};
|
|
778
|
+
const commit = () => {
|
|
779
|
+
if (commitStatus.current !== "pending" || invalid || inputValue === value) return;
|
|
780
|
+
apply(inputValue || null);
|
|
781
|
+
};
|
|
782
|
+
return {
|
|
783
|
+
value: inputValue,
|
|
784
|
+
invalid,
|
|
785
|
+
edit: (next) => {
|
|
786
|
+
commitStatus.current = "pending";
|
|
787
|
+
setDraft({
|
|
788
|
+
baseValue: value,
|
|
789
|
+
value: next
|
|
790
|
+
});
|
|
791
|
+
},
|
|
792
|
+
discard,
|
|
793
|
+
apply,
|
|
794
|
+
submit: () => {
|
|
795
|
+
if (!invalid) apply(inputValue || null);
|
|
796
|
+
},
|
|
797
|
+
handleBlur: (event) => {
|
|
798
|
+
if (event.currentTarget.contains(event.relatedTarget)) return;
|
|
799
|
+
commit();
|
|
800
|
+
}
|
|
801
|
+
};
|
|
802
|
+
};
|
|
803
|
+
const TypedDateField = ({ mode, label, value, invalid, errorLabel = "Choose a date within the allowed range.", onDraftChange, onCommit }) => {
|
|
804
|
+
const id = useId();
|
|
805
|
+
if (!label || mode !== DATE_PICKER_MODE.date) return null;
|
|
806
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
807
|
+
className: "mt-2 flex flex-col gap-1",
|
|
808
|
+
children: [
|
|
809
|
+
/* @__PURE__ */ jsx("label", {
|
|
810
|
+
htmlFor: id,
|
|
811
|
+
className: "text-xs",
|
|
812
|
+
children: label
|
|
813
|
+
}),
|
|
814
|
+
/* @__PURE__ */ jsx(Input, {
|
|
815
|
+
id,
|
|
816
|
+
value,
|
|
817
|
+
placeholder: "YYYY-MM-DD",
|
|
818
|
+
dir: "ltr",
|
|
819
|
+
"aria-invalid": invalid,
|
|
820
|
+
"aria-describedby": invalid ? `${id}-error` : void 0,
|
|
821
|
+
onChange: (event) => onDraftChange(event.currentTarget.value),
|
|
822
|
+
onKeyDown: (event) => {
|
|
823
|
+
if (event.key !== "Enter" || event.nativeEvent.isComposing) return;
|
|
824
|
+
event.preventDefault();
|
|
825
|
+
onCommit();
|
|
826
|
+
}
|
|
827
|
+
}),
|
|
828
|
+
invalid && /* @__PURE__ */ jsx("p", {
|
|
829
|
+
id: `${id}-error`,
|
|
830
|
+
role: "alert",
|
|
831
|
+
className: "text-destructive text-xs",
|
|
832
|
+
children: errorLabel
|
|
833
|
+
})
|
|
834
|
+
]
|
|
835
|
+
});
|
|
836
|
+
};
|
|
741
837
|
//#endregion
|
|
742
838
|
export { DatePickerPopover };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { DatePickerPopoverProps } from "./date-picker-popover.js";
|
|
2
|
+
//#region src/components/date-range-filter.d.ts
|
|
3
|
+
type DateRangeFilterProps = Omit<DatePickerPopoverProps, "value" | "onChange" | "minDate" | "open" | "defaultOpen" | "onOpenChange" | "triggerRef" | "focusTriggerOnOpen" | "id" | "labelledBy" | "mode"> & {
|
|
4
|
+
from: string | null;
|
|
5
|
+
to: string | null;
|
|
6
|
+
onFromChange: (value: string | null) => void;
|
|
7
|
+
onToChange: (value: string | null) => void;
|
|
8
|
+
fromLabel: string;
|
|
9
|
+
toLabel: string;
|
|
10
|
+
};
|
|
11
|
+
/** Both calendar bounds and the focus handoff belong to the range. */
|
|
12
|
+
declare const DateRangeFilter: ({ from, to, onFromChange, onToChange, fromLabel, toLabel, className, maxDate, ...pickerProps }: DateRangeFilterProps) => import("react").JSX.Element;
|
|
13
|
+
//#endregion
|
|
14
|
+
export { DateRangeFilter, type DateRangeFilterProps };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cn } from "../lib/utils.js";
|
|
3
|
+
import { DatePickerPopover } from "./date-picker-popover.js";
|
|
4
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
import { useId, useRef, useState } from "react";
|
|
6
|
+
//#region src/components/date-range-filter.tsx
|
|
7
|
+
/** Both calendar bounds and the focus handoff belong to the range. */
|
|
8
|
+
const DateRangeFilter = ({ from, to, onFromChange, onToChange, fromLabel, toLabel, className, maxDate, ...pickerProps }) => {
|
|
9
|
+
const id = useId();
|
|
10
|
+
const toRef = useRef(null);
|
|
11
|
+
const [picker, setPicker] = useState({ type: "closed" });
|
|
12
|
+
const fromMax = to && (!maxDate || to < maxDate) ? to : maxDate;
|
|
13
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
14
|
+
className: cn("flex flex-col gap-1.5", className),
|
|
15
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
16
|
+
className: "flex min-w-0 flex-col gap-1.5",
|
|
17
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
18
|
+
id: `${id}-from-label`,
|
|
19
|
+
className: "text-muted-foreground text-xs font-medium",
|
|
20
|
+
children: fromLabel
|
|
21
|
+
}), /* @__PURE__ */ jsx(DatePickerPopover, {
|
|
22
|
+
...pickerProps,
|
|
23
|
+
mode: "date",
|
|
24
|
+
id: `${id}-from`,
|
|
25
|
+
defaultOpen: false,
|
|
26
|
+
labelledBy: `${id}-from-label`,
|
|
27
|
+
dateInputLabel: fromLabel,
|
|
28
|
+
value: from,
|
|
29
|
+
maxDate: fromMax,
|
|
30
|
+
minDate: void 0,
|
|
31
|
+
triggerRef: void 0,
|
|
32
|
+
focusTriggerOnOpen: false,
|
|
33
|
+
open: picker.type === "from",
|
|
34
|
+
onOpenChange: (open) => setPicker(open ? { type: "from" } : { type: "closed" }),
|
|
35
|
+
onChange: (next) => {
|
|
36
|
+
onFromChange(next);
|
|
37
|
+
if (!next) return;
|
|
38
|
+
setPicker({
|
|
39
|
+
type: "to",
|
|
40
|
+
focus: "trigger"
|
|
41
|
+
});
|
|
42
|
+
toRef.current?.focus();
|
|
43
|
+
}
|
|
44
|
+
})]
|
|
45
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
46
|
+
className: "flex min-w-0 flex-col gap-1.5",
|
|
47
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
48
|
+
id: `${id}-to-label`,
|
|
49
|
+
className: "text-muted-foreground text-xs font-medium",
|
|
50
|
+
children: toLabel
|
|
51
|
+
}), /* @__PURE__ */ jsx(DatePickerPopover, {
|
|
52
|
+
...pickerProps,
|
|
53
|
+
mode: "date",
|
|
54
|
+
id: `${id}-to`,
|
|
55
|
+
defaultOpen: false,
|
|
56
|
+
labelledBy: `${id}-to-label`,
|
|
57
|
+
dateInputLabel: toLabel,
|
|
58
|
+
value: to,
|
|
59
|
+
minDate: from ?? void 0,
|
|
60
|
+
maxDate,
|
|
61
|
+
open: picker.type === "to",
|
|
62
|
+
triggerRef: toRef,
|
|
63
|
+
focusTriggerOnOpen: picker.type === "to" && picker.focus === "trigger",
|
|
64
|
+
onOpenChange: (open) => setPicker(open ? {
|
|
65
|
+
type: "to",
|
|
66
|
+
focus: "calendar"
|
|
67
|
+
} : { type: "closed" }),
|
|
68
|
+
onChange: onToChange
|
|
69
|
+
})]
|
|
70
|
+
})]
|
|
71
|
+
});
|
|
72
|
+
};
|
|
73
|
+
//#endregion
|
|
74
|
+
export { DateRangeFilter };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { CourtBadgeSize } from "./court-badge.js";
|
|
2
|
+
import { DocumentIdentity } from "./document-identity-badge.logic.js";
|
|
3
|
+
//#region src/components/document-identity-badge.d.ts
|
|
4
|
+
export declare const DocumentIdentityBadge: ({ identity, size, title }: DocumentIdentityBadgeProps) => import("react").JSX.Element;
|
|
5
|
+
type DocumentIdentityBadgeProps = {
|
|
6
|
+
identity: DocumentIdentity;
|
|
7
|
+
/** Decision chips can fit an inline metadata row. */
|
|
8
|
+
size?: CourtBadgeSize | undefined;
|
|
9
|
+
/** Omit when the enclosing tab already supplies the full-title tooltip. */
|
|
10
|
+
title?: string | undefined;
|
|
11
|
+
};
|
|
12
|
+
//#endregion
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { FileTextIcon } from "../icons.js";
|
|
2
|
+
import { cn } from "../lib/utils.js";
|
|
3
|
+
import { Tooltip, TooltipContent as TooltipPopup, TooltipTrigger } from "./tooltip.js";
|
|
4
|
+
import { CourtBadge } from "./court-badge.js";
|
|
5
|
+
import { COURT_TIER_WEIGHT, statuteIdentityLabels } from "./document-identity-badge.logic.js";
|
|
6
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
+
import { panic } from "better-result";
|
|
8
|
+
//#region src/components/document-identity-badge.tsx
|
|
9
|
+
const DocumentIdentityBadge = ({ identity, size, title }) => {
|
|
10
|
+
const content = documentIdentityMark(identity, size);
|
|
11
|
+
if (title === void 0) return content;
|
|
12
|
+
return /* @__PURE__ */ jsxs(Tooltip, { children: [/* @__PURE__ */ jsx(TooltipTrigger, {
|
|
13
|
+
render: /* @__PURE__ */ jsx("span", { className: "inline-flex max-w-full min-w-0" }),
|
|
14
|
+
children: content
|
|
15
|
+
}), /* @__PURE__ */ jsx(TooltipPopup, { children: title })] });
|
|
16
|
+
};
|
|
17
|
+
function documentIdentityMark(identity, size) {
|
|
18
|
+
switch (identity.kind) {
|
|
19
|
+
case "statute": {
|
|
20
|
+
const labels = statuteIdentityLabels(identity);
|
|
21
|
+
if (labels === null) return unknownDocumentMark();
|
|
22
|
+
return /* @__PURE__ */ jsxs("span", {
|
|
23
|
+
className: cn("@container/document-identity inline-flex max-w-full shrink-0 items-center justify-center font-mono leading-4 font-semibold tabular-nums", labels.short.length > 7 ? "text-[9px]" : "text-[10px]"),
|
|
24
|
+
style: { width: `${labels.long.length + .25}ch` },
|
|
25
|
+
"data-slot": "document-identity-badge",
|
|
26
|
+
"data-kind": "statute",
|
|
27
|
+
dir: "ltr",
|
|
28
|
+
children: [/* @__PURE__ */ jsx("bdi", {
|
|
29
|
+
className: statuteLabelClasses(labels.long.length).short,
|
|
30
|
+
children: labels.short
|
|
31
|
+
}), /* @__PURE__ */ jsx("bdi", {
|
|
32
|
+
className: statuteLabelClasses(labels.long.length).long,
|
|
33
|
+
children: labels.long
|
|
34
|
+
})]
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
case "decision": {
|
|
38
|
+
const abbreviation = identity.courtAbbreviation?.trim();
|
|
39
|
+
if (!abbreviation) return unknownDocumentMark();
|
|
40
|
+
return /* @__PURE__ */ jsx("span", {
|
|
41
|
+
className: "inline-flex max-w-full min-w-0",
|
|
42
|
+
"data-slot": "document-identity-badge",
|
|
43
|
+
"data-kind": "decision",
|
|
44
|
+
children: /* @__PURE__ */ jsx(CourtBadge, {
|
|
45
|
+
abbreviation,
|
|
46
|
+
size,
|
|
47
|
+
weight: identity.courtTier === void 0 ? "outline" : COURT_TIER_WEIGHT[identity.courtTier],
|
|
48
|
+
className: cn("max-w-full overflow-hidden", abbreviation.length > 4 && "px-0.5 text-[9px] tracking-normal")
|
|
49
|
+
})
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
case "unknown": return unknownDocumentMark();
|
|
53
|
+
default: return panic("Unhandled document identity");
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function unknownDocumentMark() {
|
|
57
|
+
return /* @__PURE__ */ jsx(FileTextIcon, {
|
|
58
|
+
"aria-hidden": "true",
|
|
59
|
+
className: "size-3.5 shrink-0",
|
|
60
|
+
"data-slot": "document-identity-badge",
|
|
61
|
+
"data-kind": "unknown"
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
const STATUTE_LABEL_CLASSES = {
|
|
65
|
+
6: {
|
|
66
|
+
short: "@[6ch]/document-identity:hidden",
|
|
67
|
+
long: "hidden @[6ch]/document-identity:inline"
|
|
68
|
+
},
|
|
69
|
+
7: {
|
|
70
|
+
short: "@[7ch]/document-identity:hidden",
|
|
71
|
+
long: "hidden @[7ch]/document-identity:inline"
|
|
72
|
+
},
|
|
73
|
+
8: {
|
|
74
|
+
short: "@[8ch]/document-identity:hidden",
|
|
75
|
+
long: "hidden @[8ch]/document-identity:inline"
|
|
76
|
+
},
|
|
77
|
+
9: {
|
|
78
|
+
short: "@[9ch]/document-identity:hidden",
|
|
79
|
+
long: "hidden @[9ch]/document-identity:inline"
|
|
80
|
+
},
|
|
81
|
+
10: {
|
|
82
|
+
short: "@[10ch]/document-identity:hidden",
|
|
83
|
+
long: "hidden @[10ch]/document-identity:inline"
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
function statuteLabelClasses(length) {
|
|
87
|
+
if (length <= 6) return STATUTE_LABEL_CLASSES[6];
|
|
88
|
+
if (length === 7) return STATUTE_LABEL_CLASSES[7];
|
|
89
|
+
if (length === 8) return STATUTE_LABEL_CLASSES[8];
|
|
90
|
+
if (length === 9) return STATUTE_LABEL_CLASSES[9];
|
|
91
|
+
return STATUTE_LABEL_CLASSES[10];
|
|
92
|
+
}
|
|
93
|
+
//#endregion
|
|
94
|
+
export { DocumentIdentityBadge };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
//#region src/components/document-identity-badge.logic.d.ts
|
|
2
|
+
export declare const COURT_TIER_WEIGHT: {
|
|
3
|
+
readonly constitutional: "solid";
|
|
4
|
+
readonly supreme: "tinted";
|
|
5
|
+
readonly regional: "outline";
|
|
6
|
+
readonly other: "dashed";
|
|
7
|
+
};
|
|
8
|
+
export type DocumentIdentity = {
|
|
9
|
+
kind: "statute";
|
|
10
|
+
number: string | null;
|
|
11
|
+
year: string | null;
|
|
12
|
+
} | {
|
|
13
|
+
kind: "decision";
|
|
14
|
+
courtAbbreviation: string | null | undefined;
|
|
15
|
+
courtTier?: keyof typeof COURT_TIER_WEIGHT | undefined;
|
|
16
|
+
} | {
|
|
17
|
+
kind: "unknown";
|
|
18
|
+
};
|
|
19
|
+
export declare const statuteIdentityLabels: ({ number, year }: Extract<DocumentIdentity, {
|
|
20
|
+
kind: "statute";
|
|
21
|
+
}>) => {
|
|
22
|
+
short: string;
|
|
23
|
+
long: string;
|
|
24
|
+
} | null;
|
|
25
|
+
//#endregion
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
//#region src/components/document-identity-badge.logic.ts
|
|
2
|
+
const COURT_TIER_WEIGHT = {
|
|
3
|
+
constitutional: "solid",
|
|
4
|
+
supreme: "tinted",
|
|
5
|
+
regional: "outline",
|
|
6
|
+
other: "dashed"
|
|
7
|
+
};
|
|
8
|
+
const statuteIdentityLabels = ({ number, year }) => {
|
|
9
|
+
const ordinal = number?.trim() || null;
|
|
10
|
+
const fullYear = year?.trim() || null;
|
|
11
|
+
if (ordinal === null) return fullYear === null ? null : {
|
|
12
|
+
short: fullYear,
|
|
13
|
+
long: fullYear
|
|
14
|
+
};
|
|
15
|
+
if (fullYear === null) return {
|
|
16
|
+
short: ordinal,
|
|
17
|
+
long: ordinal
|
|
18
|
+
};
|
|
19
|
+
return {
|
|
20
|
+
short: `${ordinal}/${fullYear.slice(-2)}`,
|
|
21
|
+
long: `${ordinal}/${fullYear}`
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
//#endregion
|
|
25
|
+
export { COURT_TIER_WEIGHT, statuteIdentityLabels };
|
|
@@ -25,7 +25,7 @@ type LandingGreetingProps = {
|
|
|
25
25
|
};
|
|
26
26
|
export declare const LandingGreeting: ({ icon, children }: LandingGreetingProps) => import("react").JSX.Element;
|
|
27
27
|
/** A column's heading: an icon and a sentence-case label, as a link or a trigger. */
|
|
28
|
-
export declare const LANDING_SECTION_HEADING_CLASS = "text-
|
|
28
|
+
export declare const LANDING_SECTION_HEADING_CLASS = "text-foreground hover:bg-accent/50 focus-visible:ring-ring [&>svg]:text-muted-foreground flex items-center gap-2 rounded-md px-2 py-0.5 text-sm font-semibold outline-none focus-visible:ring-2";
|
|
29
29
|
/** A row of a column, as a link or a button. Its text lines up with the
|
|
30
30
|
* heading's label; the icon slot under the heading icon stays empty unless
|
|
31
31
|
* the icon identifies the row (a matter's colour). */
|
|
@@ -40,7 +40,7 @@ const LandingGreeting = ({ icon, children }) => /* @__PURE__ */ jsxs("div", {
|
|
|
40
40
|
})]
|
|
41
41
|
});
|
|
42
42
|
/** A column's heading: an icon and a sentence-case label, as a link or a trigger. */
|
|
43
|
-
const LANDING_SECTION_HEADING_CLASS = "text-
|
|
43
|
+
const LANDING_SECTION_HEADING_CLASS = "text-foreground hover:bg-accent/50 focus-visible:ring-ring [&>svg]:text-muted-foreground flex items-center gap-2 rounded-md px-2 py-0.5 text-sm font-semibold outline-none focus-visible:ring-2";
|
|
44
44
|
/** A row of a column, as a link or a button. Its text lines up with the
|
|
45
45
|
* heading's label; the icon slot under the heading icon stays empty unless
|
|
46
46
|
* the icon identifies the row (a matter's colour). */
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//#region src/components/matter-icon.d.ts
|
|
2
|
+
type MatterIconProps = {
|
|
3
|
+
className?: string | undefined;
|
|
4
|
+
} & ({
|
|
5
|
+
matter: {
|
|
6
|
+
id: string;
|
|
7
|
+
color: string | null;
|
|
8
|
+
};
|
|
9
|
+
} | {
|
|
10
|
+
variant: "none" | "all";
|
|
11
|
+
});
|
|
12
|
+
export declare const MatterIcon: (props: MatterIconProps) => import("react").JSX.Element;
|
|
13
|
+
export declare const MattersNavIcon: ({ className }: {
|
|
14
|
+
className?: string | undefined;
|
|
15
|
+
}) => import("react").JSX.Element;
|
|
16
|
+
//#endregion
|