@lotics/ui 5.7.0 → 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.
Files changed (65) hide show
  1. package/AGENTS.md +41 -26
  2. package/examples/tpl_approvals.tsx +10 -10
  3. package/examples/tpl_crm_desk.tsx +3 -3
  4. package/examples/tpl_dieline.tsx +7 -6
  5. package/examples/tpl_directory.tsx +3 -3
  6. package/examples/tpl_dossier.tsx +7 -7
  7. package/examples/tpl_draft.tsx +4 -4
  8. package/examples/tpl_extract.tsx +7 -7
  9. package/examples/tpl_inventory.tsx +3 -3
  10. package/examples/tpl_lookup.tsx +4 -3
  11. package/examples/tpl_match.tsx +1 -1
  12. package/examples/tpl_ratedesk.tsx +11 -11
  13. package/examples/tpl_report.tsx +3 -3
  14. package/examples/tpl_stock.tsx +3 -3
  15. package/examples/tpl_triage.tsx +1 -1
  16. package/package.json +3 -3
  17. package/src/agent_progress.tsx +14 -12
  18. package/src/agent_run.tsx +253 -66
  19. package/src/back_button.tsx +3 -2
  20. package/src/badge.tsx +2 -1
  21. package/src/button.tsx +11 -32
  22. package/src/button_colors.ts +23 -0
  23. package/src/calendar/calendar_view.tsx +2 -2
  24. package/src/card_select_item.tsx +2 -1
  25. package/src/change_review.tsx +5 -6
  26. package/src/{pill_button.tsx → chip.tsx} +7 -7
  27. package/src/chip_group.tsx +2 -2
  28. package/src/column_filter.tsx +4 -4
  29. package/src/combobox.tsx +2 -9
  30. package/src/composer.tsx +4 -3
  31. package/src/control_surface.ts +15 -15
  32. package/src/date_field.tsx +2 -1
  33. package/src/date_filter.tsx +2 -1
  34. package/src/date_range_filter_field.tsx +2 -1
  35. package/src/dialog.tsx +2 -2
  36. package/src/discrepancy.tsx +1 -1
  37. package/src/drawer.tsx +1 -2
  38. package/src/{filter_pill.tsx → filter_chip.tsx} +8 -8
  39. package/src/finding.tsx +1 -1
  40. package/src/gantt/gantt_view.tsx +2 -2
  41. package/src/icon_button.tsx +38 -14
  42. package/src/inline_edit.tsx +2 -2
  43. package/src/link_button.tsx +2 -1
  44. package/src/menu_button.tsx +2 -1
  45. package/src/menu_list_item.tsx +2 -1
  46. package/src/number_input.tsx +2 -1
  47. package/src/picker.tsx +3 -3
  48. package/src/popover_header.tsx +2 -2
  49. package/src/radio_picker.tsx +2 -1
  50. package/src/range_slider.tsx +1 -1
  51. package/src/review_card.tsx +2 -2
  52. package/src/scan_field.tsx +2 -1
  53. package/src/scored_option.tsx +0 -1
  54. package/src/search_input.tsx +3 -2
  55. package/src/segmented_control.tsx +2 -1
  56. package/src/status_badge.tsx +2 -1
  57. package/src/stepper.tsx +3 -2
  58. package/src/switch_button.tsx +2 -1
  59. package/src/switcher.tsx +3 -2
  60. package/src/tabs.tsx +2 -1
  61. package/src/tag_input.tsx +2 -1
  62. package/src/text_input_field.tsx +2 -1
  63. package/src/time_field.tsx +2 -1
  64. package/src/time_picker.tsx +2 -1
  65. package/src/triage_row.tsx +3 -3
@@ -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" shape="rounded" onPress={() => props.onRejectItem?.(i)} />
177
- <Button title={props.acceptLabel ?? "Keep"} color="secondary" shape="rounded" onPress={() => props.onAcceptItem?.(i)} />
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" shape="rounded" onPress={() => props.onUndoItem?.(i)} />
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" shape="rounded" onPress={acceptAll} /> : null}
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" shape="rounded" onPress={props.onDiscard} /> : null}
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 { pillSurfaceStyle } from "./control_surface";
4
+ import { chipSurfaceStyle } from "./control_surface";
5
5
  import { StyleSheet, View } from "react-native";
6
6
 
7
- interface PillButtonProps {
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 PillButton(props: PillButtonProps) {
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) => [pillSurfaceStyle(state), styles.pillLayout, onDismiss && styles.pillWithDismiss]}
25
+ style={(state) => [chipSurfaceStyle(state), styles.pillLayout, onDismiss && styles.pillWithDismiss]}
26
26
  >
27
27
  {children}
28
28
  </PressableHighlight>
