@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.
Files changed (69) hide show
  1. package/AGENTS.md +44 -27
  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 +12 -3
  17. package/src/agent_progress.tsx +14 -12
  18. package/src/agent_run.tsx +248 -86
  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/markdown.css +209 -0
  45. package/src/markdown.tsx +17 -0
  46. package/src/markdown.web.tsx +69 -0
  47. package/src/menu_button.tsx +2 -1
  48. package/src/menu_list_item.tsx +2 -1
  49. package/src/number_input.tsx +2 -1
  50. package/src/picker.tsx +3 -3
  51. package/src/popover_header.tsx +2 -2
  52. package/src/primitives_purity.test.ts +13 -7
  53. package/src/radio_picker.tsx +2 -1
  54. package/src/range_slider.tsx +1 -1
  55. package/src/review_card.tsx +2 -2
  56. package/src/scan_field.tsx +2 -1
  57. package/src/scored_option.tsx +0 -1
  58. package/src/search_input.tsx +3 -2
  59. package/src/segmented_control.tsx +2 -1
  60. package/src/status_badge.tsx +2 -1
  61. package/src/stepper.tsx +3 -2
  62. package/src/switch_button.tsx +2 -1
  63. package/src/switcher.tsx +3 -2
  64. package/src/tabs.tsx +2 -1
  65. package/src/tag_input.tsx +2 -1
  66. package/src/text_input_field.tsx +2 -1
  67. package/src/time_field.tsx +2 -1
  68. package/src/time_picker.tsx +2 -1
  69. package/src/triage_row.tsx +3 -3
@@ -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
  });
