@lotics/ui 41.4.0 → 42.0.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/AGENTS.md +1 -0
- package/MIGRATION.md +34 -0
- package/docs/catalog.md +6 -0
- package/docs/composition.md +23 -0
- package/docs/testing.md +70 -0
- package/examples/tpl_dashboard.tsx +1 -1
- package/package.json +1 -1
- package/src/date_filter.tsx +54 -34
- package/src/date_filter_presets.ts +15 -10
- package/src/locale.tsx +2 -2
package/AGENTS.md
CHANGED
|
@@ -18,6 +18,7 @@ CURRENT major only — upgrading an app across majors is `MIGRATION.md`.
|
|
|
18
18
|
| [docs/data_entry.md](./docs/data_entry.md) | Which editing pattern for which job — inline edit, fieldset forms, browser-autofill suppression (search controls only), find-or-create (`Combobox`), line items, handoffs, phased records, billing, tags, dispositions, attachments (`InlineFiles` for a record ROW — list + one CTA, `multiple` decides add-vs-replace; the `FilesEditor` COMPOUND for a whole section — root owns selection/gallery/confirm, you compose the bar, a HOST verb reads `useFilesEditorSelection` — plus the three-way file INTAKE: CTA + `FileDropTarget` + `usePasteFiles`), stage gates, whether a multiline value keeps its fixed reserve or `autoGrow`s (who decides the length — the field, or whoever is typing), the commit-on-blur vs action-press ordering law (the kit gates the press — `pending_commits`). |
|
|
19
19
|
| [docs/ai_patterns.md](./docs/ai_patterns.md) | AI acts, the human stays in charge — composer, live run feed (`AgentRun`), the one law's split — it turns on WHO supplied the values (machine → a gate: a diff when something is being replaced, a full editable preview when records are being created from a document; human-typed → save-direct + the `ResultHeader` receipt), findings, provenance, confidence; **after the run** — a stored record that fills up from several writers (a person, a chat agent, an extraction, an automation) and remembers none of them: an unwritten value must not render like a written one, a machine's prose and a person's must not share a treatment, model markdown goes in `variant="embedded"`, and the read path must project every field the write paths set; the UI half of the SDK's [ai doc](../app-sdk/docs/ai.md)., the whole run in a dialog (`AgentRunScope`/`AgentRunPane`/`AgentRunActions` — a parked question REPLACES the feed, actions in the footer, **Stop** while streaming), **stopping** (`cancel` stops the run, `abort` only stops listening — so closing a dialog must `cancel` or it keeps billing); **review surfaces compose from atoms** — `DiffValue` (a changed value, droppable in any cell/row/total), `DiffMark` (what happened to the row — ONE circular disc, every surface), `useChangeSet` (accept/reject/undo bookkeeping, no layout) — see [MIGRATION.md](./MIGRATION.md) for the `ChangeReview` family they replace |
|
|
20
20
|
| [docs/composition.md](./docs/composition.md) | The design-language contract — canvas + content column, heading altitude (incl. eyebrow vs group lead — a label is one or the other), banded cards, register vs inset rows (incl. the register laws a row centres its cells by: every cell a FIXED height, a pressable cell on the shared hover token, a column sized by what it carries), the button ladder and **what underlined text may mean** (it GOES somewhere or REVEALS something — never mutates; blue leaves the surface, muted stays on it, and the one in-prose disclosure exception is scoped there), master-detail `Drawer` on a LIST screen vs a child collection's row EXPANDING inside a record, view controls, RECORD EXTENT (one page, sections scrolled to and never routed to), color discipline, typography, whitespace, and how to TEST an overlay component (a `Popover`-backed surface never mounts under jsdom). |
|
|
21
|
+
| [docs/testing.md](./docs/testing.md) | Driving the kit in a browser — the three anatomies where the a11y tree says one thing and a driver must do another: a `PressDoor` row whose named button always intercepts pointer events (by design), portalled overlays that render at the top of the DOM, and custom pointer drag that `dragTo` cannot move. |
|
|
21
22
|
| [docs/templates.md](./docs/templates.md) | The map of `examples/tpl_*.tsx` — what shape each template solves and which to start from (copy + adapt, never import) — plus the record-surface composition rules (pipeline order, static shape, decision budget) and the ACTIVITY shape — a communications feed where the row's label is the GIST and the body varies by medium, one anatomy rather than a row type per kind. |
|
|
22
23
|
|
|
23
24
|
## Iron rules
|
package/MIGRATION.md
CHANGED
|
@@ -4,6 +4,40 @@ Breaking changes, newest first — normally per major, plus the rare minor that
|
|
|
4
4
|
anyway (recorded under its exact version). The current contract lives in `AGENTS.md` + `docs/`;
|
|
5
5
|
this file exists only to move an app from one release to the next.
|
|
6
6
|
|
|
7
|
+
## 42.0.0 — `DateFilter`'s presets are a listbox, and "Custom" is not one of them
|
|
8
|
+
|
|
9
|
+
Three defects with one root: the preset row used EMPHASIS to mean "current state", inside a
|
|
10
|
+
panel where emphasis means "action".
|
|
11
|
+
|
|
12
|
+
**The active preset was painted like a primary button.** `zinc.800` with inverted text — the
|
|
13
|
+
loudest treatment the system has — spent on a row that reports state rather than offering one.
|
|
14
|
+
It outshouted the panel's own Done button. It now uses `MenuButton`'s `selected`, the kit's
|
|
15
|
+
listbox row: a `zinc.100` resting highlight. That prop also carries `aria-selected`, which the
|
|
16
|
+
hand-rolled fill never did — until now the active preset announced **nothing**, so which period
|
|
17
|
+
was selected was information available only to someone who could see the fill.
|
|
18
|
+
|
|
19
|
+
**The panel rendered its phone layout on desktop.** It sized itself with `useContainerSize()`,
|
|
20
|
+
which inside a `Popover` measures the popover BODY — and the body's width is decided by the
|
|
21
|
+
branch that reading picks. Two calendars come to ~617px, always under the 768 phone threshold,
|
|
22
|
+
so the panel resolved "small" on a 1440px viewport and its sidebar branch was unreachable: the
|
|
23
|
+
presets collapsed into a horizontal strip of bare text where the only item with any chrome was
|
|
24
|
+
the active one. It now reads `useScreenSize()` — the same instrument, and for the same reason,
|
|
25
|
+
that `Popover` already uses to choose sheet vs anchored.
|
|
26
|
+
|
|
27
|
+
**`"custom"` is gone from `PresetId`, `PRESET_IDS`, and `DateFilterLabels`.** It was the one
|
|
28
|
+
entry that set no range: it CLEARED the value. So it lit up whenever the range matched no
|
|
29
|
+
preset — the normal state for any hand-picked range, and the opening state of any app whose
|
|
30
|
+
default period is not a preset — and clicking the lit row, the obvious move for "let me pick my
|
|
31
|
+
own dates", threw the range away. The calendar is the custom picker and the footer's Clear is
|
|
32
|
+
the clear.
|
|
33
|
+
|
|
34
|
+
`getPresetValue` is now total: it returns `DateFilterValue`, never `| null`.
|
|
35
|
+
|
|
36
|
+
**To migrate:** delete `custom` from any `DateFilterLabels` / `DateRangeFilterFieldLabels`
|
|
37
|
+
object you pass — it is an excess property now and the compiler will point at it. If you called
|
|
38
|
+
`getPresetValue` and branched on `null`, drop the branch. Nothing else changes: a range that
|
|
39
|
+
matches no preset simply leaves every row unselected, which is what it always meant.
|
|
40
|
+
|
|
7
41
|
## 41.4.0 — embedded markdown is demoted in the OUTLINE, not only on the type ladder
|
|
8
42
|
|
|
9
43
|
`<Markdown variant="embedded">` sized its headings down and left them as the tags the author
|
package/docs/catalog.md
CHANGED
|
@@ -857,6 +857,12 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
857
857
|
for it bare only when the calendar lives permanently on the surface, not behind a field.
|
|
858
858
|
- **`date_filter`** — `DateFilter`: the date+time period filter panel — presets
|
|
859
859
|
(`PresetId`), calendar, optional time segments; the body `DateRangeFilterField` opens.
|
|
860
|
+
**Every preset SETS a range** — the list holds no mode and no null case, so a hand-picked
|
|
861
|
+
range selects nothing rather than lighting a "custom" row whose click cleared the value. The
|
|
862
|
+
presets are a `listbox` of `MenuButton` options: selection is the kit's `zinc.100` row
|
|
863
|
+
highlight plus `aria-selected`, never a filled primary treatment, because the row reports
|
|
864
|
+
state rather than offering an action. Sidebar at viewports of 768 and up, strip below —
|
|
865
|
+
measured on the VIEWPORT, since a popover's own width is decided by that very branch.
|
|
860
866
|
**One selection rule, no modes**: a click OPENS a range, the next CLOSES it (in either
|
|
861
867
|
order — bounds come back sorted), and a single day is the same day clicked twice
|
|
862
868
|
(`start`/`end` equal — which is how a consumer detects "one day" and how the trigger
|
package/docs/composition.md
CHANGED
|
@@ -1047,6 +1047,29 @@ most common, because it survives every other check: each piece is defensible alo
|
|
|
1047
1047
|
question "what does this tell me that the screen does not already say" catches decoration wearing
|
|
1048
1048
|
an information costume.
|
|
1049
1049
|
|
|
1050
|
+
## `accessibilityState` never reaches the DOM — write `aria-*` yourself
|
|
1051
|
+
|
|
1052
|
+
React Native Web maps a subset of RN's accessibility props and **silently drops
|
|
1053
|
+
`accessibilityState`**. So a control written the RN way announces nothing:
|
|
1054
|
+
|
|
1055
|
+
```tsx
|
|
1056
|
+
// renders, typechecks, and the DOM carries no state at all
|
|
1057
|
+
<Pressable accessibilityRole="tab" accessibilityState={{ selected: isActive }} />
|
|
1058
|
+
|
|
1059
|
+
// what actually reaches the DOM
|
|
1060
|
+
<Pressable accessibilityRole="tab" aria-selected={isActive} />
|
|
1061
|
+
```
|
|
1062
|
+
|
|
1063
|
+
The trap is the failure's shape rather than its cause. There is no type error —
|
|
1064
|
+
the prop is declared — no runtime warning, and the control looks and behaves
|
|
1065
|
+
correctly; only a screen reader and the DOM inspector disagree with the source.
|
|
1066
|
+
Nothing in a normal loop surfaces it, so it survives review, tests and QA.
|
|
1067
|
+
|
|
1068
|
+
Write the raw `aria-*` attribute for any STATE (`aria-selected`, `aria-checked`,
|
|
1069
|
+
`aria-expanded`, `aria-disabled`, `aria-current`). `accessibilityRole` and
|
|
1070
|
+
`accessibilityLabel` do map and are fine as-is. **Verify in the DOM, never in the
|
|
1071
|
+
source** — the source is what lies here.
|
|
1072
|
+
|
|
1050
1073
|
## Testing an overlay component — assert through the logic, prove in a browser
|
|
1051
1074
|
|
|
1052
1075
|
**A `Popover`-backed surface does not mount under jsdom.** It positions in a `useLayoutEffect` +
|
package/docs/testing.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Driving the kit in a browser
|
|
2
|
+
|
|
3
|
+
Three kit anatomies where the accessibility tree says one thing and an automated
|
|
4
|
+
driver has to do another. Each is **by design** — the shape that makes the
|
|
5
|
+
component correct for a keyboard and a screen reader is the shape that defeats a
|
|
6
|
+
naive `click()` — so each will read as a bug the first time, and the wrong
|
|
7
|
+
reaction (`force: true`, or "the component is broken") costs a debugging session.
|
|
8
|
+
|
|
9
|
+
Everything here is true wherever the kit renders. The iframe that a Lotics app
|
|
10
|
+
runs inside adds one more rule on top — `lotics docs building_an_app` § 8.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## A pressable row: click the surface, not the named button
|
|
15
|
+
|
|
16
|
+
A row that presses open **and** carries its own controls is a role-less
|
|
17
|
+
`PressableRow` with a `PressDoor` sibling (`@lotics/ui` AGENTS.md — *a button
|
|
18
|
+
never contains a control*). The door is what a keyboard and a screen reader
|
|
19
|
+
use: it holds the tab stop, the accessible name and the focus ring, and it is an
|
|
20
|
+
**empty absolutely-positioned element**. The cells render above it.
|
|
21
|
+
|
|
22
|
+
So the a11y tree shows `button "Open ACME-1042"`, and clicking it fails:
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
subtree intercepts pointer events
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
That is the design working, not a defect — the visible row is above the door
|
|
29
|
+
precisely so a pointer lands on the row rather than the door. Drive it the way a
|
|
30
|
+
mouse user does:
|
|
31
|
+
|
|
32
|
+
- **click the row container** — the `generic [cursor=pointer]` wrapping the door;
|
|
33
|
+
the click bubbles to `PressableRow`
|
|
34
|
+
- or `dispatchEvent('click')` on the door directly
|
|
35
|
+
- or exercise the keyboard path, which is the one the door exists for: focus the
|
|
36
|
+
door and press `Enter`
|
|
37
|
+
|
|
38
|
+
**Never reach for `force: true`.** It suppresses the actionability check that is
|
|
39
|
+
telling you the truth, and the click then lands somewhere you did not choose.
|
|
40
|
+
|
|
41
|
+
## Overlays render at the top of the DOM, not inside their trigger
|
|
42
|
+
|
|
43
|
+
Popovers, tooltips and dialogs go through a portal (`dom_portal`), so their
|
|
44
|
+
content is a sibling near the end of the document rather than a descendant of
|
|
45
|
+
the control that opened it. Looking for it under the trigger finds nothing.
|
|
46
|
+
|
|
47
|
+
Assert two things, not one: that the content appeared, and that it **dismisses**
|
|
48
|
+
— outside-click and `Escape` both. An open overlay usually has a click-catching
|
|
49
|
+
backdrop, so clicking the trigger a second time is often intercepted; click the
|
|
50
|
+
backdrop or press `Escape`.
|
|
51
|
+
|
|
52
|
+
## Custom pointer drag is not `dragTo`
|
|
53
|
+
|
|
54
|
+
The calendar and gantt drags are built on `use_pointer_drag`, which listens for
|
|
55
|
+
real `pointerdown` / `pointermove` / `pointerup`. Playwright's `dragTo` and mouse
|
|
56
|
+
emulation do not drive them — the sequence simply does nothing, with no error.
|
|
57
|
+
|
|
58
|
+
Dispatch the events yourself, **in the frame's own context** so the coordinates
|
|
59
|
+
and the window are the right ones. From the dragged element,
|
|
60
|
+
`el.ownerDocument.defaultView` is that window:
|
|
61
|
+
|
|
62
|
+
1. `pointerdown` on the element
|
|
63
|
+
2. `pointermove` on the window, past the drag threshold (a few pixels — a smaller
|
|
64
|
+
move is treated as a click, deliberately)
|
|
65
|
+
3. `pointerup` on the window, with `clientX`/`clientY` at the drop target
|
|
66
|
+
|
|
67
|
+
Then check **both** halves: re-snapshot for the optimistic move, and re-read the
|
|
68
|
+
record to confirm the mutation actually persisted. An optimistic move that never
|
|
69
|
+
reached the server looks identical on screen, which is the whole reason to check
|
|
70
|
+
the second one.
|
|
@@ -98,7 +98,7 @@ const KY_CUOI = "2026-06";
|
|
|
98
98
|
const NHAN_BO_LOC: Partial<DateRangeFilterFieldLabels> = {
|
|
99
99
|
year: "Year", month: "Month", day: "Day", hour: "Hour", minute: "Minute", dayPeriod: "AM/PM",
|
|
100
100
|
today: "Today", yesterday: "Yesterday", tomorrow: "Tomorrow", thisWeek: "This week",
|
|
101
|
-
thisMonth: "This month", lastMonth: "Last month",
|
|
101
|
+
thisMonth: "This month", lastMonth: "Last month", from: "From", to: "To",
|
|
102
102
|
selectDateRange: "Select date range", selectDate: "Select date",
|
|
103
103
|
clear: "Clear", done: "Done", placeholder: "All time",
|
|
104
104
|
};
|
package/package.json
CHANGED
package/src/date_filter.tsx
CHANGED
|
@@ -6,7 +6,7 @@ import { CONTROL_RADIUS } from "./control_surface";
|
|
|
6
6
|
import { MenuButton } from "./menu_button";
|
|
7
7
|
import { Calendar, CalendarRangeValue, CalendarRef } from "./date_calendar";
|
|
8
8
|
import { TimePicker } from "./time_picker";
|
|
9
|
-
import {
|
|
9
|
+
import { useScreenSize } from "./use_screen_size";
|
|
10
10
|
import { SegmentLabels } from "./date_segments";
|
|
11
11
|
import { PresetId, PRESET_IDS, getPresetValue } from "./date_filter_presets";
|
|
12
12
|
import { formatDate } from "./format_date";
|
|
@@ -33,7 +33,6 @@ export interface DateFilterLabels extends SegmentLabels {
|
|
|
33
33
|
thisWeek: string;
|
|
34
34
|
thisMonth: string;
|
|
35
35
|
lastMonth: string;
|
|
36
|
-
custom: string;
|
|
37
36
|
from: string;
|
|
38
37
|
to: string;
|
|
39
38
|
/** Accessible name of the field that OPENS this panel (`DateRangeFilterField`). */
|
|
@@ -72,8 +71,6 @@ function presetLabel(id: PresetId, labels: DateFilterLabels): string {
|
|
|
72
71
|
return labels.thisMonth;
|
|
73
72
|
case "last_month":
|
|
74
73
|
return labels.lastMonth;
|
|
75
|
-
case "custom":
|
|
76
|
-
return labels.custom;
|
|
77
74
|
}
|
|
78
75
|
}
|
|
79
76
|
|
|
@@ -134,7 +131,21 @@ export function DateFilter(props: DateFilterProps) {
|
|
|
134
131
|
() => ({ ...loc, ...props.labels }),
|
|
135
132
|
[loc, props.labels],
|
|
136
133
|
);
|
|
137
|
-
|
|
134
|
+
// The VIEWPORT, never this panel's own box.
|
|
135
|
+
//
|
|
136
|
+
// This read `useContainerSize()`, which inside a `Popover` measures the popover
|
|
137
|
+
// BODY — and the body's width is decided by which branch this very line picks.
|
|
138
|
+
// Two calendars come to ~617px, always under the 768 phone threshold, so the
|
|
139
|
+
// panel resolved "small" on a 1440px desktop and the sidebar branch below was
|
|
140
|
+
// unreachable: the presets rendered as the phone strip on every screen, which
|
|
141
|
+
// is why the only item in them with any chrome read as a button.
|
|
142
|
+
//
|
|
143
|
+
// `Popover` already carries this rule for its own sheet-vs-anchored decision,
|
|
144
|
+
// and its comment names the same bug being introduced by the same hook. Same
|
|
145
|
+
// instrument here, so the two agree: under 768 the popover is a bottom sheet
|
|
146
|
+
// and the presets are a strip; at or above it, an anchored panel with the
|
|
147
|
+
// sidebar.
|
|
148
|
+
const screenSize = useScreenSize();
|
|
138
149
|
const calendarRef = useRef<CalendarRef>(null);
|
|
139
150
|
|
|
140
151
|
// A half-picked range is held HERE and never emitted — the same rule
|
|
@@ -192,13 +203,15 @@ export function DateFilter(props: DateFilterProps) {
|
|
|
192
203
|
[onValueChange, value],
|
|
193
204
|
);
|
|
194
205
|
|
|
195
|
-
// Which preset (if any) the current value matches.
|
|
206
|
+
// Which preset (if any) the current value matches. A hand-picked range matches
|
|
207
|
+
// none, and that is `null` — no row is selected. It used to fall through to
|
|
208
|
+
// "custom", so the panel opened with a row lit for a range the member had
|
|
209
|
+
// never chosen from this list.
|
|
196
210
|
const activePresetId = useMemo((): PresetId | null => {
|
|
197
211
|
if (!value.start.date && !value.end.date) return null;
|
|
198
212
|
const now = new Date();
|
|
199
213
|
for (const id of PRESET_IDS) {
|
|
200
214
|
const presetValue = getPresetValue(id, now);
|
|
201
|
-
if (!presetValue) continue; // "custom"
|
|
202
215
|
if (
|
|
203
216
|
value.start.date?.toDateString() === presetValue.start.date?.toDateString() &&
|
|
204
217
|
value.end.date?.toDateString() === presetValue.end.date?.toDateString()
|
|
@@ -206,7 +219,7 @@ export function DateFilter(props: DateFilterProps) {
|
|
|
206
219
|
return id;
|
|
207
220
|
}
|
|
208
221
|
}
|
|
209
|
-
return
|
|
222
|
+
return null;
|
|
210
223
|
}, [value]);
|
|
211
224
|
|
|
212
225
|
const handlePresetSelect = useCallback(
|
|
@@ -220,12 +233,6 @@ export function DateFilter(props: DateFilterProps) {
|
|
|
220
233
|
}
|
|
221
234
|
|
|
222
235
|
const presetValue = getPresetValue(id, new Date());
|
|
223
|
-
if (!presetValue) {
|
|
224
|
-
// "Custom" clears the value so the user can select manually
|
|
225
|
-
onValueChange({ start: { date: null, time: null }, end: { date: null, time: null } });
|
|
226
|
-
return;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
236
|
onValueChange(presetValue);
|
|
230
237
|
|
|
231
238
|
if (presetValue.start.date) {
|
|
@@ -238,27 +245,39 @@ export function DateFilter(props: DateFilterProps) {
|
|
|
238
245
|
[onValueChange, activePresetId],
|
|
239
246
|
);
|
|
240
247
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
248
|
+
// `selected` + `role="option"` is the kit's listbox row, and `MenuButton` owns
|
|
249
|
+
// both halves of it: the resting `zinc.100` highlight and the `aria-selected`
|
|
250
|
+
// that carries the state to a screen reader.
|
|
251
|
+
//
|
|
252
|
+
// This used to paint `zinc.800` with inverted text and pass neither. That is
|
|
253
|
+
// the PRIMARY BUTTON treatment — the loudest thing the system has, spent on a
|
|
254
|
+
// row that reports state rather than offering an action, so the selected
|
|
255
|
+
// preset outshouted the panel's own Done button. And because no `selected` or
|
|
256
|
+
// `current` ever reached `MenuButton`, the active preset announced nothing at
|
|
257
|
+
// all: the state was visible only to someone who could see the fill.
|
|
258
|
+
const renderPreset = (id: PresetId) => (
|
|
259
|
+
<MenuButton
|
|
260
|
+
key={id}
|
|
261
|
+
role="option"
|
|
262
|
+
selected={activePresetId === id}
|
|
263
|
+
title={<Text size="sm">{presetLabel(id, labels)}</Text>}
|
|
264
|
+
onPress={() => handlePresetSelect(id)}
|
|
265
|
+
style={styles.presetItem}
|
|
266
|
+
/>
|
|
267
|
+
);
|
|
256
268
|
|
|
257
269
|
return (
|
|
258
270
|
<View style={[styles.container, { flexDirection: screenSize.small ? "column" : "row" }]}>
|
|
259
271
|
{/* Presets sidebar (desktop only) */}
|
|
260
272
|
{!screenSize.small && (
|
|
261
|
-
<View
|
|
273
|
+
<View
|
|
274
|
+
style={styles.presetsSidebar}
|
|
275
|
+
accessibilityLabel={labels.selectDateRange}
|
|
276
|
+
// `listbox` is valid ARIA but absent from RN's Role enum; rn-web forwards it.
|
|
277
|
+
role={"listbox" as "list"}
|
|
278
|
+
>
|
|
279
|
+
{PRESET_IDS.map(renderPreset)}
|
|
280
|
+
</View>
|
|
262
281
|
)}
|
|
263
282
|
|
|
264
283
|
{/* Calendar and time pickers */}
|
|
@@ -322,7 +341,11 @@ const styles = StyleSheet.create({
|
|
|
322
341
|
gap: 16,
|
|
323
342
|
},
|
|
324
343
|
presetsSidebar: {
|
|
325
|
-
|
|
344
|
+
// 128, not 140: the calendars come to ~617px and a popover is only anchored
|
|
345
|
+
// (rather than a bottom sheet) at viewports of 768 and up, so the sidebar
|
|
346
|
+
// has ~135px to spend before the panel is wider than the narrowest screen
|
|
347
|
+
// that shows it. The longest preset label sits well inside 128.
|
|
348
|
+
minWidth: 128,
|
|
326
349
|
},
|
|
327
350
|
presetsScrollView: {
|
|
328
351
|
borderTopWidth: 1,
|
|
@@ -337,9 +360,6 @@ const styles = StyleSheet.create({
|
|
|
337
360
|
marginHorizontal: 0,
|
|
338
361
|
marginBottom: 2,
|
|
339
362
|
},
|
|
340
|
-
presetItemActive: {
|
|
341
|
-
backgroundColor: colors.zinc["800"],
|
|
342
|
-
},
|
|
343
363
|
mainContent: {
|
|
344
364
|
flex: 1,
|
|
345
365
|
},
|
|
@@ -5,7 +5,6 @@ import type { DateFilterValue } from "./date_filter";
|
|
|
5
5
|
// `now`, never an ambient clock.
|
|
6
6
|
|
|
7
7
|
export type PresetId =
|
|
8
|
-
| "custom"
|
|
9
8
|
| "today"
|
|
10
9
|
| "yesterday"
|
|
11
10
|
| "tomorrow"
|
|
@@ -13,7 +12,17 @@ export type PresetId =
|
|
|
13
12
|
| "this_month"
|
|
14
13
|
| "last_month";
|
|
15
14
|
|
|
16
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* Display order. Every id here SETS a range — that is what makes the list a set
|
|
17
|
+
* of presets rather than a set of modes.
|
|
18
|
+
*
|
|
19
|
+
* "custom" used to sit at the end and it was the one entry that set nothing: it
|
|
20
|
+
* CLEARED the value. So it lit up whenever the range matched no preset — which
|
|
21
|
+
* is the normal state for any hand-picked range — and clicking the lit item,
|
|
22
|
+
* the obvious move for "let me pick my own dates", threw the range away. The
|
|
23
|
+
* calendar above it is the custom picker, and the panel's Clear button is the
|
|
24
|
+
* clear; the item was a third name for two controls that were already there.
|
|
25
|
+
*/
|
|
17
26
|
export const PRESET_IDS: PresetId[] = [
|
|
18
27
|
"today",
|
|
19
28
|
"yesterday",
|
|
@@ -21,7 +30,6 @@ export const PRESET_IDS: PresetId[] = [
|
|
|
21
30
|
"this_week",
|
|
22
31
|
"this_month",
|
|
23
32
|
"last_month",
|
|
24
|
-
"custom",
|
|
25
33
|
];
|
|
26
34
|
|
|
27
35
|
function startOfDay(date: Date): Date {
|
|
@@ -66,12 +74,11 @@ function range(start: Date, end: Date): DateFilterValue {
|
|
|
66
74
|
}
|
|
67
75
|
|
|
68
76
|
/**
|
|
69
|
-
* Resolve a preset to a concrete date range relative to `now`.
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
* resolved range round-trips back to the same preset.
|
|
77
|
+
* Resolve a preset to a concrete date range relative to `now`. Total — every
|
|
78
|
+
* `PresetId` names a range. The boundary math is identical to the view-page
|
|
79
|
+
* filter so a resolved range round-trips back to the same preset.
|
|
73
80
|
*/
|
|
74
|
-
export function getPresetValue(id: PresetId, now: Date): DateFilterValue
|
|
81
|
+
export function getPresetValue(id: PresetId, now: Date): DateFilterValue {
|
|
75
82
|
switch (id) {
|
|
76
83
|
case "today":
|
|
77
84
|
return range(startOfDay(now), endOfDay(now));
|
|
@@ -94,7 +101,5 @@ export function getPresetValue(id: PresetId, now: Date): DateFilterValue | null
|
|
|
94
101
|
d.setMonth(d.getMonth() - 1);
|
|
95
102
|
return range(startOfMonth(d), endOfMonth(d));
|
|
96
103
|
}
|
|
97
|
-
case "custom":
|
|
98
|
-
return null;
|
|
99
104
|
}
|
|
100
105
|
}
|
package/src/locale.tsx
CHANGED
|
@@ -308,7 +308,7 @@ export const en: LoticsLocale = {
|
|
|
308
308
|
year: "Year", month: "Month", day: "Day", hour: "Hour", minute: "Minute", dayPeriod: "AM/PM",
|
|
309
309
|
today: "Today", yesterday: "Yesterday", tomorrow: "Tomorrow",
|
|
310
310
|
thisWeek: "This week", thisMonth: "This month", lastMonth: "Last month",
|
|
311
|
-
|
|
311
|
+
from: "From", to: "To",
|
|
312
312
|
selectDateRange: "Select date range", selectDate: "Select date",
|
|
313
313
|
clear: "Clear", done: "Done", placeholder: "All time",
|
|
314
314
|
},
|
|
@@ -479,7 +479,7 @@ export const vi: LoticsLocale = {
|
|
|
479
479
|
year: "Năm", month: "Tháng", day: "Ngày", hour: "Giờ", minute: "Phút", dayPeriod: "SA/CH",
|
|
480
480
|
today: "Hôm nay", yesterday: "Hôm qua", tomorrow: "Ngày mai",
|
|
481
481
|
thisWeek: "Tuần này", thisMonth: "Tháng này", lastMonth: "Tháng trước",
|
|
482
|
-
|
|
482
|
+
from: "Từ", to: "Đến",
|
|
483
483
|
selectDateRange: "Chọn khoảng ngày", selectDate: "Chọn ngày",
|
|
484
484
|
clear: "Xóa", done: "Xong", placeholder: "Tất cả thời gian",
|
|
485
485
|
},
|