@xpufx/paseo-top 0.4.2 → 0.4.3

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.
@@ -439,30 +439,64 @@ export function registerComposerPill<TPayload = any>(
439
439
  color={theme.colors.foreground}
440
440
  />
441
441
  );
442
+ const stopBubbling = (e: any) => {
443
+ if (e && typeof e.stopPropagation === "function") {
444
+ e.stopPropagation();
445
+ }
446
+ };
447
+
448
+ const eventBoundaryProps = {
449
+ onClick: stopBubbling,
450
+ onMouseDown: stopBubbling,
451
+ onMouseUp: stopBubbling,
452
+ onPointerDown: stopBubbling,
453
+ onPointerUp: stopBubbling,
454
+ onTouchStart: stopBubbling,
455
+ onTouchEnd: stopBubbling,
456
+ onKeyDown: stopBubbling,
457
+ onKeyUp: stopBubbling,
458
+ };
459
+
442
460
  return (
443
461
  <>
444
462
  {icon}
445
- <Modal
446
- title={options.modalTitle ?? options.title}
447
- icon={modalIconElement}
448
- open={centeredOpen}
449
- onOpenChange={(nextOpen: boolean) => setCenteredOpen(agentId, nextOpen)}
463
+ <View
464
+ onStartShouldSetResponder={() => true}
465
+ onTouchEnd={stopBubbling}
466
+ {...(eventBoundaryProps as any)}
450
467
  >
451
- <Modal.Content scrollable={resolvePillModalScrollable(options.hostScroll)}>
452
- {centeredOpen ? (
453
- <PluginThemeProvider theme={theme} layout={layout} flair={options.flair}>
454
- {options.renderModal({
455
- agentId,
456
- workspaceId: props.workspaceId ?? "",
457
- theme,
458
- layout,
459
- host: props.host ?? { id: "", label: "" },
460
- close: () => setCenteredOpen(agentId, false),
461
- })}
462
- </PluginThemeProvider>
463
- ) : null}
464
- </Modal.Content>
465
- </Modal>
468
+ <Modal
469
+ title={options.modalTitle ?? options.title}
470
+ icon={modalIconElement}
471
+ open={centeredOpen}
472
+ onOpenChange={(nextOpen: boolean) => setCenteredOpen(agentId, nextOpen)}
473
+ >
474
+ <Modal.Content scrollable={resolvePillModalScrollable(options.hostScroll)}>
475
+ {centeredOpen ? (
476
+ <View
477
+ onStartShouldSetResponder={() => true}
478
+ onTouchEnd={stopBubbling}
479
+ {...(eventBoundaryProps as any)}
480
+ >
481
+ <PluginThemeProvider theme={theme} layout={layout} flair={options.flair}>
482
+ <ModalBodyScrollOwnerContext.Provider
483
+ value={resolvePillModalScrollable(options.hostScroll) ? "host" : "required"}
484
+ >
485
+ {options.renderModal({
486
+ agentId,
487
+ workspaceId: props.workspaceId ?? "",
488
+ theme,
489
+ layout,
490
+ host: props.host ?? { id: "", label: "" },
491
+ close: () => setCenteredOpen(agentId, false),
492
+ })}
493
+ </ModalBodyScrollOwnerContext.Provider>
494
+ </PluginThemeProvider>
495
+ </View>
496
+ ) : null}
497
+ </Modal.Content>
498
+ </Modal>
499
+ </View>
466
500
  </>
467
501
  );
468
502
  };
