@lotics/ui 1.18.0 → 1.19.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/ui",
3
- "version": "1.18.0",
3
+ "version": "1.19.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "./tokens": "./src/tokens.ts",
@@ -105,6 +105,7 @@
105
105
  "./pager_view": "./src/pager_view.tsx",
106
106
  "./date_picker": "./src/date_picker.tsx",
107
107
  "./date_filter": "./src/date_filter.tsx",
108
+ "./date_range_filter_field": "./src/date_range_filter_field.tsx",
108
109
  "./time_picker": "./src/time_picker.tsx",
109
110
  "./time_field": "./src/time_field.tsx",
110
111
  "./date_calendar": "./src/date_calendar.tsx",
@@ -0,0 +1,116 @@
1
+ import React, { useMemo, useState } from "react";
2
+ import { View } from "react-native";
3
+ import { Text } from "./text";
4
+ import { colors } from "./colors";
5
+ import { Icon } from "./icon";
6
+ import { Button } from "./button";
7
+ import { PressableHighlight } from "./pressable_highlight";
8
+ import { Popover, PopoverTrigger, PopoverContent, PopoverFooter } from "./popover";
9
+ import { DateFilter, DateFilterValue, DateFilterLabels } from "./date_filter";
10
+
11
+ // =============================================================================
12
+ // DateRangeFilterField — the common filter composition over DateFilter:
13
+ // a trigger button showing the selected range, a popover holding the DateFilter
14
+ // preset panel, and a Clear/Done footer. Use this for a date-range filter on a
15
+ // toolbar/form; use the bare `DateFilter` panel when the host owns its own
16
+ // trigger + footer (e.g. a grid column header).
17
+ // =============================================================================
18
+
19
+ export interface DateRangeFilterFieldLabels extends DateFilterLabels {
20
+ /** Footer: reset to no range. */
21
+ clear: string;
22
+ /** Footer: close the popover. */
23
+ done: string;
24
+ /** Trigger text when no range is selected. */
25
+ placeholder: string;
26
+ }
27
+
28
+ const DEFAULT_FIELD_LABELS = { clear: "Clear", done: "Done", placeholder: "All time" };
29
+
30
+ export interface DateRangeFilterFieldProps {
31
+ value: DateFilterValue;
32
+ onValueChange: (value: DateFilterValue) => void;
33
+ includeTime?: boolean;
34
+ /** Translated labels (presets + footer + placeholder). Defaults to English. */
35
+ labels?: Partial<DateRangeFilterFieldLabels>;
36
+ /** BCP-47 locale for the calendar + trigger date display. Defaults to "en-US". */
37
+ locale?: string;
38
+ testID?: string;
39
+ }
40
+
41
+ const EMPTY_VALUE: DateFilterValue = {
42
+ start: { date: null, time: null },
43
+ end: { date: null, time: null },
44
+ };
45
+
46
+ function formatDate(date: Date | null, locale: string | undefined): string {
47
+ if (!date) return "";
48
+ try {
49
+ return new Intl.DateTimeFormat(locale, {
50
+ day: "2-digit",
51
+ month: "2-digit",
52
+ year: "numeric",
53
+ }).format(date);
54
+ } catch {
55
+ return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
56
+ }
57
+ }
58
+
59
+ export function DateRangeFilterField(props: DateRangeFilterFieldProps) {
60
+ const { value, onValueChange, includeTime, locale, testID } = props;
61
+ const labels = useMemo(() => ({ ...DEFAULT_FIELD_LABELS, ...props.labels }), [props.labels]);
62
+ const [open, setOpen] = useState(false);
63
+
64
+ const hasValue = Boolean(value.start.date || value.end.date);
65
+ const display = hasValue
66
+ ? `${formatDate(value.start.date, locale)} – ${formatDate(value.end.date, locale)}`
67
+ : labels.placeholder;
68
+
69
+ return (
70
+ <Popover open={open} onOpenChange={setOpen} side="bottom" align="start">
71
+ <PopoverTrigger>
72
+ <PressableHighlight
73
+ testID={testID}
74
+ accessibilityRole="button"
75
+ accessibilityLabel={labels.selectDateRange}
76
+ style={{
77
+ flexDirection: "row",
78
+ alignItems: "center",
79
+ gap: 8,
80
+ paddingVertical: 9,
81
+ paddingHorizontal: 12,
82
+ borderWidth: 1,
83
+ borderColor: colors.zinc[300],
84
+ borderRadius: 8,
85
+ backgroundColor: colors.white,
86
+ }}
87
+ >
88
+ <Icon name="calendar" size={16} color={hasValue ? colors.zinc[700] : colors.zinc[400]} />
89
+ <Text size="sm" color={hasValue ? "default" : "muted"} numberOfLines={1} style={{ flex: 1 }}>
90
+ {display}
91
+ </Text>
92
+ <Icon name="chevron-down" size={14} color={colors.zinc[400]} />
93
+ </PressableHighlight>
94
+ </PopoverTrigger>
95
+ <PopoverContent disableBodyScroll>
96
+ <DateFilter
97
+ value={value}
98
+ onValueChange={onValueChange}
99
+ includeTime={includeTime}
100
+ labels={props.labels}
101
+ locale={locale}
102
+ />
103
+ <PopoverFooter>
104
+ <View style={{ flexDirection: "row", alignItems: "center", justifyContent: "space-between" }}>
105
+ {hasValue ? (
106
+ <Button title={labels.clear} onPress={() => onValueChange(EMPTY_VALUE)} />
107
+ ) : (
108
+ <View />
109
+ )}
110
+ <Button title={labels.done} color="secondary" onPress={() => setOpen(false)} />
111
+ </View>
112
+ </PopoverFooter>
113
+ </PopoverContent>
114
+ </Popover>
115
+ );
116
+ }