@lotics/ui 7.5.0 → 7.6.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.
Files changed (3) hide show
  1. package/AGENTS.md +5 -4
  2. package/package.json +2 -1
  3. package/src/modal.tsx +163 -0
package/AGENTS.md CHANGED
@@ -115,9 +115,10 @@ Pick by capability, not by name. (→ the source file for the API.)
115
115
  (bulk-select bar).
116
116
  - **Filters & view controls** — `SearchInput`, `ChipGroup`, `FilterChip` (+ `RangeSlider`,
117
117
  `Counter`), `Chip`.
118
- - **Overlays** — `Dialog`, `Drawer` (+ `DrawerFooter`), `Popover`, `Tooltip`, `OptionList`
119
- (the searchable list body host it in a `Popover`/`Dialog` for a command palette),
120
- `Alert` (the blocking confirm).
118
+ - **Overlays** — `Dialog` (centered card over a scrim), `Modal` (+ `ModalHeader`/`ModalBody`/
119
+ `ModalFooter` a full-bleed, edge-to-edge takeover with NO scrim), `Drawer` (+ `DrawerFooter`),
120
+ `Popover`, `Tooltip`, `OptionList` (the searchable list body — host it in a `Popover`/`Dialog` for
121
+ a command palette), `Alert` (the blocking confirm).
121
122
  - **Status / feedback** — `Badge` / `StatusBadge`, `Callout` (inline status), `EmptyState`,
122
123
  `CompletionState`, `ActivityIndicator` / `Loading`, `Skeleton`.
123
124
  - **Files** — `FilesEditor` (THE all-in-one attachment field: an upload-aware grid + a toolbar below
@@ -804,7 +805,7 @@ check_circle (CheckCircle — the completion ring: an empty ring that springs to
804
805
  floating_action_bar · filter_chip · column_filter (ColumnFilter — the typed per-column filter pill +
805
806
  columnFilterToConditions; for a register filtering on several columns) · chip_group · search_input ·
806
807
  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. Examples: `tpl_task_board`, `tpl_pipeline`) · 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) ·
807
- step_progress · timeline (heterogeneous event LOG — per-row icon + expandable details, models the past; NOT progress) · drawer (+ DrawerFooter) · dialog · popover · tooltip ·
808
+ 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) · popover · tooltip ·
808
809
  alert · peek · empty_state · completion_state · callout (Callout · CalloutTitle ·
809
810
  CalloutText · CalloutActions) · kpi_card · kpi_strip · metric · trend_chip · sparkline ·
810
811
  bar_chart · line_chart · pie_chart · ring_gauge · progress_bar · stacked_progress_bar · breakdown ·
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/ui",
3
- "version": "7.5.0",
3
+ "version": "7.6.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "./tokens": "./src/tokens.ts",
@@ -193,6 +193,7 @@
193
193
  "./date_calendar": "./src/date_calendar.tsx",
194
194
  "./dialog": "./src/dialog.tsx",
195
195
  "./drawer": "./src/drawer.tsx",
196
+ "./modal": "./src/modal.tsx",
196
197
  "./screen_router": "./src/screen_router.tsx",
197
198
  "./route_matching": "./src/route_matching.ts",
198
199
  "./menu_title": "./src/menu_title.tsx",
