@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.
- package/AGENTS.md +5 -4
- package/package.json +2 -1
- 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
|
|
119
|
-
|
|
120
|
-
`
|
|
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.
|
|
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
|
+
});
|