@lotics/ui 44.3.0 → 44.5.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/MIGRATION.md +46 -0
- package/docs/catalog.md +14 -4
- package/docs/composition.md +35 -2
- package/docs/reviewing.md +14 -0
- package/examples/tpl_item_list.tsx +125 -47
- package/package.json +1 -1
- package/src/locale.tsx +0 -4
- package/src/member_chip.tsx +13 -23
- package/src/member_peek.tsx +6 -3
- package/src/pagination.tsx +28 -17
- package/src/peek.tsx +37 -5
package/MIGRATION.md
CHANGED
|
@@ -4,6 +4,52 @@ 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
|
+
## 44.5.0 — `Pagination` drops the page index
|
|
8
|
+
|
|
9
|
+
The pager rendered a range AND a page index — `16–30 of 45` beside `Page 2 of 3`. Both answer
|
|
10
|
+
"where am I", and the second enables no decision the first does not: how much is left is in the
|
|
11
|
+
total, and whether this is the end is in the disabled arrow. The index is gone; the range is the
|
|
12
|
+
whole label.
|
|
13
|
+
|
|
14
|
+
**`PaginationLabels.pageWithTotal` and `.page` are removed.** Delete them from any `labels` object
|
|
15
|
+
— every other field is unchanged, so that is the whole edit.
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
// before
|
|
19
|
+
labels={{ rangeWithTotal: (s, e, t) => `${s}–${e} trên ${t}`,
|
|
20
|
+
pageWithTotal: (p, n) => `Trang ${p} / ${n}`, page: (p) => `Trang ${p}`,
|
|
21
|
+
previous: "Trước", next: "Sau" }}
|
|
22
|
+
// after
|
|
23
|
+
labels={{ rangeWithTotal: (s, e, t) => `${s}–${e} trên ${t}`, previous: "Trước", next: "Sau" }}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Not breaking, but do it in the same pass: the range is now the register's only statement of the
|
|
27
|
+
total, so a `Table` paired with the pager takes **`counted`** instead of `count`. Left as `count`,
|
|
28
|
+
the matched total prints twice a few rows apart.
|
|
29
|
+
|
|
30
|
+
## 44.4.0 — `MemberChip`'s `marker` is gone
|
|
31
|
+
|
|
32
|
+
A minor that broke a type, recorded here under its exact version rather than held
|
|
33
|
+
for a major: `marker` existed for one release, had a single consumer inside the
|
|
34
|
+
kit, and underlined the chip's name to say "this opens something".
|
|
35
|
+
|
|
36
|
+
Delete the prop. A chip that opens something is a `MemberPeek`, which now carries
|
|
37
|
+
its own affordance through the trigger's control-band surface — the underline was
|
|
38
|
+
decorating the text to describe the container.
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
// before
|
|
42
|
+
<MemberChip name={m.name} marker />
|
|
43
|
+
|
|
44
|
+
// after
|
|
45
|
+
<MemberPeek name={m.name} identity={m.email} groups={m.groups} />
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
A chip carrying `secondary` also takes a larger mark now (`lg` rather than the
|
|
49
|
+
requested `md`), because two lines of text stand taller than a 28px avatar. Single
|
|
50
|
+
-line chips are unchanged. Nothing to do — but a layout that hard-coded 28px
|
|
51
|
+
beside a two-line chip will need the number.
|
|
52
|
+
|
|
7
53
|
## 44.0.0 — `Finding` is horizontal, and `FindingComparison` is gone
|
|
8
54
|
|
|
9
55
|
`Finding` stacked six blocks — badge, title, detail, a labelled row per side, a hairline, the
|
package/docs/catalog.md
CHANGED
|
@@ -1295,9 +1295,16 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1295
1295
|
reference is a FIELD, not a section — one pointer to one other row never earns a heading
|
|
1296
1296
|
and a rail entry. Worked example: [`tpl_record`](../examples/tpl_record.tsx) § General
|
|
1297
1297
|
(customer) and its Transport parties.
|
|
1298
|
-
- **`peek`** — `Peek`: drill-down for
|
|
1299
|
-
|
|
1300
|
-
|
|
1298
|
+
- **`peek`** — `Peek`: drill-down for a reference — press a name/id where it appears and get its
|
|
1299
|
+
details in an anchored popover, without leaving the screen; keep the content a summary with ONE
|
|
1300
|
+
action to the full record. **`variant` picks the register the TRIGGER sits in, and getting it
|
|
1301
|
+
wrong is visible:** `text` (default) boxes the child at the 32px chip scale and bleeds the extra
|
|
1302
|
+
height back out, so a reference inside a line of reading never grows that line; `control` takes
|
|
1303
|
+
the full `CONTROL_HEIGHT` band and the control radius, for a token that is a control-band citizen
|
|
1304
|
+
— anything built on `MemberChip`, whose `md` avatar is sized to `CONTROL_CONTENT_HEIGHT` for
|
|
1305
|
+
exactly that. On the wrong one a chip under-fills the band and stands a different height from the
|
|
1306
|
+
same chip un-peeked beside it. The horizontal bleed is common to both, so a peekable token keeps
|
|
1307
|
+
the surface's left edge.
|
|
1301
1308
|
|
|
1302
1309
|
### Lists, tables & registers
|
|
1303
1310
|
|
|
@@ -1359,7 +1366,10 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1359
1366
|
- **`data_grid`** — `DataGrid` + `gridRowStyle`: the inline-managed grouped table (see
|
|
1360
1367
|
[Tabular data](#tabular-data--pick-by-scale--intent)); `labels` localizes the sort-header
|
|
1361
1368
|
a11y via `SortHeaderLabels`.
|
|
1362
|
-
- **`pagination`** — `Pagination` (+ `PaginationLabels`): the register's pager
|
|
1369
|
+
- **`pagination`** — `Pagination` (+ `PaginationLabels`): the register's pager — a range and two
|
|
1370
|
+
arrows, no page index. ONE per register, ABOVE the rows on the `SummaryLine` row; its range is
|
|
1371
|
+
the register's count (pair with `Table` `counted`), and `onPageChange` owes a scroll reset the
|
|
1372
|
+
control cannot do itself — see [composition.md](./composition.md) §register.
|
|
1363
1373
|
- **`column_filter`** — `ColumnFilter` + `columnFilterToConditions` +
|
|
1364
1374
|
`isColumnFilterActive` + `columnFilterSummary`: the typed per-column filter pill; for a
|
|
1365
1375
|
register filtering on several columns.
|
package/docs/composition.md
CHANGED
|
@@ -587,8 +587,41 @@ hook (`@lotics/ui/use_selection`: `selected` / `has` / `count` / `toggle` / `set
|
|
|
587
587
|
`allSelected` / `indeterminate` / `clear`) — gating which rows CAN be ticked stays with you: pass
|
|
588
588
|
only the selectable ids to `setAll`/`allSelected`/`indeterminate` (a non-selectable row gets a
|
|
589
589
|
disabled checkbox, keeping the gutter aligned). Selection persists across pages, so `count` is the
|
|
590
|
-
running total. `tpl_item_list` is the reference. Paginate OUTSIDE (slice + `Pagination`
|
|
591
|
-
|
|
590
|
+
running total. `tpl_item_list` is the reference. Paginate OUTSIDE (slice + `Pagination`).
|
|
591
|
+
|
|
592
|
+
**A paginated register owes the reader a scroll reset, and it is not the pager's to give.** Putting
|
|
593
|
+
the pager under the rows is inherited from full page loads, where the browser reset the scroll for
|
|
594
|
+
free; an SPA swaps the rows under a viewport that does not move, so paging from the bottom lands on
|
|
595
|
+
the LAST rows of the next page with its first ones already scrolled past. Every control that changes
|
|
596
|
+
WHICH rows are on screen owes the same reset — page, tab, search, sort, each filter — so route them
|
|
597
|
+
all through ONE function that moves the window and scrolls (`goToPage` / `resetPaging` in
|
|
598
|
+
`tpl_item_list`), never a `setPage(0)` at each call site that also has to remember to scroll.
|
|
599
|
+
|
|
600
|
+
**ONE pager, ABOVE the rows.** Once the reset returns the viewport to the top on every press, the
|
|
601
|
+
top is where the reader's cursor already is. A second copy under the rows is the same state said
|
|
602
|
+
twice, and two identical clusters of arrows read as two controls.
|
|
603
|
+
|
|
604
|
+
**Put it on the `SummaryLine` row, not a row of its own** — summary at `flex: 1`, pager sized to its
|
|
605
|
+
content, the row aligned `flex-start` so the arrows stay on the summary's FIRST line when it wraps.
|
|
606
|
+
Both describe the window the toolbar just set (what matched, and which slice is on screen), so they
|
|
607
|
+
are one line. A pager given its own row is a small cluster against a mostly empty band — measure it
|
|
608
|
+
before believing the row is needed; a four-item summary and a full pager leave a quarter of a
|
|
609
|
+
1000px band spare.
|
|
610
|
+
|
|
611
|
+
Never hand `Pagination` the full width with an empty left slot. It gives its range `flex: 1` so a
|
|
612
|
+
band whose left slot HOLDS something pushes the arrows to the far edge; with nothing there, that
|
|
613
|
+
same rule strands the range hundreds of pixels from the arrows it belongs to — two halves of one
|
|
614
|
+
control reading as two unrelated things on a line.
|
|
615
|
+
|
|
616
|
+
**The pager's range IS the register's count** (`16–30 of 45`), so pair it with `Table` `counted`,
|
|
617
|
+
never `count` — the latter prints the matched total again a few rows below, and one number in two
|
|
618
|
+
places is two numbers that can disagree. Row totals beside the pager go the same way when a
|
|
619
|
+
`SummaryLine` above already states them.
|
|
620
|
+
|
|
621
|
+
There is deliberately no page index. `Page 2 of 3` beside `16–30 of 45` answers the same question a
|
|
622
|
+
second way and enables no decision the range does not — how much is left is in the total, whether
|
|
623
|
+
this is the end is in the disabled arrow. A control that is reached for rarely (a register built
|
|
624
|
+
around search and filters is meant to be NARROWED, not paged) earns one label, not two.
|
|
592
625
|
|
|
593
626
|
Right-hand columns align only if every trailing element is FIXED-width — give each trailing action
|
|
594
627
|
a fixed `width`, so amount/status columns don't jitter.
|
package/docs/reviewing.md
CHANGED
|
@@ -419,6 +419,20 @@ is the usual cause.
|
|
|
419
419
|
Note the RN-Web trap: once a `Text` contains a nested pressable, `numberOfLines` degrades to bare
|
|
420
420
|
`overflow: hidden` and loses the ellipsis.
|
|
421
421
|
|
|
422
|
+
### Measure INK, not the box — a stretched label hides the gap it sits in
|
|
423
|
+
|
|
424
|
+
```js
|
|
425
|
+
// getBoundingClientRect on an RN-Web Text returns the DIV, which stretches to
|
|
426
|
+
// its flex slot. Range over the contents to get where the glyphs actually end.
|
|
427
|
+
(sel) => { const rg = document.createRange(); rg.selectNodeContents(document.querySelector(sel)); return rg.getBoundingClientRect(); }
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
A label inside a `flex: 1` slot measures as wide as the slot, so a box-to-box gap between it and
|
|
431
|
+
its neighbour reads as the container's `gap` — 12px — while the reader sees hundreds of pixels of
|
|
432
|
+
void between the last glyph and the next element. Any gap RATIO taken across a stretching text is
|
|
433
|
+
wrong in the reassuring direction. Use this whenever one side of a measured gap is text that could
|
|
434
|
+
be filling a flex slot rather than sizing to its content.
|
|
435
|
+
|
|
422
436
|
### Grouping — is one `gap` flattening the hierarchy?
|
|
423
437
|
|
|
424
438
|
```js
|
|
@@ -159,6 +159,34 @@ const HO_SO: HoSo[] = [
|
|
|
159
159
|
{ ma: "RC-2026-0061", binhLuan: 1, khach: "Harbor Industries", dienThoai: "(555) 047-5829", trangThai: "dangxuly", phi: 6_800_000, daThu: true, ngayThu: "11/06", ngayNhan: "10/06", phuTrach: "Maria Lopez" },
|
|
160
160
|
{ ma: "RC-2026-0062", binhLuan: 0, khach: "Grace Holloway", dienThoai: "(555) 826-3050", trangThai: "hoantat", phi: 3_100_000, daThu: true, ngayThu: "05/06", ngayNhan: "31/05", phuTrach: "David Park" },
|
|
161
161
|
{ ma: "RC-2026-0063", binhLuan: 3, khach: "Olivia Manning", dienThoai: "(555) 631-2904", trangThai: "dangxuly", phi: 1_200_000, daThu: false, ngayNhan: "11/06", phuTrach: "Sarah Chen" },
|
|
162
|
+
// The book runs past one SCREEN on purpose, not just past one page. A register
|
|
163
|
+
// whose page fits the viewport cannot exhibit the defect the paging controls
|
|
164
|
+
// below are shaped around — the reader never has to scroll, so it never
|
|
165
|
+
// matters where the viewport is left when the rows change. This example ran on
|
|
166
|
+
// 23 records at 8 a page for exactly that reason, and the reset was missing
|
|
167
|
+
// from the reference the whole time.
|
|
168
|
+
{ ma: "RC-2026-0064", binhLuan: 0, khach: "Marcus Webb", dienThoai: "(555) 318-7742", trangThai: "hoantat", phi: 2_500_000, daThu: true, ngayThu: "07/06", ngayNhan: "01/06", phuTrach: "David Park" },
|
|
169
|
+
{ ma: "RC-2026-0065", binhLuan: 2, khach: "Ironwood Textiles", dienThoai: "(555) 924-6018", trangThai: "dangxuly", phi: 5_600_000, daThu: false, ngayNhan: "12/06", phuTrach: "Maria Lopez" },
|
|
170
|
+
{ ma: "RC-2026-0066", binhLuan: 0, khach: "Helen Vargas", dienThoai: "(555) 673-2295", trangThai: "chobosung", phi: 1_800_000, daThu: true, ngayThu: "11/06", ngayNhan: "10/06", phuTrach: "Sarah Chen" },
|
|
171
|
+
{ ma: "RC-2026-0067", binhLuan: 4, khach: "Peter Nakamura", dienThoai: "(555) 405-8830", trangThai: "dangxuly", phi: 3_100_000, daThu: true, ngayThu: "12/06", ngayNhan: "11/06", phuTrach: "James Walker" },
|
|
172
|
+
{ ma: "RC-2026-0068", binhLuan: 0, khach: "Redstone Millwork", dienThoai: "(555) 217-5546", trangThai: "hoantat", phi: 6_800_000, daThu: true, ngayThu: "04/06", ngayNhan: "30/05", phuTrach: "David Park" },
|
|
173
|
+
{ ma: "RC-2026-0069", binhLuan: 1, khach: "Sandra Piper", dienThoai: "(555) 860-3971", trangThai: "dangxuly", phi: 1_200_000, daThu: false, ngayNhan: "12/06", phuTrach: "Maria Lopez" },
|
|
174
|
+
{ ma: "RC-2026-0070", binhLuan: 0, khach: "Owen Fitzgerald", dienThoai: "(555) 592-1487", trangThai: "chobosung", phi: 2_500_000, daThu: false, ngayNhan: "13/06", phuTrach: "Sarah Chen" },
|
|
175
|
+
{ ma: "RC-2026-0071", binhLuan: 3, khach: "Lakeshore Dairy", dienThoai: "(555) 736-9204", trangThai: "hoantat", phi: 4_200_000, daThu: true, ngayThu: "02/06", ngayNhan: "27/05", phuTrach: "James Walker" },
|
|
176
|
+
{ ma: "RC-2026-0072", binhLuan: 0, khach: "Deborah Quinn", dienThoai: "(555) 148-6673", trangThai: "dangxuly", phi: 1_800_000, daThu: true, ngayThu: "13/06", ngayNhan: "12/06", phuTrach: "David Park" },
|
|
177
|
+
{ ma: "RC-2026-0073", binhLuan: 2, khach: "Vincent Osei", dienThoai: "(555) 389-2015", trangThai: "chobosung", phi: 3_100_000, daThu: false, ngayNhan: "13/06", phuTrach: "Maria Lopez" },
|
|
178
|
+
{ ma: "RC-2026-0074", binhLuan: 0, khach: "Pinehill Ceramics", dienThoai: "(555) 651-4738", trangThai: "hoantat", phi: 5_600_000, daThu: true, ngayThu: "06/06", ngayNhan: "31/05", phuTrach: "Sarah Chen" },
|
|
179
|
+
{ ma: "RC-2026-0075", binhLuan: 1, khach: "Alan Whitcombe", dienThoai: "(555) 902-8564", trangThai: "dangxuly", phi: 2_500_000, daThu: false, ngayNhan: "14/06", phuTrach: "James Walker" },
|
|
180
|
+
{ ma: "RC-2026-0076", binhLuan: 0, khach: "Priya Raghavan", dienThoai: "(555) 473-1290", trangThai: "dangxuly", phi: 6_800_000, daThu: true, ngayThu: "14/06", ngayNhan: "13/06", phuTrach: "David Park" },
|
|
181
|
+
{ ma: "RC-2026-0077", binhLuan: 5, khach: "Fairview Optics", dienThoai: "(555) 260-7451", trangThai: "chobosung", phi: 1_200_000, daThu: false, ngayNhan: "14/06", phuTrach: "Maria Lopez" },
|
|
182
|
+
{ ma: "RC-2026-0078", binhLuan: 0, khach: "Gregory Salazar", dienThoai: "(555) 815-3607", trangThai: "hoantat", phi: 3_100_000, daThu: true, ngayThu: "03/06", ngayNhan: "29/05", phuTrach: "Sarah Chen" },
|
|
183
|
+
{ ma: "RC-2026-0079", binhLuan: 2, khach: "Tessa Lindqvist", dienThoai: "(555) 547-9382", trangThai: "dangxuly", phi: 4_200_000, daThu: true, ngayThu: "15/06", ngayNhan: "14/06", phuTrach: "James Walker" },
|
|
184
|
+
{ ma: "RC-2026-0080", binhLuan: 0, khach: "Copperfield Tools", dienThoai: "(555) 031-6825", trangThai: "chobosung", phi: 1_800_000, daThu: false, ngayNhan: "15/06", phuTrach: "David Park" },
|
|
185
|
+
{ ma: "RC-2026-0081", binhLuan: 1, khach: "Naomi Ferrell", dienThoai: "(555) 794-2158", trangThai: "hoantat", phi: 2_500_000, daThu: true, ngayThu: "05/06", ngayNhan: "01/06", phuTrach: "Maria Lopez" },
|
|
186
|
+
{ ma: "RC-2026-0082", binhLuan: 0, khach: "Bridgeport Marine", dienThoai: "(555) 368-5093", trangThai: "dangxuly", phi: 5_600_000, daThu: false, ngayNhan: "16/06", phuTrach: "Sarah Chen" },
|
|
187
|
+
{ ma: "RC-2026-0083", binhLuan: 3, khach: "Ethan Castellanos", dienThoai: "(555) 620-4716", trangThai: "dangxuly", phi: 1_200_000, daThu: true, ngayThu: "16/06", ngayNhan: "15/06", phuTrach: "James Walker" },
|
|
188
|
+
{ ma: "RC-2026-0084", binhLuan: 0, khach: "Willow Creek Farms", dienThoai: "(555) 187-9340", trangThai: "chobosung", phi: 3_100_000, daThu: false, ngayNhan: "16/06", phuTrach: "David Park" },
|
|
189
|
+
{ ma: "RC-2026-0085", binhLuan: 2, khach: "Simone Beaufort", dienThoai: "(555) 452-8671", trangThai: "hoantat", phi: 6_800_000, daThu: true, ngayThu: "07/06", ngayNhan: "02/06", phuTrach: "Maria Lopez" },
|
|
162
190
|
];
|
|
163
191
|
|
|
164
192
|
const TABS: { value: Tab; label: string }[] = [
|
|
@@ -168,7 +196,9 @@ const TABS: { value: Tab; label: string }[] = [
|
|
|
168
196
|
{ value: "hoantat", label: "Complete" },
|
|
169
197
|
];
|
|
170
198
|
|
|
171
|
-
|
|
199
|
+
// Fifteen rows at the register's rhythm is taller than the viewport, which is
|
|
200
|
+
// the point: paging only has ergonomics once a page is something you SCROLL.
|
|
201
|
+
const PAGE_SIZE = 15;
|
|
172
202
|
|
|
173
203
|
// Distinct case handlers — the secondary filter dimension (a multi-select FilterChip).
|
|
174
204
|
const ASSIGNEES = [...new Set(HO_SO.map((r) => r.phuTrach))].map((n) => ({ label: n, value: n }));
|
|
@@ -1041,7 +1071,34 @@ export function TplItemList() {
|
|
|
1041
1071
|
// The register rows are state: the create dialog PREPENDS a real row and
|
|
1042
1072
|
// opens its drawer — create-then-refine.
|
|
1043
1073
|
const [rows, setRows] = useState<HoSo[]>(HO_SO);
|
|
1044
|
-
|
|
1074
|
+
// Past the LAST seeded code, not past the old one — a created row that reuses
|
|
1075
|
+
// an existing `ma` collides on the register's key and on every lookup by code.
|
|
1076
|
+
const nextMa = useRef(86);
|
|
1077
|
+
|
|
1078
|
+
/** The page's scroll container, so a control that changes the rows can put the
|
|
1079
|
+
* reader back at the first of them. */
|
|
1080
|
+
const scrollRef = useRef<ScrollView>(null);
|
|
1081
|
+
|
|
1082
|
+
/**
|
|
1083
|
+
* Move the window, and return the viewport to the top of it.
|
|
1084
|
+
*
|
|
1085
|
+
* The scroll is HALF the act, not a courtesy on top of it. Bottom pagination
|
|
1086
|
+
* is inherited from full page loads, where the browser reset the scroll for
|
|
1087
|
+
* free; an SPA swaps the rows under a viewport that does not move, so paging
|
|
1088
|
+
* from the bottom lands the reader on the LAST rows of the next page with its
|
|
1089
|
+
* first ones already behind them.
|
|
1090
|
+
*
|
|
1091
|
+
* EVERY control that changes which rows are on screen goes through here —
|
|
1092
|
+
* paging, tabs, search, sort, each filter — rather than each one calling
|
|
1093
|
+
* `setPage(0)` and remembering the scroll. That list is the reason this is one
|
|
1094
|
+
* function: nine call sites each responsible for two things is nine chances to
|
|
1095
|
+
* do one of them, and the one that gets forgotten fails silently.
|
|
1096
|
+
*/
|
|
1097
|
+
const goToPage = (p: number) => {
|
|
1098
|
+
setPage(p);
|
|
1099
|
+
scrollRef.current?.scrollTo({ y: 0, animated: false });
|
|
1100
|
+
};
|
|
1101
|
+
const resetPaging = () => goToPage(0);
|
|
1045
1102
|
const addRow = (khach: string, dienThoai: string, phi: number): string => {
|
|
1046
1103
|
const now = new Date();
|
|
1047
1104
|
const ma = `RC-2026-${String(nextMa.current++).padStart(4, "0")}`;
|
|
@@ -1067,7 +1124,7 @@ export function TplItemList() {
|
|
|
1067
1124
|
setAssignee([]);
|
|
1068
1125
|
setFeeStatus(null);
|
|
1069
1126
|
setSort(null);
|
|
1070
|
-
|
|
1127
|
+
resetPaging();
|
|
1071
1128
|
};
|
|
1072
1129
|
const createRecord = (khach: string, dienThoai: string, phi: number) => {
|
|
1073
1130
|
const ma = addRow(khach, dienThoai, phi);
|
|
@@ -1101,16 +1158,16 @@ export function TplItemList() {
|
|
|
1101
1158
|
// meaningless under the new one.
|
|
1102
1159
|
const chonTab = (t: Tab) => {
|
|
1103
1160
|
setTab(t);
|
|
1104
|
-
|
|
1161
|
+
resetPaging();
|
|
1105
1162
|
};
|
|
1106
1163
|
const doiTimKiem = (s: string) => {
|
|
1107
1164
|
setSearch(s);
|
|
1108
|
-
|
|
1165
|
+
resetPaging();
|
|
1109
1166
|
};
|
|
1110
1167
|
// One sort at a time; the third toggle on a column clears it.
|
|
1111
1168
|
const onSort = (key: string) => {
|
|
1112
1169
|
setSort(cycleSort(sort, key));
|
|
1113
|
-
|
|
1170
|
+
resetPaging();
|
|
1114
1171
|
};
|
|
1115
1172
|
|
|
1116
1173
|
// Search + status tab filter FIRST, then paging over the filtered ordering.
|
|
@@ -1140,7 +1197,6 @@ export function TplItemList() {
|
|
|
1140
1197
|
: key === "ngayNhan" ? r.ngayNhan.split("/").reverse().join("")
|
|
1141
1198
|
: r.ma,
|
|
1142
1199
|
);
|
|
1143
|
-
const tongPhi = filtered.reduce((s, r) => s + r.phi, 0);
|
|
1144
1200
|
const pageRows = sorted.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE);
|
|
1145
1201
|
const hasMore = (page + 1) * PAGE_SIZE < sorted.length;
|
|
1146
1202
|
|
|
@@ -1171,7 +1227,7 @@ export function TplItemList() {
|
|
|
1171
1227
|
on top); the accent ring frames the page on drag. A real app opts in
|
|
1172
1228
|
per-app — this page is the reference shape. */}
|
|
1173
1229
|
<FileDropTarget onFiles={openIntakeSeeded} paste style={{ flex: 1 }}>
|
|
1174
|
-
<ScrollView style={{ flex: 1, backgroundColor: colors.white }} contentContainerStyle={{ padding: 28 }}>
|
|
1230
|
+
<ScrollView ref={scrollRef} style={{ flex: 1, backgroundColor: colors.white }} contentContainerStyle={{ padding: 28 }}>
|
|
1175
1231
|
<View style={{ width: "100%", maxWidth: 1040, alignSelf: "center", gap: 16 }}>
|
|
1176
1232
|
{/* The page band — the screen's TYPE ANCHOR before it is a label. Without
|
|
1177
1233
|
it this page's largest text is 16px against a 12px floor, and a 1.33x
|
|
@@ -1208,7 +1264,7 @@ export function TplItemList() {
|
|
|
1208
1264
|
<FilterChip
|
|
1209
1265
|
label="Assignee"
|
|
1210
1266
|
summary={assigneeSummary}
|
|
1211
|
-
onClear={() => { setAssignee([]);
|
|
1267
|
+
onClear={() => { setAssignee([]); resetPaging(); }}
|
|
1212
1268
|
clearLabel="Clear assignee filter"
|
|
1213
1269
|
>
|
|
1214
1270
|
<OptionList
|
|
@@ -1216,7 +1272,7 @@ export function TplItemList() {
|
|
|
1216
1272
|
multi
|
|
1217
1273
|
options={ASSIGNEES}
|
|
1218
1274
|
value={assignee}
|
|
1219
|
-
onValueChange={(v) => { setAssignee(v);
|
|
1275
|
+
onValueChange={(v) => { setAssignee(v); resetPaging(); }}
|
|
1220
1276
|
/>
|
|
1221
1277
|
</FilterChip>
|
|
1222
1278
|
{/* Single-select sibling — the render-prop `close` shuts the popover on
|
|
@@ -1224,7 +1280,7 @@ export function TplItemList() {
|
|
|
1224
1280
|
<FilterChip
|
|
1225
1281
|
label="Payment"
|
|
1226
1282
|
summary={feeStatus === "paid" ? "Paid" : feeStatus === "unpaid" ? "Unpaid" : undefined}
|
|
1227
|
-
onClear={() => { setFeeStatus(null);
|
|
1283
|
+
onClear={() => { setFeeStatus(null); resetPaging(); }}
|
|
1228
1284
|
clearLabel="Clear payment filter"
|
|
1229
1285
|
>
|
|
1230
1286
|
{({ close }) => (
|
|
@@ -1232,7 +1288,7 @@ export function TplItemList() {
|
|
|
1232
1288
|
search={{ mode: "none" }}
|
|
1233
1289
|
options={[{ value: "paid", label: "Paid" }, { value: "unpaid", label: "Unpaid" }]}
|
|
1234
1290
|
value={feeStatus}
|
|
1235
|
-
onValueChange={(v) => { setFeeStatus(v);
|
|
1291
|
+
onValueChange={(v) => { setFeeStatus(v); resetPaging(); }}
|
|
1236
1292
|
onRequestClose={close}
|
|
1237
1293
|
/>
|
|
1238
1294
|
)}
|
|
@@ -1243,7 +1299,7 @@ export function TplItemList() {
|
|
|
1243
1299
|
<ColumnFilter
|
|
1244
1300
|
column={{ key: "phi", label: "Fee", type: "number" }}
|
|
1245
1301
|
value={feeAmount}
|
|
1246
|
-
onChange={(v) => { setFeeAmount(v);
|
|
1302
|
+
onChange={(v) => { setFeeAmount(v); resetPaging(); }}
|
|
1247
1303
|
/>
|
|
1248
1304
|
</View>
|
|
1249
1305
|
{/* the intake TRIGGER — the register's whole-surface drop/paste opens
|
|
@@ -1262,25 +1318,66 @@ export function TplItemList() {
|
|
|
1262
1318
|
It is not a page-band subtitle for the same reason. A subtitle
|
|
1263
1319
|
describes the screen; these describe the QUERY, and the query is what
|
|
1264
1320
|
the toolbar above just wrote. */}
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1321
|
+
{/* THE SUMMARY ROW IS THE PAGING ROW. Both describe the window the
|
|
1322
|
+
toolbar above just set — what matched, and which slice of it is on
|
|
1323
|
+
screen — so they are one line, not two.
|
|
1324
|
+
|
|
1325
|
+
PAGING SITS ABOVE THE ROWS, and there is only one of it. Under them is
|
|
1326
|
+
where it has always gone, and that is inherited from full page loads:
|
|
1327
|
+
the browser reset the scroll, so a reader who pressed Next at the
|
|
1328
|
+
bottom arrived at the top of the next page for free. An SPA swaps the
|
|
1329
|
+
rows under a viewport that does not move, so the same press now lands
|
|
1330
|
+
on that page's LAST rows with its first ones already scrolled past.
|
|
1331
|
+
`goToPage` restores the reset — and once the viewport returns here on
|
|
1332
|
+
every press, here is where the control should be: under the reader's
|
|
1333
|
+
cursor, not a page-height below it. A second copy at the bottom would
|
|
1334
|
+
be the same state said twice.
|
|
1335
|
+
|
|
1336
|
+
It carries the RANGE and nothing else. The totals that used to ride
|
|
1337
|
+
the old footer (`45 records, ₫X in fees`) said what this summary
|
|
1338
|
+
already says, and `Table`'s own `count` prints the matched total in
|
|
1339
|
+
the eyebrow below.
|
|
1340
|
+
|
|
1341
|
+
The pager sizes to its CONTENT and the summary takes `flex: 1`.
|
|
1342
|
+
Reversed — the pager handed the full width — its own `flex: 1` range
|
|
1343
|
+
stretches and strands the range hundreds of pixels from the arrows it
|
|
1344
|
+
belongs to, two halves of one control reading as two things. Given a
|
|
1345
|
+
row of its own it read worse still: 236px of control against 778px of
|
|
1346
|
+
nothing. `flex-start` keeps the arrows on the summary's FIRST line
|
|
1347
|
+
when the summary wraps at a narrow width. */}
|
|
1348
|
+
<View style={{ flexDirection: "row", alignItems: "flex-start", gap: 24 }}>
|
|
1349
|
+
<View style={{ flex: 1, minWidth: 0 }}>
|
|
1350
|
+
<SummaryLine
|
|
1351
|
+
items={[
|
|
1352
|
+
{ label: "processing", value: dem("dangxuly") },
|
|
1353
|
+
{ label: "awaiting docs", value: dem("chobosung"), tone: "warning", info: "Records waiting on customer paperwork — they aren't complete, so they can't be ticked for export until the docs arrive." },
|
|
1354
|
+
{ label: "complete", value: dem("hoantat") },
|
|
1355
|
+
{ label: "fees collected", value: phiDaThu, format: "currency", compact: true },
|
|
1356
|
+
]}
|
|
1357
|
+
/>
|
|
1358
|
+
</View>
|
|
1359
|
+
<Pagination
|
|
1360
|
+
page={page}
|
|
1361
|
+
pageSize={PAGE_SIZE}
|
|
1362
|
+
rowCount={pageRows.length}
|
|
1363
|
+
hasMore={hasMore}
|
|
1364
|
+
total={filtered.length}
|
|
1365
|
+
onPageChange={goToPage}
|
|
1366
|
+
/>
|
|
1367
|
+
</View>
|
|
1368
|
+
|
|
1369
|
+
{/* the register — eyebrow band (Table header) → list rows */}
|
|
1275
1370
|
<View>
|
|
1276
|
-
{/* THE COUNTED REGISTER
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1371
|
+
{/* THE COUNTED REGISTER — `counted`, not `count`, because the PAGER
|
|
1372
|
+
states the total now (`16–30 of 45`) and a number printed twice a
|
|
1373
|
+
few rows apart is two numbers that can disagree. `counted` keeps the
|
|
1374
|
+
ordinal gutter reserved and leaves the figure to the row that owns
|
|
1375
|
+
it. Each row's `ordinal` still continues across pages rather than
|
|
1376
|
+
restarting at 1, so the last row of page 2 reads 30, not 15. */}
|
|
1280
1377
|
<Table
|
|
1281
1378
|
columns={COLUMNS}
|
|
1282
1379
|
leading={24}
|
|
1283
|
-
|
|
1380
|
+
counted
|
|
1284
1381
|
trailing={132}
|
|
1285
1382
|
sort={sort}
|
|
1286
1383
|
onSort={onSort}
|
|
@@ -1304,25 +1401,6 @@ export function TplItemList() {
|
|
|
1304
1401
|
<EmptyState message="No matching records" hint="Try another keyword or switch status" />
|
|
1305
1402
|
</>
|
|
1306
1403
|
) : null}
|
|
1307
|
-
{/* NOT `CardFooter` — that is a Card PART, and its 20px is the card's
|
|
1308
|
-
interior gutter. There is no card here, so it put the totals 20px
|
|
1309
|
-
right of the register's own rows and of the toolbar above them.
|
|
1310
|
-
A bare table's footer is a divider and a row; borrowing furniture
|
|
1311
|
-
from a container that is not there is what put it out of true. */}
|
|
1312
|
-
<Divider />
|
|
1313
|
-
<View style={{ flexDirection: "row", alignItems: "center", gap: 12, paddingVertical: 12 }}>
|
|
1314
|
-
<Text size="sm" color="muted" tabular style={{ flex: 1 }}>
|
|
1315
|
-
{`${filtered.length} records, ${formatMoney(tongPhi)} in fees`}
|
|
1316
|
-
</Text>
|
|
1317
|
-
<Pagination
|
|
1318
|
-
page={page}
|
|
1319
|
-
pageSize={PAGE_SIZE}
|
|
1320
|
-
rowCount={pageRows.length}
|
|
1321
|
-
hasMore={hasMore}
|
|
1322
|
-
total={filtered.length}
|
|
1323
|
-
onPageChange={setPage}
|
|
1324
|
-
/>
|
|
1325
|
-
</View>
|
|
1326
1404
|
</View>
|
|
1327
1405
|
</View>
|
|
1328
1406
|
</ScrollView>
|
package/package.json
CHANGED
package/src/locale.tsx
CHANGED
|
@@ -265,8 +265,6 @@ export const en: LoticsLocale = {
|
|
|
265
265
|
pagination: {
|
|
266
266
|
rangeWithTotal: (start, end, total) => `${start}–${end} of ${total.toLocaleString()}`,
|
|
267
267
|
range: (start, end) => `${start}–${end}`,
|
|
268
|
-
pageWithTotal: (page, pageCount) => `Page ${page} of ${pageCount}`,
|
|
269
|
-
page: (page) => `Page ${page}`,
|
|
270
268
|
previous: "Previous",
|
|
271
269
|
next: "Next",
|
|
272
270
|
},
|
|
@@ -445,8 +443,6 @@ export const vi: LoticsLocale = {
|
|
|
445
443
|
pagination: {
|
|
446
444
|
rangeWithTotal: (start, end, total) => `${start}–${end} trên ${total.toLocaleString("vi-VN")}`,
|
|
447
445
|
range: (start, end) => `${start}–${end}`,
|
|
448
|
-
pageWithTotal: (page, pageCount) => `Trang ${page} / ${pageCount}`,
|
|
449
|
-
page: (page) => `Trang ${page}`,
|
|
450
446
|
previous: "Trang trước",
|
|
451
447
|
next: "Trang sau",
|
|
452
448
|
},
|
package/src/member_chip.tsx
CHANGED
|
@@ -2,7 +2,7 @@ import React from "react";
|
|
|
2
2
|
import { View, StyleSheet, StyleProp, ViewStyle } from "react-native";
|
|
3
3
|
import { Avatar } from "./avatar";
|
|
4
4
|
import { Text } from "./text";
|
|
5
|
-
import type
|
|
5
|
+
import { AVATAR_PX, type AvatarSize } from "./avatar_size";
|
|
6
6
|
import { useLoticsLocale } from "./locale";
|
|
7
7
|
|
|
8
8
|
/**
|
|
@@ -34,22 +34,6 @@ interface MemberChipProps {
|
|
|
34
34
|
* than as a current assignment. The chip keeps its height, because its usual
|
|
35
35
|
* home is a fixed row: this is a colour change, never an extra line. */
|
|
36
36
|
inactive?: boolean;
|
|
37
|
-
/**
|
|
38
|
-
* This chip is a DOOR — something opens when it is pressed. Underlines the
|
|
39
|
-
* name in neutral ink, the kit's marker for "leads somewhere, and not away"
|
|
40
|
-
* (see `TextLink` with no `href`).
|
|
41
|
-
*
|
|
42
|
-
* It exists because a pressable that paints nothing at rest is text: with no
|
|
43
|
-
* ground, no border and no underline, its only affordance is a hover wash,
|
|
44
|
-
* which does not exist on touch and has not happened yet for anyone who has
|
|
45
|
-
* not already pointed at it. The chip then measures perfectly, carries a real
|
|
46
|
-
* focus ring, and is never pressed.
|
|
47
|
-
*
|
|
48
|
-
* Set it ONLY when the chip itself opens something. A chip inside a row that
|
|
49
|
-
* navigates must not wear it — the row is the door, and marking the chip
|
|
50
|
-
* promises a second destination that is not there.
|
|
51
|
-
*/
|
|
52
|
-
marker?: boolean;
|
|
53
37
|
style?: StyleProp<ViewStyle>;
|
|
54
38
|
}
|
|
55
39
|
|
|
@@ -75,7 +59,6 @@ export function MemberChip({
|
|
|
75
59
|
secondary,
|
|
76
60
|
size = "md",
|
|
77
61
|
inactive,
|
|
78
|
-
marker,
|
|
79
62
|
style,
|
|
80
63
|
}: MemberChipProps) {
|
|
81
64
|
// `Avatar` has always taken this word from the pack; the NAME beside it was a
|
|
@@ -105,9 +88,20 @@ export function MemberChip({
|
|
|
105
88
|
// name is not a pair, and leaving it on prose leading keeps every single-line
|
|
106
89
|
// chip — every picker option, every cell — pixel-identical to before.
|
|
107
90
|
const pairLeading = secondary ? ("tight" as const) : undefined;
|
|
91
|
+
// THE MARK IS NEVER SHORTER THAN WHAT IT MARKS. Two tight lines measure ~38
|
|
92
|
+
// at this rung, so beside a 28px avatar the text overflowed the mark by ten
|
|
93
|
+
// pixels and the person's face read as a decoration hung off their name.
|
|
94
|
+
// Tight leading is already the floor — 38 is as short as the pair gets — so
|
|
95
|
+
// the fix is the MARK, not the type: a chip carrying a supporting line is a
|
|
96
|
+
// taller object, and it takes the rung that covers the stack. That rung is
|
|
97
|
+
// `lg` = `CONTROL_HEIGHT`, which is the band a two-line chip belongs in
|
|
98
|
+
// anyway. A caller already above it (`xl`, a profile header) keeps what it
|
|
99
|
+
// asked for.
|
|
100
|
+
const markSize: AvatarSize =
|
|
101
|
+
secondary && AVATAR_PX[size] < AVATAR_PX.lg ? "lg" : size;
|
|
108
102
|
return (
|
|
109
103
|
<View style={[styles.row, style]}>
|
|
110
|
-
<Avatar size={
|
|
104
|
+
<Avatar size={markSize} name={displayName} source={image ? { uri: image } : undefined} />
|
|
111
105
|
<View style={styles.text}>
|
|
112
106
|
<Text
|
|
113
107
|
userSelect="none"
|
|
@@ -118,10 +112,6 @@ export function MemberChip({
|
|
|
118
112
|
// supporting line except by colour.
|
|
119
113
|
weight="medium"
|
|
120
114
|
leading={pairLeading}
|
|
121
|
-
// NEUTRAL underline, never the navigation blue: pressing this opens
|
|
122
|
-
// the person in place rather than travelling to them, and blue
|
|
123
|
-
// underlined text promises a destination.
|
|
124
|
-
decoration={marker ? "underline" : undefined}
|
|
125
115
|
color={inactive ? "zinc-500" : undefined}
|
|
126
116
|
numberOfLines={1}
|
|
127
117
|
>
|
package/src/member_peek.tsx
CHANGED
|
@@ -72,6 +72,12 @@ export function MemberPeek(props: MemberPeekProps) {
|
|
|
72
72
|
<Peek
|
|
73
73
|
accessibilityLabel={accessibilityLabel ?? words.profile(displayName)}
|
|
74
74
|
content={<MemberProfileCard {...card} />}
|
|
75
|
+
// A chip is a CONTROL-BAND citizen — `md` is `CONTROL_CONTENT_HEIGHT`
|
|
76
|
+
// precisely so it seats in one. On the default `text` register the
|
|
77
|
+
// trigger boxed it at 32 and bled 4 off each side, so a peeking chip
|
|
78
|
+
// under-filled the band and stood shorter than the same chip un-peeked
|
|
79
|
+
// beside it.
|
|
80
|
+
variant="control"
|
|
75
81
|
side={side}
|
|
76
82
|
align={align}
|
|
77
83
|
>
|
|
@@ -81,9 +87,6 @@ export function MemberPeek(props: MemberPeekProps) {
|
|
|
81
87
|
secondary={secondary}
|
|
82
88
|
size={size}
|
|
83
89
|
inactive={card.inactive}
|
|
84
|
-
// This chip IS the door — without the marker it paints nothing at rest
|
|
85
|
-
// and only a pointer already on it would ever learn that.
|
|
86
|
-
marker
|
|
87
90
|
style={style}
|
|
88
91
|
/>
|
|
89
92
|
</Peek>
|
package/src/pagination.tsx
CHANGED
|
@@ -16,10 +16,6 @@ export interface PaginationLabels {
|
|
|
16
16
|
rangeWithTotal?: (start: number, end: number, total: number) => string;
|
|
17
17
|
/** Range summary without a total. Default `${start}–${end}`. */
|
|
18
18
|
range?: (start: number, end: number) => string;
|
|
19
|
-
/** Page indicator with a known total. Default `Page ${page} of ${pageCount}`. */
|
|
20
|
-
pageWithTotal?: (page: number, pageCount: number) => string;
|
|
21
|
-
/** Page indicator without a total. Default `Page ${page}`. */
|
|
22
|
-
page?: (page: number) => string;
|
|
23
19
|
/** Previous-page button tooltip. Default "Previous". */
|
|
24
20
|
previous?: string;
|
|
25
21
|
/** Next-page button tooltip. Default "Next". */
|
|
@@ -46,11 +42,34 @@ export interface PaginationProps {
|
|
|
46
42
|
}
|
|
47
43
|
|
|
48
44
|
/**
|
|
49
|
-
* Pagination control:
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
45
|
+
* Pagination control: which rows are on screen, and arrows to change them. A
|
|
46
|
+
* pure control — it owns no border, background, or padding. The container that
|
|
47
|
+
* frames it (a summary row, a grid footer, a bordered band) supplies the divider
|
|
48
|
+
* and insets, so the same control composes anywhere without doubling up a border.
|
|
49
|
+
*
|
|
50
|
+
* The range is the WHOLE label. A page index beside it ("Page 2 of 3") answers
|
|
51
|
+
* the same question a second way and enables no decision the range does not —
|
|
52
|
+
* how much is left is in the total, and whether this is the end is in the
|
|
53
|
+
* disabled arrow. It was rendered here once and cut.
|
|
54
|
+
*
|
|
55
|
+
* The range is also the register's COUNT. Pass `counted` rather than `count` to
|
|
56
|
+
* a `Table` paired with this, or the matched total is printed twice a few rows
|
|
57
|
+
* apart.
|
|
58
|
+
*
|
|
59
|
+
* **`onPageChange` must also return the viewport to the first row.** This
|
|
60
|
+
* control cannot do it: the scroll container belongs to the page, not to the
|
|
61
|
+
* kit. Bottom pagination is a browser-reload-era convention — a full page load
|
|
62
|
+
* reset the scroll, and that reset is what made it work. An SPA changes the rows
|
|
63
|
+
* underneath a viewport that does not move, so a reader who pages from the
|
|
64
|
+
* bottom lands on the LAST rows of the next page and never sees its first ones.
|
|
65
|
+
* The same is true of every other control that changes which rows are on screen
|
|
66
|
+
* — a filter, a sort, a search — so the reset belongs to "the window changed",
|
|
67
|
+
* not to this component's arrows. `tpl_item_list` is the reference.
|
|
68
|
+
*
|
|
69
|
+
* ONE per register, ABOVE the rows. Once the reset returns the viewport here on
|
|
70
|
+
* every press, this is where the reader's cursor already is; a second copy under
|
|
71
|
+
* the rows is the same state said twice, and two identical clusters of arrows
|
|
72
|
+
* read as two controls rather than one.
|
|
54
73
|
*/
|
|
55
74
|
export function Pagination(props: PaginationProps): React.ReactNode {
|
|
56
75
|
const { page, pageSize, rowCount, hasMore, total, loading, onPageChange, labels } = props;
|
|
@@ -65,11 +84,6 @@ export function Pagination(props: PaginationProps): React.ReactNode {
|
|
|
65
84
|
: (labels?.range ?? loc.range)(start, end)
|
|
66
85
|
: "";
|
|
67
86
|
|
|
68
|
-
const pageLabel =
|
|
69
|
-
total !== undefined
|
|
70
|
-
? (labels?.pageWithTotal ?? loc.pageWithTotal)(page + 1, Math.max(1, Math.ceil(total / pageSize)))
|
|
71
|
-
: (labels?.page ?? loc.page)(page + 1);
|
|
72
|
-
|
|
73
87
|
return (
|
|
74
88
|
<View style={styles.container}>
|
|
75
89
|
<View style={styles.summary}>
|
|
@@ -78,9 +92,6 @@ export function Pagination(props: PaginationProps): React.ReactNode {
|
|
|
78
92
|
</Text>
|
|
79
93
|
</View>
|
|
80
94
|
<View style={styles.controls}>
|
|
81
|
-
<Text size="sm" color="zinc-500">
|
|
82
|
-
{pageLabel}
|
|
83
|
-
</Text>
|
|
84
95
|
<IconButton
|
|
85
96
|
icon="chevron-left"
|
|
86
97
|
color="secondary"
|
package/src/peek.tsx
CHANGED
|
@@ -3,6 +3,7 @@ import { StyleSheet } from "react-native";
|
|
|
3
3
|
import { Popover, PopoverTrigger, PopoverContent } from "./popover";
|
|
4
4
|
import type { PopoverSide, PopoverAlign } from "./popover";
|
|
5
5
|
import { PressableHighlight } from "./pressable_highlight";
|
|
6
|
+
import { CONTROL_HEIGHT, CONTROL_RADIUS } from "./control_surface";
|
|
6
7
|
|
|
7
8
|
export interface PeekProps {
|
|
8
9
|
/** The inline reference that becomes the trigger — a customer name, a
|
|
@@ -15,6 +16,27 @@ export interface PeekProps {
|
|
|
15
16
|
content: ReactNode;
|
|
16
17
|
/** Announced name for the trigger ("Hồ sơ KOMASPEC VIỆT NAM"). */
|
|
17
18
|
accessibilityLabel: string;
|
|
19
|
+
/**
|
|
20
|
+
* WHICH REGISTER the trigger sits in — the two surfaces a peek lives on need
|
|
21
|
+
* different boxes, and picking by taste puts the wrong one on both.
|
|
22
|
+
*
|
|
23
|
+
* `text` (default) is a reference INSIDE a line of reading — a customer name
|
|
24
|
+
* in a sentence, an id in a table cell. It takes the 32px chip register and
|
|
25
|
+
* bleeds the extra height back out with negative margins, so wrapping a
|
|
26
|
+
* `Text` never grows the line it sits in.
|
|
27
|
+
*
|
|
28
|
+
* `control` is a reference that IS a control-band citizen — anything built on
|
|
29
|
+
* `MemberChip`, whose `md` avatar is sized to `CONTROL_CONTENT_HEIGHT` for
|
|
30
|
+
* exactly this. It takes the full `CONTROL_HEIGHT` band and no vertical
|
|
31
|
+
* bleed, so the token seats where every other control on the surface does
|
|
32
|
+
* instead of under-filling the band and standing a different height from the
|
|
33
|
+
* same chip un-peeked beside it.
|
|
34
|
+
*
|
|
35
|
+
* The HORIZONTAL bleed is common to both: the trigger's padding must not
|
|
36
|
+
* shift its content off the surface's left edge, or a peekable token sits 8px
|
|
37
|
+
* right of the plain one above it.
|
|
38
|
+
*/
|
|
39
|
+
variant?: "text" | "control";
|
|
18
40
|
side?: PopoverSide;
|
|
19
41
|
align?: PopoverAlign;
|
|
20
42
|
}
|
|
@@ -28,7 +50,7 @@ export interface PeekProps {
|
|
|
28
50
|
* customer names, order ids, member names, linked records.
|
|
29
51
|
*/
|
|
30
52
|
export function Peek(props: PeekProps) {
|
|
31
|
-
const { children, content, accessibilityLabel, side = "bottom", align = "start" } = props;
|
|
53
|
+
const { children, content, accessibilityLabel, variant = "text", side = "bottom", align = "start" } = props;
|
|
32
54
|
return (
|
|
33
55
|
<Popover side={side} align={align}>
|
|
34
56
|
<PopoverTrigger>
|
|
@@ -36,7 +58,7 @@ export function Peek(props: PeekProps) {
|
|
|
36
58
|
focusRing
|
|
37
59
|
accessibilityRole="button"
|
|
38
60
|
accessibilityLabel={accessibilityLabel}
|
|
39
|
-
style={styles.trigger}
|
|
61
|
+
style={[styles.trigger, variant === "control" ? styles.triggerControl : null]}
|
|
40
62
|
// 32px visual, 40px touch target (32 + 2×4) — same as IconButton.
|
|
41
63
|
hitSlop={4}
|
|
42
64
|
>
|
|
@@ -51,9 +73,9 @@ export function Peek(props: PeekProps) {
|
|
|
51
73
|
}
|
|
52
74
|
|
|
53
75
|
const styles = StyleSheet.create({
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
76
|
+
// The `text` register: a 32px chip box (Badge scale) whose extra height is
|
|
77
|
+
// bled back out with negative margins, so a reference inside a line of
|
|
78
|
+
// reading never grows that line. hitSlop keeps the 40px touch target.
|
|
57
79
|
trigger: {
|
|
58
80
|
flexDirection: "row",
|
|
59
81
|
alignItems: "center",
|
|
@@ -63,6 +85,16 @@ const styles = StyleSheet.create({
|
|
|
63
85
|
marginHorizontal: -8,
|
|
64
86
|
marginVertical: -4,
|
|
65
87
|
},
|
|
88
|
+
// The `control` register: the surface's own band. The vertical bleed goes —
|
|
89
|
+
// a control-band token is SUPPOSED to occupy the band — and the radius joins
|
|
90
|
+
// every other control's, so a peekable chip corners like the inputs it sits
|
|
91
|
+
// among rather than like a badge. The horizontal bleed stays, so the token
|
|
92
|
+
// keeps the left edge (see `variant`).
|
|
93
|
+
triggerControl: {
|
|
94
|
+
minHeight: CONTROL_HEIGHT,
|
|
95
|
+
borderRadius: CONTROL_RADIUS,
|
|
96
|
+
marginVertical: 0,
|
|
97
|
+
},
|
|
66
98
|
content: {
|
|
67
99
|
width: 320,
|
|
68
100
|
},
|