@colixsystems/widget-sdk 0.236.0 → 0.238.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/dist/list-view.js CHANGED
@@ -14,6 +14,9 @@ import {
14
14
  resolveListTokens,
15
15
  listRowFrame,
16
16
  listSideEdgeWidth,
17
+ listCardRowFrame,
18
+ listCardGap,
19
+ listVariant,
17
20
  LIST_TOKEN_NAMES,
18
21
  LIST_SELECTED_BAR_WIDTH,
19
22
  } from "./list-tokens.js";
@@ -33,6 +36,10 @@ function listOverrides(props) {
33
36
  return out;
34
37
  }
35
38
 
39
+ function hasNode(node) {
40
+ return node !== undefined && node !== null && node !== false;
41
+ }
42
+
36
43
  function soleElement(children) {
37
44
  const parts = React.Children.toArray(children);
38
45
  return parts.length === 1 && React.isValidElement(parts[0]) ? parts[0] : null;
@@ -50,16 +57,31 @@ const HORIZONTAL_PADDING = [
50
57
  /**
51
58
  * Bind the shared list components to one platform's primitives.
52
59
  *
53
- * @param {{View: any, Text: any, Pressable: any}} rn
60
+ * @param {{View: any, Text: any, Pressable: any, Card: any}} rn — `Card` is
61
+ * the platform's bound `<Card>`, the surface of a card-style list's rows.
54
62
  */
55
63
  export function makeList(rn) {
56
- const { View, Text, Pressable } = rn;
64
+ const { View, Text, Pressable, Card } = rn;
57
65
  const ListContext = React.createContext(null);
58
66
 
67
+ // What a `<List>` hands its rows: overrides, the widget's defaults, its variant.
68
+ // Tokens carry into a nested list; the variant is each list's own.
69
+ function listContextValue(props, inherited) {
70
+ const { variant, defaults } = props;
71
+ return {
72
+ overrides: { ...(inherited && inherited.overrides), ...listOverrides(props) },
73
+ defaults: { ...(inherited && inherited.defaults), ...defaults },
74
+ variant: listVariant(variant),
75
+ };
76
+ }
77
+
59
78
  function useRowTokens(props) {
60
79
  const theme = useListTheme();
61
80
  const inherited = React.useContext(ListContext);
62
- return [theme, resolveListTokens(theme, { ...inherited, ...listOverrides(props) })];
81
+ const list = listContextValue(props, inherited);
82
+ // Only a `<List>` chooses the variant; a row follows it.
83
+ const variant = inherited ? inherited.variant : "plain";
84
+ return [theme, resolveListTokens(theme, list.overrides, list.defaults), { ...list, variant }];
63
85
  }
64
86
 
65
87
  // A lone row View spans the row, as the title block does. Anything
@@ -71,52 +93,67 @@ export function makeList(rn) {
71
93
  : children;
72
94
  }
73
95
 
96
+ function titleBlock(title, subtitle, typography, colors) {
97
+ return React.createElement(
98
+ View,
99
+ { style: { flex: 1, gap: 2 } },
100
+ title !== undefined && title !== null
101
+ ? React.createElement(
102
+ Text,
103
+ { style: textRoleStyle("subtitle", typography, colors), numberOfLines: 1 },
104
+ title,
105
+ )
106
+ : null,
107
+ subtitle !== undefined && subtitle !== null
108
+ ? React.createElement(
109
+ Text,
110
+ { style: textRoleStyle("caption", typography, colors), numberOfLines: 2 },
111
+ subtitle,
112
+ )
113
+ : null,
114
+ );
115
+ }
116
+
117
+ function rowBody(props, typography, colors) {
118
+ return props.children !== undefined
119
+ ? customBody(props.children)
120
+ : titleBlock(props.title, props.subtitle, typography, colors);
121
+ }
122
+
74
123
  function setsOwnInset(style) {
75
124
  const flat = flattenStyle(style);
76
125
  return HORIZONTAL_PADDING.some((key) => flat[key] !== undefined);
77
126
  }
78
127
 
79
- function List(props) {
80
- const { children, style, testID } = props;
81
- const theme = useListTheme();
82
- const inherited = React.useContext(ListContext);
83
- const overrides = { ...inherited, ...listOverrides(props) };
84
- const tokens = resolveListTokens(theme, overrides);
85
- // No divider under the last row: the list's own edge ends it.
86
- const rows = React.Children.toArray(children);
87
- const last = rows.length - 1;
88
- const content = rows.map((row, i) =>
89
- i === last && React.isValidElement(row) && row.props.divider === undefined
90
- ? React.cloneElement(row, { divider: false })
91
- : row,
92
- );
93
- return React.createElement(
94
- ListContext.Provider,
95
- { value: overrides },
96
- React.createElement(View, { style: [{ gap: tokens.gap }, style], testID }, content),
97
- );
98
- }
99
- List.displayName = "List";
100
-
101
- function ListRow(props) {
102
- const {
103
- title,
104
- subtitle,
105
- leading,
106
- trailing,
128
+ // accessibilityRole + aria-*: the pair that announces on both hosts.
129
+ function pressProps({ onPress, disabled = false, selected = false, accessibilityLabel }) {
130
+ return {
107
131
  onPress,
108
- selected = false,
109
- disabled = false,
110
- divider = true,
132
+ disabled,
133
+ accessibilityRole: "button",
111
134
  accessibilityLabel,
112
- style,
113
- testID,
114
- children,
115
- } = props;
116
- const [theme, tokens] = useRowTokens(props);
117
- const { colors, spacing, typography } = theme;
135
+ "aria-selected": selected,
136
+ "aria-disabled": disabled,
137
+ };
138
+ }
118
139
 
119
- const pressable = typeof onPress === "function";
140
+ // Beside `actions`, the tap target is the row minus its buttons,
141
+ // laid out as the row is so nothing moves.
142
+ function pressRegionStyle(style, spacing) {
143
+ const flat = flattenStyle(style);
144
+ return {
145
+ flex: 1,
146
+ minWidth: 0,
147
+ alignSelf: "stretch",
148
+ flexDirection: "row",
149
+ alignItems: flat.alignItems ?? "center",
150
+ gap: flat.gap ?? spacing.md,
151
+ };
152
+ }
153
+
154
+ function plainRowStyle(props, tokens, spacing) {
155
+ const { selected = false, divider = true, style } = props;
156
+ const pressable = typeof props.onPress === "function";
120
157
  // A tile is outlined; a flush band is separated by its bottom divider.
121
158
  const edge = tokens.separated
122
159
  ? tokens.dividerWidth > 0
@@ -125,7 +162,7 @@ export function makeList(rn) {
125
162
  : divider && tokens.dividerWidth > 0
126
163
  ? { borderBottomWidth: tokens.dividerWidth, borderColor: tokens.dividerColor }
127
164
  : null;
128
- const rowStyle = {
165
+ return {
129
166
  // An inset the widget sets itself is left exactly as it asked.
130
167
  ...listRowFrame(tokens, spacing, selected && !setsOwnInset(style)),
131
168
  paddingVertical: tokens.rowPadding,
@@ -143,44 +180,44 @@ export function makeList(rn) {
143
180
  }
144
181
  : null),
145
182
  };
183
+ }
184
+
185
+ // The row's slots, with its own buttons kept outside the tap target.
186
+ function rowSlots(props, row) {
187
+ const { leading, trailing, actions, disabled = false, style } = props;
188
+ const body = rowBody(props, row.theme.typography, row.colors);
189
+ if (!row.splitPress) return [leading || null, body, trailing || null, actions || null];
190
+ return [
191
+ React.createElement(
192
+ Pressable,
193
+ {
194
+ ...pressProps(props),
195
+ onPressIn: () => row.setPressed(true),
196
+ onPressOut: () => row.setPressed(false),
197
+ style: [pressRegionStyle(style, row.theme.spacing), disabled ? { opacity: 0.5 } : null],
198
+ },
199
+ leading || null,
200
+ body,
201
+ trailing || null,
202
+ ),
203
+ actions,
204
+ ];
205
+ }
146
206
 
147
- const body =
148
- children !== undefined
149
- ? customBody(children)
150
- : React.createElement(
151
- View,
152
- { style: { flex: 1, gap: 2 } },
153
- title !== undefined && title !== null
154
- ? React.createElement(
155
- Text,
156
- {
157
- style: textRoleStyle("subtitle", typography, colors),
158
- numberOfLines: 1,
159
- },
160
- title,
161
- )
162
- : null,
163
- subtitle !== undefined && subtitle !== null
164
- ? React.createElement(
165
- Text,
166
- {
167
- style: textRoleStyle("caption", typography, colors),
168
- numberOfLines: 2,
169
- },
170
- subtitle,
171
- )
172
- : null,
173
- );
174
- const content = [leading || null, body, trailing || null];
207
+ function PlainRow({ props, row }) {
208
+ const { disabled = false, accessibilityLabel, style, testID } = props;
209
+ const rowStyle = plainRowStyle(props, row.tokens, row.theme.spacing);
210
+ const pressedFill = { backgroundColor: row.tokens.pressedBackground };
211
+ const content = rowSlots(props, { ...row, colors: row.theme.colors });
175
212
 
176
- if (!pressable) {
213
+ if (!row.pressable || row.splitPress) {
177
214
  return React.createElement(
178
215
  View,
179
216
  {
180
- style: [rowStyle, style],
217
+ style: [rowStyle, row.pressed ? pressedFill : null, style],
181
218
  testID,
182
- accessibilityLabel,
183
- accessible: accessibilityLabel ? true : undefined,
219
+ accessibilityLabel: row.splitPress ? undefined : accessibilityLabel,
220
+ accessible: accessibilityLabel && !row.splitPress ? true : undefined,
184
221
  },
185
222
  ...content,
186
223
  );
@@ -188,17 +225,11 @@ export function makeList(rn) {
188
225
  return React.createElement(
189
226
  Pressable,
190
227
  {
191
- onPress,
192
- disabled,
193
- // accessibilityRole + aria-*: the pair that announces on both hosts.
194
- accessibilityRole: "button",
195
- accessibilityLabel,
196
- "aria-selected": selected,
197
- "aria-disabled": disabled,
228
+ ...pressProps(props),
198
229
  testID,
199
230
  style: ({ pressed }) => [
200
231
  rowStyle,
201
- pressed ? { backgroundColor: tokens.pressedBackground } : null,
232
+ pressed ? pressedFill : null,
202
233
  disabled ? { opacity: 0.5 } : null,
203
234
  style,
204
235
  ],
@@ -206,6 +237,92 @@ export function makeList(rn) {
206
237
  ...content,
207
238
  );
208
239
  }
240
+
241
+ // Each row is the app's `<Card>`; the card scope paints it, the list
242
+ // scope arranges what is inside.
243
+ function CardRow({ props, row }) {
244
+ const { selected = false, disabled = false, accessibilityLabel, style, testID } = props;
245
+ const { tokens, theme } = row;
246
+ const frame = listCardRowFrame(tokens, theme.spacing, row.pressable);
247
+ // The card's own border carries the accent, so nothing shifts.
248
+ const mark = selected
249
+ ? { backgroundColor: tokens.selectedBackground, borderColor: tokens.selectedAccent }
250
+ : null;
251
+ const pressedFill = { backgroundColor: tokens.pressedBackground };
252
+ // A function child reads the inks of the card's own fill.
253
+ const content = ({ colors }) =>
254
+ React.createElement(React.Fragment, null, ...rowSlots(props, { ...row, colors }));
255
+
256
+ if (!row.pressable || row.splitPress) {
257
+ return React.createElement(
258
+ Card,
259
+ {
260
+ testID,
261
+ accessibilityLabel: row.splitPress ? undefined : accessibilityLabel,
262
+ accessible: accessibilityLabel && !row.splitPress ? true : undefined,
263
+ containerStyle: [frame, mark, row.pressed ? pressedFill : null, style],
264
+ },
265
+ content,
266
+ );
267
+ }
268
+ return React.createElement(
269
+ Card,
270
+ {
271
+ as: Pressable,
272
+ fillInGradient: true,
273
+ ...pressProps(props),
274
+ testID,
275
+ containerStyle: ({ pressed }) => [
276
+ frame,
277
+ mark,
278
+ pressed ? pressedFill : null,
279
+ disabled ? { opacity: 0.5 } : null,
280
+ style,
281
+ ],
282
+ },
283
+ content,
284
+ );
285
+ }
286
+
287
+ function List(props) {
288
+ const { children, style, testID } = props;
289
+ const theme = useListTheme();
290
+ const value = listContextValue(props, React.useContext(ListContext));
291
+ const tokens = resolveListTokens(theme, value.overrides, value.defaults);
292
+ const gap = value.variant === "card" ? listCardGap(tokens, theme.spacing) : tokens.gap;
293
+ // No divider under the last row: the list's own edge ends it.
294
+ const rows = React.Children.toArray(children);
295
+ const last = rows.length - 1;
296
+ const content = rows.map((row, i) =>
297
+ i === last && React.isValidElement(row) && row.props.divider === undefined
298
+ ? React.cloneElement(row, { divider: false })
299
+ : row,
300
+ );
301
+ return React.createElement(
302
+ ListContext.Provider,
303
+ { value },
304
+ React.createElement(View, { style: [{ gap }, style], testID }, content),
305
+ );
306
+ }
307
+ List.displayName = "List";
308
+
309
+ function ListRow(props) {
310
+ const [theme, tokens, list] = useRowTokens(props);
311
+ const [pressed, setPressed] = React.useState(false);
312
+ const pressable = typeof props.onPress === "function";
313
+ const row = {
314
+ theme,
315
+ tokens,
316
+ pressable,
317
+ // Buttons inside a button are unreachable; `actions` sit beside it.
318
+ splitPress: pressable && hasNode(props.actions),
319
+ pressed,
320
+ setPressed,
321
+ };
322
+ return list.variant === "card"
323
+ ? React.createElement(CardRow, { props, row })
324
+ : React.createElement(PlainRow, { props, row });
325
+ }
209
326
  ListRow.displayName = "ListRow";
210
327
 
211
328
  // A table's column labels: a ListRow's frame and body slot with none of its
package/dist/list.js CHANGED
@@ -2,6 +2,12 @@
2
2
  // ./list-view.js to react-native-web; see ./primitives.js for why not the bare specifier.
3
3
 
4
4
  import * as ReactNative from "react-native-web";
5
+ import { Card } from "./card.js";
5
6
  import { makeList } from "./list-view.js";
6
7
 
7
- export const { List, ListRow, ListHeader, EmptyState } = makeList(ReactNative);
8
+ export const { List, ListRow, ListHeader, EmptyState } = makeList({
9
+ View: ReactNative.View,
10
+ Text: ReactNative.Text,
11
+ Pressable: ReactNative.Pressable,
12
+ Card,
13
+ });
@@ -1,7 +1,8 @@
1
1
  // `<List>`, `<ListRow>`, `<ListHeader>`, `<EmptyState>` (native). Binds
2
- // ./list-view.js to React Native's own primitives; only the import differs from ./list.js.
2
+ // ./list-view.js to React Native's own primitives; only the imports differ from ./list.js.
3
3
 
4
4
  import { View, Text, Pressable } from "react-native";
5
+ import { Card } from "./card.native.js";
5
6
  import { makeList } from "./list-view.js";
6
7
 
7
- export const { List, ListRow, ListHeader, EmptyState } = makeList({ View, Text, Pressable });
8
+ export const { List, ListRow, ListHeader, EmptyState } = makeList({ View, Text, Pressable, Card });
@@ -73,9 +73,7 @@ export { Icon } from "./icon.js";
73
73
  export { DateTimePicker } from "./datetimepicker.js";
74
74
  // sc-1378 — `<FilePicker>` (web). Wraps a hidden DOM `<input type="file">`
75
75
  // because react-native-web has no native equivalent. The native build's
76
- // counterpart (./filepicker.native.js) degrades to a disabled trigger until
77
- // a vetted expo-document-picker pin lands. `isSupported` is a static
78
- // boolean widgets branch on to hide the trigger on native.
76
+ // counterpart (./filepicker.native.js) opens the OS document picker.
79
77
  export { FilePicker } from "./filepicker.js";
80
78
  // REQ-AI-AGENT-DESIGN (sc-3696) — `<Gradient>` (web). Paints a CSS
81
79
  // `linear-gradient` on a View; the native build (./gradient.native.js) delegates
@@ -31,10 +31,8 @@ export { Icon } from "./icon.js";
31
31
  // `./datetimepicker.js`. Both files honour the same ISO 8601 value /
32
32
  // onChange contract.
33
33
  export { DateTimePicker } from "./datetimepicker.native.js";
34
- // sc-1378 — `<FilePicker>` (native). Web counterpart (./filepicker.js)
35
- // wraps a hidden DOM `<input type="file">`; native has no portable
36
- // equivalent yet, so this build renders a disabled trigger and exposes
37
- // `FilePicker.isSupported = false` for widgets to branch on.
34
+ // sc-1378 — `<FilePicker>` (native). Opens the OS document picker
35
+ // (expo-document-picker); the web counterpart wraps a hidden file input.
38
36
  export { FilePicker } from "./filepicker.native.js";
39
37
  // REQ-AI-AGENT-DESIGN (sc-3696) — `<Gradient>` (native). Delegates to
40
38
  // expo-linear-gradient (pinned unconditionally in every export); the web build
@@ -0,0 +1,85 @@
1
+ // What `<SearchField>` draws from the theme's `search` scope: where its
2
+ // magnifier sits, and the field look it lays over `<TextField>`'s `muted` one.
3
+ // Platform-free, so both hosts lay the field out from the same answer.
4
+
5
+ import { CONTRACT } from "./contract.js";
6
+ import { normaliseThemeComponents } from "./theme-components.js";
7
+ import { resolveTextFieldTokens } from "./text-field-tokens.js";
8
+
9
+ export const SEARCH_ICON_POSITIONS = CONTRACT.themeComponentSearchIconPositions;
10
+ export const DEFAULT_SEARCH_ICON_POSITION = "start";
11
+
12
+ // The `search` tokens that restyle the field; each is a `<TextField>` override.
13
+ export const SEARCH_FIELD_LOOK_TOKENS = Object.freeze([
14
+ "background",
15
+ "borderColor",
16
+ "radius",
17
+ "height",
18
+ ]);
19
+
20
+ function searchScope(theme) {
21
+ const components = theme && typeof theme === "object" ? theme.components : undefined;
22
+ return normaliseThemeComponents({ search: components && components.search }).search || {};
23
+ }
24
+
25
+ /**
26
+ * The magnifier's position: the field's own prop, then the theme, then start.
27
+ *
28
+ * @param {unknown} own — the `iconPosition` prop.
29
+ * @param {unknown} themed — the theme's `search.iconPosition`.
30
+ */
31
+ export function searchIconPosition(own, themed) {
32
+ if (SEARCH_ICON_POSITIONS.includes(own)) return own;
33
+ if (SEARCH_ICON_POSITIONS.includes(themed)) return themed;
34
+ return DEFAULT_SEARCH_ICON_POSITION;
35
+ }
36
+
37
+ /**
38
+ * Resolve one search field. `field` holds only the tokens the theme set, so
39
+ * an unset one leaves `<TextField>` on the `muted` look it always drew.
40
+ *
41
+ * @param {object} theme — the resolved workspace theme (`useTheme()` shape).
42
+ * @param {{ iconPosition?: string }} [own] — the field's own props.
43
+ * @returns {{ iconPosition: string, field: Record<string, string|number> }}
44
+ */
45
+ export function resolveSearchFieldTokens(theme, own) {
46
+ const scope = searchScope(theme);
47
+ const field = {};
48
+ for (const name of SEARCH_FIELD_LOOK_TOKENS) {
49
+ if (scope[name] !== undefined) field[name] = scope[name];
50
+ }
51
+ return {
52
+ iconPosition: searchIconPosition(own && own.iconPosition, scope.iconPosition),
53
+ field,
54
+ };
55
+ }
56
+
57
+ /**
58
+ * Where the glyph and the clear control go. At the end, the clear control
59
+ * takes the glyph's slot while there is text, so the two never overlap.
60
+ *
61
+ * @param {string} iconPosition — a resolved SEARCH_ICON_POSITIONS value.
62
+ * @param {boolean} clearable — the field has text and is enabled.
63
+ * @returns {{ glyph: "start"|"end"|null, padStart: boolean, padEnd: boolean }}
64
+ */
65
+ export function searchFieldLayout(iconPosition, clearable) {
66
+ const glyphAtEnd = iconPosition === "end" && !clearable;
67
+ return {
68
+ glyph: iconPosition === "start" ? "start" : glyphAtEnd ? "end" : null,
69
+ padStart: iconPosition === "start",
70
+ padEnd: clearable || iconPosition === "end",
71
+ };
72
+ }
73
+
74
+ /**
75
+ * What each `search` token resolves to while unset, keyed by token name, for
76
+ * the Studio to preview: the `muted` field the search box falls back to.
77
+ *
78
+ * @param {object} theme — the resolved workspace theme.
79
+ */
80
+ export function searchScopeDefaults(theme) {
81
+ const field = resolveTextFieldTokens(theme);
82
+ const out = { iconPosition: DEFAULT_SEARCH_ICON_POSITION };
83
+ for (const name of SEARCH_FIELD_LOOK_TOKENS) out[name] = field[name];
84
+ return out;
85
+ }
@@ -1,13 +1,15 @@
1
1
  // The ONE implementation behind the `<SearchField>` SDK primitive. It is
2
2
  // `<TextField>` with a search glyph and a clear control, so it wears the same
3
- // field and takes the same props; the platform bindings (`search-field.js` /
4
- // `search-field.native.js`) differ only in where the primitives come from.
3
+ // field and takes the same props, plus the theme's `search` look; the platform
4
+ // bindings (`search-field.js` / `search-field.native.js`) differ only in where
5
+ // the primitives come from.
5
6
 
6
7
  import React from "react";
7
8
  import { useHostTheme } from "./hooks.js";
8
9
  import { DEFAULT_THEME_TOKENS } from "./_theme-tokens.js";
9
10
  import { Icon } from "./icon.js";
10
11
  import { resolveFormControlTokens } from "./form-control-tokens.js";
12
+ import { resolveSearchFieldTokens, searchFieldLayout } from "./search-field-tokens.js";
11
13
  import {
12
14
  errorMessage,
13
15
  hasNote,
@@ -24,6 +26,17 @@ const ADORNMENT_WIDTH = 40;
24
26
  // A real target: the web host ignores hitSlop, so size carries it.
25
27
  const CLEAR_MIN_SIZE = 36;
26
28
 
29
+ function adornmentSlot(side) {
30
+ return {
31
+ position: "absolute",
32
+ top: 0,
33
+ bottom: 0,
34
+ alignItems: "center",
35
+ justifyContent: "center",
36
+ ...(side === "start" ? { left: 0 } : { right: 0 }),
37
+ };
38
+ }
39
+
27
40
  /**
28
41
  * Bind the shared search field to one platform's primitives.
29
42
  *
@@ -45,6 +58,7 @@ export function makeSearchField(rn) {
45
58
  error,
46
59
  disabled = false,
47
60
  accessibilityLabel,
61
+ iconPosition,
48
62
  style,
49
63
  containerStyle,
50
64
  testID,
@@ -52,6 +66,7 @@ export function makeSearchField(rn) {
52
66
  } = props;
53
67
  const theme = useHostTheme() || DEFAULT_THEME_TOKENS;
54
68
  const t = resolveFormControlTokens(theme);
69
+ const search = resolveSearchFieldTokens(theme, { iconPosition });
55
70
  const inputRef = React.useRef(null);
56
71
  const setRef = React.useMemo(() => shareRef(ref, inputRef), [ref]);
57
72
  const noteId = React.useId();
@@ -60,6 +75,8 @@ export function makeSearchField(rn) {
60
75
  if (input && typeof input.focus === "function") input.focus();
61
76
  };
62
77
  const hasText = typeof value === "string" && value.length > 0;
78
+ const clearable = hasText && !disabled;
79
+ const layout = searchFieldLayout(search.iconPosition, clearable);
63
80
  const clear = () => {
64
81
  if (typeof onChangeText === "function") onChangeText("");
65
82
  if (typeof onClear === "function") onClear();
@@ -98,30 +115,30 @@ export function makeSearchField(rn) {
98
115
  autoCapitalize: "none",
99
116
  autoCorrect: false,
100
117
  testID,
118
+ // The field's own props beat the theme's search look.
119
+ ...search.field,
101
120
  ...rest,
102
121
  style: [
103
- { paddingLeft: ADORNMENT_WIDTH },
104
- hasText && !disabled ? { paddingRight: ADORNMENT_WIDTH } : null,
122
+ layout.padStart ? { paddingLeft: ADORNMENT_WIDTH } : null,
123
+ layout.padEnd ? { paddingRight: ADORNMENT_WIDTH } : null,
105
124
  style,
106
125
  ],
107
126
  }),
108
- h(
109
- View,
110
- {
111
- style: {
112
- position: "absolute",
113
- left: 0,
114
- top: 0,
115
- bottom: 0,
116
- width: ADORNMENT_WIDTH,
117
- alignItems: "center",
118
- justifyContent: "center",
119
- pointerEvents: "none",
120
- },
121
- },
122
- h(Icon, { name: "search", size: GLYPH_SIZE, color: t.placeholderColor }),
123
- ),
124
- hasText && !disabled
127
+ layout.glyph
128
+ ? h(
129
+ View,
130
+ {
131
+ testID: testID ? `${testID}-icon` : undefined,
132
+ style: {
133
+ ...adornmentSlot(layout.glyph),
134
+ width: ADORNMENT_WIDTH,
135
+ pointerEvents: "none",
136
+ },
137
+ },
138
+ h(Icon, { name: "search", size: GLYPH_SIZE, color: t.placeholderColor }),
139
+ )
140
+ : null,
141
+ clearable
125
142
  ? h(
126
143
  Pressable,
127
144
  {
@@ -131,14 +148,10 @@ export function makeSearchField(rn) {
131
148
  testID: testID ? `${testID}-clear` : undefined,
132
149
  style: ({ pressed }) => [
133
150
  {
134
- position: "absolute",
151
+ ...adornmentSlot("end"),
135
152
  right: 2,
136
- top: 0,
137
- bottom: 0,
138
153
  minWidth: CLEAR_MIN_SIZE,
139
154
  minHeight: CLEAR_MIN_SIZE,
140
- alignItems: "center",
141
- justifyContent: "center",
142
155
  },
143
156
  pressed ? { opacity: 0.6 } : null,
144
157
  ],