@@ -0,0 +1,209 @@
1
+ .ui-markdown {
2
+ font-family: Inter_400Regular, "apple-system", "BlinkMacSystemFont", "Segoe UI",
3
+ "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans",
4
+ "Helvetica Neue", sans-serif;
5
+ font-size: 14px;
6
+ line-height: 24px;
7
+ letter-spacing: -0.4px;
8
+ color: rgba(24, 24, 27, 1);
9
+ }
10
+
11
+ /* Headings */
12
+
13
+ .ui-markdown h1 {
14
+ font-family: Inter_600SemiBold, "apple-system", "BlinkMacSystemFont", "Segoe UI",
15
+ "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans",
16
+ "Helvetica Neue", sans-serif;
17
+ font-size: 24px;
18
+ line-height: 30px;
19
+ font-weight: 600;
20
+ padding-top: 12px;
21
+ padding-bottom: 4px;
22
+ margin: 0;
23
+ }
24
+
25
+ .ui-markdown h2 {
26
+ font-family: Inter_600SemiBold, "apple-system", "BlinkMacSystemFont", "Segoe UI",
27
+ "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans",
28
+ "Helvetica Neue", sans-serif;
29
+ font-size: 18px;
30
+ line-height: 24px;
31
+ font-weight: 600;
32
+ padding-top: 12px;
33
+ padding-bottom: 4px;
34
+ margin: 0;
35
+ }
36
+
37
+ .ui-markdown h3 {
38
+ font-family: Inter_600SemiBold, "apple-system", "BlinkMacSystemFont", "Segoe UI",
39
+ "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans",
40
+ "Helvetica Neue", sans-serif;
41
+ font-size: 16px;
42
+ line-height: 22px;
43
+ font-weight: 600;
44
+ padding-top: 8px;
45
+ padding-bottom: 4px;
46
+ margin: 0;
47
+ }
48
+
49
+ .ui-markdown h4,
50
+ .ui-markdown h5,
51
+ .ui-markdown h6 {
52
+ font-family: Inter_500Medium, "apple-system", "BlinkMacSystemFont", "Segoe UI",
53
+ "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans",
54
+ "Helvetica Neue", sans-serif;
55
+ font-size: 14px;
56
+ line-height: 20px;
57
+ font-weight: 500;
58
+ padding-top: 8px;
59
+ padding-bottom: 4px;
60
+ margin: 0;
61
+ }
62
+
63
+ /* Block elements */
64
+
65
+ .ui-markdown p {
66
+ margin: 0;
67
+ padding: 4px 0;
68
+ }
69
+
70
+ .ui-markdown blockquote {
71
+ border-left: 3px solid rgba(228, 228, 231, 1);
72
+ margin: 4px 0;
73
+ padding-left: 12px;
74
+ color: rgba(113, 113, 122, 1);
75
+ }
76
+
77
+ .ui-markdown hr {
78
+ border: none;
79
+ border-top: 1px solid rgba(228, 228, 231, 1);
80
+ margin: 8px 0;
81
+ }
82
+
83
+ /* Lists */
84
+
85
+ .ui-markdown ul,
86
+ .ui-markdown ol {
87
+ padding-left: 24px;
88
+ margin: 4px 0;
89
+ }
90
+
91
+ .ui-markdown li {
92
+ padding-top: 2px;
93
+ padding-bottom: 2px;
94
+ }
95
+
96
+ /* Inline */
97
+
98
+ .ui-markdown strong {
99
+ font-family: Inter_500Medium, "apple-system", "BlinkMacSystemFont", "Segoe UI",
100
+ "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans",
101
+ "Helvetica Neue", sans-serif;
102
+ font-weight: 500;
103
+ }
104
+
105
+ .ui-markdown a {
106
+ color: rgba(59, 130, 246, 1);
107
+ text-decoration: underline;
108
+ cursor: pointer;
109
+ }
110
+
111
+ .ui-markdown del {
112
+ color: rgba(113, 113, 122, 1);
113
+ }
114
+
115
+ /* Code */
116
+
117
+ .ui-markdown code {
118
+ padding: 1px 5px;
119
+ color: rgba(127, 29, 29, 1);
120
+ background-color: rgba(244, 244, 245, 1);
121
+ border-radius: 4px;
122
+ font-size: 13px;
123
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
124
+ }
125
+
126
+ .ui-markdown pre {
127
+ background-color: rgba(244, 244, 245, 1);
128
+ padding: 12px;
129
+ border-radius: 8px;
130
+ margin: 4px 0;
131
+ overflow-x: auto;
132
+ }
133
+
134
+ .ui-markdown pre code {
135
+ padding: 0;
136
+ color: inherit;
137
+ background-color: transparent;
138
+ border-radius: 0;
139
+ font-size: 13px;
140
+ line-height: 20px;
141
+ }
142
+
143
+ /* Tables */
144
+
145
+ .ui-markdown table {
146
+ border-collapse: collapse;
147
+ margin: 4px 0;
148
+ width: max-content;
149
+ min-width: 100%;
150
+ }
151
+
152
+ .ui-markdown th,
153
+ .ui-markdown td {
154
+ border: 1px solid rgba(228, 228, 231, 1);
155
+ padding: 6px 12px;
156
+ text-align: left;
157
+ white-space: nowrap;
158
+ }
159
+
160
+ .ui-markdown th {
161
+ background-color: rgba(244, 244, 245, 1);
162
+ font-family: Inter_500Medium, "apple-system", "BlinkMacSystemFont", "Segoe UI",
163
+ "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans",
164
+ "Helvetica Neue", sans-serif;
165
+ font-weight: 500;
166
+ }
167
+
168
+ /* Table copy button */
169
+
170
+ .ui-markdown-table-wrapper {
171
+ position: relative;
172
+ margin: 16px 0;
173
+ max-width: 100%;
174
+ }
175
+
176
+ .ui-markdown-table-scroll {
177
+ overflow-x: auto;
178
+ max-width: 100%;
179
+ }
180
+
181
+ .ui-markdown-table-wrapper table {
182
+ margin: 0;
183
+ }
184
+
185
+ .ui-markdown-table-copy {
186
+ position: absolute;
187
+ top: 1px;
188
+ right: 1px;
189
+ opacity: 0;
190
+ transition: opacity 0.15s ease-out;
191
+ background: rgba(244, 244, 245, 0.9);
192
+ border: 1px solid rgba(228, 228, 231, 1);
193
+ border-radius: 0 0 0 6px;
194
+ width: 28px;
195
+ height: 28px;
196
+ padding: 0;
197
+ cursor: pointer;
198
+ display: flex;
199
+ align-items: center;
200
+ justify-content: center;
201
+ }
202
+
203
+ .ui-markdown-table-wrapper:hover .ui-markdown-table-copy {
204
+ opacity: 1;
205
+ }
206
+
207
+ .ui-markdown-table-copy:hover {
208
+ background: rgba(228, 228, 231, 1);
209
+ }
@@ -0,0 +1,17 @@
1
+ import { View } from "react-native";
2
+ import { Text } from "./text";
3
+
4
+ /**
5
+ * Native fallback. The rich renderer (`markdown.web.tsx`) is react-markdown over
6
+ * the DOM (`<div>`/`<table>`/clipboard) — web-only. `@lotics/ui` also builds for
7
+ * native, so this renders the source as plain text. Rich markdown shows on web
8
+ * (custom-code apps + the frontend on web), which is where it's used; the web
9
+ * variant is picked via the conditional `exports` in package.json.
10
+ */
11
+ export function Markdown({ children }: { children: string }) {
12
+ return (
13
+ <View>
14
+ <Text size="sm">{children}</Text>
15
+ </View>
16
+ );
17
+ }
@@ -0,0 +1,69 @@
1
+ import "./markdown.css";
2
+ import React, { useCallback, useRef, useState } from "react";
3
+ import { View } from "react-native";
4
+ import ReactMarkdown from "react-markdown";
5
+ import remarkGfm from "remark-gfm";
6
+ import { Icon } from "./icon";
7
+ import { colors } from "./colors";
8
+
9
+ export function Markdown({ children }: { children: string }) {
10
+ return (
11
+ <View>
12
+ <div className="ui-markdown">
13
+ <ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
14
+ {children}
15
+ </ReactMarkdown>
16
+ </div>
17
+ </View>
18
+ );
19
+ }
20
+
21
+ const markdownComponents = {
22
+ table: CopyableTable,
23
+ };
24
+
25
+ function CopyableTable(props: React.ComponentPropsWithoutRef<"table"> & { node?: unknown }) {
26
+ const { node: _node, children, ...rest } = props;
27
+ const tableRef = useRef<HTMLTableElement>(null);
28
+ const [copied, setCopied] = useState(false);
29
+
30
+ const handleCopy = useCallback(async () => {
31
+ const table = tableRef.current;
32
+ if (!table) return;
33
+
34
+ const rows = table.querySelectorAll("tr");
35
+ const tsv = Array.from(rows)
36
+ .map((row) =>
37
+ Array.from(row.querySelectorAll("th, td"))
38
+ .map((cell) => (cell.textContent ?? "").replace(/\t/g, " ").replace(/\n/g, " ").trim())
39
+ .join("\t"),
40
+ )
41
+ .join("\n");
42
+
43
+ await navigator.clipboard.writeText(tsv);
44
+ setCopied(true);
45
+ setTimeout(() => setCopied(false), 2000);
46
+ }, []);
47
+
48
+ return (
49
+ <div className="ui-markdown-table-wrapper">
50
+ <div className="ui-markdown-table-scroll">
51
+ <table ref={tableRef} {...rest}>
52
+ {children}
53
+ </table>
54
+ </div>
55
+ <button
56
+ className="ui-markdown-table-copy"
57
+ onClick={handleCopy}
58
+ type="button"
59
+ title={copied ? "Copied!" : "Copy table"}
60
+ >
61
+ <Icon
62
+ name={copied ? "check" : "copy"}
63
+ size={14}
64
+ color={copied ? colors.green[600] : colors.zinc[500]}
65
+ />
66
+ </button>
67
+ </div>
68
+ );
69
+ }
@@ -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,
@@ -1,5 +1,6 @@
1
1
  import type { KeyboardEvent } from "react";
