@lotics/ui 5.6.1 → 5.8.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 +44 -27
- package/examples/tpl_approvals.tsx +10 -10
- package/examples/tpl_crm_desk.tsx +3 -3
- package/examples/tpl_dieline.tsx +7 -6
- package/examples/tpl_directory.tsx +3 -3
- package/examples/tpl_dossier.tsx +7 -7
- package/examples/tpl_draft.tsx +4 -4
- package/examples/tpl_extract.tsx +7 -7
- package/examples/tpl_inventory.tsx +3 -3
- package/examples/tpl_lookup.tsx +4 -3
- package/examples/tpl_match.tsx +1 -1
- package/examples/tpl_ratedesk.tsx +11 -11
- package/examples/tpl_report.tsx +3 -3
- package/examples/tpl_stock.tsx +3 -3
- package/examples/tpl_triage.tsx +1 -1
- package/package.json +12 -3
- package/src/agent_progress.tsx +14 -12
- package/src/agent_run.tsx +248 -86
- package/src/back_button.tsx +3 -2
- package/src/badge.tsx +2 -1
- package/src/button.tsx +11 -32
- package/src/button_colors.ts +23 -0
- package/src/calendar/calendar_view.tsx +2 -2
- package/src/card_select_item.tsx +2 -1
- package/src/change_review.tsx +5 -6
- package/src/{pill_button.tsx → chip.tsx} +7 -7
- package/src/chip_group.tsx +2 -2
- package/src/column_filter.tsx +4 -4
- package/src/combobox.tsx +2 -9
- package/src/composer.tsx +4 -3
- package/src/control_surface.ts +15 -15
- package/src/date_field.tsx +2 -1
- package/src/date_filter.tsx +2 -1
- package/src/date_range_filter_field.tsx +2 -1
- package/src/dialog.tsx +2 -2
- package/src/discrepancy.tsx +1 -1
- package/src/drawer.tsx +1 -2
- package/src/{filter_pill.tsx → filter_chip.tsx} +8 -8
- package/src/finding.tsx +1 -1
- package/src/gantt/gantt_view.tsx +2 -2
- package/src/icon_button.tsx +38 -14
- package/src/inline_edit.tsx +2 -2
- package/src/link_button.tsx +2 -1
- package/src/markdown.css +209 -0
- package/src/markdown.tsx +17 -0
- package/src/markdown.web.tsx +69 -0
- package/src/menu_button.tsx +2 -1
- package/src/menu_list_item.tsx +2 -1
- package/src/number_input.tsx +2 -1
- package/src/picker.tsx +3 -3
- package/src/popover_header.tsx +2 -2
- package/src/primitives_purity.test.ts +13 -7
- package/src/radio_picker.tsx +2 -1
- package/src/range_slider.tsx +1 -1
- package/src/review_card.tsx +2 -2
- package/src/scan_field.tsx +2 -1
- package/src/scored_option.tsx +0 -1
- package/src/search_input.tsx +3 -2
- package/src/segmented_control.tsx +2 -1
- package/src/status_badge.tsx +2 -1
- package/src/stepper.tsx +3 -2
- package/src/switch_button.tsx +2 -1
- package/src/switcher.tsx +3 -2
- package/src/tabs.tsx +2 -1
- package/src/tag_input.tsx +2 -1
- package/src/text_input_field.tsx +2 -1
- package/src/time_field.tsx +2 -1
- package/src/time_picker.tsx +2 -1
- package/src/triage_row.tsx +3 -3
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type ButtonColor } from "./button";
|
|
2
|
+
import { colors } from "./colors";
|
|
3
|
+
|
|
4
|
+
/** The icon/glyph ink for each button colour — shared by `Button` and
|
|
5
|
+
* `IconButton` so an icon-only action migrated to `IconButton` keeps the EXACT
|
|
6
|
+
* same colour. Lives in its own module (not `button.tsx`) so a test that mocks
|
|
7
|
+
* `@lotics/ui/button` can't leave `IconButton` without it. */
|
|
8
|
+
export function getButtonIconColor(color?: ButtonColor, disabled?: boolean): string {
|
|
9
|
+
switch (color) {
|
|
10
|
+
case "primary":
|
|
11
|
+
return colors.white;
|
|
12
|
+
case "secondary":
|
|
13
|
+
return colors.zinc["900"];
|
|
14
|
+
case "danger":
|
|
15
|
+
return colors.red["900"];
|
|
16
|
+
case "danger-secondary":
|
|
17
|
+
return colors.red["900"];
|
|
18
|
+
case "muted":
|
|
19
|
+
return colors.zinc["600"];
|
|
20
|
+
default:
|
|
21
|
+
return disabled ? colors.zinc["300"] : colors.zinc["950"];
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -125,7 +125,7 @@ const styles = StyleSheet.create({
|
|
|
125
125
|
iconBtn: { width: 30, height: 30, borderRadius: 6, alignItems: "center", justifyContent: "center" },
|
|
126
126
|
todayBtn: { paddingHorizontal: 12, height: 30, borderRadius: 6, borderWidth: 1, borderColor: colors.border, alignItems: "center", justifyContent: "center" },
|
|
127
127
|
title: { flex: 1, textAlign: "center" },
|
|
128
|
-
viewSwitch: { flexDirection: "row", backgroundColor: colors.zinc[100], borderRadius:
|
|
129
|
-
viewBtn: { paddingHorizontal: 12, paddingVertical: 5, borderRadius:
|
|
128
|
+
viewSwitch: { flexDirection: "row", backgroundColor: colors.zinc[100], borderRadius: 10, padding: 2 },
|
|
129
|
+
viewBtn: { paddingHorizontal: 12, paddingVertical: 5, borderRadius: 8 },
|
|
130
130
|
viewBtnActive: { backgroundColor: colors.white },
|
|
131
131
|
});
|
package/src/card_select_item.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { PressableStateCallbackType, StyleProp, StyleSheet, ViewStyle } from "react-native";
|
|
2
2
|
import { colors } from "./colors";
|
|
3
|
+
import { CONTROL_RADIUS } from "./control_surface";
|
|
3
4
|
import { PressableHighlight } from "./pressable_highlight";
|
|
4
5
|
|
|
5
6
|
interface CardSelectItemProps {
|
|
@@ -45,7 +46,7 @@ export function CardSelectItem(props: CardSelectItemProps) {
|
|
|
45
46
|
const styles = StyleSheet.create({
|
|
46
47
|
container: {
|
|
47
48
|
padding: 16,
|
|
48
|
-
borderRadius:
|
|
49
|
+
borderRadius: CONTROL_RADIUS,
|
|
49
50
|
backgroundColor: colors.background,
|
|
50
51
|
borderColor: colors.border,
|
|
51
52
|
borderWidth: 1,
|
package/src/change_review.tsx
CHANGED
|
@@ -173,8 +173,8 @@ export function ChangeReview<T>(props: ChangeReviewProps<T>) {
|
|
|
173
173
|
<View key={props.getKey(item, i)} style={styles.itemCard}>
|
|
174
174
|
{props.renderItem(item, i)}
|
|
175
175
|
<View style={styles.itemActions}>
|
|
176
|
-
<Button title={props.rejectLabel ?? "Drop"} color="muted"
|
|
177
|
-
<Button title={props.acceptLabel ?? "Keep"} color="secondary"
|
|
176
|
+
<Button title={props.rejectLabel ?? "Drop"} color="muted" onPress={() => props.onRejectItem?.(i)} />
|
|
177
|
+
<Button title={props.acceptLabel ?? "Keep"} color="secondary" onPress={() => props.onAcceptItem?.(i)} />
|
|
178
178
|
</View>
|
|
179
179
|
</View>
|
|
180
180
|
);
|
|
@@ -193,7 +193,7 @@ export function ChangeReview<T>(props: ChangeReviewProps<T>) {
|
|
|
193
193
|
)}
|
|
194
194
|
</View>
|
|
195
195
|
{props.onUndoItem ? (
|
|
196
|
-
<Button title={L.undo} color="muted"
|
|
196
|
+
<Button title={L.undo} color="muted" onPress={() => props.onUndoItem?.(i)} />
|
|
197
197
|
) : null}
|
|
198
198
|
</View>
|
|
199
199
|
);
|
|
@@ -243,14 +243,13 @@ export function ChangeReviewActions<T>(props: ChangeReviewActionsProps<T>) {
|
|
|
243
243
|
});
|
|
244
244
|
return (
|
|
245
245
|
<View style={styles.actionsRow}>
|
|
246
|
-
{perItem ? <Button title={props.acceptAllLabel ?? "Accept all"} color="muted"
|
|
246
|
+
{perItem ? <Button title={props.acceptAllLabel ?? "Accept all"} color="muted" onPress={acceptAll} /> : null}
|
|
247
247
|
<View style={{ flex: 1 }} />
|
|
248
|
-
{props.onDiscard ? <Button title={props.discardLabel ?? "Discard"} color="muted"
|
|
248
|
+
{props.onDiscard ? <Button title={props.discardLabel ?? "Discard"} color="muted" onPress={props.onDiscard} /> : null}
|
|
249
249
|
{props.onApply ? (
|
|
250
250
|
<Button
|
|
251
251
|
title={props.applyLabel ?? (perItem ? "Apply kept" : "Apply")}
|
|
252
252
|
color="primary"
|
|
253
|
-
shape="rounded"
|
|
254
253
|
disabled={perItem && keptCount === 0}
|
|
255
254
|
onPress={props.onApply}
|
|
256
255
|
/>
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { Ref } from "react";
|
|
2
2
|
import { IconButton } from "./icon_button";
|
|
3
3
|
import { PressableHighlight } from "./pressable_highlight";
|
|
4
|
-
import {
|
|
4
|
+
import { chipSurfaceStyle } from "./control_surface";
|
|
5
5
|
import { StyleSheet, View } from "react-native";
|
|
6
6
|
|
|
7
|
-
interface
|
|
7
|
+
interface ChipProps {
|
|
8
8
|
testID?: string;
|
|
9
9
|
children: React.ReactNode;
|
|
10
10
|
onPress?: () => void;
|
|
@@ -13,7 +13,7 @@ interface PillButtonProps {
|
|
|
13
13
|
ref?: Ref<View>;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
export function
|
|
16
|
+
export function Chip(props: ChipProps) {
|
|
17
17
|
const { testID, children, onPress, onDismiss, dismissTooltip, ref } = props;
|
|
18
18
|
|
|
19
19
|
return (
|
|
@@ -22,12 +22,12 @@ export function PillButton(props: PillButtonProps) {
|
|
|
22
22
|
<PressableHighlight
|
|
23
23
|
testID={testID}
|
|
24
24
|
onPress={onPress}
|
|
25
|
-
style={(state) => [
|
|
25
|
+
style={(state) => [chipSurfaceStyle(state), styles.pillLayout, onDismiss && styles.pillWithDismiss]}
|
|
26
26
|
>
|
|
27
27
|
{children}
|
|
28
28
|
</PressableHighlight>
|
|
29
29
|
) : (
|
|
30
|
-
<View style={[
|
|
30
|
+
<View style={[chipSurfaceStyle({}), styles.pillLayout, onDismiss && styles.pillWithDismiss]}>{children}</View>
|
|
31
31
|
)}
|
|
32
32
|
{onDismiss && (
|
|
33
33
|
<View style={styles.dismissButton}>
|
|
@@ -45,8 +45,8 @@ export function PillButton(props: PillButtonProps) {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
const styles = StyleSheet.create({
|
|
48
|
-
// Surface (height/border/radius/white + hover) comes from
|
|
49
|
-
//
|
|
48
|
+
// Surface (height/border/radius/white + hover) comes from chipSurfaceStyle;
|
|
49
|
+
// Chip owns only its row layout.
|
|
50
50
|
pillLayout: {
|
|
51
51
|
flexDirection: "row",
|
|
52
52
|
alignItems: "center",
|
package/src/chip_group.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { View } from "react-native";
|
|
2
2
|
import { Text } from "./text";
|
|
3
3
|
import { PressableHighlight } from "./pressable_highlight";
|
|
4
|
-
import {
|
|
4
|
+
import { chipSurfaceStyle } from "./control_surface";
|
|
5
5
|
|
|
6
6
|
// One-of-N chips: every option visible, one tap to switch, the row wraps on
|
|
7
7
|
// narrow widths. Quiet zinc styling — bordered white at rest, dark fill when
|
|
@@ -55,7 +55,7 @@ export function ChipGroup<T extends string = string>(props: ChipGroupProps<T>) {
|
|
|
55
55
|
// The shared pill-surface contract (height/border/radius/white +
|
|
56
56
|
// hover/press wash + the selected box-shadow ring); the chip only
|
|
57
57
|
// adds its horizontal padding.
|
|
58
|
-
style={(state) => [
|
|
58
|
+
style={(state) => [chipSurfaceStyle(state, { selected: active }), { paddingHorizontal: 14 }]}
|
|
59
59
|
>
|
|
60
60
|
<Text size="sm" weight={active ? "semibold" : "medium"} color={active ? "default" : "muted"}>
|
|
61
61
|
{option.label}
|
package/src/column_filter.tsx
CHANGED
|
@@ -3,7 +3,7 @@ import { Text } from "./text";
|
|
|
3
3
|
import { TextInputField } from "./text_input_field";
|
|
4
4
|
import { NumberInput } from "./number_input";
|
|
5
5
|
import { PickerMenu } from "./picker_menu";
|
|
6
|
-
import {
|
|
6
|
+
import { FilterChip } from "./filter_chip";
|
|
7
7
|
import type { PickerOption } from "./picker";
|
|
8
8
|
|
|
9
9
|
/** A column the picker can filter on. `type` selects the control + operators. */
|
|
@@ -97,7 +97,7 @@ export interface ColumnFilterProps {
|
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
/**
|
|
100
|
-
* A toolbar-style filter pill: the same `
|
|
100
|
+
* A toolbar-style filter pill: the same `Chip` the table toolbar uses, so
|
|
101
101
|
* a picker's filters read identically. Inactive → "Label ⌄"; active →
|
|
102
102
|
* "Label: summary" with an X to clear. Pressing the pill opens a type-aware
|
|
103
103
|
* editor (text contains / number range / multi-select). Controlled — the
|
|
@@ -109,7 +109,7 @@ export function ColumnFilter(props: ColumnFilterProps) {
|
|
|
109
109
|
const active = isColumnFilterActive(value);
|
|
110
110
|
|
|
111
111
|
return (
|
|
112
|
-
<
|
|
112
|
+
<FilterChip
|
|
113
113
|
label={column.label}
|
|
114
114
|
summary={active ? columnFilterSummary(column, value) : undefined}
|
|
115
115
|
onClear={() => onChange(undefined)}
|
|
@@ -150,7 +150,7 @@ export function ColumnFilter(props: ColumnFilterProps) {
|
|
|
150
150
|
onValueChange={(selected) => onChange({ kind: "select", selected })}
|
|
151
151
|
/>
|
|
152
152
|
)}
|
|
153
|
-
</
|
|
153
|
+
</FilterChip>
|
|
154
154
|
);
|
|
155
155
|
}
|
|
156
156
|
|
package/src/combobox.tsx
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
} from "react-native";
|
|
10
10
|
import { useCallback, useEffect, useId, useMemo, useRef, useState, type ReactNode } from "react";
|
|
11
11
|
import { colors } from "./colors";
|
|
12
|
-
import { ACTIVE_RING,
|
|
12
|
+
import { ACTIVE_RING, CONTROL_RADIUS } from "./control_surface";
|
|
13
13
|
import { Text } from "./text";
|
|
14
14
|
import { Icon, type IconName } from "./icon";
|
|
15
15
|
import { TextInputField } from "./text_input_field";
|
|
@@ -70,11 +70,6 @@ export interface ComboboxProps<T extends string = string, D = unknown> {
|
|
|
70
70
|
* picker, not a free-text search box. Opt IN to the search-box look explicitly
|
|
71
71
|
* with `icon="search"` (leading glyph, no chevron). Multi-select shows neither. */
|
|
72
72
|
icon?: IconName;
|
|
73
|
-
/** Render the input on the white toolbar-pill surface (rounded-full, white
|
|
74
|
-
* fill, zinc-300 border) — the same surface SearchInput/ChipGroup/FilterPill
|
|
75
|
-
* wear — so a combobox used as a toolbar filter reads as a view-control on the
|
|
76
|
-
* zinc-50 canvas, not a form field. Single-select only. */
|
|
77
|
-
pill?: boolean;
|
|
78
73
|
placeholder?: string;
|
|
79
74
|
recentsLabel?: string;
|
|
80
75
|
emptyText?: string;
|
|
@@ -128,7 +123,6 @@ export function Combobox<T extends string = string, D = unknown>(props: Combobox
|
|
|
128
123
|
loading = false,
|
|
129
124
|
searchDebounceMs = 200,
|
|
130
125
|
icon,
|
|
131
|
-
pill = false,
|
|
132
126
|
placeholder,
|
|
133
127
|
recentsLabel = "Recent",
|
|
134
128
|
emptyText = "No results",
|
|
@@ -346,7 +340,6 @@ export function Combobox<T extends string = string, D = unknown>(props: Combobox
|
|
|
346
340
|
autoCorrect={false}
|
|
347
341
|
style={[
|
|
348
342
|
multi ? styles.multiInput : showChevron ? styles.selectInput : undefined,
|
|
349
|
-
!multi && pill ? inputPillStyle : undefined,
|
|
350
343
|
// Single-select wears the open ring on the input (which holds the
|
|
351
344
|
// border); multi wears it on the wrapper above (the input is borderless).
|
|
352
345
|
// Gate on `showList`, not `open`: an empty query with no options has
|
|
@@ -471,7 +464,7 @@ const styles = StyleSheet.create({
|
|
|
471
464
|
minHeight: 40,
|
|
472
465
|
borderWidth: 1,
|
|
473
466
|
borderColor: colors.border,
|
|
474
|
-
borderRadius:
|
|
467
|
+
borderRadius: CONTROL_RADIUS,
|
|
475
468
|
},
|
|
476
469
|
multiInput: {
|
|
477
470
|
flexGrow: 1,
|
package/src/composer.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React, { type ReactNode, type Ref, useCallback, useEffect, useState } from "react";
|
|
2
2
|
import { View, TextInput as RNTextInput, ScrollView, StyleSheet, type TextInputProps } from "react-native";
|
|
3
|
-
import {
|
|
3
|
+
import { IconButton } from "./icon_button";
|
|
4
4
|
import { colors } from "./colors";
|
|
5
5
|
import { fontFamilyRegular, getInputTextStyle } from "./text_utils";
|
|
6
6
|
import { useAutoGrowHeight } from "./use_auto_grow_height";
|
|
@@ -156,10 +156,11 @@ export function Composer(props: ComposerProps) {
|
|
|
156
156
|
|
|
157
157
|
const sendButton =
|
|
158
158
|
disabled && onStop ? (
|
|
159
|
-
<
|
|
159
|
+
<IconButton onPress={onStop} size="lg" color="primary" icon="square" tooltip={stopLabel} accessibilityLabel={stopLabel} />
|
|
160
160
|
) : (
|
|
161
|
-
<
|
|
161
|
+
<IconButton
|
|
162
162
|
onPress={handleSend}
|
|
163
|
+
size="lg"
|
|
163
164
|
color="primary"
|
|
164
165
|
disabled={disabled || effectiveSendDisabled}
|
|
165
166
|
icon="arrow-up"
|
package/src/control_surface.ts
CHANGED
|
@@ -1,10 +1,21 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type ViewStyle } from "react-native";
|
|
2
2
|
import { colors } from "./colors";
|
|
3
3
|
|
|
4
4
|
/** The system control height — every band control (chips, pills, search,
|
|
5
5
|
* buttons) aligns to it so a toolbar row reads as one band. */
|
|
6
6
|
export const CONTROL_HEIGHT = 40;
|
|
7
7
|
|
|
8
|
+
/** The system control radius — every interactive control surface (Button, the
|
|
9
|
+
* inputs/selects/pickers, SearchInput, MenuButton, Tabs, the SegmentedControl
|
|
10
|
+
* track, and ChipGroup/FilterChip/Chip via the surfaces below) wears it,
|
|
11
|
+
* so the whole control vocabulary reads as one family. Containers are
|
|
12
|
+
* deliberately larger (Card 16, the review card 12); a nested element inside a
|
|
13
|
+
* padded control (a segmented thumb, a combobox chip) takes `CONTROL_RADIUS −
|
|
14
|
+
* padding` so its corner stays CONCENTRIC with the parent's — set it equal and
|
|
15
|
+
* the inner corner bulges past the outer curve. Round-by-function controls
|
|
16
|
+
* (avatars, dots, switches, sliders, progress, icon buttons) stay full. */
|
|
17
|
+
export const CONTROL_RADIUS = 10;
|
|
18
|
+
|
|
8
19
|
/** The active/open edge for an interactive control — a 2px zinc-900 ring flush
|
|
9
20
|
* against the box (offset 0), MIRRORING the keyboard `:focus-visible` outline
|
|
10
21
|
* (index.css). A mouse-opened popover trigger (Picker / Combobox / InlineSelect /
|
|
@@ -13,17 +24,6 @@ export const CONTROL_HEIGHT = 40;
|
|
|
13
24
|
* beside the kit's focus outline and selected-pill ring. */
|
|
14
25
|
export const ACTIVE_RING = `0 0 0 2px ${colors.zinc[900]}`;
|
|
15
26
|
|
|
16
|
-
/** THE white toolbar-input surface — a rounded-full white pill with a one-step-up
|
|
17
|
-
* zinc-300 border. A search/select control (SearchInput, and Combobox via `pill`)
|
|
18
|
-
* wears it so it reads as a view-control beside ChipGroup/FilterPill on the
|
|
19
|
-
* zinc-50 canvas (where the fill pops), not as a form field that vanishes into a
|
|
20
|
-
* white card. One definition so the two controls never drift. */
|
|
21
|
-
export const inputPillStyle: TextStyle = {
|
|
22
|
-
borderRadius: 999,
|
|
23
|
-
backgroundColor: colors.white,
|
|
24
|
-
borderColor: colors.zinc[300],
|
|
25
|
-
};
|
|
26
|
-
|
|
27
27
|
/** THE review-card surface — the bordered white card every "AI proposes, you
|
|
28
28
|
* decide" surface wears (`ReviewCard`, and the whole-set `ChangeReview`), so the
|
|
29
29
|
* review family reads as ONE contract instead of look-alikes drifting apart. One
|
|
@@ -41,7 +41,7 @@ export const reviewResolvedStyle: ViewStyle = { backgroundColor: colors.zinc[50]
|
|
|
41
41
|
|
|
42
42
|
/**
|
|
43
43
|
* THE pill-surface contract — the single definition of the bordered, white,
|
|
44
|
-
* rounded interactive surface shared by `ChipGroup` chips and `
|
|
44
|
+
* rounded interactive surface shared by `ChipGroup` chips and `Chip` (and
|
|
45
45
|
* anything composed on them). Returns the style for the CURRENT pressable state,
|
|
46
46
|
* so the hover (white → zinc-100) and press (zinc-200) washes live in ONE place
|
|
47
47
|
* instead of drifting per control — and any control built on it hovers without
|
|
@@ -49,7 +49,7 @@ export const reviewResolvedStyle: ViewStyle = { backgroundColor: colors.zinc[50]
|
|
|
49
49
|
* crisp 2px dark edge, no layout shift). Each control adds its own padding/layout
|
|
50
50
|
* on top.
|
|
51
51
|
*/
|
|
52
|
-
export function
|
|
52
|
+
export function chipSurfaceStyle(
|
|
53
53
|
state: { hovered?: boolean; pressed?: boolean },
|
|
54
54
|
opts?: { selected?: boolean },
|
|
55
55
|
): ViewStyle {
|
|
@@ -57,7 +57,7 @@ export function pillSurfaceStyle(
|
|
|
57
57
|
return {
|
|
58
58
|
height: CONTROL_HEIGHT,
|
|
59
59
|
justifyContent: "center",
|
|
60
|
-
borderRadius:
|
|
60
|
+
borderRadius: CONTROL_RADIUS,
|
|
61
61
|
borderWidth: 1,
|
|
62
62
|
borderColor: selected ? colors.zinc[900] : colors.border,
|
|
63
63
|
backgroundColor: state.pressed ? colors.zinc[200] : state.hovered ? colors.zinc[100] : colors.white,
|
package/src/date_field.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
2
|
import { StyleProp, StyleSheet, View, ViewStyle } from "react-native";
|
|
3
3
|
import { colors } from "./colors";
|
|
4
|
+
import { CONTROL_RADIUS } from "./control_surface";
|
|
4
5
|
import { Text } from "./text";
|
|
5
6
|
import { DateSegments } from "./date_segments_field";
|
|
6
7
|
import { SegmentLabels, dateSegmentsConfig } from "./date_segments";
|
|
@@ -144,7 +145,7 @@ const styles = StyleSheet.create({
|
|
|
144
145
|
height: 40,
|
|
145
146
|
paddingHorizontal: 8,
|
|
146
147
|
gap: 4,
|
|
147
|
-
borderRadius:
|
|
148
|
+
borderRadius: CONTROL_RADIUS,
|
|
148
149
|
borderWidth: 1,
|
|
149
150
|
borderColor: colors.border,
|
|
150
151
|
backgroundColor: colors.background,
|
package/src/date_filter.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import React, { useCallback, useMemo, useRef, useState } from "react";
|
|
|
2
2
|
import { ScrollView, StyleSheet, View } from "react-native";
|
|
3
3
|
import { Text } from "./text";
|
|
4
4
|
import { colors } from "./colors";
|
|
5
|
+
import { CONTROL_RADIUS } from "./control_surface";
|
|
5
6
|
import { MenuButton } from "./menu_button";
|
|
6
7
|
import { Calendar, CalendarRangeValue, CalendarRef } from "./date_calendar";
|
|
7
8
|
import { TimeField } from "./time_field";
|
|
@@ -336,7 +337,7 @@ export function DateFilter(props: DateFilterProps) {
|
|
|
336
337
|
const styles = StyleSheet.create({
|
|
337
338
|
container: {
|
|
338
339
|
backgroundColor: colors.white,
|
|
339
|
-
borderRadius:
|
|
340
|
+
borderRadius: CONTROL_RADIUS,
|
|
340
341
|
gap: 16,
|
|
341
342
|
},
|
|
342
343
|
presetsSidebar: {
|
|
@@ -2,6 +2,7 @@ import React, { useMemo, useState } from "react";
|
|
|
2
2
|
import { View } from "react-native";
|
|
3
3
|
import { Text } from "./text";
|
|
4
4
|
import { colors } from "./colors";
|
|
5
|
+
import { CONTROL_RADIUS } from "./control_surface";
|
|
5
6
|
import { Icon } from "./icon";
|
|
6
7
|
import { Button } from "./button";
|
|
7
8
|
import { PressableHighlight } from "./pressable_highlight";
|
|
@@ -130,7 +131,7 @@ export function DateRangeFilterField(props: DateRangeFilterFieldProps) {
|
|
|
130
131
|
paddingHorizontal: 12,
|
|
131
132
|
borderWidth: 1,
|
|
132
133
|
borderColor: colors.zinc[300],
|
|
133
|
-
borderRadius:
|
|
134
|
+
borderRadius: CONTROL_RADIUS,
|
|
134
135
|
backgroundColor: colors.white,
|
|
135
136
|
}}
|
|
136
137
|
>
|
package/src/dialog.tsx
CHANGED
|
@@ -3,7 +3,7 @@ import { Animated, Modal, StyleSheet, View, ScrollView } from "react-native";
|
|
|
3
3
|
import { useScreenSize } from "@lotics/ui/use_screen_size";
|
|
4
4
|
import { PortalHost } from "@lotics/ui/portal";
|
|
5
5
|
import { colors } from "@lotics/ui/colors";
|
|
6
|
-
import {
|
|
6
|
+
import { IconButton } from "@lotics/ui/icon_button";
|
|
7
7
|
import { Text } from "@lotics/ui/text";
|
|
8
8
|
import { BackButton } from "@lotics/ui/back_button";
|
|
9
9
|
import { useOverlayScope } from "@lotics/ui/overlay_scope";
|
|
@@ -163,7 +163,7 @@ export function Dialog(props: DialogProps) {
|
|
|
163
163
|
<PortalHost>
|
|
164
164
|
<View testID={testID} style={[styles.dialogContainer, { borderRadius }]}>
|
|
165
165
|
<View style={[styles.closeButtonContainer, { paddingHorizontal: screenSize.small ? 16 : 24 }]}>
|
|
166
|
-
<
|
|
166
|
+
<IconButton icon="x" size="lg" accessibilityLabel="Close" onPress={handleClose} />
|
|
167
167
|
</View>
|
|
168
168
|
<View style={styles.container}>{children}</View>
|
|
169
169
|
</View>
|
package/src/discrepancy.tsx
CHANGED
|
@@ -88,7 +88,7 @@ export function Discrepancy(props: DiscrepancyProps) {
|
|
|
88
88
|
|
|
89
89
|
{onFlag ? (
|
|
90
90
|
<View style={styles.footer}>
|
|
91
|
-
<Button title="Flag for review" color="muted"
|
|
91
|
+
<Button title="Flag for review" color="muted" onPress={onFlag} />
|
|
92
92
|
</View>
|
|
93
93
|
) : null}
|
|
94
94
|
</>
|
package/src/drawer.tsx
CHANGED
|
@@ -3,7 +3,6 @@ import { Modal, Pressable, StyleSheet, View } from "react-native";
|
|
|
3
3
|
import { useScreenSize } from "@lotics/ui/use_screen_size";
|
|
4
4
|
import { PortalHost } from "@lotics/ui/portal";
|
|
5
5
|
import { colors } from "@lotics/ui/colors";
|
|
6
|
-
import { Button } from "@lotics/ui/button";
|
|
7
6
|
import { IconButton } from "@lotics/ui/icon_button";
|
|
8
7
|
import { Text } from "@lotics/ui/text";
|
|
9
8
|
import { useOverlayScope } from "@lotics/ui/overlay_scope";
|
|
@@ -112,7 +111,7 @@ export function Drawer(props: DrawerProps) {
|
|
|
112
111
|
/>
|
|
113
112
|
</View>
|
|
114
113
|
) : null}
|
|
115
|
-
<
|
|
114
|
+
<IconButton icon="x" size="lg" accessibilityLabel="Close" onPress={handleClose} />
|
|
116
115
|
</View>
|
|
117
116
|
<View testID={testID} style={styles.body}>
|
|
118
117
|
{children}
|
|
@@ -4,11 +4,11 @@ import { Text } from "./text";
|
|
|
4
4
|
import { Icon } from "./icon";
|
|
5
5
|
import { colors } from "./colors";
|
|
6
6
|
import { LinkButton } from "./link_button";
|
|
7
|
-
import {
|
|
7
|
+
import { Chip } from "./chip";
|
|
8
8
|
import { Popover, PopoverTrigger, PopoverContent, PopoverFooter } from "./popover";
|
|
9
9
|
import type { PopoverSide, PopoverAlign } from "./popover";
|
|
10
10
|
|
|
11
|
-
export interface
|
|
11
|
+
export interface FilterChipProps {
|
|
12
12
|
/** The dimension name — shown alone when inactive ("Owner"), prefixed when
|
|
13
13
|
* active ("Owner: Maria, James"). */
|
|
14
14
|
label: string;
|
|
@@ -22,7 +22,7 @@ export interface FilterPillProps {
|
|
|
22
22
|
/** Label for the clear affordances (pass a translated string). */
|
|
23
23
|
clearLabel?: string;
|
|
24
24
|
/** The editor revealed on press — a premium primitive (`RangeSlider`,
|
|
25
|
-
* `Counter`, `PickerMenu` multi) or any composed control. `
|
|
25
|
+
* `Counter`, `PickerMenu` multi) or any composed control. `FilterChip` owns
|
|
26
26
|
* the pill + popover chrome; the consumer owns the value and applies it.
|
|
27
27
|
* Pass a render function to receive `close` — the one editor that closes on
|
|
28
28
|
* action is single-select (`<PickerMenu>` → close on pick); range/counter/
|
|
@@ -39,7 +39,7 @@ export interface FilterPillProps {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
|
-
* A short summary of a multi-select for a `
|
|
42
|
+
* A short summary of a multi-select for a `FilterChip` preview — "Maria, James"
|
|
43
43
|
* up to `max` labels, else "N selected", and `undefined` when nothing is
|
|
44
44
|
* chosen. The select sibling of `rangeSummary`.
|
|
45
45
|
*/
|
|
@@ -59,7 +59,7 @@ export function selectSummary(
|
|
|
59
59
|
* filter dimension, the sibling of `ChipGroup` (which lays ONE hot dimension's
|
|
60
60
|
* options out inline). Many dimensions stay scannable because each collapses to
|
|
61
61
|
* a single pill: inactive reads "Label ⌄", active reads "Label: summary" with an
|
|
62
|
-
* × to clear. `
|
|
62
|
+
* × to clear. `FilterChip` bakes the consistent chrome — the preview pill, a
|
|
63
63
|
* padded popover body for the composed editor, and a Clear footer when active —
|
|
64
64
|
* so every filter looks and behaves the same. Drop a premium primitive inside
|
|
65
65
|
* (`RangeSlider`, `Counter`, `PickerMenu` multi). With `footer` (+ controlled
|
|
@@ -67,7 +67,7 @@ export function selectSummary(
|
|
|
67
67
|
* setting gate with Cancel/Save — keeping it on the one pill surface. The table
|
|
68
68
|
* toolbar's `ColumnFilter` is this pill plus its query-condition mapping.
|
|
69
69
|
*/
|
|
70
|
-
export function
|
|
70
|
+
export function FilterChip(props: FilterChipProps) {
|
|
71
71
|
const { label, summary, onClear, clearLabel = "Clear", children, side = "bottom", align = "start", open, onOpenChange, footer } = props;
|
|
72
72
|
const active = summary != null && summary.length > 0;
|
|
73
73
|
// The clear × / Clear footer only when there's a clearable selection AND no
|
|
@@ -87,12 +87,12 @@ export function FilterPill(props: FilterPillProps) {
|
|
|
87
87
|
return (
|
|
88
88
|
<Popover side={side} align={align} open={isOpen} onOpenChange={setOpen}>
|
|
89
89
|
<PopoverTrigger>
|
|
90
|
-
<
|
|
90
|
+
<Chip onDismiss={showClear ? onClear : undefined} dismissTooltip={clearLabel}>
|
|
91
91
|
<Text size="sm" weight="medium" color={active ? "zinc-900" : "zinc-700"} numberOfLines={1}>
|
|
92
92
|
{active ? `${label}: ${summary}` : label}
|
|
93
93
|
</Text>
|
|
94
94
|
{!showClear ? <Icon name="chevron-down" size={14} color={colors.zinc["400"]} /> : null}
|
|
95
|
-
</
|
|
95
|
+
</Chip>
|
|
96
96
|
</PopoverTrigger>
|
|
97
97
|
<PopoverContent style={styles.body} disableBodyScroll>
|
|
98
98
|
{typeof children === "function" ? children({ close: () => setOpen(false) }) : children}
|
package/src/finding.tsx
CHANGED
|
@@ -81,7 +81,7 @@ export function Finding(props: FindingProps) {
|
|
|
81
81
|
) : null}
|
|
82
82
|
{props.action ? (
|
|
83
83
|
<View style={styles.actionRow}>
|
|
84
|
-
<Button title={props.action.label} color="secondary"
|
|
84
|
+
<Button title={props.action.label} color="secondary" onPress={props.action.onPress} />
|
|
85
85
|
</View>
|
|
86
86
|
) : null}
|
|
87
87
|
</View>
|
package/src/gantt/gantt_view.tsx
CHANGED
|
@@ -163,8 +163,8 @@ export function GanttView<T = unknown>(props: GanttViewProps<T>) {
|
|
|
163
163
|
const styles = StyleSheet.create({
|
|
164
164
|
root: { flex: 1, backgroundColor: colors.white },
|
|
165
165
|
toolbar: { flexDirection: "row", alignItems: "center", justifyContent: "space-between", paddingHorizontal: 14, paddingVertical: 10, borderBottomWidth: 1, borderBottomColor: colors.border },
|
|
166
|
-
zoomSwitch: { flexDirection: "row", backgroundColor: colors.zinc[100], borderRadius:
|
|
167
|
-
zoomBtn: { paddingHorizontal: 12, paddingVertical: 5, borderRadius:
|
|
166
|
+
zoomSwitch: { flexDirection: "row", backgroundColor: colors.zinc[100], borderRadius: 10, padding: 2 },
|
|
167
|
+
zoomBtn: { paddingHorizontal: 12, paddingVertical: 5, borderRadius: 8 },
|
|
168
168
|
zoomBtnActive: { backgroundColor: colors.white },
|
|
169
169
|
headerCell: { borderBottomWidth: 1, borderBottomColor: colors.border },
|
|
170
170
|
labelRow: { flexDirection: "row", alignItems: "center", gap: 8, paddingHorizontal: 12, borderBottomWidth: 1, borderBottomColor: colors.zinc[100] },
|
package/src/icon_button.tsx
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { GestureResponderEvent, StyleProp, StyleSheet, View, ViewStyle } from "react-native";
|
|
1
|
+
import { ActivityIndicator, GestureResponderEvent, StyleProp, StyleSheet, View, ViewStyle } from "react-native";
|
|
2
2
|
import { Icon, IconName } from "./icon";
|
|
3
3
|
import { colors } from "./colors";
|
|
4
4
|
import { PressableHighlight } from "./pressable_highlight";
|
|
5
|
+
import { type ButtonColor } from "./button";
|
|
6
|
+
import { getButtonIconColor } from "./button_colors";
|
|
5
7
|
import { Ref, useCallback } from "react";
|
|
6
8
|
import { TooltipSide } from "./tooltip";
|
|
7
9
|
|
|
@@ -9,16 +11,23 @@ interface IconButtonBase {
|
|
|
9
11
|
ref?: Ref<View>;
|
|
10
12
|
testID?: string;
|
|
11
13
|
icon: IconName;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
/** Matches `Button`'s palette so an icon-only action migrates 1:1 — a filled
|
|
15
|
+
* disc for primary/secondary/danger, a ghost for muted/danger-secondary/none. */
|
|
16
|
+
color?: ButtonColor | "none" | "white";
|
|
17
|
+
/** `lg` (40px, 20px glyph) for a circular primary action at the system control
|
|
18
|
+
* height — a composer send, a drawer/popover back or close; `md` (28px, 18px
|
|
19
|
+
* glyph) for toolbars; `sm` (24px, 14px glyph) for inline affordances next to
|
|
20
|
+
* body/sm text (e.g. the ⓘ in a CardHeader). md/sm keep a 40px touch target via
|
|
21
|
+
* hitSlop; `lg` is already 40. */
|
|
22
|
+
size?: "lg" | "md" | "sm";
|
|
17
23
|
iconColor?: string;
|
|
18
24
|
tooltipSide?: TooltipSide;
|
|
19
25
|
onPress?: (event: GestureResponderEvent) => void;
|
|
20
26
|
style?: StyleProp<ViewStyle>;
|
|
21
27
|
disabled?: boolean;
|
|
28
|
+
/** Show a spinner in place of the icon (and block press) — for an icon action
|
|
29
|
+
* that triggers async work, e.g. a delete. */
|
|
30
|
+
loading?: boolean;
|
|
22
31
|
}
|
|
23
32
|
|
|
24
33
|
/**
|
|
@@ -43,6 +52,7 @@ export function IconButton(props: IconButtonProps) {
|
|
|
43
52
|
tooltipSide,
|
|
44
53
|
accessibilityLabel,
|
|
45
54
|
disabled,
|
|
55
|
+
loading,
|
|
46
56
|
style,
|
|
47
57
|
} = props;
|
|
48
58
|
|
|
@@ -53,6 +63,11 @@ export function IconButton(props: IconButtonProps) {
|
|
|
53
63
|
[onPress],
|
|
54
64
|
);
|
|
55
65
|
|
|
66
|
+
const disabledOrLoading = disabled || loading;
|
|
67
|
+
const tint =
|
|
68
|
+
iconColor ??
|
|
69
|
+
(color === "none" ? colors.zinc[700] : color === "white" ? colors.white : getButtonIconColor(color));
|
|
70
|
+
|
|
56
71
|
return (
|
|
57
72
|
<PressableHighlight
|
|
58
73
|
ref={ref}
|
|
@@ -61,18 +76,18 @@ export function IconButton(props: IconButtonProps) {
|
|
|
61
76
|
tooltipSide={tooltipSide}
|
|
62
77
|
accessibilityRole="button"
|
|
63
78
|
accessibilityLabel={accessibilityLabel ?? tooltip}
|
|
64
|
-
style={[styles.button, styles[size], styles[color],
|
|
79
|
+
style={[styles.button, styles[size], styles[color], disabledOrLoading && styles.disabled, style]}
|
|
65
80
|
onPress={handlePress}
|
|
66
|
-
disabled={
|
|
81
|
+
disabled={disabledOrLoading}
|
|
67
82
|
// 40px touch target regardless of visual size (28 + 2×6 / 24 + 2×8)
|
|
68
83
|
// without shifting surrounding layouts.
|
|
69
|
-
hitSlop={size === "sm" ? 8 : 6}
|
|
84
|
+
hitSlop={size === "lg" ? 0 : size === "sm" ? 8 : 6}
|
|
70
85
|
>
|
|
71
|
-
|
|
72
|
-
size=
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
86
|
+
{loading ? (
|
|
87
|
+
<ActivityIndicator size="small" color={tint} />
|
|
88
|
+
) : (
|
|
89
|
+
<Icon size={size === "sm" ? 14 : size === "lg" ? 20 : 18} name={icon} color={tint} />
|
|
90
|
+
)}
|
|
76
91
|
</PressableHighlight>
|
|
77
92
|
);
|
|
78
93
|
}
|
|
@@ -85,6 +100,10 @@ const styles = StyleSheet.create({
|
|
|
85
100
|
justifyContent: "center",
|
|
86
101
|
borderRadius: 999,
|
|
87
102
|
},
|
|
103
|
+
lg: {
|
|
104
|
+
width: 40,
|
|
105
|
+
height: 40,
|
|
106
|
+
},
|
|
88
107
|
md: {
|
|
89
108
|
width: 28,
|
|
90
109
|
height: 28,
|
|
@@ -103,5 +122,10 @@ const styles = StyleSheet.create({
|
|
|
103
122
|
secondary: {
|
|
104
123
|
backgroundColor: colors.zinc[100],
|
|
105
124
|
},
|
|
125
|
+
danger: {
|
|
126
|
+
backgroundColor: colors.red[100],
|
|
127
|
+
},
|
|
128
|
+
muted: {},
|
|
129
|
+
"danger-secondary": {},
|
|
106
130
|
white: {},
|
|
107
131
|
});
|
package/src/inline_edit.tsx
CHANGED
|
@@ -5,7 +5,7 @@ import { IconButton } from "./icon_button";
|
|
|
5
5
|
import { ActivityIndicator } from "./activity_indicator";
|
|
6
6
|
import { PressableHighlight } from "./pressable_highlight";
|
|
7
7
|
import { colors } from "./colors";
|
|
8
|
-
import { ACTIVE_RING } from "./control_surface";
|
|
8
|
+
import { ACTIVE_RING, CONTROL_RADIUS } from "./control_surface";
|
|
9
9
|
import { fontFamilyRegular, getInputTextStyle } from "./text_utils";
|
|
10
10
|
|
|
11
11
|
/** The kit's standard control height (TextInputField, NumberInput, Picker, …).
|
|
@@ -221,7 +221,7 @@ const viewTextStyle = [getInputTextStyle(), { flex: 1, fontFamily: fontFamilyReg
|
|
|
221
221
|
const styles = StyleSheet.create({
|
|
222
222
|
view: {
|
|
223
223
|
minHeight: INLINE_CONTROL_HEIGHT,
|
|
224
|
-
borderRadius:
|
|
224
|
+
borderRadius: CONTROL_RADIUS,
|
|
225
225
|
borderWidth: 1,
|
|
226
226
|
borderColor: "transparent",
|
|
227
227
|
paddingHorizontal: 8,
|