29
29
  ) : (
30
- <View style={[pillSurfaceStyle({}), styles.pillLayout, onDismiss && styles.pillWithDismiss]}>{children}</View>
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 pillSurfaceStyle;
49
- // PillButton owns only its row layout.
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",
@@ -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 { pillSurfaceStyle } from "./control_surface";
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) => [pillSurfaceStyle(state, { selected: active }), { paddingHorizontal: 14 }]}
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}
@@ -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 { FilterPill } from "./filter_pill";
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 `PillButton` the table toolbar uses, so
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
- <FilterPill
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
- </FilterPill>
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, inputPillStyle } from "./control_surface";
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: 8,
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 { Button } from "./button";
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
- <Button onPress={onStop} color="primary" icon="square" tooltip={stopLabel} accessibilityLabel={stopLabel} />
159
+ <IconButton onPress={onStop} size="lg" color="primary" icon="square" tooltip={stopLabel} accessibilityLabel={stopLabel} />
160
160
  ) : (
161
- <Button
161
+ <IconButton
162
162
  onPress={handleSend}
163
+ size="lg"
163
164
  color="primary"
164
165
  disabled={disabled || effectiveSendDisabled}
165
166
  icon="arrow-up"
@@ -1,10 +1,21 @@
1
- import { type TextStyle, type ViewStyle } from "react-native";
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 `PillButton` (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 pillSurfaceStyle(
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: 999,
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,
@@ -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: 8,
148
+ borderRadius: CONTROL_RADIUS,
148
149
  borderWidth: 1,
149
150
  borderColor: colors.border,
150
151
  backgroundColor: colors.background,
@@ -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: 8,
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: 8,
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 { Button } from "@lotics/ui/button";
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
- <Button icon="x" accessibilityLabel="Close" onPress={handleClose} />
166
+ <IconButton icon="x" size="lg" accessibilityLabel="Close" onPress={handleClose} />
167
167
  </View>
168
168
  <View style={styles.container}>{children}</View>
169
169
  </View>
@@ -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" shape="rounded" onPress={onFlag} />
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
- <Button icon="x" accessibilityLabel="Close" onPress={handleClose} />
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 { PillButton } from "./pill_button";
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 FilterPillProps {
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. `FilterPill` owns
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 `FilterPill` preview — "Maria, James"
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. `FilterPill` bakes the consistent chrome — the preview pill, a
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 FilterPill(props: FilterPillProps) {
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
- <PillButton onDismiss={showClear ? onClear : undefined} dismissTooltip={clearLabel}>
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
- </PillButton>
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" shape="rounded" onPress={props.action.onPress} />
84
+ <Button title={props.action.label} color="secondary" onPress={props.action.onPress} />
85
85
  </View>
86
86
  ) : null}
87
87
  </View>
@@ -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: 8, padding: 2 },
167
- zoomBtn: { paddingHorizontal: 12, paddingVertical: 5, borderRadius: 6 },
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] },
@@ -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
- color?: "none" | "primary" | "secondary" | "white";
13
- /** `md` (28px, 18px glyph) for toolbars; `sm` (24px, 14px glyph) for
14
- * inline affordances sitting next to body/sm text — e.g. the ⓘ in a
15
- * CardHeader. Both keep a 40px touch target via hitSlop. */
16
- size?: "md" | "sm";
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], disabled && styles.disabled, style]}
79
+ style={[styles.button, styles[size], styles[color], disabledOrLoading && styles.disabled, style]}
65
80
  onPress={handlePress}
66
- disabled={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
- <Icon
72
- size={size === "sm" ? 14 : 18}
73
- name={icon}
74
- color={iconColor || (color === "white" || color === "primary" ? colors.white : colors.zinc[700])}
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
  });
@@ -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: 8,
224
+ borderRadius: CONTROL_RADIUS,
225
225
  borderWidth: 1,
226
226
  borderColor: "transparent",
227
227
  paddingHorizontal: 8,
@@ -1,6 +1,7 @@
1
1
  import { StyleSheet } from "react-native";
2
2
  import { Text } from "./text";
3
3
  import { PressableHighlight } from "./pressable_highlight";
4
+ import { CONTROL_RADIUS } from "./control_surface";
4
5
 
5
6
  export interface LinkButtonProps {
6
7
  title: string;
@@ -44,7 +45,7 @@ const styles = StyleSheet.create({
44
45
  paddingVertical: 6,
45
46
  paddingHorizontal: 8,
46
47
  marginHorizontal: -8,
47
- borderRadius: 8,
48
+ borderRadius: CONTROL_RADIUS,
48
49
  alignSelf: "flex-start",
49
50
  },
50
51
  });
@@ -2,6 +2,7 @@ import { Ref } from "react";
2
2
  import { StyleProp, StyleSheet, View, ViewStyle } 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, IconName } from "./icon";
6
7
  import { PressableHighlight } from "./pressable_highlight";
7
8
  export interface MenuButtonProps {
@@ -126,7 +127,7 @@ const styles = StyleSheet.create({
126
127
  gap: 8,
127
128
  height: 40,
128
129
  paddingHorizontal: 8,
129
- borderRadius: 8,
130
+ borderRadius: CONTROL_RADIUS,
130
131
  },
131
132
  textContainer: {
132
133
  flex: 1,
@@ -2,6 +2,7 @@ import { Ref } from "react";
2
2
  import { StyleProp, StyleSheet, View, ViewStyle } 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, IconName } from "./icon";
6
7
  import { PressableHighlight } from "./pressable_highlight";
7
8
  import type { TextColor } from "./text_utils";
@@ -111,7 +112,7 @@ const styles = StyleSheet.create({
111
112
  gap: 8,
112
113
  paddingHorizontal: 8,
113
114
  paddingVertical: 8,
114
- borderRadius: 8,
115
+ borderRadius: CONTROL_RADIUS,
115
116
  },
116
117
  textContainer: {
117
118
  flex: 1,