2
2
  import { colors } from "./colors";
3
+ import { CONTROL_RADIUS } from "./control_surface";
3
4
  import { fontFamilyRegular, inputTextStyleWeb } from "./text_utils";
4
5
  import { useFormField } from "./form_field";
5
6
 
@@ -50,7 +51,7 @@ export function NumberInput(props: NumberInputProps) {
50
51
  height: 40,
51
52
  paddingLeft: 8,
52
53
  paddingRight: 8,
53
- borderRadius: 8,
54
+ borderRadius: CONTROL_RADIUS,
54
55
  borderWidth: 1,
55
56
  borderStyle: "solid",
56
57
  borderColor: colors.border,
package/src/picker.tsx CHANGED
@@ -2,7 +2,7 @@ import { Picker as RNPicker } from "@react-native-picker/picker";
2
2
  import { StyleSheet, View, Pressable, StyleProp, ViewStyle, TextStyle } from "react-native";
3
3
  import { useState, useCallback, useMemo, useEffect, useRef } from "react";
4
4
  import { colors } from "./colors";
5
- import { ACTIVE_RING } from "./control_surface";
5
+ import { ACTIVE_RING, CONTROL_RADIUS } from "./control_surface";
6
6
  import { Text } from "./text";
7
7
  import { fontFamilyRegular, getInputTextStyle } from "./text_utils";
8
8
  import { Icon } from "./icon";
@@ -310,7 +310,7 @@ const styles = StyleSheet.create({
310
310
  height: 40,
311
311
  paddingHorizontal: 6,
312
312
  paddingRight: 28,
313
- borderRadius: 8,
313
+ borderRadius: CONTROL_RADIUS,
314
314
  borderWidth: 1,
315
315
  borderColor: colors.border,
316
316
  fontFamily: fontFamilyRegular,
@@ -324,7 +324,7 @@ const styles = StyleSheet.create({
324
324
  paddingHorizontal: 6,
325
325
  borderWidth: 1,
326
326
  borderColor: colors.border,
327
- borderRadius: 8,
327
+ borderRadius: CONTROL_RADIUS,
328
328
  cursor: "auto",
329
329
  height: 40,
330
330
  },
@@ -1,6 +1,6 @@
1
1
  import { ReactNode } from "react";
2
2
  import { View, StyleSheet } from "react-native";
3
- import { Button } from "./button";
3
+ import { IconButton } from "./icon_button";
4
4
  import { Text } from "./text";
5
5
  import { usePopoverNav } from "./popover_nav";
6
6
 
@@ -18,7 +18,7 @@ export function PopoverHeader(props: PopoverHeaderProps) {
18
18
  return (
19
19
  <View style={styles.container}>
20
20
  {canGoBack && (
21
- <Button icon="chevron-left" color="secondary" accessibilityLabel={backLabel} onPress={goBack} />
21
+ <IconButton icon="chevron-left" size="lg" color="secondary" accessibilityLabel={backLabel} onPress={goBack} />
22
22
  )}
23
23
  <Text size="sm" weight="medium" style={{ flex: 1 }}>
24
24
  {title}
@@ -8,10 +8,18 @@ import * as path from "node:path";
8
8
  * `@lotics/ui` is the public, published, universal-rendering primitives
9
9
  * package. Every file in `src/` is a primitive. It must not import any
10
10
  * dependency that ties it to a specific Lotics runtime context: no chat
11
- * SDK, no analytics, no i18n, no markdown libraries, and no Lotics
12
- * domain types (which live in `@lotics/shared`). Lotics-coupled UI is
13
- * in the workspace-private `@lotics/ui-internal` package, which may
14
- * import from here and from `@lotics/shared` freely.
11
+ * SDK, no analytics, no i18n, and no Lotics domain types (which live in
12
+ * `@lotics/shared`). Lotics-coupled UI is in the workspace-private
13
+ * `@lotics/ui-internal` package, which may import from here and from
14
+ * `@lotics/shared` freely.
15
+ *
16
+ * Markdown (react-markdown/remark) is the one heavy third-party dependency
17
+ * the kit carries on purpose — it's the single canonical renderer for chat,
18
+ * apps, and `AgentRun`. It's generic, not a Lotics coupling; it stays
19
+ * universal via the `markdown.web.tsx` (DOM) / `markdown.tsx` (native
20
+ * plain-text) split and is tree-shaken out of any consumer that doesn't
21
+ * import `./markdown`. So it's allowed — the guards below remain for the
22
+ * genuine context couplings.
15
23
  *
16
24
  * The boundary is enforced by this test — without it primitives drift
17
25
  * back into the parent app's contexts over months.
@@ -22,8 +30,6 @@ const FORBIDDEN_PATTERNS = [
22
30
  /from\s+["']@lingui\//,
23
31
  /from\s+["']@ai-sdk\//,
24
32
  /from\s+["']ai["']/,
25
- /from\s+["']react-markdown/,
26
- /from\s+["']remark-/,
27
33
  /from\s+["']@lotics\/shared/,
28
34
  ];
29
35
 
@@ -38,7 +44,7 @@ function listTopLevelSourceFiles(): string[] {
38
44
  }
39
45
 
40
46
  describe("packages/ui primitives — purity", () => {
41
- it("no primitive imports posthog, lingui, ai-sdk, markdown, or @lotics/shared", () => {
47
+ it("no primitive imports posthog, lingui, ai-sdk, or @lotics/shared", () => {
42
48
  const violations: string[] = [];
43
49
  for (const file of listTopLevelSourceFiles()) {
44
50
  const src = fs.readFileSync(file, "utf-8");
@@ -1,6 +1,7 @@
1
1
  import { useCallback, useRef } from "react";
2
2
  import { View } 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 { PressableHighlight } from "./pressable_highlight";
6
7
  import { Icon } from "./icon";
@@ -120,7 +121,7 @@ function RadioOption<T extends string | number | symbol>(
120
121
  flexDirection: "row",
121
122
  alignItems: "center",
122
123
  padding: compact ? 8 : 12,
123
- borderRadius: 8,
124
+ borderRadius: CONTROL_RADIUS,
124
125
  gap: compact ? 8 : 16,
125
126
  }}
126
127
  onPress={handlePress}
@@ -24,7 +24,7 @@ export interface RangeSliderProps {
24
24
  type Thumb = "low" | "high";
25
25
 
26
26
  /**
27
- * A short human summary of a range selection for a `FilterPill` preview —
27
+ * A short human summary of a range selection for a `FilterChip` preview —
28
28
  * "5tr–10tr", or "≥ 5tr" / "≤ 10tr" when one end sits at its bound, and
29
29
  * `undefined` at the full range (no active filter). Mirrors `columnFilterSummary`.
30
30
  */
@@ -79,7 +79,7 @@ export function ReviewCard(props: ReviewCardProps) {
79
79
  <Text size="sm" weight="medium" color="muted" numberOfLines={1}>
80
80
  {accepted ? props.acceptedLabel ?? "Accepted" : props.dismissedLabel ?? "Dismissed"}
81
81
  </Text>
82
- {props.onUndo ? <Button title="Undo" color="muted" shape="rounded" onPress={props.onUndo} /> : null}
82
+ {props.onUndo ? <Button title="Undo" color="muted" onPress={props.onUndo} /> : null}
83
83
  </View>
84
84
  );
85
85
  }
@@ -132,7 +132,7 @@ export function ReviewCard(props: ReviewCardProps) {
132
132
  <View style={styles.footer}>
133
133
  {actions.map((a, i) => {
134
134
  const kind = a.kind ?? (i === actions.length - 1 ? "primary" : "muted");
135
- return <Button key={a.label} title={a.label} color={kind} shape="rounded" onPress={a.onPress} />;
135
+ return <Button key={a.label} title={a.label} color={kind} onPress={a.onPress} />;
136
136
  })}
137
137
  </View>
138
138
  ) : null}
@@ -1,5 +1,6 @@
1
1
  import { StyleSheet, View } from "react-native";
2
2
  import { colors, solid } from "./colors";
3
+ import { CONTROL_RADIUS } from "./control_surface";
3
4
  import { Text } from "./text";
4
5
  import { TextInputField } from "./text_input_field";
5
6
 
@@ -52,7 +53,7 @@ export function ScanField(props: ScanFieldProps) {
52
53
 
53
54
  const styles = StyleSheet.create({
54
55
  field: {
55
- borderRadius: 10,
56
+ borderRadius: CONTROL_RADIUS,
56
57
  backgroundColor: colors.white,
57
58
  },
58
59
  });
@@ -94,7 +94,6 @@ export function ScoredOption(props: ScoredOptionProps) {
94
94
  title={selected ? "Selected" : selectLabel ?? "Choose"}
95
95
  icon={selected ? "check" : undefined}
96
96
  color={selected ? "secondary" : "primary"}
97
- shape="rounded"
98
97
  onPress={onSelect}
99
98
  />
100
99
  </View>
@@ -9,6 +9,7 @@ import {
9
9
  type ViewStyle,
10
10
  } from "react-native";
11
11
  import { colors } from "./colors";
12
+ import { CONTROL_RADIUS } from "./control_surface";
12
13
  import { TextInputField } from "./text_input_field";
13
14
 
14
15
  type SearchInputProps = Omit<
@@ -57,7 +58,7 @@ export function SearchInput(props: SearchInputProps) {
57
58
 
58
59
  const styles = StyleSheet.create({
59
60
  // Search reads as a view-control by its leading search glyph + white fill —
60
- // NOT by a distinct shape. It shares the rounded-8 radius of every other
61
+ // NOT by a distinct shape. It shares the kit-wide `CONTROL_RADIUS` of every other
61
62
  // input/picker (TextInputField, Combobox, the date filter), so a toolbar of
62
63
  // controls reads as one consistent band. The white fill still pops against
63
64
  // the zinc-50 canvas where most toolbars live; a zinc-300 border (one step up
@@ -65,7 +66,7 @@ const styles = StyleSheet.create({
65
66
  // the fill alone would vanish. 40px (TextInputField default) — the system
66
67
  // control height every band aligns to.
67
68
  search: {
68
- borderRadius: 8,
69
+ borderRadius: CONTROL_RADIUS,
69
70
  backgroundColor: colors.white,
70
71
  // A thin 1px resting border (TextInputField's default width) — interaction
71
72
  // emphasis comes from the input's own focus outline, so it stays quiet at
@@ -3,6 +3,7 @@ import { StyleSheet, View, type StyleProp, type ViewStyle } from "react-native";
3
3
  import { PressableHighlight } from "./pressable_highlight";
4
4
  import { Text } from "./text";
5
5
  import { colors, withAlpha } from "./colors";
6
+ import { CONTROL_RADIUS } from "./control_surface";
6
7
 
7
8
  export interface SegmentOption<T extends string> {
8
9
  label: string;
@@ -183,7 +184,7 @@ const styles = StyleSheet.create({
183
184
  height: 40,
184
185
  gap: 2,
185
186
  padding: 3,
186
- borderRadius: 10,
187
+ borderRadius: CONTROL_RADIUS,
187
188
  backgroundColor: colors.zinc["100"],
188
189
  },
189
190
  trackDisabled: {
@@ -2,6 +2,7 @@ import { useEffect, useRef } from "react";
2
2
  import { View, StyleSheet, Animated, Easing } from "react-native";
3
3
  import { Text } from "./text";
4
4
  import { colors } from "./colors";
5
+ import { CONTROL_RADIUS } from "./control_surface";
5
6
 
6
7
  interface StatusBadgeProps {
7
8
  enabled: boolean;
@@ -69,7 +70,7 @@ const styles = StyleSheet.create({
69
70
  gap: 8,
70
71
  paddingVertical: 4,
71
72
  paddingHorizontal: 8,
72
- borderRadius: 8,
73
+ borderRadius: CONTROL_RADIUS,
73
74
  },
74
75
  dotWrapper: {
75
76
  width: 10,
package/src/stepper.tsx CHANGED
@@ -155,8 +155,9 @@ export function Step(props: StepProps) {
155
155
 
156
156
  // The node — status drives fill, never size. Monochrome: reached fills with ink,
157
157
  // the current step is a ring around white (pulsing when live), what's ahead is a
158
- // faint grey ring, the terminal is a checked fill.
159
- function Marker({ status, color, live }: { status: StepStatus; color: string; live: boolean }) {
158
+ // faint grey ring, the terminal is a checked fill. Exported so `AgentRun` reuses
159
+ // the exact same dot in its flat activity rows.
160
+ export function Marker({ status, color, live }: { status: StepStatus; color: string; live: boolean }) {
160
161
  if (status === "done") {
161
162
  // A passed step: filled in the accent ink with a white check.
162
163
  return (
@@ -3,6 +3,7 @@ import { Text } from "./text";
3
3
  import { Switch, type SwitchProps } from "./switch";
4
4
  import { PressableHighlight } from "./pressable_highlight";
5
5
  import { Icon, type IconName } from "./icon";
6
+ import { CONTROL_RADIUS } from "./control_surface";
6
7
 
7
8
  export interface SwitchButtonProps extends SwitchProps {
8
9
  icon?: IconName;
@@ -22,7 +23,7 @@ export function SwitchButton(props: SwitchButtonProps) {
22
23
  paddingVertical: 4,
23
24
  paddingLeft: 8,
24
25
  paddingRight: 6,
25
- borderRadius: 8,
26
+ borderRadius: CONTROL_RADIUS,
26
27
  }}
27
28
  onPress={() => onChange?.(!value)}
28
29
  tooltip={tooltip}
package/src/switcher.tsx CHANGED
@@ -7,6 +7,7 @@ import { Popover, PopoverContent, PopoverTrigger } from "./popover";
7
7
  import { Stack } from "./stack";
8
8
  import { Text } from "./text";
9
9
  import { colors } from "./colors";
10
+ import { CONTROL_HEIGHT, CONTROL_RADIUS } from "./control_surface";
10
11
 
11
12
  export interface SwitcherItem {
12
13
  id: string;
@@ -100,9 +101,9 @@ const styles = StyleSheet.create({
100
101
  flexDirection: "row",
101
102
  alignItems: "center",
102
103
  gap: 6,
104
+ height: CONTROL_HEIGHT,
103
105
  paddingHorizontal: 10,
104
- paddingVertical: 6,
105
- borderRadius: 8,
106
+ borderRadius: CONTROL_RADIUS,
106
107
  },
107
108
  popoverBody: {
108
109
  minWidth: 220,
package/src/tabs.tsx CHANGED
@@ -3,6 +3,7 @@ import { View } from "react-native";
3
3
  import { PressableHighlight } from "./pressable_highlight";
4
4
  import { Text } from "./text";
5
5
  import { colors } from "./colors";
6
+ import { CONTROL_RADIUS } from "./control_surface";
6
7
 
7
8
  export interface TabOption<T extends string> {
8
9
  label: string;
@@ -138,7 +139,7 @@ function TabButton<T extends string>(props: TabButtonProps<T>) {
138
139
  style={{
139
140
  paddingVertical: 8,
140
141
  paddingHorizontal: 16,
141
- borderRadius: 8,
142
+ borderRadius: CONTROL_RADIUS,
142
143
  backgroundColor: pressed
143
144
  ? colors.zinc["100"]
144
145
  : hovered
package/src/tag_input.tsx CHANGED
@@ -1,6 +1,7 @@
1
1
  import { useMemo, useRef, useState } from "react";
2
2
  import { Pressable, ScrollView, StyleSheet, View, type StyleProp, type 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 { Icon } from "./icon";
6
7
  import { TextInputField } from "./text_input_field";
@@ -161,7 +162,7 @@ const styles = StyleSheet.create({
161
162
  paddingVertical: 5,
162
163
  borderWidth: 1,
163
164
  borderColor: colors.border,
164
- borderRadius: 8,
165
+ borderRadius: CONTROL_RADIUS,
165
166
  backgroundColor: colors.background,
166
167
  },
167
168
  chip: {