@lotics/ui 46.14.1 → 47.1.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 +20 -0
- package/MIGRATION.md +58 -0
- package/docs/catalog.md +72 -13
- package/docs/composition.md +94 -6
- package/docs/reviewing.md +56 -5
- package/docs/templates.md +8 -2
- package/examples/tpl_calendar.tsx +22 -29
- package/examples/tpl_item_list.tsx +7 -2
- package/package.json +1 -1
- package/src/accordion.tsx +11 -5
- package/src/bar_chart.tsx +3 -1
- package/src/board.tsx +1 -2
- package/src/breakdown.tsx +3 -1
- package/src/calendar/agenda_view.tsx +136 -0
- package/src/calendar/calendar_toolbar.tsx +84 -0
- package/src/calendar/calendar_view.tsx +196 -103
- package/src/calendar/context.ts +47 -0
- package/src/calendar/dates.ts +36 -6
- package/src/calendar/event_chip.tsx +141 -0
- package/src/calendar/index.ts +31 -8
- package/src/calendar/layout.ts +113 -11
- package/src/calendar/month_view.tsx +194 -150
- package/src/calendar/repeat.ts +174 -0
- package/src/calendar/time_grid_view.tsx +182 -202
- package/src/calendar/types.ts +37 -11
- package/src/charge_lines.tsx +24 -5
- package/src/control_surface.ts +28 -0
- package/src/deadline.ts +10 -0
- package/src/file_row.tsx +26 -3
- package/src/finding.tsx +1 -1
- package/src/form_text_input.tsx +9 -2
- package/src/gantt/gantt_view.tsx +212 -119
- package/src/gantt/index.ts +2 -2
- package/src/gantt/scale.ts +38 -2
- package/src/gantt/types.ts +34 -7
- package/src/inline_slot.tsx +16 -2
- package/src/inline_static.tsx +18 -4
- package/src/legend_item.tsx +14 -1
- package/src/locale.tsx +30 -0
- package/src/matrix.tsx +19 -5
- package/src/member_chip.tsx +18 -1
- package/src/menu_button.tsx +12 -2
- package/src/option_list.tsx +11 -1
- package/src/progress_bar.tsx +14 -1
- package/src/record_summary.tsx +5 -0
- package/src/stacked_bar_chart.tsx +4 -1
- package/src/table_fit.ts +18 -1
- package/src/thumbnail_stack.tsx +18 -3
- package/src/use_option_list.ts +13 -1
package/AGENTS.md
CHANGED
|
@@ -61,6 +61,26 @@ CURRENT major only — upgrading an app across majors is `MIGRATION.md`.
|
|
|
61
61
|
Worked in `examples/tpl_record.tsx` § Progress.
|
|
62
62
|
→ [catalog.md §"Tasks, to-dos, phased work"](./docs/catalog.md),
|
|
63
63
|
[catalog.md §"Tasks & checklists"](./docs/catalog.md).
|
|
64
|
+
- **A calendar is for WHEN; a Gantt is for WHO/WHAT.** Days on the axis → `CalendarView`
|
|
65
|
+
(compound: `CalendarToolbar` + `CalendarMonth`/`CalendarWeek`/`CalendarDay`/`CalendarAgenda`,
|
|
66
|
+
controlled via `date`/`view`). Resources on the axis — a channel, an owner, a vehicle →
|
|
67
|
+
`GanttView`, which draws lanes (`group`), milestones and progress. And
|
|
68
|
+
a grid is unreadable in TWO directions: too narrow OR too short, month/week fall back to the
|
|
69
|
+
agenda on their own, so never wrap one in a horizontal scroller to force it.
|
|
70
|
+
→ [catalog.md §"Scheduling & time"](./docs/catalog.md).
|
|
71
|
+
- **A repeating event is ONE row with a `repeat`, and there is no `timeZone` prop.** The
|
|
72
|
+
calendar expands a repeat to the view's own range, so a timetable is one event rather than a
|
|
73
|
+
year of them; an occurrence's id resolves back to the source row. And a stored Lotics
|
|
74
|
+
datetime is a naive WALL CLOCK — "14:30" means 14:30 to the business — so there is nothing to
|
|
75
|
+
convert and a calendar that re-projected it would move every event. Only *what day it is*
|
|
76
|
+
depends on the reader's location: that is the `now` prop.
|
|
77
|
+
→ [catalog.md §"Scheduling & time"](./docs/catalog.md).
|
|
78
|
+
- **A wash gives a surface its body; a border — or an accent stripe down one side — is that
|
|
79
|
+
same edge asserted twice.** Light ground + dark ink of one family (`Badge`'s tonal pairing),
|
|
80
|
+
never the solid shade under white text; `solid()` is for a DOT, where a few pixels carry
|
|
81
|
+
identity. And a radius is a PROPORTION of the box (`proportionalRadius`), because a component
|
|
82
|
+
sized by data renders as a pill at one height and barely a corner at another.
|
|
83
|
+
→ [composition.md §"A body, not an edge"](./docs/composition.md).
|
|
64
84
|
- **`Badge` = STATUS only; supporting detail is the muted second line.** A type / category /
|
|
65
85
|
attribute / count is not a status — it belongs under its identity as `size="xs" color="muted"`,
|
|
66
86
|
never a second chip. →
|
package/MIGRATION.md
CHANGED
|
@@ -4,6 +4,64 @@ 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
|
+
## 47.0.0
|
|
8
|
+
|
|
9
|
+
**`@lotics/ui/calendar` is a compound, and `CalendarEvent.color` is a token.**
|
|
10
|
+
|
|
11
|
+
`CalendarView` still exists and `<CalendarView events={…} />` still renders a working calendar,
|
|
12
|
+
so the drop-in call site is unchanged. Four things did change:
|
|
13
|
+
|
|
14
|
+
- **`MonthView` / `TimeGridView` are gone.** The parts are `CalendarMonth`, `CalendarWeek`,
|
|
15
|
+
`CalendarDay`, `CalendarAgenda`, `CalendarToolbar` and `CalendarBody`, and they read their
|
|
16
|
+
state from the root rather than taking `date`/`events` props. Mount them as children of
|
|
17
|
+
`CalendarView` to compose your own chrome.
|
|
18
|
+
- **`color` is a `ColorName`, not a string.** `color: solid("blue")` becomes `color: "blue"`.
|
|
19
|
+
The views derive a fill, a tint and a border from the token; the old code appended `"1f"` to
|
|
20
|
+
the string for the tint, which produced garbage for anything that was not a 6-digit hex.
|
|
21
|
+
- **`CalendarLabels` is `CalendarViewLabels`, and it is provider-wired.** Strings now resolve
|
|
22
|
+
prop → `LoticsLocale.calendarView` → English, so a localized app can delete its hand-passed
|
|
23
|
+
set. `DEFAULT_CALENDAR_LABELS` is `DEFAULT_CALENDAR_VIEW_LABELS`. (`CalendarLabels` still
|
|
24
|
+
exists and still belongs to `@lotics/ui/date_calendar`, the date PICKER's month grid — the
|
|
25
|
+
two had the same name.) Two labels are new: `agenda`, `noEvents`, `addAt`.
|
|
26
|
+
- **`onEventDrop` is gone.** Dragging an event to another day wrote through no seam an app
|
|
27
|
+
could gate. Press-to-open is the gesture: `onEventPress`, plus `onSlotPress(start, end)` on
|
|
28
|
+
an empty day cell or hour slot.
|
|
29
|
+
|
|
30
|
+
**Two behaviours changed without an API change, and both are worth looking at.** A week or day
|
|
31
|
+
grid now opens on the earliest event IN VIEW rather than an hour before the wall clock (a week
|
|
32
|
+
of 09:00 meetings viewed at 13:00 used to open on empty grid). And month/week fall back to the
|
|
33
|
+
agenda when the surface is too narrow OR too short for a grid to name a single event — if you
|
|
34
|
+
wrapped a calendar in a horizontal `ScrollView` with a `minWidth` to work around the old
|
|
35
|
+
behaviour, delete that: it now fights the fallback.
|
|
36
|
+
|
|
37
|
+
**Additive in the same release:** `CalendarEvent.repeat` expands a series to the view's own
|
|
38
|
+
range (a timetable is one row, not a year of them) and `CalendarView`'s `now` supplies the
|
|
39
|
+
reference instant for the today badge, the now line and the opening scroll — which also
|
|
40
|
+
makes "today" pinnable in a test. There is deliberately **no `timeZone` prop**: Lotics
|
|
41
|
+
stores a datetime as a naive wall clock, so a calendar that re-projected it would move
|
|
42
|
+
every event.
|
|
43
|
+
|
|
44
|
+
### `GanttView`, in the same major
|
|
45
|
+
|
|
46
|
+
It keeps the promise its type was already making, and `color` becomes a token.
|
|
47
|
+
|
|
48
|
+
- **`group` now renders.** It was declared on `GanttTask`, documented as a lane label, passed by
|
|
49
|
+
callers — and read by nothing; every task rendered in one flat list. Tasks sharing a `group`
|
|
50
|
+
now sit under one heading, in first-seen order, with ungrouped tasks leading. If you were
|
|
51
|
+
passing `group` and relying on the flat rendering, drop the field.
|
|
52
|
+
- **`color` is a `ColorName`, not a string** — `solid("blue")` becomes `"blue"`, matching
|
|
53
|
+
`CalendarEvent`.
|
|
54
|
+
- **`onTaskResize` is gone.** Dragging a bar's edge wrote through no seam an app could gate,
|
|
55
|
+
the same reason the calendar's drag went in 47.0.0. Press is the gesture.
|
|
56
|
+
- **Labels are provider-wired** — prop → `LoticsLocale.gantt` → English, so a localized app can
|
|
57
|
+
delete its hand-passed set. One label is new: `empty`.
|
|
58
|
+
|
|
59
|
+
New, additive: `milestone` draws a diamond, `progress` fills part of a bar. `buildRows` is
|
|
60
|
+
exported for anything that needs the same lane geometry.
|
|
61
|
+
|
|
62
|
+
There are deliberately **no dependency connectors**. Lane order and bar position already carry
|
|
63
|
+
the sequence, and elbow arrows over an already-ruled grid restate it as line noise.
|
|
64
|
+
|
|
7
65
|
## 46.3.0
|
|
8
66
|
|
|
9
67
|
**A `Text` with `numberOfLines` now declares `flexShrink: 1` + `minWidth: 0` for you.**
|
package/docs/catalog.md
CHANGED
|
@@ -86,6 +86,13 @@ inline: a picker option, an assignee, a `select_member` value. Pure: resolve the
|
|
|
86
86
|
your directory and pass `name` / `image`; never hand-roll `Avatar` + `Text`. (`MemberSelect`
|
|
87
87
|
renders these per option.)
|
|
88
88
|
|
|
89
|
+
**`showAvatar={false}` where the member is ONE FIELD AMONG COLUMNS** — an owner, an assignee,
|
|
90
|
+
a last-edited-by. Keep the face where the member IS the subject (a roster, a picker option, a
|
|
91
|
+
profile). In a supporting column it fails twice: it competes with the mark the row already
|
|
92
|
+
carries for its own subject, and on a real book a few people own hundreds of rows, so the
|
|
93
|
+
column draws one repeated disc — which `composition.md` §Identity marks calls a mark that
|
|
94
|
+
"carries nothing and is the largest, brightest thing there".
|
|
95
|
+
|
|
89
96
|
**To answer "who IS this?" — `MemberPeek`**, the same chip made a door onto a
|
|
90
97
|
`MemberProfileCard`: a 72px avatar, what they sign in as, their role, their teams and when they
|
|
91
98
|
joined. The trigger takes `Peek`'s `variant="control"` there, so the chip seats in the 40px
|
|
@@ -130,7 +137,11 @@ that a whole office shares.
|
|
|
130
137
|
`ThumbnailStack` is `AvatarGroup`'s sibling over a different subject: a capped, overlapping set
|
|
131
138
|
of PICTURES answering "an order of what?", "which parts?", "what arrived?" in one register cell.
|
|
132
139
|
Pass `{ id, label, image }` per thing; `max` (default 3) caps it and the rest become the `+N`
|
|
133
|
-
mark at the end of the stack.
|
|
140
|
+
mark at the end of the stack. Each mark ANNOUNCES its label by default — a standalone stack is
|
|
141
|
+
unreadable otherwise — so pass **`decorative`** where the row already names the thing in text, or
|
|
142
|
+
the pictures read it a second time on every row. (The default is the opposite of
|
|
143
|
+
`Avatar.announce`, and for the same reason: an avatar almost always sits beside its name, a stack
|
|
144
|
+
often does not. The default follows where the component actually sits.) Both components read their separating edge from `mark_stack.ts`,
|
|
134
145
|
so a row carrying people in one column and goods in the next reads as one device used twice
|
|
135
146
|
rather than two — but they **tuck by different fractions, and that is deliberate**: a face is
|
|
136
147
|
edge-anchored and survives being covered two thirds of the way, while a product shot centres its
|
|
@@ -279,7 +290,12 @@ by size.)
|
|
|
279
290
|
search, you don't group. Worked example: [`tpl_item_list`](../examples/tpl_item_list.tsx).
|
|
280
291
|
Never an HTML `<table>` or a `.map` of rows. The register adapts to its container on its
|
|
281
292
|
own — hides columns by `priority`, stacks rows below the two-column floor (see the `table`
|
|
282
|
-
inventory entry) — so a narrow screen or panel needs NO branching
|
|
293
|
+
inventory entry) — so a narrow screen or panel needs NO branching. **A flexible column that
|
|
294
|
+
carries fixed chrome inside its cell declares it as `lead`** (a leading mark plus its gap): the
|
|
295
|
+
fit guarantees a READ floor for the text, and without `lead` the mark eats part of that floor,
|
|
296
|
+
so a register reports "fitted" while its subject clips. `composition.md` requires a leading mark
|
|
297
|
+
on every register row and `tpl_item_list` puts it inside the subject cell, so this is the
|
|
298
|
+
ordinary case, not an exotic one; compose a hand-designed
|
|
283
299
|
card pile (`PressableRow` + your own hierarchy) over `useScreenSize` only when a screen
|
|
284
300
|
deserves a better mobile shape than the automatic stack.
|
|
285
301
|
- **Inline-managed grouped table** (MODERATE — hundreds, low-thousands — you MANAGE in view)
|
|
@@ -482,7 +498,8 @@ patterns doc indexed in [AGENTS.md](../AGENTS.md)).
|
|
|
482
498
|
`ScanField` (scan/verify), `Stepper` (a guided run / progress sequence — done, current,
|
|
483
499
|
upcoming, horizontal OR vertical), `RemainderMeter` + `AllocationRow` (allocation),
|
|
484
500
|
`Timeline` (a heterogeneous event LOG — icons + expandable details, not progress),
|
|
485
|
-
`
|
|
501
|
+
`CalendarView` (the `calendar` module — days are the axis), `GanttView` (RESOURCES are the
|
|
502
|
+
axis: a channel, an owner, a vehicle), `comments_thread`.
|
|
486
503
|
|
|
487
504
|
### AI surfaces
|
|
488
505
|
|
|
@@ -1350,7 +1367,10 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1350
1367
|
EXACTLY (height, padding, 1px transparent border) so a non-editable field — a computed
|
|
1351
1368
|
total, a system ID, a synced/locked value — aligns pixel-for-pixel in the same column;
|
|
1352
1369
|
non-interactive, NOT a disabled input; `muted`/`tabular`/`align="right"` for a number
|
|
1353
|
-
column, `weight="medium"` to emphasise a total among plain rows, `
|
|
1370
|
+
column, `weight="medium"` to emphasise a total among plain rows, **`actions` for a verb about
|
|
1371
|
+
the value** (look this id up, copy it, request the gated change) — a static value is still a
|
|
1372
|
+
field, and its verb belongs on the field's surface, never on the row's ground where an
|
|
1373
|
+
`InlineButton` has nothing under it; `multiline` to WRAP rather
|
|
1354
1374
|
than truncate (single-line is the default, because a field column reads as a column only if
|
|
1355
1375
|
its rows are one height — pass it where the counterpart editor is itself `multiline`, or a
|
|
1356
1376
|
truncated value beside a wrapping input reintroduces the misalignment on the other axis).
|
|
@@ -1375,7 +1395,18 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1375
1395
|
- **`inline_button`** — `InlineButton`: a verb that sits INSIDE an inline field's surface
|
|
1376
1396
|
(the `Open` on a reference, a `Copy` on a value worth copying) rather than in the row's
|
|
1377
1397
|
trailing column — put it inside when the act is ABOUT THE VALUE, so it travels with what it
|
|
1378
|
-
acts on and the trailing column stays free for row-level verbs.
|
|
1398
|
+
acts on and the trailing column stays free for row-level verbs.
|
|
1399
|
+
|
|
1400
|
+
**NEVER use it outside an inline field.** Not in a section header, not in a toolbar, not as a
|
|
1401
|
+
row action, not as a card's own verb — it is an *inner* control of a field, and the whole of its
|
|
1402
|
+
design says so: it is FILLED against the field's white ground (a relationship to a surface it no
|
|
1403
|
+
longer has when you move it), it is 28px against a 40px band, it carries no border because the
|
|
1404
|
+
field already draws one, and **it stops propagation** because the field beneath it is pressable.
|
|
1405
|
+
Outside a field every one of those becomes wrong: it reads as a heavier `Button secondary` that
|
|
1406
|
+
is mysteriously too small, it sits on a ground it was not contrasted against, and its swallowed
|
|
1407
|
+
press is now swallowing nothing. If the verb is about the ROW use the row's trailing action; if
|
|
1408
|
+
it is about the SECTION use its heading row; if it is about the SCREEN it is a `Button`. The
|
|
1409
|
+
test is one question — **is there a field surface under it?** If not, it is the wrong control. FILLED (`Button secondary`'s own
|
|
1379
1410
|
zinc-100) on the field's white surface — the contrast is a RELATIONSHIP, not a colour: invert it
|
|
1380
1411
|
if the resting field surface ever changes, or the verb dissolves into the value. 28px, radius 8,
|
|
1381
1412
|
no border (the field already has one) and no shadow (depth belongs to the primary action alone). **It stops propagation** — that is the contract: the
|
|
@@ -2048,14 +2079,42 @@ component rather than showing it at zero.
|
|
|
2048
2079
|
|
|
2049
2080
|
### Scheduling & time
|
|
2050
2081
|
|
|
2051
|
-
- **`calendar`** —
|
|
2052
|
-
`
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
`
|
|
2058
|
-
|
|
2082
|
+
- **`calendar`** — a COMPOUND calendar of events. `CalendarView` is the root and holds the
|
|
2083
|
+
state; `CalendarToolbar`, `CalendarMonth`, `CalendarWeek`, `CalendarDay`, `CalendarAgenda`
|
|
2084
|
+
and `CalendarBody` are the parts. Mount it bare (`<CalendarView events={…} />`) for the
|
|
2085
|
+
toolbar plus the current view, or pass children to compose your own chrome — an app screen
|
|
2086
|
+
already has a header band, and a calendar that welds one on stacks two. `date` / `view` with
|
|
2087
|
+
their `on*Change` make it controlled, so a filter chip or a route can drive it. It NEVER
|
|
2088
|
+
writes: `onEventPress` and `onSlotPress` hand you the event or an empty range and your own
|
|
2089
|
+
form does the rest. `renderEvent` replaces a chip's contents while the calendar keeps the
|
|
2090
|
+
press target and the geometry. Also `EventChip`, `useCalendar`, and the pure helpers
|
|
2091
|
+
(`layoutDayColumns`, `packEventLanes`, `compareEvents`, `fitLanes`, `initialScrollMinutes`,
|
|
2092
|
+
`gridWindow`, `weekRowsInMonth`, `addDays`, `startOfWeek`, …).
|
|
2093
|
+
**A repeating event costs one row, not a year of them:** put a `repeat` on the event
|
|
2094
|
+
(`{ every: "day" | "week" | "month", interval?, on?, until?, count? }`) and the calendar
|
|
2095
|
+
expands it to the occurrences in the view's own range. Deliberately NOT RFC 5545 — a
|
|
2096
|
+
timetable, a standup, a monthly close; see `expandRepeats` for why the long tail is a
|
|
2097
|
+
platform field type rather than a wider rule. An occurrence's id encodes its source, so
|
|
2098
|
+
`onEventPress` still hands you the row the series came from (`sourceEventId`).
|
|
2099
|
+
**There is no `timeZone` prop, on purpose:** Lotics stores a datetime as a naive WALL CLOCK,
|
|
2100
|
+
so "14:30" means 14:30 to the business and there is nothing to convert — re-projecting it
|
|
2101
|
+
would move every event. What does depend on where the reader sits is *what day it is*, so
|
|
2102
|
+
that alone is the `now` prop (which also pins "today" in a test).
|
|
2103
|
+
Three things it decides for you: **which shape** — below `compactBelow` (720px) or too short
|
|
2104
|
+
for a month to name one event per row, month/week become the agenda, because seven columns on
|
|
2105
|
+
a phone can only say WHICH days have something; **how many lanes** — measured against the row,
|
|
2106
|
+
not a constant; and **the hour window** — `dayStartHour`/`dayEndHour` are a VIEWPORT, so a
|
|
2107
|
+
06:00 delivery widens the grid instead of disappearing from it. `color` is a `ColorName`, not
|
|
2108
|
+
a hex. Labels resolve prop → `LoticsLocale.calendarView` → English.
|
|
2109
|
+
- **`gantt`** — `GanttView`: a frozen label column beside a zoomable, horizontally
|
|
2110
|
+
scrollable axis. Reach for it over a calendar when the **rows** are the subject (a phase, an
|
|
2111
|
+
owner, a vehicle, a channel) and dates are the horizontal axis. **Lanes** come from `group`,
|
|
2112
|
+
**milestones** render as diamonds, and `progress` fills part of a bar. No dependency
|
|
2113
|
+
connectors: lane order and bar position already carry the sequence, and elbow arrows over an
|
|
2114
|
+
already-ruled grid restate it as line noise. The bar carries no label, because the frozen
|
|
2115
|
+
column beside it already names the task. Helpers: `buildRows`, `barGeometry`, `axisRange`,
|
|
2116
|
+
`buildTicks`, `pxPerDay`; types `GanttTask`/`GanttRow`/`GanttLabels`. `color` is a
|
|
2117
|
+
`ColorName`; labels resolve prop → `LoticsLocale.gantt` → English.
|
|
2059
2118
|
- **`timeline`** — `Timeline`: a heterogeneous event LOG — per-row icon + expandable
|
|
2060
2119
|
details; models the past, NOT progress. Three things it decides for you, because a row
|
|
2061
2120
|
cannot be trusted to a caller's data: the **label clamps to two lines WHILE COLLAPSED**
|
package/docs/composition.md
CHANGED
|
@@ -360,6 +360,33 @@ badge. Every period-dependent number MUST follow the selection. Pass `includeTim
|
|
|
360
360
|
time-of-day matters: the trigger previews the chosen time (locale-aware) and the `labels` prop
|
|
361
361
|
translates the presets, footer buttons, placeholder, and the time-segment editors.
|
|
362
362
|
|
|
363
|
+
## A calendar answers WHEN — and only when the position on a timeline is the question
|
|
364
|
+
|
|
365
|
+
A calendar is not a prettier register. Reach for one when the reader's question is *when does
|
|
366
|
+
this happen* and the answer is read off a POSITION — which day, how long, what else is running
|
|
367
|
+
at the same time. When the question is *which of these needs me*, the answer is a register with
|
|
368
|
+
a date column, and a calendar buries it: thirty rows of a table are scannable, thirty rows
|
|
369
|
+
scattered across a month grid are a search.
|
|
370
|
+
|
|
371
|
+
Three rules follow from that, and each of them is about legibility rather than taste.
|
|
372
|
+
|
|
373
|
+
**A grid is unreadable in two directions.** Seven equal columns need roughly 110px each to hold
|
|
374
|
+
a real title; below that every event reads "Cont…" and the grid can only say WHICH days have
|
|
375
|
+
something. Height fails the same way — a month too short to give each week row one event plus
|
|
376
|
+
its overflow chip renders nothing but "+3 more" on all thirty-one days. Both are the same
|
|
377
|
+
finding, and the answer to both is a different SHAPE: `CalendarView` falls back to the agenda on
|
|
378
|
+
its own. Never wrap a month grid in a horizontal scroller to force it — that trades a readable
|
|
379
|
+
list for a wide thing the reader has to drag.
|
|
380
|
+
|
|
381
|
+
**A time window is a viewport, never a filter.** Declaring the working day as 08:00–18:00 sets
|
|
382
|
+
where the grid OPENS. An event outside it widens the grid; a calendar that silently refuses to
|
|
383
|
+
draw a 06:00 delivery is a calendar that lies, and nothing on screen says so.
|
|
384
|
+
|
|
385
|
+
**Rows of days, or rows of resources — pick by what the reader is comparing.** Comparing days
|
|
386
|
+
is a calendar. Comparing a channel against a channel, an owner against an owner, a vehicle
|
|
387
|
+
against a vehicle — with days as the horizontal axis — is `GanttView`. A campaign plan is
|
|
388
|
+
usually the second one, however much it is called a calendar.
|
|
389
|
+
|
|
363
390
|
## Keyboard & focus
|
|
364
391
|
|
|
365
392
|
- **Use `tabIndex`, never `focusable`.** React Native Web's `Pressable` silently ignores
|
|
@@ -529,9 +556,25 @@ have decided the thing itself (which one they want).
|
|
|
529
556
|
The toggle earns its place when the two states differ in more than a number — different rows,
|
|
530
557
|
different units, a different shape of answer. Swapping `1.700.000` for `2.000.000` is not that.
|
|
531
558
|
|
|
559
|
+
**A container's own gap must exceed the largest gap INSIDE its children — not just the gap to its
|
|
560
|
+
siblings.** The comparison authors are told to make is within-group against between-group, which
|
|
561
|
+
reads as "compare the two numbers *you* wrote". The failure that actually ships compares a number
|
|
562
|
+
you wrote against one the KIT wrote inside a component you dropped in: a section body at 12 or 14
|
|
563
|
+
around a field table that spaces its own rows at 16. Both are conventional, neither looks wrong
|
|
564
|
+
alone, and the boundary BETWEEN two objects ends up tighter than the boundary inside one. Measured
|
|
565
|
+
form: per container, collect its own `row-gap` and the **max `row-gap` of its visible children**;
|
|
566
|
+
`own <= child-max` is the finding.
|
|
567
|
+
|
|
532
568
|
## Numbers, money, dates
|
|
533
569
|
|
|
534
570
|
- Free-standing numerals: `<Text tabular>` — always.
|
|
571
|
+
- **A kit component that RENDERS a number takes the app's formatter — it must never `String()` a
|
|
572
|
+
decimal.** In a locale that writes `28,9`, the one figure the kit prints for itself becomes the
|
|
573
|
+
one figure on the screen that is wrong, sitting beside numbers the app formatted correctly — and
|
|
574
|
+
in a locale where `.` is the *thousands* separator that is not a cosmetic difference, it is a
|
|
575
|
+
different quantity. `ChargeLines` takes `formatMoney` and `formatQuantity` for this reason, both
|
|
576
|
+
at the BAND so two lines cannot disagree, and threads them to the locked value and the inline
|
|
577
|
+
editor alike. Any new component that displays a caller's number owes the same hook.
|
|
535
578
|
- **A column states its unit ONCE — which is only possible if it holds ONE unit.** State it in the
|
|
536
579
|
header band and leave the cells bare. The corollary is the one that bites: a label/value list
|
|
537
580
|
whose rows are a date range, a sum of money and a COUNT has no single unit to state, so each
|
|
@@ -1638,6 +1681,19 @@ information.
|
|
|
1638
1681
|
identity a reader actually scans for — a deal takes its account's mark, since "whose deal is
|
|
1639
1682
|
this" is the question being asked of the list. Only rows with genuinely no identity are exempt:
|
|
1640
1683
|
a list of amounts, a log of events.
|
|
1684
|
+
- **A member as one FIELD among columns is TEXT, not a face** (`MemberChip showAvatar={false}`).
|
|
1685
|
+
Keep the face where the member IS the subject — a roster, a picker option, a profile. In an
|
|
1686
|
+
owner / assignee / last-edited-by column it fails twice over: it competes with the mark the row
|
|
1687
|
+
already carries for its own subject, and on a real book a handful of people own hundreds of rows,
|
|
1688
|
+
so the column draws the same disc over and over. Shipped example: an owner column rendering four
|
|
1689
|
+
identical discs for two people, both initial "T", both hashed to a purple.
|
|
1690
|
+
- **A mark derived from POSITION is not identity — the list already draws position.** An ordinal
|
|
1691
|
+
disc, a rank badge, an index chip *does* vary per row, so it passes "the mark must not look the
|
|
1692
|
+
same on every row" and every probe built on that test. It fails for a different reason: it
|
|
1693
|
+
restates what top-to-bottom order says, and it spends the one slot that could have carried the
|
|
1694
|
+
subject. Keep the ordinal where order is the thing being EDITED, and then as text in the
|
|
1695
|
+
supporting line — never as the mark. A surface that needs to state position has its own readout
|
|
1696
|
+
(a drawer's `3/8`, a stepper's rail).
|
|
1641
1697
|
- **The rung follows what the row is ABOUT**: `lg` where the entity is the row's subject, `md`
|
|
1642
1698
|
where it is one field among columns (see `avatar_size`).
|
|
1643
1699
|
- **A column of faces must not draw an UNBOUNDED audience as faces.** Where a cell answers "who
|
|
@@ -1665,12 +1721,19 @@ Every rule above removes a defect. None of them ADDS anything, so a screen can p
|
|
|
1665
1721
|
and still be reported as plain, boring, or unfinished — which is a different complaint with a
|
|
1666
1722
|
different fix.
|
|
1667
1723
|
|
|
1668
|
-
- **What is inherently visual in THIS data?**
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1724
|
+
- **What is inherently visual in THIS data?** Mostly the data itself: identity marks (above),
|
|
1725
|
+
status colour, file-type badges, sparklines, proportion bars, tabular figures aligning down a
|
|
1726
|
+
column. Every one of these is information first and decoration never — which is why they are the
|
|
1727
|
+
only kind of visual interest that survives contact with a real dataset. Reaching instead for a
|
|
1728
|
+
gradient, an illustration or a hero band adds weight without adding a single fact.
|
|
1729
|
+
- **But some entities DO have a likeness, and then it is the strongest mark available.** A product,
|
|
1730
|
+
a property, a vehicle, a dish, a person — a reader recognises the thing itself faster than any
|
|
1731
|
+
derived initial, and the picture is doing the identifying rather than decorating. Where the entity
|
|
1732
|
+
has one, an empty image field is a **DATA gap, not a design constraint**: the fix is to put the
|
|
1733
|
+
image on the record, not to restyle the row around its absence. A catalogue of sixty rows opening
|
|
1734
|
+
with text reads as a fixture no treatment pass will rescue. The test is unchanged — it must make
|
|
1735
|
+
the screen clearer AND better-looking — which is exactly what a thumbnail that is also the
|
|
1736
|
+
identifier does, and exactly what a stock photo bolted onto a task list does not.
|
|
1674
1737
|
- **What does the screen DECIDE?** A register listing every row at equal weight has no point of
|
|
1675
1738
|
view: it hands the reader the same undifferentiated wall the query returned. A screen with a
|
|
1676
1739
|
point of view puts what needs attention first (`TableGroup`), spends its one accent on where the
|
|
@@ -1827,6 +1890,31 @@ the ring. Collapsing them into one "active" style makes the selected item pixel-
|
|
|
1827
1890
|
hovered one, so "which did I pick" is answerable only by moving the mouse away. The tell in code
|
|
1828
1891
|
is a single boolean OR-ing selection together with hover, press and focus.
|
|
1829
1892
|
|
|
1893
|
+
## A body, not an edge — and a corner proportionate to the box
|
|
1894
|
+
|
|
1895
|
+
Two rules that decide how any small coloured surface is drawn: an event chip, a board card, a
|
|
1896
|
+
timeline bar, a status pill.
|
|
1897
|
+
|
|
1898
|
+
**A wash gives it a body; a border on top is the same edge asserted twice.** A transparent
|
|
1899
|
+
rectangle on a white canvas *is* its border — so the fill is what makes it an object, and once
|
|
1900
|
+
it has one, the outline says nothing new. This matters most where the surface already sits on a
|
|
1901
|
+
ruled ground (a calendar grid, a timeline, a table): every extra hairline competes with the
|
|
1902
|
+
rules that carry the structure. The same goes for an accent stripe down one side — it is a
|
|
1903
|
+
border wearing a colour, and the wash it sits on has already stated where the thing begins.
|
|
1904
|
+
|
|
1905
|
+
**The wash is the family's light ground with its dark ink, never the solid shade with white on
|
|
1906
|
+
it.** A saturated fill per item turns a dense surface into a paint chart, and a screen with a
|
|
1907
|
+
dozen categories on it stops being something anyone can look at. The pairing is the one `Badge`
|
|
1908
|
+
uses for `tonal`. The solid shade keeps exactly one job: a DOT — a few pixels carrying identity
|
|
1909
|
+
rather than area, where the contrast is the point.
|
|
1910
|
+
|
|
1911
|
+
**A radius is a proportion of the box, not a constant.** A component whose height is data —
|
|
1912
|
+
a chip sized to its duration, a bar sized to its span — cannot take one number: at 22px the
|
|
1913
|
+
control rung renders a pill, and the same value on a tall block is barely a corner, so two
|
|
1914
|
+
boxes of one family read as two vocabularies. Scale by height and cap at the container rung
|
|
1915
|
+
(`proportionalRadius`). Keep the ratio under a third, or the corner eats the side and the shape
|
|
1916
|
+
stops being a rounded rectangle.
|
|
1917
|
+
|
|
1830
1918
|
## `Badge` is for STATUS only — everything else is text
|
|
1831
1919
|
|
|
1832
1920
|
A `Badge` means STATE that must read at a GLANCE — a lifecycle status, a risk level, a quality
|
package/docs/reviewing.md
CHANGED
|
@@ -543,15 +543,23 @@ this converges on *distinctive*.
|
|
|
543
543
|
largest, brightest thing there.
|
|
544
544
|
- **What does the screen DECIDE?** A register listing every row at equal weight has no point of
|
|
545
545
|
view. Usually the real answer when treatment fixes keep not landing.
|
|
546
|
-
- **What is inherently visual in THIS data?**
|
|
547
|
-
|
|
548
|
-
|
|
546
|
+
- **What is inherently visual in THIS data?** Usually the data itself: identity marks, status
|
|
547
|
+
colour, file-type badges, sparklines, proportion bars, tabular figures. **But check whether the
|
|
548
|
+
entity has a LIKENESS** — a product, a property, a vehicle, a person. If it does and the rows
|
|
549
|
+
open without one, query the image field's fill rate before writing a treatment finding: an empty
|
|
550
|
+
field makes this a DATA defect, and no amount of restyling fixes it.
|
|
549
551
|
- **What makes this belong to THIS product?** Default typeface, default greys, default accent,
|
|
550
552
|
default control shapes is a well-built generic screen, and no polish changes that. The fix is a
|
|
551
553
|
theme, not another treatment pass.
|
|
552
554
|
|
|
553
555
|
→ [composition.md](./composition.md) §"Identity marks", §"Character comes from the DATA", §"Where the accent goes".
|
|
554
556
|
|
|
557
|
+
**A driver's `.focus()` does not set `:focus-visible`** — so a focus-ring check driven
|
|
558
|
+
programmatically reports *no ring* on a control whose ring is fine, and the audit spends its time
|
|
559
|
+
on a defect that does not exist. Walk the surface with real `Tab` presses and read the computed
|
|
560
|
+
`box-shadow` at each stop; `.focus()` is only good for asking what CAN take focus, never for how it
|
|
561
|
+
looks when it does.
|
|
562
|
+
|
|
555
563
|
### 10. Microcopy
|
|
556
564
|
- **No punctuation doing a word's job.** ` · ` is banned outright — it claims a relationship while
|
|
557
565
|
refusing to name it, and it is the loudest templated-metadata tell there is. If a mark cannot be
|
|
@@ -637,6 +645,32 @@ variant that walks ELEMENTS rather than text nodes. Those tags hold text nodes t
|
|
|
637
645
|
sees, and counting them produces confident nonsense — a page "with eight nodes in Times New Roman"
|
|
638
646
|
that has none, or a clean tally on a screen with an untracked component still on it.
|
|
639
647
|
|
|
648
|
+
### Identity marks — N identical is a finding, and only a per-COLUMN count sees it
|
|
649
|
+
Probe 9 says "count them. Zero is the finding, and so is N identical" — and every other snippet
|
|
650
|
+
here reads type, rules, gaps, states or reachability, so nothing fired on a column of identical
|
|
651
|
+
discs. Group by the x band (i.e. by column), because that is what separates the row's own subject
|
|
652
|
+
mark from a supporting column that borrowed identity weight.
|
|
653
|
+
|
|
654
|
+
```js
|
|
655
|
+
// → [{ x, count, distinct, sample }] — flag any band with count > 1 && distinct === 1
|
|
656
|
+
Object.values(
|
|
657
|
+
[...document.querySelectorAll('[data-avatar], [class*="avatar"], img')]
|
|
658
|
+
.filter((n) => n.getBoundingClientRect().width > 0)
|
|
659
|
+
.reduce((acc, n) => {
|
|
660
|
+
const band = Math.round(n.getBoundingClientRect().x / 24) * 24; // column, not pixel
|
|
661
|
+
const cs = getComputedStyle(n);
|
|
662
|
+
const key = [cs.backgroundColor, n.textContent?.trim(), n.getAttribute("src") ?? ""].join("|");
|
|
663
|
+
acc[band] ??= { x: band, keys: new Set(), count: 0 };
|
|
664
|
+
acc[band].keys.add(key);
|
|
665
|
+
acc[band].count++;
|
|
666
|
+
return acc;
|
|
667
|
+
}, {}),
|
|
668
|
+
).map((b) => ({ x: b.x, count: b.count, distinct: b.keys.size, sample: [...b.keys][0] }));
|
|
669
|
+
```
|
|
670
|
+
|
|
671
|
+
A band with `count > 1 && distinct === 1` is the reported defect. A band whose `distinct` is well
|
|
672
|
+
under `count` is the softer version — a palette collapsing on real names — and worth reading by eye.
|
|
673
|
+
|
|
640
674
|
### Separator beats — is the biggest boundary the widest?
|
|
641
675
|
|
|
642
676
|
**A rule is a painted LINE, and it is drawn two ways.** Selecting "thin and wide" alone gets both
|
|
@@ -740,17 +774,34 @@ Snapshot the SAME selector four times — at rest, hovered, focused, and while e
|
|
|
740
774
|
const r = e.getBoundingClientRect(), cs = getComputedStyle(e);
|
|
741
775
|
const inner = e.querySelector('input, textarea, [contenteditable]') || e;
|
|
742
776
|
const ir = inner.getBoundingClientRect();
|
|
777
|
+
// The PAINTED node, which is often not the one you selected: a press target
|
|
778
|
+
// is frequently transparent with its ground on a child (a chip's body, a
|
|
779
|
+
// bar's fill). Reading the target's own `backgroundColor` returns
|
|
780
|
+
// `rgba(0,0,0,0)` at rest AND hovered, so a component with a perfectly good
|
|
781
|
+
// hover reports as having none — a probe that fails silently in the
|
|
782
|
+
// reassuring direction.
|
|
783
|
+
const opaque = (x) => { const b = getComputedStyle(x).backgroundColor; return b && b !== 'rgba(0, 0, 0, 0)' ? b : null; };
|
|
784
|
+
const painted = opaque(e) || [...e.querySelectorAll('*')].map(opaque).find(Boolean) || 'none';
|
|
743
785
|
return {
|
|
744
786
|
box: [Math.round(r.x), Math.round(r.y), Math.round(r.width), Math.round(r.height)],
|
|
745
787
|
text: [Math.round(ir.x), Math.round(ir.y)],
|
|
746
788
|
border: `${cs.borderTopWidth} ${cs.borderTopColor}`,
|
|
747
|
-
bg:
|
|
789
|
+
bg: painted,
|
|
748
790
|
shadow: cs.boxShadow,
|
|
749
791
|
};
|
|
750
792
|
})
|
|
751
793
|
```
|
|
752
794
|
|
|
753
|
-
`border`/`bg`/`shadow` differing is the design; `box`/`text` differing is the defect.
|
|
795
|
+
`border`/`bg`/`shadow` differing is the design; `box`/`text` differing is the defect.
|
|
796
|
+
|
|
797
|
+
**A control that differs in NOTHING is the finding this catches most often**, and it does not
|
|
798
|
+
look like a bug in a screenshot — the thing renders correctly, presses correctly, announces
|
|
799
|
+
correctly, and simply never says it can be pressed. Run it against anything the eye reads as a
|
|
800
|
+
card, chip or bar: identical rest and hover means the affordance was never built. What should
|
|
801
|
+
change is the surface's OWN signature (a chip's fill, a row's wash, a field's border) one rung
|
|
802
|
+
deeper, never a different colour and never the cursor — see composition.md §"The pointer
|
|
803
|
+
cursor". And a surface with no press handler must NOT light up: an affordance on something
|
|
804
|
+
inert advertises an action that does not exist. The usual
|
|
754
805
|
cause is two components spending the same inset or the same padding — a wrapper AND the input both
|
|
755
806
|
paying `CONTROL_TEXT_INSET`, a focus ring owned by both a shell and a nested pressable.
|
|
756
807
|
|
package/docs/templates.md
CHANGED
|
@@ -1015,8 +1015,14 @@ when you want them in the context of a whole record surface, and here when money
|
|
|
1015
1015
|
One real `CalendarView` week grid over the current week's events, then a "Today" agenda card
|
|
1016
1016
|
of pressable rows. Every slot is a door: pressing a grid event or an agenda row opens the
|
|
1017
1017
|
sequenced workspace `Drawer`; agenda rows also carry the `⋯` quick-actions menu. Event colors
|
|
1018
|
-
carry meaning (one `ColorName` per event kind
|
|
1019
|
-
week so the copied
|
|
1018
|
+
carry meaning (one `ColorName` per event kind, stated ONCE — the chip's colour and the agenda
|
|
1019
|
+
row's swatch read the same field). The mock data anchors to the real current week so the copied
|
|
1020
|
+
screen is evergreen.
|
|
1021
|
+
|
|
1022
|
+
**Read the header band for the controlled pattern.** The screen owns `date`, so its own "Today"
|
|
1023
|
+
button sets it — the calendar has no second toolbar of its own to fight, and nothing is
|
|
1024
|
+
remounted to force a jump. That is the shape to copy whenever a calendar sits under a header
|
|
1025
|
+
band you already have.
|
|
1020
1026
|
|
|
1021
1027
|
### `tpl_attendance` — the attendance desk
|
|
1022
1028
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useState } from "react";
|
|
2
2
|
import { ScrollView, View } from "react-native";
|
|
3
3
|
import { Text } from "@lotics/ui/text";
|
|
4
|
-
import { colors,
|
|
4
|
+
import { colors, type ColorName } from "@lotics/ui/colors";
|
|
5
5
|
import { ActionMenu } from "@lotics/ui/action_menu";
|
|
6
6
|
import { Badge } from "@lotics/ui/badge";
|
|
7
7
|
import { Button } from "@lotics/ui/button";
|
|
@@ -52,47 +52,48 @@ function evt(
|
|
|
52
52
|
m: number,
|
|
53
53
|
durMin: number,
|
|
54
54
|
title: string,
|
|
55
|
-
color: string,
|
|
56
55
|
data: AgendaMeta,
|
|
57
56
|
): CalendarEvent<AgendaMeta> {
|
|
58
57
|
const start = new Date(day.getFullYear(), day.getMonth(), day.getDate(), h, m);
|
|
59
|
-
|
|
58
|
+
// The chip's colour IS the row's colour — `data.mau` is the one statement of
|
|
59
|
+
// it, so the two surfaces cannot drift apart.
|
|
60
|
+
return { id, title, start, end: new Date(start.getTime() + durMin * 60_000), color: data.mau, data };
|
|
60
61
|
}
|
|
61
62
|
|
|
62
63
|
const EVENTS: CalendarEvent<AgendaMeta>[] = [
|
|
63
64
|
// Today — the dispatch desk's three slots.
|
|
64
|
-
evt("hn-1", TODAY, 8, 0, 90, "Deliver APEX PLASTICS — VTD063518",
|
|
65
|
+
evt("hn-1", TODAY, 8, 0, 90, "Deliver APEX PLASTICS — VTD063518", {
|
|
65
66
|
diaDiem: "Eastport industrial zone",
|
|
66
67
|
trangThai: "Truck loaded",
|
|
67
68
|
mau: "blue",
|
|
68
69
|
}),
|
|
69
|
-
evt("hn-2", TODAY, 10, 30, 60, "Reconcile delivery notes — HANDAN",
|
|
70
|
+
evt("hn-2", TODAY, 10, 30, 60, "Reconcile delivery notes — HANDAN", {
|
|
70
71
|
diaDiem: "Head office",
|
|
71
72
|
trangThai: "Prepared",
|
|
72
73
|
mau: "emerald",
|
|
73
74
|
}),
|
|
74
|
-
evt("hn-3", TODAY, 15, 0, 60, "Customer sample review — VITTORIA",
|
|
75
|
+
evt("hn-3", TODAY, 15, 0, 60, "Customer sample review — VITTORIA", {
|
|
75
76
|
diaDiem: "Factory sample room",
|
|
76
77
|
trangThai: "Awaiting confirmation",
|
|
77
78
|
mau: "amber",
|
|
78
79
|
}),
|
|
79
80
|
// The rest of the week.
|
|
80
|
-
evt("t-1", addDays(WEEK_START, SLOTS[0]), 8, 0, 90, "Deliver KOMASPEC — blanks 675×325",
|
|
81
|
+
evt("t-1", addDays(WEEK_START, SLOTS[0]), 8, 0, 90, "Deliver KOMASPEC — blanks 675×325", {
|
|
81
82
|
diaDiem: "Northgate industrial park",
|
|
82
83
|
trangThai: "Truck loaded",
|
|
83
84
|
mau: "blue",
|
|
84
85
|
}),
|
|
85
|
-
evt("t-2", addDays(WEEK_START, SLOTS[1]), 14, 0, 60, "Reconcile delivery notes — NEWTECONS",
|
|
86
|
+
evt("t-2", addDays(WEEK_START, SLOTS[1]), 14, 0, 60, "Reconcile delivery notes — NEWTECONS", {
|
|
86
87
|
diaDiem: "Head office",
|
|
87
88
|
trangThai: "Prepared",
|
|
88
89
|
mau: "emerald",
|
|
89
90
|
}),
|
|
90
|
-
evt("t-3", addDays(WEEK_START, SLOTS[2]), 10, 0, 60, "Kick off the VITTORIA order",
|
|
91
|
+
evt("t-3", addDays(WEEK_START, SLOTS[2]), 10, 0, 60, "Kick off the VITTORIA order", {
|
|
91
92
|
diaDiem: "Online meeting",
|
|
92
93
|
trangThai: "Awaiting confirmation",
|
|
93
94
|
mau: "amber",
|
|
94
95
|
}),
|
|
95
|
-
evt("t-4", addDays(WEEK_START, SLOTS[3]), 8, 30, 90, "Deliver BRIGHTCELL BATTERIES — TS1250",
|
|
96
|
+
evt("t-4", addDays(WEEK_START, SLOTS[3]), 8, 30, 90, "Deliver BRIGHTCELL BATTERIES — TS1250", {
|
|
96
97
|
diaDiem: "Brightcell plant, Eastport",
|
|
97
98
|
trangThai: "Truck loaded",
|
|
98
99
|
mau: "blue",
|
|
@@ -104,20 +105,10 @@ const EVENTS: CalendarEvent<AgendaMeta>[] = [
|
|
|
104
105
|
start: addDays(WEEK_START, 2),
|
|
105
106
|
end: addDays(WEEK_START, 4),
|
|
106
107
|
allDay: true,
|
|
107
|
-
color:
|
|
108
|
+
color: "zinc",
|
|
108
109
|
},
|
|
109
110
|
];
|
|
110
111
|
|
|
111
|
-
const CAL_LABELS = {
|
|
112
|
-
today: "Today",
|
|
113
|
-
month: "Month",
|
|
114
|
-
week: "Week",
|
|
115
|
-
day: "Day",
|
|
116
|
-
previous: "Previous",
|
|
117
|
-
next: "Next",
|
|
118
|
-
allDay: "all day",
|
|
119
|
-
more: (n: number) => `+${n} more`,
|
|
120
|
-
};
|
|
121
112
|
|
|
122
113
|
const p2 = (n: number) => String(n).padStart(2, "0");
|
|
123
114
|
const fmtTime = (d: Date) => `${p2(d.getHours())}:${p2(d.getMinutes())}`;
|
|
@@ -187,9 +178,10 @@ function SlotWorkspace({ e }: { e: CalendarEvent<AgendaMeta> }) {
|
|
|
187
178
|
}
|
|
188
179
|
|
|
189
180
|
export function TplCalendar() {
|
|
190
|
-
//
|
|
191
|
-
//
|
|
192
|
-
|
|
181
|
+
// The screen owns the date, so its own header band can drive the calendar —
|
|
182
|
+
// no remount, no second toolbar. This used to be `key={calKey}`, forcing a
|
|
183
|
+
// fresh mount because the calendar's date was private to it.
|
|
184
|
+
const [date, setDate] = useState(TODAY);
|
|
193
185
|
const [openId, setOpenId] = useState<string | null>(null);
|
|
194
186
|
|
|
195
187
|
const openEvent = EVENTS.find((e) => e.id === openId) ?? null;
|
|
@@ -215,21 +207,22 @@ export function TplCalendar() {
|
|
|
215
207
|
<Text size="sm" color="muted">This week — deliveries, reconciliations and customer visits</Text>
|
|
216
208
|
</View>
|
|
217
209
|
<Text size="sm" color="muted" tabular>
|
|
218
|
-
{viewTitle("week",
|
|
210
|
+
{viewTitle("week", date, 1, "vi")}
|
|
219
211
|
</Text>
|
|
220
|
-
<Button title="Today" color="secondary" onPress={() =>
|
|
212
|
+
<Button title="Today" color="secondary" onPress={() => setDate(TODAY)} />
|
|
221
213
|
</View>
|
|
222
214
|
|
|
223
215
|
{/* the calendar — real week grid, internal toolbar + scroll */}
|
|
224
216
|
<Card style={{ padding: 0, height: 520, overflow: "hidden" }}>
|
|
225
217
|
<CalendarView<AgendaMeta>
|
|
226
|
-
key={calKey}
|
|
227
218
|
events={EVENTS}
|
|
228
219
|
defaultView="week"
|
|
229
|
-
|
|
220
|
+
date={date}
|
|
221
|
+
onDateChange={setDate}
|
|
230
222
|
weekStartsOn={1}
|
|
231
223
|
locale="vi"
|
|
232
|
-
|
|
224
|
+
dayStartHour={7}
|
|
225
|
+
dayEndHour={19}
|
|
233
226
|
onEventPress={(e) => setOpenId(e.id)}
|
|
234
227
|
/>
|
|
235
228
|
</Card>
|
|
@@ -1244,8 +1244,13 @@ export function TplItemList() {
|
|
|
1244
1244
|
<Text size="xxl" weight="semibold">Records</Text>
|
|
1245
1245
|
|
|
1246
1246
|
{/* toolbar — search + consolidated filters LEFT, New case RIGHT, one row.
|
|
1247
|
-
A search is present, so status is a dropdown (Select), not pills.
|
|
1248
|
-
|
|
1247
|
+
A search is present, so status is a dropdown (Select), not pills.
|
|
1248
|
+
|
|
1249
|
+
`flex-start`, not `center`: the filter block wraps to two or three
|
|
1250
|
+
lines on a phone, and centring floats the CTA against the whole
|
|
1251
|
+
wrapped block — measured landing level with the SECOND row of chips,
|
|
1252
|
+
anchored to nothing. The band's top edge is what it belongs to. */}
|
|
1253
|
+
<View style={{ flexDirection: "row", alignItems: "flex-start", gap: 8 }}>
|
|
1249
1254
|
<View style={{ flexDirection: "row", flexWrap: "wrap", gap: 8, alignItems: "center", flex: 1 }}>
|
|
1250
1255
|
<View style={{ flexGrow: 1, flexBasis: 220, minWidth: 200, maxWidth: 340 }}>
|
|
1251
1256
|
<SearchInput
|