@lotics/ui 11.0.0 → 11.2.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 +5 -2
- package/package.json +1 -1
- package/src/icon.tsx +2 -0
- package/src/inline_date_picker.tsx +5 -1
- package/src/inline_edit.tsx +6 -0
- package/src/inline_time_picker.tsx +3 -0
- package/src/use_section_nav.ts +25 -5
package/AGENTS.md
CHANGED
|
@@ -233,7 +233,10 @@ value · trailing laid out like a TABLE: `labelWidth` / `trailingWidth` /
|
|
|
233
233
|
need) holding `DetailRow`s — set `trailingWidth` when ANY row carries a
|
|
234
234
|
trailing action/badge, so EVERY row reserves the column and one row's
|
|
235
235
|
`Copy` button never makes its editor narrower than its neighbours'. An editor AT REST sits
|
|
236
|
-
on a zinc-50 chip — THE editability affordance: users see what's editable without hovering
|
|
236
|
+
on a zinc-50 chip — THE editability affordance: users see what's editable without hovering; the
|
|
237
|
+
picker-opening editors (`InlineSelect`/`InlineDatePicker`/`InlineTimePicker`) additionally carry a
|
|
238
|
+
rest glyph (chevron / calendar / clock) marking them tappable pickers without hover — the
|
|
239
|
+
touch-critical cue plain text/number fields don't get.
|
|
237
240
|
`background="transparent"` opts a field out of the chip — for DENSE, uniformly-editable
|
|
238
241
|
surfaces (a task list/board where EVERY cell edits: the chip repeated everywhere is noise and
|
|
239
242
|
distinguishes nothing; hover/focus still reveal the input). Keep the chip wherever editable and
|
|
@@ -1062,7 +1065,7 @@ check_circle (CheckCircle — the completion ring: an empty ring that springs to
|
|
|
1062
1065
|
floating_action_bar · filter_chip · column_filter (ColumnFilter — the typed per-column filter pill +
|
|
1063
1066
|
columnFilterToConditions; for a register filtering on several columns) · chip_group · search_input ·
|
|
1064
1067
|
sort_header · table · data_grid (DataGrid — the inline-managed grouped table: a grouped, sortable grid of LIVE inline-editor cells (`columns[].cell` → ANY field) + optional per-row `leading` (a CheckCircle) + `renderGroupFooter` (per-group add, align with the exported `gridRowStyle`) + `labels` (localize the sort-header a11y via `SortHeaderLabels`). Owns header/sections/rows; consumer owns data + sort/group/filter/collapse state + toolbar. Renders ALL rows — MODERATE data; 10k+ → the paginated `Table` register. Example: `tpl_task_board`) · pagination · accordion · stepper (Stepper + Step — done/current/upcoming/warning/complete progress on a track (horizontal) or spine (vertical); compound `<Step status>children` OR data `steps[]`+`current`; **navigable** via `Step.onPress` (both orientations — the whole step is the tap target) + `active` to wash the selected one, so it doubles as a section/phase switcher; the guided-run / agent-feed primitive — subsumes the old StepList) ·
|
|
1065
|
-
step_progress · timeline (heterogeneous event LOG — per-row icon + expandable details, models the past; NOT progress) · drawer (+ DrawerFooter) · dialog · modal (Modal + ModalHeader + ModalBody + ModalFooter — the full-bleed, edge-to-edge takeover: an OPAQUE surface that COVERS THE WHOLE SCREEN, so unlike Dialog (centered card WITH scrim) and Drawer (docked panel WITH scrim) there is nothing behind it to dim — NO scrim, NO backdrop. Lays children as a flex column: a pinned ModalHeader (eyebrow/title + an actions slot + close), a flex:1 scrolling ModalBody, a pinned ModalFooter (the commit bar, same chrome as DialogFooter/DrawerFooter). Reach for it for a focused capture / multi-step wizard / a console the user steps INTO, where surrounding chrome is a distraction; pick Dialog when the surface is a card the user can see context around) · screen_router (ScreenRouter + Screen + useScreenRouter — the SCREENS compound: a flat navigation stack (`navigate("/case/:id")` pushes, `goBack` pops, `canGoBack`, route `params`; stacked screens stay mounted `display:none` so scroll survives the round trip). Dialog BAKES a router in (`<Dialog><Screen route="">…`); ANY other container hosts the standalone `<ScreenRouter>` — and it wraps AROUND the container so the CHROME can read the stack: a Drawer drilling into a LINKED record swaps its header to a BACK IconButton + the pushed record's id while `canGoBack` (sequence ◀ ▶ hides — stepping the root from inside a linked record disorients), and the pushed `<Screen route="/case/:id">` is a REAL editable workspace with its own footer CTAs. Key the router by record id so stepping ◀ ▶ resets the stack. Worked example: `tpl_item_list` drawer) · popover · popover_nav (usePopoverNav + PopoverScreen + PopoverNavHeader — the popover's built-in mini-router: EVERY `Popover` provides the nav context (`navigate(route)` pushes, `goBack`, `currentRoute`, `canGoBack`; resets on close), `PopoverScreen route=""` is the root and screens render conditionally (unmounted when inactive — no scroll preservation), `PopoverNavHeader` is the title row whose back chevron auto-appears while `canGoBack` (`right` slot, `backLabel`). For a multi-screen menu inside ONE popover (an avatar/settings menu drilling into a sub-panel); route PATTERNS, `params`, and stacked-alive screens are `screen_router`'s job. Distinct from `Popover`'s plain `PopoverHeader` children container) · tooltip ·
|
|
1068
|
+
step_progress · timeline (heterogeneous event LOG — per-row icon + expandable details, models the past; NOT progress) · drawer (+ DrawerFooter) · dialog · modal (Modal + ModalHeader + ModalBody + ModalFooter — the full-bleed, edge-to-edge takeover: an OPAQUE surface that COVERS THE WHOLE SCREEN, so unlike Dialog (centered card WITH scrim) and Drawer (docked panel WITH scrim) there is nothing behind it to dim — NO scrim, NO backdrop. Lays children as a flex column: a pinned ModalHeader (eyebrow/title + an actions slot + close), a flex:1 scrolling ModalBody, a pinned ModalFooter (the commit bar, same chrome as DialogFooter/DrawerFooter). Reach for it for a focused capture / multi-step wizard / a console the user steps INTO, where surrounding chrome is a distraction; pick Dialog when the surface is a card the user can see context around) · screen_router (ScreenRouter + Screen + useScreenRouter — the SCREENS compound: a flat navigation stack (`navigate("/case/:id")` pushes, `goBack` pops, `canGoBack`, route `params`; stacked screens stay mounted `display:none` so scroll survives the round trip). Dialog BAKES a router in (`<Dialog><Screen route="">…`); ANY other container hosts the standalone `<ScreenRouter>` — and it wraps AROUND the container so the CHROME can read the stack: a Drawer drilling into a LINKED record swaps its header to a BACK IconButton + the pushed record's id while `canGoBack` (sequence ◀ ▶ hides — stepping the root from inside a linked record disorients), and the pushed `<Screen route="/case/:id">` is a REAL editable workspace with its own footer CTAs. Key the router by record id so stepping ◀ ▶ resets the stack. Worked example: `tpl_item_list` drawer) · popover (Popover + PopoverTrigger + PopoverContent — **PopoverContent already insets its body 12px; put content directly in it, NEVER add your own padding View (that double-pads). Title/actions via PopoverHeader / PopoverFooter**) · popover_nav (usePopoverNav + PopoverScreen + PopoverNavHeader — the popover's built-in mini-router: EVERY `Popover` provides the nav context (`navigate(route)` pushes, `goBack`, `currentRoute`, `canGoBack`; resets on close), `PopoverScreen route=""` is the root and screens render conditionally (unmounted when inactive — no scroll preservation), `PopoverNavHeader` is the title row whose back chevron auto-appears while `canGoBack` (`right` slot, `backLabel`). For a multi-screen menu inside ONE popover (an avatar/settings menu drilling into a sub-panel); route PATTERNS, `params`, and stacked-alive screens are `screen_router`'s job. Distinct from `Popover`'s plain `PopoverHeader` children container) · tooltip ·
|
|
1066
1069
|
alert · peek · empty_state · completion_state · callout (Callout · CalloutTitle ·
|
|
1067
1070
|
CalloutText · CalloutActions) · kpi_card · kpi_strip · summary_line (SummaryLine — the light inline summary of a register/list's FILTERED view, sits below the toolbar; NOT the boxed dashboard `kpi_strip` band) · metric · trend_chip · sparkline ·
|
|
1068
1071
|
bar_chart · line_chart · pie_chart · ring_gauge · progress_bar · stacked_progress_bar · breakdown ·
|
package/package.json
CHANGED
package/src/icon.tsx
CHANGED
|
@@ -40,6 +40,7 @@ import Box from "lucide-react-native/dist/esm/icons/box";
|
|
|
40
40
|
import Brackets from "lucide-react-native/dist/esm/icons/brackets";
|
|
41
41
|
import Calculator from "lucide-react-native/dist/esm/icons/calculator";
|
|
42
42
|
import Calendar from "lucide-react-native/dist/esm/icons/calendar";
|
|
43
|
+
import CalendarClock from "lucide-react-native/dist/esm/icons/calendar-clock";
|
|
43
44
|
import CalendarOff from "lucide-react-native/dist/esm/icons/calendar-off";
|
|
44
45
|
import Camera from "lucide-react-native/dist/esm/icons/camera";
|
|
45
46
|
import Check from "lucide-react-native/dist/esm/icons/check";
|
|
@@ -234,6 +235,7 @@ const iconComponents = {
|
|
|
234
235
|
brackets: Brackets,
|
|
235
236
|
calculator: Calculator,
|
|
236
237
|
calendar: Calendar,
|
|
238
|
+
"calendar-clock": CalendarClock,
|
|
237
239
|
"calendar-off": CalendarOff,
|
|
238
240
|
camera: Camera,
|
|
239
241
|
check: Check,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useCallback, useRef, useState } from "react";
|
|
2
2
|
import { View } from "react-native";
|
|
3
3
|
import { Text } from "./text";
|
|
4
|
+
import { Icon } from "./icon";
|
|
4
5
|
import { colors } from "./colors";
|
|
5
6
|
import { Popover, PopoverTrigger, PopoverContent } from "./popover";
|
|
6
7
|
import { DatePickerPanel } from "./date_picker";
|
|
@@ -91,7 +92,10 @@ export function InlineDatePicker(props: InlineDatePickerProps) {
|
|
|
91
92
|
disabled={disabled}
|
|
92
93
|
active={open && !disabled}
|
|
93
94
|
accessibilityLabel={accessibilityLabel}
|
|
94
|
-
|
|
95
|
+
// A rest affordance (like the select's chevron): a calendar glyph marks
|
|
96
|
+
// the field as a tappable date control even when empty — no hover / pointer
|
|
97
|
+
// cursor needed, so it reads as interactive on touch.
|
|
98
|
+
trailing={saving ? <ActivityIndicator size={16} color={colors.zinc[400]} /> : <Icon name={format === "datetime" ? "calendar-clock" : "calendar"} size={18} color={colors.zinc[400]} />}
|
|
95
99
|
/>
|
|
96
100
|
</PopoverTrigger>
|
|
97
101
|
<PopoverContent>
|
package/src/inline_edit.tsx
CHANGED
|
@@ -112,6 +112,10 @@ interface InlineEditFrameProps {
|
|
|
112
112
|
struck?: boolean;
|
|
113
113
|
/** Resting surface — see {@link InlineEditBackground}. Default "tint". */
|
|
114
114
|
background?: InlineEditBackground;
|
|
115
|
+
/** Right-aligned rest affordance shown in VIEW mode (e.g. a clock for a time
|
|
116
|
+
* field) — a hover-independent cue the field opens a picker. Omit for plain
|
|
117
|
+
* text/number inputs (typing IS the affordance). */
|
|
118
|
+
affordance?: ReactNode;
|
|
115
119
|
}
|
|
116
120
|
|
|
117
121
|
interface InlineEditViewProps {
|
|
@@ -204,6 +208,7 @@ export function InlineEditFrame(props: InlineEditFrameProps) {
|
|
|
204
208
|
accessibilityLabel,
|
|
205
209
|
struck,
|
|
206
210
|
background,
|
|
211
|
+
affordance,
|
|
207
212
|
} = props;
|
|
208
213
|
|
|
209
214
|
if (!editing) {
|
|
@@ -216,6 +221,7 @@ export function InlineEditFrame(props: InlineEditFrameProps) {
|
|
|
216
221
|
disabled={disabled}
|
|
217
222
|
accessibilityLabel={accessibilityLabel}
|
|
218
223
|
struck={struck}
|
|
224
|
+
trailing={affordance}
|
|
219
225
|
/>
|
|
220
226
|
);
|
|
221
227
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { useCallback } from "react";
|
|
2
2
|
import type { KeyboardEvent } from "react";
|
|
3
|
+
import { Icon } from "./icon";
|
|
4
|
+
import { colors } from "./colors";
|
|
3
5
|
import { TimePicker } from "./time_picker";
|
|
4
6
|
import { type InlineEditBackground, InlineEditFrame, useInlineEdit, type InlineEditControls } from "./inline_edit";
|
|
5
7
|
|
|
@@ -53,6 +55,7 @@ export function InlineTimePicker(props: InlineTimePickerProps) {
|
|
|
53
55
|
error={edit.error}
|
|
54
56
|
disabled={disabled}
|
|
55
57
|
accessibilityLabel={accessibilityLabel}
|
|
58
|
+
affordance={<Icon name="clock" size={18} color={colors.zinc[400]} />}
|
|
56
59
|
>
|
|
57
60
|
<TimePicker
|
|
58
61
|
value={edit.draft}
|
package/src/use_section_nav.ts
CHANGED
|
@@ -31,20 +31,40 @@ export interface SectionNavHandle<K extends string> {
|
|
|
31
31
|
*
|
|
32
32
|
* Contract: every key's section stays MOUNTED (an unmounted section leaves its
|
|
33
33
|
* last offset registered — conditional sections belong inside an always-mounted
|
|
34
|
-
* wrapper that carries the `onLayout`)
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
34
|
+
* wrapper that carries the `onLayout`). On WEB, `jumpTo` re-measures the live
|
|
35
|
+
* DOM position, so async content growing ABOVE a section (a photo grid, a
|
|
36
|
+
* late-loading table) can never stale a jump; the cached offsets only drive
|
|
37
|
+
* the scroll-spy highlight, which self-corrects as jumps refresh them. On
|
|
38
|
+
* native there is no re-measure — content above should settle before precision
|
|
39
|
+
* matters (`onLayout` won't refire on a pure position shift).
|
|
38
40
|
*/
|
|
39
41
|
export function useSectionNav<K extends string>(keys: readonly [K, ...K[]]): SectionNavHandle<K> {
|
|
40
42
|
const scrollRef = useRef<ScrollView>(null);
|
|
41
43
|
const sectionY = useRef<Partial<Record<K, number>>>({});
|
|
44
|
+
// The registered wrapper's DOM node (web only) — RN-web's layout event
|
|
45
|
+
// exposes it as `nativeEvent.target`, which lets jumpTo re-measure live.
|
|
46
|
+
const sectionEl = useRef<Partial<Record<K, Element>>>({});
|
|
42
47
|
const [activeKey, setActiveKey] = useState<K>(keys[0]);
|
|
43
48
|
const register = (key: K) => (e: LayoutChangeEvent) => {
|
|
44
49
|
sectionY.current[key] = e.nativeEvent.layout.y;
|
|
50
|
+
const target: unknown = (e.nativeEvent as { target?: unknown }).target;
|
|
51
|
+
if (typeof Element !== "undefined" && target instanceof Element) sectionEl.current[key] = target;
|
|
52
|
+
};
|
|
53
|
+
/** The section's CURRENT content-relative offset — measured live on web
|
|
54
|
+
* (immune to async content shifting the page), cached `layout.y` otherwise. */
|
|
55
|
+
const currentY = (key: K): number => {
|
|
56
|
+
const el = sectionEl.current[key];
|
|
57
|
+
const sv = scrollRef.current as (ScrollView & { getScrollableNode?: () => unknown }) | null;
|
|
58
|
+
const sc = typeof sv?.getScrollableNode === "function" ? sv.getScrollableNode() : null;
|
|
59
|
+
if (typeof Element !== "undefined" && el instanceof Element && sc instanceof Element) {
|
|
60
|
+
const y = el.getBoundingClientRect().top - sc.getBoundingClientRect().top + sc.scrollTop;
|
|
61
|
+
sectionY.current[key] = y; // refresh the spy's cache while we're here
|
|
62
|
+
return y;
|
|
63
|
+
}
|
|
64
|
+
return sectionY.current[key] ?? 0;
|
|
45
65
|
};
|
|
46
66
|
const jumpTo = (key: K) => {
|
|
47
|
-
scrollRef.current?.scrollTo({ y: Math.max(0, (
|
|
67
|
+
scrollRef.current?.scrollTo({ y: Math.max(0, currentY(key) - 12), animated: true });
|
|
48
68
|
};
|
|
49
69
|
const onScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
50
70
|
const y = e.nativeEvent.contentOffset.y + 80;
|