@@ -480,7 +514,7 @@ export function registerComposerPill<TPayload = any>(
480
514
  host?: { id: string; label: string };
481
515
  close: () => void;
482
516
  }) {
483
- const { width: windowWidth } = useWindowDimensions();
517
+ const { width: windowWidth, height: windowHeight } = useWindowDimensions();
484
518
  // The anchored popover is otherwise sized from its content, so reflowing
485
519
  // content can move the frame under the pointer. Pin it to an explicit width
486
520
  // (clamped to the viewport) and let children overflow into their own
@@ -489,6 +523,11 @@ export function registerComposerPill<TPayload = any>(
489
523
  !props.layout.compact && options.popoverWidth
490
524
  ? Math.max(0, Math.min(options.popoverWidth, windowWidth - 24))
491
525
  : undefined;
526
+ // The host remains the scroll owner, but it needs a finite child extent to
527
+ // establish the popover viewport. Do not add overflow clipping or an inner
528
+ // ScrollView here: either would compete with the host's popover/sheet
529
+ // scroller.
530
+ const frameMaxHeight = Math.max(1, Math.min(560, windowHeight * 0.8));
492
531
  // Layout effect, not effect: the flag must be set in the same commit the
493
532
  // popover mounts, before any label timer can interleave. An effect runs
494
533
  // after paint, leaving a window where one tick can still publish and
@@ -508,8 +547,14 @@ export function registerComposerPill<TPayload = any>(
508
547
  };
509
548
  return (
510
549
  <PluginThemeProvider theme={props.theme} layout={props.layout} flair={options.flair}>
511
- <View style={[styles.popoverContainer, frameWidth ? { width: frameWidth } : null]}>
512
- <ModalBodyScrollOwnerContext.Provider value="host">
550
+ <View
551
+ style={{
552
+ ...styles.popoverContainer,
553
+ maxHeight: frameMaxHeight,
554
+ ...(frameWidth ? { width: frameWidth } : null),
555
+ }}
556
+ >
557
+ <ModalBodyScrollOwnerContext.Provider value="popover">
513
558
  {options.renderModal?.({ ...pillProps, close: props.close })}
514
559
  </ModalBodyScrollOwnerContext.Provider>
515
560
  </View>
@@ -589,11 +634,15 @@ export function registerComposerPill<TPayload = any>(
589
634
  <Modal.Content scrollable={resolvePillModalScrollable(options.hostScroll)}>
590
635
  {open ? (
591
636
  <PluginThemeProvider theme={props.theme} layout={props.layout} flair={options.flair}>
592
- {options.renderModal?.({
593
- ...props,
594
- close: () => setOpen(false),
595
- payload,
596
- })}
637
+ <ModalBodyScrollOwnerContext.Provider
638
+ value={resolvePillModalScrollable(options.hostScroll) ? "host" : "required"}
639
+ >
640
+ {options.renderModal?.({
641
+ ...props,
642
+ close: () => setOpen(false),
643
+ payload,
644
+ })}
645
+ </ModalBodyScrollOwnerContext.Provider>
597
646
  </PluginThemeProvider>
598
647
  ) : null}
599
648
  </Modal.Content>
@@ -0,0 +1 @@
1
+ export * from "./recipes";
@@ -0,0 +1,550 @@
1
+ import type { TextStyle, ViewStyle } from "react-native";
2
+ import type { PluginTheme, StatusVariant, ThemeColors } from "../../../../shared/vendor/paseo-plugin-helper/types";
3
+ import { alpha, getStatusColor, getVariantPalette, getContrastColor } from "../theme/color-utils";
4
+ import { defaultDarkTheme, FALLBACK_ACCENT_FOREGROUND } from "../theme/tokens";
5
+ import type { PluginThemeContextValue } from "../theme/provider";
6
+ import type { SurfaceStyle } from "../theme/flair";
7
+ import type { ButtonSize, ButtonVariant } from "../components/Button";
8
+ import type { BadgeSize, BadgeStyle } from "../components/Badge";
9
+
10
+ export type ThemeInput =
11
+ | PluginTheme
12
+ | PluginThemeContextValue
13
+ | ThemeColors
14
+ | { colors: ThemeColors }
15
+ | undefined;
16
+
17
+ /**
18
+ * Resolves ThemeColors from a PluginTheme, PluginThemeContextValue, ThemeColors, or undefined.
19
+ */
20
+ export function resolveThemeColors(themeInput?: ThemeInput): ThemeColors {
21
+ if (!themeInput || typeof themeInput !== "object") return defaultDarkTheme.colors;
22
+ if ("colors" in themeInput && themeInput.colors && typeof themeInput.colors === "object") {
23
+ return { ...defaultDarkTheme.colors, ...themeInput.colors };
24
+ }
25
+ if ("surface0" in themeInput && typeof (themeInput as ThemeColors).surface0 === "string") {
26
+ return { ...defaultDarkTheme.colors, ...(themeInput as ThemeColors) };
27
+ }
28
+ return defaultDarkTheme.colors;
29
+ }
30
+
31
+ // ---------------------------------------------------------------------------
32
+ // 1. INPUT RECIPE
33
+ // ---------------------------------------------------------------------------
34
+
35
+ export interface InputRecipeOptions {
36
+ focused?: boolean;
37
+ isFocused?: boolean;
38
+ error?: boolean;
39
+ hasError?: boolean;
40
+ disabled?: boolean;
41
+ multiline?: boolean;
42
+ mono?: boolean;
43
+ compact?: boolean;
44
+ size?: "sm" | "md" | "lg";
45
+ }
46
+
47
+ export interface InputRecipeResult extends TextStyle, ViewStyle {
48
+ input: TextStyle & ViewStyle;
49
+ container: ViewStyle;
50
+ label: TextStyle;
51
+ hint: TextStyle;
52
+ }
53
+
54
+ /**
55
+ * Generates standard React Native styles for text inputs and surrounding container/label/hint elements.
56
+ */
57
+ export function inputRecipe(
58
+ themeInput?: ThemeInput,
59
+ options?: InputRecipeOptions,
60
+ ): InputRecipeResult {
61
+ const colors = resolveThemeColors(themeInput);
62
+ const isFocused = Boolean(options?.focused ?? options?.isFocused);
63
+ const hasError = Boolean(options?.error ?? options?.hasError);
64
+ const disabled = Boolean(options?.disabled);
65
+ const multiline = Boolean(options?.multiline);
66
+ const mono = Boolean(options?.mono);
67
+ const compact = Boolean(options?.compact);
68
+
69
+ const borderColor = hasError
70
+ ? colors.statusDanger
71
+ : isFocused
72
+ ? colors.accent
73
+ : colors.border;
74
+
75
+ const minHeight = multiline
76
+ ? 64
77
+ : options?.size === "sm"
78
+ ? 30
79
+ : options?.size === "lg"
80
+ ? 42
81
+ : compact
82
+ ? 32
83
+ : 36;
84
+ const fontSize =
85
+ options?.size === "sm" ? 12 : options?.size === "lg" ? 15 : compact ? 13 : 14;
86
+ const paddingVertical = multiline
87
+ ? 8
88
+ : options?.size === "sm"
89
+ ? 4
90
+ : options?.size === "lg"
91
+ ? 8
92
+ : 6;
93
+ const paddingHorizontal =
94
+ options?.size === "sm" ? 8 : options?.size === "lg" ? 12 : 10;
95
+
96
+ const baseInput: TextStyle & ViewStyle = {
97
+ backgroundColor: disabled ? alpha(colors.surface1, 0.5) : colors.surface0,
98
+ borderColor,
99
+ borderWidth: 1,
100
+ borderRadius: 8,
101
+ color: disabled ? colors.foregroundMuted : colors.foreground,
102
+ fontSize,
103
+ fontFamily: mono ? "monospace" : undefined,
104
+ minHeight,
105
+ paddingHorizontal,
106
+ paddingVertical,
107
+ };
108
+
109
+ const container: ViewStyle = {
110
+ gap: 4,
111
+ width: "100%",
112
+ };
113
+
114
+ const label: TextStyle = {
115
+ color: hasError ? colors.statusDanger : colors.foreground,
116
+ fontSize: compact ? 12 : 13,
117
+ fontWeight: "600",
118
+ };
119
+
120
+ const hint: TextStyle = {
121
+ color: hasError ? colors.statusDanger : colors.foregroundMuted,
122
+ fontSize: 11,
123
+ marginTop: 2,
124
+ };
125
+
126
+ const input = { ...baseInput };
127
+ return Object.assign(baseInput, {
128
+ input,
129
+ container,
130
+ label,
131
+ hint,
132
+ placeholderColor: colors.foregroundMuted,
133
+ });
134
+ }
135
+
136
+ // ---------------------------------------------------------------------------
137
+ // 2. CARD RECIPE
138
+ // ---------------------------------------------------------------------------
139
+
140
+ export interface CardRecipeOptions {
141
+ variant?: SurfaceStyle;
142
+ noPadding?: boolean;
143
+ radius?: number;
144
+ borderWidth?: number;
145
+ compact?: boolean;
146
+ }
147
+
148
+ export interface CardRecipeResult extends ViewStyle {
149
+ card: ViewStyle;
150
+ header: ViewStyle;
151
+ headerTitle: TextStyle;
152
+ headerSubtitle: TextStyle;
153
+ }
154
+
155
+ /**
156
+ * Generates standard React Native styles for cards and card headers.
157
+ */
158
+ export function cardRecipe(
159
+ themeInput?: ThemeInput,
160
+ optionsOrVariant?: CardRecipeOptions | SurfaceStyle,
161
+ ): CardRecipeResult {
162
+ const colors = resolveThemeColors(themeInput);
163
+ const options: CardRecipeOptions =
164
+ typeof optionsOrVariant === "string" ? { variant: optionsOrVariant } : (optionsOrVariant ?? {});
165
+
166
+ const variant = options.variant ?? "flat";
167
+ const noPadding = Boolean(options.noPadding);
168
+ const compact = Boolean(options.compact);
169
+ const radius = options.radius ?? (compact ? 6 : 8);
170
+ const borderWidth = options.borderWidth ?? 1;
171
+
172
+ let bg = colors.surface0;
173
+ let border = colors.border;
174
+
175
+ if (variant === "tinted") {
176
+ bg = alpha(colors.accent, 0.04);
177
+ border = alpha(colors.accent, 0.2);
178
+ } else if (variant === "elevated") {
179
+ bg = colors.surface1;
180
+ border = colors.border;
181
+ }
182
+
183
+ const baseCard: ViewStyle = {
184
+ backgroundColor: bg,
185
+ borderColor: border,
186
+ borderWidth,
187
+ borderRadius: radius,
188
+ paddingHorizontal: noPadding ? 0 : compact ? 8 : 12,
189
+ paddingVertical: noPadding ? 0 : compact ? 6 : 10,
190
+ width: "100%",
191
+ overflow: "hidden",
192
+ };
193
+
194
+ const header: ViewStyle = {
195
+ flexDirection: "row",
196
+ alignItems: "center",
197
+ justifyContent: "space-between",
198
+ flexWrap: "wrap",
199
+ gap: compact ? 4 : 8,
200
+ width: "100%",
201
+ marginBottom: compact ? 4 : 8,
202
+ };
203
+
204
+ const headerTitle: TextStyle = {
205
+ color: colors.foreground,
206
+ fontSize: compact ? 13 : 14,
207
+ fontWeight: "600",
208
+ lineHeight: compact ? 18 : 20,
209
+ };
210
+
211
+ const headerSubtitle: TextStyle = {
212
+ color: colors.foregroundMuted,
213
+ fontSize: compact ? 10 : 11,
214
+ lineHeight: compact ? 14 : 15,
215
+ fontWeight: "400",
216
+ };
217
+
218
+ const card = { ...baseCard };
219
+ return Object.assign(baseCard, {
220
+ card,
221
+ header,
222
+ headerTitle,
223
+ headerSubtitle,
224
+ });
225
+ }
226
+
227
+ // ---------------------------------------------------------------------------
228
+ // 3. BUTTON RECIPE
229
+ // ---------------------------------------------------------------------------
230
+
231
+ export interface ButtonRecipeOptions {
232
+ variant?: ButtonVariant;
233
+ size?: ButtonSize;
234
+ pressed?: boolean;
235
+ disabled?: boolean;
236
+ loading?: boolean;
237
+ compact?: boolean;
238
+ }
239
+
240
+ export interface ButtonRecipeResult extends ViewStyle {
241
+ container: ViewStyle;
242
+ text: TextStyle;
243
+ }
244
+
245
+ /**
246
+ * Generates standard React Native styles for interactive buttons and text labels.
247
+ */
248
+ export function buttonRecipe(
249
+ themeInput?: ThemeInput,
250
+ optionsOrVariant?: ButtonRecipeOptions | ButtonVariant,
251
+ ): ButtonRecipeResult {
252
+ const colors = resolveThemeColors(themeInput);
253
+ const options: ButtonRecipeOptions =
254
+ typeof optionsOrVariant === "string" ? { variant: optionsOrVariant } : (optionsOrVariant ?? {});
255
+
256
+ const variant = options.variant ?? "secondary";
257
+ const size = options.size ?? "md";
258
+ const pressed = Boolean(options.pressed);
259
+ const disabled = Boolean(options.disabled || options.loading);
260
+ const compact = Boolean(options.compact);
261
+
262
+ const radius = size === "sm" ? 6 : size === "lg" ? 10 : 8;
263
+ const py = size === "sm" ? (compact ? 5 : 6) : size === "lg" ? 12 : compact ? 8 : 10;
264
+ const px = size === "sm" ? (compact ? 8 : 10) : size === "lg" ? 18 : compact ? 12 : 14;
265
+ const fontSize = size === "sm" ? 12 : size === "lg" ? 15 : 13;
266
+
267
+ let bg = "transparent";
268
+ let border = "transparent";
269
+ let textColor = colors.foreground;
270
+
271
+ switch (variant) {
272
+ case "primary":
273
+ bg = colors.accent;
274
+ textColor = colors.accentForeground || FALLBACK_ACCENT_FOREGROUND;
275
+ break;
276
+ case "danger":
277
+ bg = alpha(colors.statusDanger, 0.15);
278
+ border = alpha(colors.statusDanger, 0.4);
279
+ textColor = colors.statusDanger;
280
+ break;
281
+ case "ghost":
282
+ bg = "transparent";
283
+ textColor = colors.foregroundMuted;
284
+ break;
285
+ case "secondary":
286
+ default:
287
+ bg = colors.surface1;
288
+ border = colors.border;
289
+ textColor = colors.foreground;
290
+ break;
291
+ }
292
+
293
+ const effectiveBg = pressed && !disabled ? alpha(bg === "transparent" ? colors.surface2 : bg, 0.8) : bg;
294
+
295
+ const container: ViewStyle = {
296
+ flexDirection: "row",
297
+ alignItems: "center",
298
+ justifyContent: "center",
299
+ gap: 6,
300
+ backgroundColor: effectiveBg,
301
+ borderColor: border,
302
+ borderWidth: border !== "transparent" ? 1 : 0,
303
+ borderRadius: radius,
304
+ paddingVertical: py,
305
+ paddingHorizontal: px,
306
+ minHeight: size === "sm" ? 28 : size === "lg" ? 42 : 34,
307
+ opacity: disabled ? 0.45 : 1,
308
+ };
309
+
310
+ const text: TextStyle = {
311
+ color: textColor,
312
+ fontSize,
313
+ fontWeight: "600",
314
+ textAlign: "center",
315
+ };
316
+
317
+ const subContainer = { ...container };
318
+ return Object.assign(container, {
319
+ container: subContainer,
320
+ text,
321
+ });
322
+ }
323
+
324
+ // ---------------------------------------------------------------------------
325
+ // 4. TAB STRIP & TAB ITEM RECIPES
326
+ // ---------------------------------------------------------------------------
327
+
328
+ export interface TabStripRecipeOptions {
329
+ radius?: number;
330
+ }
331
+
332
+ export interface TabStripRecipeResult extends ViewStyle {
333
+ frame: ViewStyle;
334
+ track: ViewStyle;
335
+ }
336
+
337
+ /**
338
+ * Generates standard React Native styles for tab tracks and bounding frames.
339
+ */
340
+ export function tabStripRecipe(
341
+ themeInput?: ThemeInput,
342
+ options?: TabStripRecipeOptions,
343
+ ): TabStripRecipeResult {
344
+ const colors = resolveThemeColors(themeInput);
345
+ const radius = options?.radius ?? 8;
346
+
347
+ const frame: ViewStyle = {
348
+ width: "100%",
349
+ backgroundColor: colors.surface1,
350
+ borderRadius: radius,
351
+ borderColor: colors.border,
352
+ borderWidth: 1,
353
+ overflow: "hidden",
354
+ justifyContent: "center",
355
+ };
356
+
357
+ const track: ViewStyle = {
358
+ flexDirection: "row",
359
+ alignItems: "center",
360
+ width: "100%",
361
+ padding: 3,
362
+ gap: 2,
363
+ };
364
+
365
+ const subFrame = { ...frame };
366
+ return Object.assign(frame, {
367
+ frame: subFrame,
368
+ track,
369
+ });
370
+ }
371
+
372
+ export interface TabItemRecipeOptions {
373
+ active?: boolean;
374
+ pressed?: boolean;
375
+ compact?: boolean;
376
+ fit?: boolean;
377
+ }
378
+
379
+ export interface TabItemRecipeResult extends ViewStyle {
380
+ container: ViewStyle;
381
+ text: TextStyle;
382
+ badge: ViewStyle;
383
+ badgeText: TextStyle;
384
+ }
385
+
386
+ /**
387
+ * Generates standard React Native styles for individual tab buttons and inner badges.
388
+ */
389
+ export function tabItemRecipe(
390
+ themeInput?: ThemeInput,
391
+ activeOrOptions?: boolean | TabItemRecipeOptions,
392
+ options?: TabItemRecipeOptions,
393
+ ): TabItemRecipeResult {
394
+ const colors = resolveThemeColors(themeInput);
395
+ const mergedOptions: TabItemRecipeOptions =
396
+ typeof activeOrOptions === "boolean"
397
+ ? { active: activeOrOptions, ...options }
398
+ : (activeOrOptions ?? {});
399
+
400
+ const isActive = Boolean(mergedOptions.active);
401
+ const pressed = Boolean(mergedOptions.pressed);
402
+ const compact = Boolean(mergedOptions.compact);
403
+ const fit = mergedOptions.fit ?? true;
404
+
405
+ const bg = isActive
406
+ ? colors.surface2
407
+ : pressed
408
+ ? alpha(colors.surface2, 0.5)
409
+ : "transparent";
410
+
411
+ const container: ViewStyle = {
412
+ flexDirection: "row",
413
+ alignItems: "center",
414
+ justifyContent: "center",
415
+ gap: 4,
416
+ overflow: "hidden",
417
+ borderRadius: 6,
418
+ minHeight: compact ? 26 : 30,
419
+ backgroundColor: bg,
420
+ paddingHorizontal: compact ? 8 : 12,
421
+ paddingVertical: compact ? 4 : 6,
422
+ ...(fit ? { flex: 1, flexShrink: 1, minWidth: 0 } : { flexShrink: 0 }),
423
+ };
424
+
425
+ const text: TextStyle = {
426
+ color: isActive ? colors.foreground : colors.foregroundMuted,
427
+ fontSize: compact ? 11 : 12,
428
+ fontWeight: isActive ? "600" : "500",
429
+ textAlign: "center",
430
+ };
431
+
432
+ const badge: ViewStyle = {
433
+ borderRadius: 9999,
434
+ paddingHorizontal: 5,
435
+ paddingVertical: 1,
436
+ minWidth: 16,
437
+ alignItems: "center",
438
+ justifyContent: "center",
439
+ backgroundColor: isActive ? colors.accent : alpha(colors.foregroundMuted, 0.2),
440
+ };
441
+
442
+ const badgeText: TextStyle = {
443
+ fontSize: 10,
444
+ fontWeight: "700",
445
+ color: isActive ? colors.accentForeground || FALLBACK_ACCENT_FOREGROUND : colors.foregroundMuted,
446
+ };
447
+
448
+ const subContainer = { ...container };
449
+ return Object.assign(container, {
450
+ container: subContainer,
451
+ text,
452
+ badge,
453
+ badgeText,
454
+ });
455
+ }
456
+
457
+ // ---------------------------------------------------------------------------
458
+ // 5. BADGE RECIPE
459
+ // ---------------------------------------------------------------------------
460
+
461
+ export interface BadgeRecipeOptions {
462
+ variant?: StatusVariant;
463
+ styleVariant?: BadgeStyle;
464
+ size?: BadgeSize;
465
+ }
466
+
467
+ export interface BadgeRecipeResult extends ViewStyle {
468
+ container: ViewStyle;
469
+ text: TextStyle;
470
+ dot: ViewStyle;
471
+ }
472
+
473
+ /**
474
+ * Generates standard React Native styles for pill badges, indicators, and labels.
475
+ */
476
+ export function badgeRecipe(
477
+ themeInput?: ThemeInput,
478
+ variantOrOptions?: StatusVariant | BadgeRecipeOptions,
479
+ options?: BadgeRecipeOptions,
480
+ ): BadgeRecipeResult {
481
+ const colors = resolveThemeColors(themeInput);
482
+ const mergedOptions: BadgeRecipeOptions =
483
+ typeof variantOrOptions === "string"
484
+ ? { variant: variantOrOptions, ...options }
485
+ : (variantOrOptions ?? {});
486
+
487
+ const variant = mergedOptions.variant ?? "neutral";
488
+ const styleVariant = mergedOptions.styleVariant ?? "tinted";
489
+ const size = mergedOptions.size ?? "md";
490
+
491
+ const palette = getVariantPalette(variant, colors);
492
+ const solidColor = getStatusColor(variant, colors);
493
+
494
+ let bg = palette.bg;
495
+ let border = palette.border;
496
+ let textColor = palette.text;
497
+
498
+ if (styleVariant === "outline") {
499
+ bg = "transparent";
500
+ border = palette.border;
501
+ textColor = palette.text;
502
+ } else if (styleVariant === "solid") {
503
+ bg = solidColor;
504
+ border = "transparent";
505
+ textColor = getContrastColor(
506
+ solidColor,
507
+ colors.accentForeground || FALLBACK_ACCENT_FOREGROUND,
508
+ colors.foreground,
509
+ );
510
+ }
511
+
512
+ const fontSize = size === "sm" ? 10 : 11;
513
+ const lineHeight = size === "sm" ? 12 : 15;
514
+ const paddingVertical = size === "sm" ? 1 : 2;
515
+ const paddingHorizontal = size === "sm" ? 5 : 8;
516
+
517
+ const container: ViewStyle = {
518
+ flexDirection: "row",
519
+ alignItems: "center",
520
+ alignSelf: "flex-start",
521
+ backgroundColor: bg,
522
+ borderColor: border,
523
+ borderWidth: 1,
524
+ borderRadius: 9999,
525
+ paddingVertical,
526
+ paddingHorizontal,
527
+ gap: 4,
528
+ };
529
+
530
+ const text: TextStyle = {
531
+ color: textColor,
532
+ fontSize,
533
+ lineHeight,
534
+ fontWeight: "600",
535
+ };
536
+
537
+ const dot: ViewStyle = {
538
+ width: 6,
539
+ height: 6,
540
+ borderRadius: 3,
541
+ backgroundColor: textColor,
542
+ };
543
+
544
+ const subContainer = { ...container };
545
+ return Object.assign(container, {
546
+ container: subContainer,
547
+ text,
548
+ dot,
549
+ });
550
+ }