package/src/modal.tsx ADDED
@@ -0,0 +1,163 @@
1
+ import { ReactNode } from "react";
2
+ import { Modal as RNModal, StyleSheet, View, ScrollView, ViewStyle } from "react-native";
3
+ import { colors } from "@lotics/ui/colors";
4
+ import { IconButton } from "@lotics/ui/icon_button";
5
+ import { Text } from "@lotics/ui/text";
6
+ import { PortalHost } from "@lotics/ui/portal";
7
+ import { useOverlayScope } from "@lotics/ui/overlay_scope";
8
+
9
+ export interface ModalProps {
10
+ open: boolean;
11
+ onClose: () => void;
12
+ /**
13
+ * The modal body. Lay it out as a flex column — a fixed `ModalHeader` and
14
+ * `ModalFooter` pin while a `ModalBody` scrolls between them.
15
+ */
16
+ children: ReactNode;
17
+ testID?: string;
18
+ }
19
+
20
+ /**
21
+ * A full-bleed, edge-to-edge takeover: an OPAQUE surface that covers the entire
22
+ * screen. Unlike `Dialog` (a centered card over a scrim) or `Drawer` (a docked
23
+ * panel over a scrim), a `Modal` has nothing behind it to dim — it owns the whole
24
+ * viewport. Reach for it for a focused capture, a multi-step wizard, or a console
25
+ * the user steps INTO and out of, where the surrounding chrome is a distraction.
26
+ *
27
+ * Lays children as a flex column so a fixed `ModalHeader` + `ModalFooter` pin
28
+ * while a `ModalBody` scrolls between them. Controlled via `open`/`onClose`; the
29
+ * header close control dismisses it (Escape on web, the back gesture on Android).
30
+ */
31
+ export function Modal(props: ModalProps) {
32
+ const { open, onClose, children, testID } = props;
33
+ useOverlayScope(open);
34
+
35
+ return (
36
+ <RNModal visible={open} onRequestClose={onClose} animationType="fade">
37
+ <View testID={testID} style={styles.surface}>
38
+ <PortalHost>{children}</PortalHost>
39
+ </View>
40
+ </RNModal>
41
+ );
42
+ }
43
+
44
+ export interface ModalHeaderProps {
45
+ /** Header title. A string renders as the standard title; a node renders as-is. */
46
+ title?: ReactNode;
47
+ /** A short caption above the title (a context label / category). */
48
+ eyebrow?: ReactNode;
49
+ /** Right-aligned controls placed before the close button (e.g. a save action). */
50
+ actions?: ReactNode;
51
+ onClose: () => void;
52
+ style?: ViewStyle;
53
+ }
54
+
55
+ /**
56
+ * The fixed top bar of a `Modal`: an optional eyebrow + title on the left, an
57
+ * optional `actions` slot, and a close control wired to `onClose` on the right.
58
+ * Render it as the FIRST child of the `Modal`; it never scrolls.
59
+ */
60
+ export function ModalHeader(props: ModalHeaderProps) {
61
+ const { title, eyebrow, actions, onClose, style } = props;
62
+
63
+ return (
64
+ <View style={[styles.header, style]}>
65
+ <View style={styles.headerText}>
66
+ {eyebrow !== undefined ? (
67
+ typeof eyebrow === "string" ? (
68
+ <Text size="xs" weight="medium" color="muted">
69
+ {eyebrow}
70
+ </Text>
71
+ ) : (
72
+ eyebrow
73
+ )
74
+ ) : null}
75
+ {title !== undefined ? (
76
+ typeof title === "string" ? (
77
+ <Text size="lg" weight="semibold">
78
+ {title}
79
+ </Text>
80
+ ) : (
81
+ title
82
+ )
83
+ ) : null}
84
+ </View>
85
+ {actions !== undefined ? <View style={styles.headerActions}>{actions}</View> : null}
86
+ <IconButton icon="x" size="lg" accessibilityLabel="Close" onPress={onClose} />
87
+ </View>
88
+ );
89
+ }
90
+
91
+ export interface ModalBodyProps {
92
+ children: ReactNode;
93
+ }
94
+
95
+ /**
96
+ * The flex:1 scrolling content region between a `Modal`'s pinned header and
97
+ * footer. Fills the remaining height; its content scrolls.
98
+ */
99
+ export function ModalBody(props: ModalBodyProps) {
100
+ const { children } = props;
101
+ return <ScrollView contentContainerStyle={styles.bodyContent}>{children}</ScrollView>;
102
+ }
103
+
104
+ export interface ModalFooterProps {
105
+ /** The action(s) — typically right-aligned `Button`s. Prepend a
106
+ * `<Text style={{ flex: 1 }}>` hint to push them right with a summary on the left. */
107
+ children: ReactNode;
108
+ align?: "start" | "end" | "space-between";
109
+ }
110
+
111
+ /**
112
+ * The pinned bottom action bar of a `Modal` — a hairline-topped band (the same
113
+ * chrome as `DialogFooter` / `DrawerFooter`) that commits the modal's work.
114
+ * Render it as the LAST child of the `Modal`, after the `ModalBody`; the body's
115
+ * `flex:1` pins it to the surface bottom.
116
+ */
117
+ export function ModalFooter(props: ModalFooterProps) {
118
+ const { children, align = "end" } = props;
119
+ const justifyContent =
120
+ align === "end" ? "flex-end" : align === "space-between" ? "space-between" : "flex-start";
121
+
122
+ return <View style={[styles.footer, { justifyContent }]}>{children}</View>;
123
+ }
124
+
125
+ const styles = StyleSheet.create({
126
+ surface: {
127
+ flex: 1,
128
+ backgroundColor: colors.white,
129
+ flexDirection: "column",
130
+ },
131
+ header: {
132
+ flexDirection: "row",
133
+ alignItems: "center",
134
+ gap: 12,
135
+ paddingLeft: 24,
136
+ paddingRight: 16,
137
+ paddingVertical: 16,
138
+ minHeight: 64,
139
+ borderBottomWidth: 1,
140
+ borderBottomColor: colors.border,
141
+ },
142
+ headerText: {
143
+ flex: 1,
144
+ gap: 2,
145
+ },
146
+ headerActions: {
147
+ flexDirection: "row",
148
+ alignItems: "center",
149
+ gap: 8,
150
+ },
151
+ bodyContent: {
152
+ padding: 24,
153
+ },
154
+ footer: {
155
+ flexDirection: "row",
156
+ alignItems: "center",
157
+ gap: 12,
158
+ paddingHorizontal: 24,
159
+ paddingVertical: 16,
160
+ borderTopWidth: 1,
161
+ borderTopColor: colors.border,
162
+ },
163
+ });