@xpufx/paseo-top 0.4.3 → 0.4.5

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 (130) hide show
  1. package/OVERVIEW.md +17 -0
  2. package/client/host-ui.tsx +184 -0
  3. package/client/multi-host-view.tsx +57 -43
  4. package/client/multi-host.ts +36 -13
  5. package/client/pill-labels.ts +124 -7
  6. package/client/pill.tsx +724 -669
  7. package/client/resources-query.ts +1 -1
  8. package/client/settings-events.ts +33 -0
  9. package/client/settings-ui.tsx +86 -9
  10. package/client/surface.tsx +310 -274
  11. package/client/telemetry-copy.ts +8 -2
  12. package/client/telemetry.tsx +372 -321
  13. package/client/turn-panel.tsx +126 -104
  14. package/client/vendor/paseo-plugin-helper/{host.ts → core/host.ts} +21 -61
  15. package/client/vendor/paseo-plugin-helper/core/index.ts +76 -0
  16. package/client/vendor/paseo-plugin-helper/core/plugin-load-count.ts +89 -0
  17. package/client/vendor/paseo-plugin-helper/{query-refresh.ts → core/query-refresh.ts} +2 -2
  18. package/client/vendor/paseo-plugin-helper/{query.ts → core/query.ts} +2 -2
  19. package/client/vendor/paseo-plugin-helper/{settings.ts → core/settings.ts} +1 -1
  20. package/client/vendor/paseo-plugin-helper/{shared-settings.ts → core/shared-settings.ts} +2 -2
  21. package/client/vendor/paseo-plugin-helper/{utils → lifecycle}/clipboard.ts +2 -2
  22. package/client/vendor/paseo-plugin-helper/lifecycle/flair.ts +26 -0
  23. package/client/vendor/paseo-plugin-helper/{theme/color-utils.ts → lifecycle/host-color.ts} +95 -38
  24. package/client/vendor/paseo-plugin-helper/lifecycle/host-theme.tsx +109 -0
  25. package/client/vendor/paseo-plugin-helper/lifecycle/index.ts +23 -0
  26. package/client/vendor/paseo-plugin-helper/lifecycle/multi-host.ts +66 -0
  27. package/client/vendor/paseo-plugin-helper/{panel.tsx → lifecycle/panel.tsx} +7 -7
  28. package/client/vendor/paseo-plugin-helper/{pill.tsx → lifecycle/pill.tsx} +118 -338
  29. package/client/vendor/paseo-plugin-helper/lifecycle/providers.tsx +46 -0
  30. package/client/vendor/paseo-plugin-helper/lifecycle/scroll-owner.ts +26 -0
  31. package/client/vendor/paseo-plugin-helper/lifecycle/surface.tsx +140 -0
  32. package/client/vendor/paseo-plugin-helper/ui/color.ts +1 -0
  33. package/client/vendor/paseo-plugin-helper/ui/content.tsx +1326 -0
  34. package/client/vendor/paseo-plugin-helper/ui/controls.tsx +1018 -0
  35. package/client/vendor/paseo-plugin-helper/{components/DataTable.tsx → ui/data.tsx} +55 -47
  36. package/client/vendor/paseo-plugin-helper/ui/index.ts +44 -0
  37. package/client/vendor/paseo-plugin-helper/ui/layout.tsx +263 -0
  38. package/client/vendor/paseo-plugin-helper/ui/modal.tsx +111 -0
  39. package/client/vendor/paseo-plugin-helper/ui/plugin-reload-banner.tsx +119 -0
  40. package/client/vendor/paseo-plugin-helper/{settings-screen.tsx → ui/settings-screen.tsx} +3 -3
  41. package/client/vendor/paseo-plugin-helper/ui/settings.ts +36 -0
  42. package/client/vendor/paseo-plugin-helper/ui/text.tsx +821 -0
  43. package/client/vendor/paseo-plugin-helper/ui/theme.tsx +1 -0
  44. package/client/vendor/permission-audit/client.tsx +706 -0
  45. package/client/vendor/permission-audit/index.tsx +1 -0
  46. package/index.client.tsx +58 -37
  47. package/index.server.ts +256 -227
  48. package/package.json +14 -2
  49. package/paseo-plugin.json +2 -0
  50. package/screenshots/1-paseo-top-modal-system.jpg +0 -0
  51. package/screenshots/2-paseo-top-modal-workspace.jpg +0 -0
  52. package/screenshots/3-paseo-top-modal-settings.jpg +0 -0
  53. package/screenshots/4-paseo-top-modal-about.jpg +0 -0
  54. package/screenshots/paseo-top-dashboard.jpg +0 -0
  55. package/screenshots/paseo-top-general-view.jpg +0 -0
  56. package/server/resources.ts +98 -0
  57. package/server/vendor/paseo-plugin-helper/custom-pills.ts +70 -0
  58. package/server/vendor/paseo-plugin-helper/index.ts +4 -0
  59. package/server/vendor/paseo-plugin-helper/logger.ts +5 -16
  60. package/server/vendor/paseo-plugin-helper/mcp-injection.ts +20 -2
  61. package/server/vendor/paseo-plugin-helper/plugins.ts +184 -19
  62. package/server/vendor/paseo-plugin-helper/rpc-guard.ts +2 -2
  63. package/server/vendor/paseo-plugin-helper/storage-guard.ts +294 -0
  64. package/server/vendor/paseo-plugin-helper/storage.ts +36 -2
  65. package/server/vendor/paseo-plugin-helper/telemetry.ts +486 -0
  66. package/server/vendor/paseo-plugin-helper/tickets.ts +1232 -0
  67. package/server/vendor/permission-audit/index.ts +1 -0
  68. package/server/vendor/permission-audit/server.ts +839 -0
  69. package/shared/helper-version.ts +20 -0
  70. package/shared/multi-host.ts +156 -155
  71. package/shared/resources.ts +146 -1
  72. package/shared/vendor/paseo-plugin-helper/async.ts +14 -7
  73. package/shared/vendor/paseo-plugin-helper/features.ts +244 -0
  74. package/shared/vendor/paseo-plugin-helper/forge.ts +27 -0
  75. package/shared/vendor/paseo-plugin-helper/index.ts +2 -0
  76. package/shared/vendor/paseo-plugin-helper/tickets.ts +1104 -0
  77. package/shared/vendor/permission-audit/filter.ts +61 -0
  78. package/shared/vendor/permission-audit/index.ts +2 -0
  79. package/shared/vendor/permission-audit/shared.ts +119 -0
  80. package/shared/version.ts +1 -1
  81. package/client/vendor/paseo-plugin-helper/components/AboutSection.tsx +0 -493
  82. package/client/vendor/paseo-plugin-helper/components/AttentionBeacon.tsx +0 -250
  83. package/client/vendor/paseo-plugin-helper/components/Badge.tsx +0 -156
  84. package/client/vendor/paseo-plugin-helper/components/Button.tsx +0 -184
  85. package/client/vendor/paseo-plugin-helper/components/Card.tsx +0 -225
  86. package/client/vendor/paseo-plugin-helper/components/CodeBlock.tsx +0 -196
  87. package/client/vendor/paseo-plugin-helper/components/Collapsible.tsx +0 -277
  88. package/client/vendor/paseo-plugin-helper/components/CommandBox.tsx +0 -172
  89. package/client/vendor/paseo-plugin-helper/components/CopyButton.tsx +0 -180
  90. package/client/vendor/paseo-plugin-helper/components/EmptyState.tsx +0 -97
  91. package/client/vendor/paseo-plugin-helper/components/HighlightedText.tsx +0 -70
  92. package/client/vendor/paseo-plugin-helper/components/InlineButton.tsx +0 -75
  93. package/client/vendor/paseo-plugin-helper/components/KeyValue.tsx +0 -446
  94. package/client/vendor/paseo-plugin-helper/components/MetricGauge.tsx +0 -247
  95. package/client/vendor/paseo-plugin-helper/components/ProgressBar.tsx +0 -117
  96. package/client/vendor/paseo-plugin-helper/components/Responsive.tsx +0 -53
  97. package/client/vendor/paseo-plugin-helper/components/SearchInput.tsx +0 -118
  98. package/client/vendor/paseo-plugin-helper/components/SectionHeader.tsx +0 -80
  99. package/client/vendor/paseo-plugin-helper/components/Select.tsx +0 -215
  100. package/client/vendor/paseo-plugin-helper/components/StatusDot.tsx +0 -80
  101. package/client/vendor/paseo-plugin-helper/components/Tabs.tsx +0 -319
  102. package/client/vendor/paseo-plugin-helper/components/TextInput.tsx +0 -150
  103. package/client/vendor/paseo-plugin-helper/components/Toggle.tsx +0 -163
  104. package/client/vendor/paseo-plugin-helper/components/TruncatedText.tsx +0 -157
  105. package/client/vendor/paseo-plugin-helper/components/index.ts +0 -25
  106. package/client/vendor/paseo-plugin-helper/custom-pills.tsx +0 -224
  107. package/client/vendor/paseo-plugin-helper/forge-icon.tsx +0 -79
  108. package/client/vendor/paseo-plugin-helper/icon.tsx +0 -39
  109. package/client/vendor/paseo-plugin-helper/index.ts +0 -29
  110. package/client/vendor/paseo-plugin-helper/layout/ActionBar.tsx +0 -49
  111. package/client/vendor/paseo-plugin-helper/layout/FormRow.tsx +0 -103
  112. package/client/vendor/paseo-plugin-helper/layout/Grid.tsx +0 -65
  113. package/client/vendor/paseo-plugin-helper/layout/ModalBody.tsx +0 -378
  114. package/client/vendor/paseo-plugin-helper/layout/ModalContent.tsx +0 -51
  115. package/client/vendor/paseo-plugin-helper/layout/Row.tsx +0 -39
  116. package/client/vendor/paseo-plugin-helper/layout/Stack.tsx +0 -39
  117. package/client/vendor/paseo-plugin-helper/layout/index.ts +0 -7
  118. package/client/vendor/paseo-plugin-helper/styles/index.ts +0 -1
  119. package/client/vendor/paseo-plugin-helper/styles/recipes.ts +0 -550
  120. package/client/vendor/paseo-plugin-helper/surface.tsx +0 -80
  121. package/client/vendor/paseo-plugin-helper/theme/flair.ts +0 -76
  122. package/client/vendor/paseo-plugin-helper/theme/host-variables.ts +0 -121
  123. package/client/vendor/paseo-plugin-helper/theme/index.ts +0 -7
  124. package/client/vendor/paseo-plugin-helper/theme/provider.tsx +0 -178
  125. package/client/vendor/paseo-plugin-helper/theme/responsive.ts +0 -213
  126. package/client/vendor/paseo-plugin-helper/theme/tokens.ts +0 -200
  127. package/client/vendor/paseo-plugin-helper/theme/useResponsive.ts +0 -57
  128. /package/client/vendor/paseo-plugin-helper/{command-center.ts → core/command-center.ts} +0 -0
  129. /package/client/vendor/paseo-plugin-helper/{snapshot.ts → core/snapshot.ts} +0 -0
  130. /package/client/vendor/paseo-plugin-helper/{utils → lifecycle}/haptics.ts +0 -0
@@ -0,0 +1,1326 @@
1
+ import React, { useEffect, useRef, useState, type ReactNode } from "react";
2
+ import {
3
+ Platform,
4
+ Pressable,
5
+ ScrollView as FallbackScrollView,
6
+ StyleSheet,
7
+ Text,
8
+ View,
9
+ type LayoutChangeEvent,
10
+ type ScrollView as ScrollViewInstance,
11
+ type StyleProp,
12
+ type TextStyle,
13
+ type ViewStyle,
14
+ } from "react-native";
15
+ import { getClientHost } from "../core/host";
16
+ import type { StatusVariant, ThemeColors } from "../../../../shared/vendor/paseo-plugin-helper/types";
17
+ import { resolveMetricStatus, type MetricThresholds } from "../../../../shared/vendor/paseo-plugin-helper/formatters";
18
+ import { useHostTheme } from "./theme";
19
+ import { alpha } from "./color";
20
+ import { spacing } from "./layout";
21
+ import { HostButton } from "./controls";
22
+
23
+ /**
24
+ * Paseo Plugin Helper — UI content adapters (`paseo-plugin-helper/ui`).
25
+ *
26
+ * Thin, host-delegating content adapters (Card, Tabs, Badge, …). Contract
27
+ * shared by everything here:
28
+ *
29
+ * - Colors come from the host `theme` prop via {@link useHostTheme} — never
30
+ * from DOM CSS variables, flair, or a density scale.
31
+ * - Scroll ownership stays with the host: these components render plain
32
+ * content and add no helper-owned scroller (see `ui/modal.tsx`).
33
+ * - No width caps, no `minWidth` floors, no design-system machinery. The host
34
+ * owns the design language; these adapters only compose its color tokens.
35
+ */
36
+
37
+ /** Surface variants shared by HostCard / HostCollapsible. */
38
+ export type HostSurfaceVariant = "flat" | "elevated" | "tinted";
39
+
40
+ /**
41
+ * Resolves a surface variant to host theme colors. Single source of truth for
42
+ * the "flat / elevated / tinted" vocabulary.
43
+ */
44
+ export function resolveHostSurface(
45
+ colors: ThemeColors,
46
+ variant: HostSurfaceVariant = "flat",
47
+ ): { backgroundColor: string; borderColor: string } {
48
+ switch (variant) {
49
+ case "elevated":
50
+ return { backgroundColor: colors.surface1, borderColor: colors.border };
51
+ case "tinted":
52
+ return {
53
+ backgroundColor: alpha(colors.accent, 0.04),
54
+ borderColor: alpha(colors.accent, 0.2),
55
+ };
56
+ case "flat":
57
+ default:
58
+ return { backgroundColor: colors.surface0, borderColor: colors.border };
59
+ }
60
+ }
61
+
62
+ // --- Card -----------------------------------------------------------------
63
+
64
+ export interface HostCardProps {
65
+ children: ReactNode;
66
+ variant?: HostSurfaceVariant;
67
+ style?: StyleProp<ViewStyle>;
68
+ noPadding?: boolean;
69
+ }
70
+
71
+ export interface HostCardHeaderProps {
72
+ title: string;
73
+ subtitle?: string;
74
+ value?: string | number | ReactNode;
75
+ badge?: ReactNode;
76
+ action?: ReactNode;
77
+ icon?: string;
78
+ style?: StyleProp<ViewStyle>;
79
+ titleStyle?: StyleProp<TextStyle>;
80
+ subtitleStyle?: StyleProp<TextStyle>;
81
+ }
82
+
83
+ /** Surface card. Thin: host theme colors + border + padding, nothing else. */
84
+ export function HostCard({ children, variant, style, noPadding = false }: HostCardProps) {
85
+ const { colors } = useHostTheme();
86
+ const surface = resolveHostSurface(colors, variant);
87
+ return (
88
+ <View
89
+ style={[
90
+ styles.card,
91
+ {
92
+ backgroundColor: surface.backgroundColor,
93
+ borderColor: surface.borderColor,
94
+ padding: noPadding ? 0 : spacing.md,
95
+ },
96
+ style,
97
+ ]}
98
+ >
99
+ {children}
100
+ </View>
101
+ );
102
+ }
103
+
104
+ /** Card header row: title/subtitle on the left, value/badge/action on the right. */
105
+ export function HostCardHeader({
106
+ title,
107
+ subtitle,
108
+ value,
109
+ badge,
110
+ action,
111
+ icon,
112
+ style,
113
+ titleStyle,
114
+ subtitleStyle,
115
+ }: HostCardHeaderProps) {
116
+ const { Icon } = getClientHost();
117
+ const { colors } = useHostTheme();
118
+ return (
119
+ <View style={[styles.cardHeader, style]}>
120
+ <View style={styles.cardHeaderLeft}>
121
+ {icon ? <Icon name={icon} size={15} color={colors.foregroundMuted} /> : null}
122
+ <View style={styles.titleColumn}>
123
+ <Text
124
+ style={[styles.cardHeaderTitle, { color: colors.foreground }, titleStyle]}
125
+ numberOfLines={1}
126
+ >
127
+ {title}
128
+ </Text>
129
+ {subtitle ? (
130
+ <Text
131
+ style={[styles.cardHeaderSubtitle, { color: colors.foregroundMuted }, subtitleStyle]}
132
+ numberOfLines={1}
133
+ >
134
+ {subtitle}
135
+ </Text>
136
+ ) : null}
137
+ </View>
138
+ </View>
139
+ <View style={styles.cardHeaderRight}>
140
+ {badge ? <View style={{ marginRight: spacing.xs }}>{badge}</View> : null}
141
+ {typeof value === "string" || typeof value === "number" ? (
142
+ <Text style={[styles.cardHeaderValue, { color: colors.foreground }]}>{value}</Text>
143
+ ) : (
144
+ value
145
+ )}
146
+ {action}
147
+ </View>
148
+ </View>
149
+ );
150
+ }
151
+
152
+ HostCard.Header = HostCardHeader;
153
+
154
+ // --- Section header -------------------------------------------------------
155
+
156
+ export interface HostSectionHeaderProps {
157
+ title: string;
158
+ count?: number;
159
+ badgeVariant?: StatusVariant;
160
+ style?: StyleProp<ViewStyle>;
161
+ textStyle?: StyleProp<TextStyle>;
162
+ }
163
+
164
+ /** Section title with an optional count badge. */
165
+ export function HostSectionHeader({
166
+ title,
167
+ count,
168
+ badgeVariant,
169
+ style,
170
+ textStyle,
171
+ }: HostSectionHeaderProps) {
172
+ const { colors } = useHostTheme();
173
+ return (
174
+ <View style={[styles.sectionHeader, style]}>
175
+ <Text
176
+ numberOfLines={1}
177
+ ellipsizeMode="tail"
178
+ style={[styles.sectionTitle, { color: colors.foregroundMuted }, textStyle]}
179
+ >
180
+ {title}
181
+ </Text>
182
+ {count !== undefined ? (
183
+ <HostBadge
184
+ label={String(count)}
185
+ variant={badgeVariant ?? (count > 0 ? "warning" : "neutral")}
186
+ styleVariant={count > 0 ? "solid" : "tinted"}
187
+ size="sm"
188
+ />
189
+ ) : null}
190
+ </View>
191
+ );
192
+ }
193
+
194
+ // --- Badge ----------------------------------------------------------------
195
+
196
+ export type HostBadgeStyle = "tinted" | "outline" | "solid";
197
+ export type HostBadgeSize = "sm" | "md";
198
+
199
+ export interface HostBadgeProps {
200
+ label: string;
201
+ variant?: StatusVariant;
202
+ styleVariant?: HostBadgeStyle;
203
+ size?: HostBadgeSize;
204
+ icon?: string | ReactNode;
205
+ dot?: boolean;
206
+ style?: StyleProp<ViewStyle>;
207
+ textStyle?: StyleProp<TextStyle>;
208
+ }
209
+
210
+ /** Status pill. Colors derive from the host theme via `getVariantPalette`. */
211
+ export function HostBadge({
212
+ label,
213
+ variant = "neutral",
214
+ styleVariant = "tinted",
215
+ size = "md",
216
+ icon,
217
+ dot = false,
218
+ style,
219
+ textStyle,
220
+ }: HostBadgeProps) {
221
+ const { Icon } = getClientHost();
222
+ const { colors, getVariantPalette, getStatusColor } = useHostTheme();
223
+
224
+ const fontSize = size === "sm" ? 10 : 11;
225
+ const lineHeight = size === "sm" ? 12 : 15;
226
+ const paddingVertical = size === "sm" ? 1 : 2;
227
+ const paddingHorizontal = size === "sm" ? 5 : 8;
228
+ const iconSize = size === "sm" ? 10 : 11;
229
+
230
+ const palette = getVariantPalette(variant);
231
+ const solidColor = getStatusColor(variant);
232
+
233
+ let bg = palette.bg;
234
+ let border = palette.border;
235
+ let textColor = palette.text;
236
+
237
+ if (styleVariant === "outline") {
238
+ bg = "transparent";
239
+ border = palette.border;
240
+ textColor = palette.text;
241
+ } else if (styleVariant === "solid") {
242
+ bg = solidColor;
243
+ border = "transparent";
244
+ textColor = colors.accentForeground;
245
+ }
246
+
247
+ const renderIcon = () => {
248
+ if (dot) {
249
+ return <View style={[styles.dot, { backgroundColor: textColor }]} />;
250
+ }
251
+ if (!icon) return null;
252
+ if (typeof icon === "string") {
253
+ return <Icon name={icon} size={iconSize} color={textColor} />;
254
+ }
255
+ return icon;
256
+ };
257
+
258
+ return (
259
+ <View
260
+ style={[
261
+ styles.badge,
262
+ {
263
+ backgroundColor: bg,
264
+ borderColor: border,
265
+ paddingVertical,
266
+ paddingHorizontal,
267
+ },
268
+ style,
269
+ ]}
270
+ >
271
+ {renderIcon()}
272
+ <Text
273
+ accessibilityLabel={label}
274
+ numberOfLines={1}
275
+ ellipsizeMode="tail"
276
+ style={[
277
+ styles.badgeText,
278
+ {
279
+ color: textColor,
280
+ fontSize,
281
+ lineHeight,
282
+ },
283
+ textStyle,
284
+ ]}
285
+ >
286
+ {label}
287
+ </Text>
288
+ </View>
289
+ );
290
+ }
291
+
292
+ // --- Status dot -----------------------------------------------------------
293
+
294
+ export interface HostStatusDotProps {
295
+ variant?: StatusVariant;
296
+ size?: "sm" | "md" | "lg";
297
+ pulse?: boolean;
298
+ style?: StyleProp<ViewStyle>;
299
+ }
300
+
301
+ /** Small status indicator dot. */
302
+ export function HostStatusDot({
303
+ variant = "neutral",
304
+ size = "md",
305
+ pulse = false,
306
+ style,
307
+ }: HostStatusDotProps) {
308
+ const { getStatusColor } = useHostTheme();
309
+ const color = getStatusColor(variant);
310
+ const dimension = size === "sm" ? 6 : size === "lg" ? 10 : 8;
311
+ return (
312
+ <View
313
+ style={[
314
+ styles.statusDot,
315
+ {
316
+ width: dimension,
317
+ height: dimension,
318
+ backgroundColor: color,
319
+ opacity: pulse ? 0.6 : 1,
320
+ },
321
+ style,
322
+ ]}
323
+ />
324
+ );
325
+ }
326
+
327
+ // --- Progress bar ---------------------------------------------------------
328
+
329
+ export interface HostProgressBarProps {
330
+ value: number; // 0 to 100
331
+ color?: string;
332
+ autoStatusColor?: boolean;
333
+ thresholds?: MetricThresholds;
334
+ label?: string;
335
+ showValueText?: boolean;
336
+ height?: number;
337
+ style?: StyleProp<ViewStyle>;
338
+ }
339
+
340
+ /** Horizontal progress bar with optional status coloring. */
341
+ export function HostProgressBar({
342
+ value,
343
+ color,
344
+ autoStatusColor = true,
345
+ thresholds,
346
+ label,
347
+ showValueText = false,
348
+ height = 8,
349
+ style,
350
+ }: HostProgressBarProps) {
351
+ const { colors } = useHostTheme();
352
+ const clamped = Math.max(0, Math.min(100, Number.isFinite(value) ? value : 0));
353
+
354
+ let barColor = color || colors.accent;
355
+ if (!color && autoStatusColor) {
356
+ const status = resolveMetricStatus(clamped, thresholds);
357
+ if (status === "danger") {
358
+ barColor = colors.statusDanger;
359
+ } else if (status === "warning") {
360
+ barColor = colors.statusWarning;
361
+ } else {
362
+ barColor = colors.statusSuccess;
363
+ }
364
+ }
365
+
366
+ return (
367
+ <View style={[styles.progressContainer, style]}>
368
+ {(label || showValueText) && (
369
+ <View style={styles.progressLabelRow}>
370
+ {label ? (
371
+ <Text style={[styles.progressLabel, { color: colors.foregroundMuted }]}>{label}</Text>
372
+ ) : null}
373
+ {showValueText ? (
374
+ <Text style={[styles.progressValue, { color: colors.foreground }]}>
375
+ {Math.round(clamped)}%
376
+ </Text>
377
+ ) : null}
378
+ </View>
379
+ )}
380
+ <View
381
+ style={[
382
+ styles.progressTrack,
383
+ {
384
+ backgroundColor: colors.surface2,
385
+ height,
386
+ borderRadius: height / 2,
387
+ },
388
+ ]}
389
+ >
390
+ <View
391
+ style={[
392
+ styles.progressFill,
393
+ {
394
+ width: `${clamped}%`,
395
+ backgroundColor: barColor,
396
+ borderRadius: height / 2,
397
+ },
398
+ ]}
399
+ />
400
+ </View>
401
+ </View>
402
+ );
403
+ }
404
+
405
+ // --- Metric gauge ---------------------------------------------------------
406
+
407
+ export interface HostMetricGaugeProps {
408
+ value: number; // 0 - 100
409
+ size?: number;
410
+ strokeWidth?: number;
411
+ thresholds?: MetricThresholds;
412
+ color?: string;
413
+ autoStatusColor?: boolean;
414
+ label?: string;
415
+ showPercent?: boolean;
416
+ centerSlot?: ReactNode;
417
+ style?: StyleProp<ViewStyle>;
418
+ }
419
+
420
+ /** Radial metric gauge. Web paints a conic-gradient ring; native falls back to a track ring. */
421
+ export function HostMetricGauge({
422
+ value,
423
+ size = 76,
424
+ strokeWidth = 7,
425
+ thresholds,
426
+ color,
427
+ autoStatusColor = true,
428
+ label,
429
+ showPercent = true,
430
+ centerSlot,
431
+ style,
432
+ }: HostMetricGaugeProps) {
433
+ const { colors } = useHostTheme();
434
+ const clamped = Math.max(0, Math.min(100, Number.isFinite(value) ? value : 0));
435
+
436
+ let gaugeColor = color || colors.accent;
437
+ if (!color && autoStatusColor) {
438
+ const status = resolveMetricStatus(clamped, thresholds);
439
+ if (status === "danger") {
440
+ gaugeColor = colors.statusDanger;
441
+ } else if (status === "warning") {
442
+ gaugeColor = colors.statusWarning;
443
+ } else {
444
+ gaugeColor = colors.statusSuccess;
445
+ }
446
+ }
447
+
448
+ const radius = size / 2;
449
+ const innerSize = Math.max(0, size - strokeWidth * 2);
450
+ const innerRadius = innerSize / 2;
451
+ const trackColor = colors.surface2;
452
+
453
+ if (Platform.OS === "web") {
454
+ const webBackground = `conic-gradient(${gaugeColor} 0% ${clamped}%, ${trackColor} ${clamped}% 100%)`;
455
+ return (
456
+ <View style={[styles.gaugeWrapper, style]}>
457
+ <View
458
+ style={[
459
+ styles.gaugeBox,
460
+ { width: size, height: size, borderRadius: radius },
461
+ { background: webBackground } as any,
462
+ ]}
463
+ >
464
+ <View
465
+ style={[
466
+ styles.gaugeCenterHole,
467
+ {
468
+ width: innerSize,
469
+ height: innerSize,
470
+ borderRadius: innerRadius,
471
+ backgroundColor: colors.surface0,
472
+ },
473
+ ]}
474
+ >
475
+ {centerSlot ? (
476
+ centerSlot
477
+ ) : showPercent ? (
478
+ <Text style={[styles.gaugePercent, { color: colors.foreground }]}>
479
+ {Math.round(clamped)}%
480
+ </Text>
481
+ ) : null}
482
+ </View>
483
+ </View>
484
+ {label ? (
485
+ <Text style={[styles.gaugeLabel, { color: colors.foregroundMuted }]}>{label}</Text>
486
+ ) : null}
487
+ </View>
488
+ );
489
+ }
490
+
491
+ return (
492
+ <View style={[styles.gaugeWrapper, style]}>
493
+ <View
494
+ style={[
495
+ styles.gaugeBox,
496
+ { width: size, height: size, borderRadius: radius },
497
+ ]}
498
+ >
499
+ <View
500
+ style={[
501
+ StyleSheet.absoluteFillObject,
502
+ {
503
+ borderRadius: radius,
504
+ borderWidth: strokeWidth,
505
+ borderColor: trackColor,
506
+ },
507
+ ]}
508
+ />
509
+ <View
510
+ style={[
511
+ styles.gaugeFill,
512
+ {
513
+ width: innerSize,
514
+ height: innerSize,
515
+ borderRadius: innerRadius,
516
+ backgroundColor: gaugeColor,
517
+ opacity: 0.25 + 0.75 * (clamped / 100),
518
+ },
519
+ ]}
520
+ />
521
+ </View>
522
+ {label ? (
523
+ <Text style={[styles.gaugeLabel, { color: colors.foregroundMuted }]}>{label}</Text>
524
+ ) : null}
525
+ </View>
526
+ );
527
+ }
528
+
529
+ // --- Key-value ------------------------------------------------------------
530
+
531
+ export type HostKeyValueTruncateMode = "end" | "middle" | "path";
532
+
533
+ export interface HostKeyValueProps {
534
+ label: string;
535
+ value: string | number | null | undefined;
536
+ subValue?: string;
537
+ mono?: boolean;
538
+ copyable?: boolean;
539
+ truncate?: boolean | HostKeyValueTruncateMode;
540
+ truncateMaxLength?: number;
541
+ layout?: "stacked" | "inline";
542
+ style?: StyleProp<ViewStyle>;
543
+ labelStyle?: StyleProp<TextStyle>;
544
+ valueStyle?: StyleProp<TextStyle>;
545
+ }
546
+
547
+ /** Label/value row with optional copy-to-clipboard (host `copyText`). */
548
+ export function HostKeyValue({
549
+ label,
550
+ value,
551
+ subValue,
552
+ mono = false,
553
+ copyable = false,
554
+ truncate: truncateProp = false,
555
+ truncateMaxLength = 32,
556
+ layout = "stacked",
557
+ style,
558
+ labelStyle,
559
+ valueStyle,
560
+ }: HostKeyValueProps) {
561
+ const { Icon, useToast } = getClientHost();
562
+ const { colors } = useHostTheme();
563
+ const toast = useToast();
564
+ const [copied, setCopied] = useState(false);
565
+
566
+ const rawString = value === null || value === undefined ? "" : String(value);
567
+
568
+ let displayValue = rawString || "-";
569
+ if (truncateProp && rawString.length > truncateMaxLength) {
570
+ const mode: HostKeyValueTruncateMode =
571
+ typeof truncateProp === "string" ? truncateProp : "middle";
572
+ if (mode === "path") {
573
+ displayValue = rawString.length > truncateMaxLength
574
+ ? `…${rawString.slice(-truncateMaxLength)}`
575
+ : rawString;
576
+ } else if (mode === "end") {
577
+ displayValue = `${rawString.slice(0, truncateMaxLength)}…`;
578
+ } else {
579
+ const half = Math.floor(truncateMaxLength / 2);
580
+ displayValue = `${rawString.slice(0, half)}…${rawString.slice(-half)}`;
581
+ }
582
+ }
583
+
584
+ const handleCopy = async () => {
585
+ if (!copyable || !rawString) return;
586
+ const copyText = getClientHost().copyText;
587
+ if (!copyText) return;
588
+ try {
589
+ await copyText(rawString);
590
+ toast?.copied?.(label);
591
+ setCopied(true);
592
+ setTimeout(() => setCopied(false), 2000);
593
+ } catch {
594
+ toast?.error?.("Copy failed");
595
+ }
596
+ };
597
+
598
+ const copyButton = copyable && value ? (
599
+ <Pressable
600
+ onPress={handleCopy}
601
+ hitSlop={8}
602
+ style={styles.kvCopyBtn}
603
+ accessibilityRole="button"
604
+ accessibilityLabel={`Copy ${label}`}
605
+ >
606
+ <Icon
607
+ name={copied ? "Check" : "Copy"}
608
+ size={13}
609
+ color={copied ? colors.statusSuccess : colors.foregroundMuted}
610
+ />
611
+ </Pressable>
612
+ ) : null;
613
+
614
+ const valueText = (
615
+ <Text
616
+ selectable
617
+ numberOfLines={1}
618
+ ellipsizeMode="middle"
619
+ style={[
620
+ styles.kvValue,
621
+ {
622
+ color: colors.foreground,
623
+ fontFamily: mono ? "monospace" : undefined,
624
+ },
625
+ valueStyle,
626
+ ]}
627
+ >
628
+ {displayValue}
629
+ </Text>
630
+ );
631
+
632
+ if (layout === "inline") {
633
+ return (
634
+ <View style={[styles.kvContainer, styles.kvInline, style]}>
635
+ <Text
636
+ numberOfLines={1}
637
+ style={[styles.kvInlineLabel, { color: colors.foregroundMuted }, labelStyle]}
638
+ >
639
+ {label}
640
+ </Text>
641
+ <View style={styles.kvInlineValue}>
642
+ {valueText}
643
+ {copyButton}
644
+ </View>
645
+ </View>
646
+ );
647
+ }
648
+
649
+ return (
650
+ <View style={[styles.kvContainer, style]}>
651
+ <Text
652
+ numberOfLines={1}
653
+ style={[styles.kvLabel, { color: colors.foregroundMuted }, labelStyle]}
654
+ >
655
+ {label}
656
+ </Text>
657
+ <View style={styles.kvValueRow}>
658
+ {valueText}
659
+ {copyButton}
660
+ </View>
661
+ {subValue ? (
662
+ <Text numberOfLines={1} style={[styles.kvSubValue, { color: colors.foregroundMuted }]}>
663
+ {subValue}
664
+ </Text>
665
+ ) : null}
666
+ </View>
667
+ );
668
+ }
669
+
670
+ // --- Empty state ----------------------------------------------------------
671
+
672
+ export interface HostEmptyStateProps {
673
+ icon?: string | ReactNode;
674
+ title: string;
675
+ description?: string;
676
+ actionLabel?: string;
677
+ onAction?: () => void;
678
+ style?: StyleProp<ViewStyle>;
679
+ }
680
+
681
+ /** Centered empty-state block. */
682
+ export function HostEmptyState({
683
+ icon = "Inbox",
684
+ title,
685
+ description,
686
+ actionLabel,
687
+ onAction,
688
+ style,
689
+ }: HostEmptyStateProps) {
690
+ const { Icon } = getClientHost();
691
+ const { colors } = useHostTheme();
692
+ return (
693
+ <View style={[styles.emptyContainer, style]}>
694
+ {icon ? (
695
+ typeof icon === "string" ? (
696
+ <View style={[styles.emptyIconWrapper, { backgroundColor: colors.surface1 }]}>
697
+ <Icon name={icon} size={32} color={colors.foregroundMuted} />
698
+ </View>
699
+ ) : (
700
+ icon
701
+ )
702
+ ) : null}
703
+ <Text style={[styles.emptyTitle, { color: colors.foreground }]}>{title}</Text>
704
+ {description ? (
705
+ <Text style={[styles.emptyDescription, { color: colors.foregroundMuted }]}>
706
+ {description}
707
+ </Text>
708
+ ) : null}
709
+ {actionLabel && onAction ? (
710
+ <View style={styles.emptyActionRow}>
711
+ <HostButton label={actionLabel} onPress={onAction} variant="secondary" size="sm" />
712
+ </View>
713
+ ) : null}
714
+ </View>
715
+ );
716
+ }
717
+
718
+ // --- Collapsible ----------------------------------------------------------
719
+
720
+ export interface HostCollapsibleProps {
721
+ title?: string | ReactNode;
722
+ subtitle?: string | ReactNode;
723
+ children: ReactNode;
724
+ initiallyExpanded?: boolean;
725
+ isExpanded?: boolean;
726
+ onToggle?: (expanded: boolean) => void;
727
+ badge?: ReactNode;
728
+ headerRight?: ReactNode;
729
+ icon?: string;
730
+ style?: StyleProp<ViewStyle>;
731
+ headerStyle?: StyleProp<ViewStyle>;
732
+ contentStyle?: StyleProp<ViewStyle>;
733
+ variant?: HostSurfaceVariant;
734
+ }
735
+
736
+ /** Expandable section with a host-themed header row. */
737
+ export function HostCollapsible({
738
+ title,
739
+ subtitle,
740
+ children,
741
+ initiallyExpanded = false,
742
+ isExpanded: controlledExpanded,
743
+ onToggle,
744
+ badge,
745
+ headerRight,
746
+ icon,
747
+ style,
748
+ headerStyle,
749
+ contentStyle,
750
+ variant = "flat",
751
+ }: HostCollapsibleProps) {
752
+ const { Icon } = getClientHost();
753
+ const { colors } = useHostTheme();
754
+ const [internalExpanded, setInternalExpanded] = useState(initiallyExpanded);
755
+
756
+ const isExpanded = controlledExpanded !== undefined ? controlledExpanded : internalExpanded;
757
+ const surface = resolveHostSurface(colors, variant);
758
+
759
+ const handlePress = () => {
760
+ const next = !isExpanded;
761
+ if (controlledExpanded === undefined) {
762
+ setInternalExpanded(next);
763
+ }
764
+ onToggle?.(next);
765
+ };
766
+
767
+ return (
768
+ <View
769
+ style={[
770
+ styles.collapsibleContainer,
771
+ {
772
+ borderColor: surface.borderColor,
773
+ backgroundColor: surface.backgroundColor,
774
+ },
775
+ style,
776
+ ]}
777
+ >
778
+ <Pressable
779
+ onPress={handlePress}
780
+ accessibilityRole="button"
781
+ accessibilityState={{ expanded: isExpanded }}
782
+ style={({ pressed }) => [
783
+ styles.collapsibleHeader,
784
+ {
785
+ backgroundColor: pressed ? colors.surface1 : "transparent",
786
+ },
787
+ headerStyle,
788
+ ]}
789
+ >
790
+ {icon ? (
791
+ <Icon name={icon} size={14} color={colors.foregroundMuted} />
792
+ ) : null}
793
+ <View style={styles.collapsibleTitleColumn}>
794
+ {title ? (
795
+ <Text style={[styles.collapsibleTitle, { color: colors.foreground }]}>
796
+ {typeof title === "string" ? title : null}
797
+ </Text>
798
+ ) : null}
799
+ {typeof title !== "string" ? title : null}
800
+ {subtitle ? (
801
+ <Text style={[styles.collapsibleSubtitle, { color: colors.foregroundMuted }]}>
802
+ {typeof subtitle === "string" ? subtitle : null}
803
+ </Text>
804
+ ) : null}
805
+ {typeof subtitle !== "string" ? subtitle : null}
806
+ </View>
807
+ {badge}
808
+ {headerRight}
809
+ <Icon
810
+ name={isExpanded ? "ChevronDown" : "ChevronRight"}
811
+ size={14}
812
+ color={colors.foregroundMuted}
813
+ />
814
+ </Pressable>
815
+ {isExpanded ? (
816
+ <View style={[styles.collapsibleContent, contentStyle]}>{children}</View>
817
+ ) : null}
818
+ </View>
819
+ );
820
+ }
821
+
822
+ // --- Tabs -----------------------------------------------------------------
823
+
824
+ export interface HostTabItem {
825
+ id: string;
826
+ label: string;
827
+ shortLabel?: string;
828
+ icon?: string;
829
+ badge?: string | number;
830
+ }
831
+
832
+ export interface HostTabsProps {
833
+ tabs: HostTabItem[];
834
+ activeTab: string;
835
+ onTabChange: (tabId: string) => void;
836
+ mode?: "auto" | "fit" | "scroll";
837
+ style?: StyleProp<ViewStyle>;
838
+ }
839
+
840
+ interface WheelScrollEvent {
841
+ deltaX?: number;
842
+ deltaY?: number;
843
+ preventDefault(): void;
844
+ }
845
+
846
+ interface WheelScrollNode {
847
+ scrollLeft: number;
848
+ clientWidth: number;
849
+ scrollWidth: number;
850
+ addEventListener(type: "wheel", listener: (event: WheelScrollEvent) => void, options?: { passive?: boolean }): void;
851
+ removeEventListener(type: "wheel", listener: (event: WheelScrollEvent) => void): void;
852
+ }
853
+
854
+ interface WheelScrollInstance {
855
+ getScrollableNode?(): WheelScrollNode | null;
856
+ }
857
+
858
+ /**
859
+ * Tab strip. `mode="auto"` fits the track when there are few tabs and scrolls
860
+ * otherwise; the strip itself is a plain horizontal scroller (never the host
861
+ * sheet-gesture scroller, #219).
862
+ */
863
+ export function HostTabs({
864
+ tabs,
865
+ activeTab,
866
+ onTabChange,
867
+ mode = "auto",
868
+ style,
869
+ }: HostTabsProps) {
870
+ const { Icon } = getClientHost();
871
+ const { colors, alpha: alphaColor } = useHostTheme();
872
+ const scrollRef = useRef<ScrollViewInstance>(null);
873
+ const tabLayouts = useRef<Record<string, { x: number; width: number }>>({});
874
+ const [viewportWidth, setViewportWidth] = useState<number>(0);
875
+
876
+ const shouldFit = mode === "fit" || (mode === "auto" && tabs.length <= 4);
877
+
878
+ const prevActiveTab = useRef<string>(activeTab);
879
+ useEffect(() => {
880
+ if (!shouldFit && scrollRef.current && tabLayouts.current[activeTab] && viewportWidth > 0) {
881
+ if (prevActiveTab.current !== activeTab) {
882
+ prevActiveTab.current = activeTab;
883
+ const { x, width } = tabLayouts.current[activeTab];
884
+ const targetX = Math.max(0, x - (viewportWidth - width) / 2);
885
+ scrollRef.current.scrollTo({
886
+ x: targetX,
887
+ animated: true,
888
+ });
889
+ }
890
+ }
891
+ }, [activeTab, shouldFit, viewportWidth]);
892
+
893
+ const handleTabLayout = (tabId: string, event: LayoutChangeEvent) => {
894
+ const { x, width } = event.nativeEvent.layout;
895
+ tabLayouts.current[tabId] = { x, width };
896
+ };
897
+
898
+ const handleContainerLayout = (event: LayoutChangeEvent) => {
899
+ const width = event.nativeEvent.layout.width;
900
+ setViewportWidth(width);
901
+ };
902
+
903
+ // Translate vertical mouse-wheel motion into horizontal tab-strip scrolling
904
+ // on web, while allowing the surrounding modal to scroll at either edge.
905
+ useEffect(() => {
906
+ if (shouldFit || Platform.OS !== "web") return;
907
+ const instance = scrollRef.current as unknown as WheelScrollInstance | null;
908
+ const node = instance?.getScrollableNode?.() ?? (instance as unknown as WheelScrollNode | null);
909
+ if (!node || typeof node.addEventListener !== "function") return;
910
+ const onWheel = (event: WheelScrollEvent) => {
911
+ const deltaX = event.deltaX ?? 0;
912
+ const deltaY = event.deltaY ?? 0;
913
+ if (Math.abs(deltaY) <= Math.abs(deltaX)) return;
914
+ const max = node.scrollWidth - node.clientWidth;
915
+ const next = Math.min(Math.max(0, node.scrollLeft + deltaY), max);
916
+ if (next === node.scrollLeft) return;
917
+ event.preventDefault();
918
+ node.scrollLeft = next;
919
+ };
920
+ node.addEventListener("wheel", onWheel, { passive: false });
921
+ return () => node.removeEventListener("wheel", onWheel);
922
+ }, [shouldFit]);
923
+
924
+ const renderTab = (tab: HostTabItem) => {
925
+ const isActive = tab.id === activeTab;
926
+ return (
927
+ <Pressable
928
+ key={tab.id}
929
+ onPress={() => {
930
+ onTabChange(tab.id);
931
+ }}
932
+ onLayout={(e) => handleTabLayout(tab.id, e)}
933
+ accessibilityRole="tab"
934
+ accessibilityState={{ selected: isActive }}
935
+ style={({ pressed }) => [
936
+ styles.tab,
937
+ shouldFit ? styles.tabFit : styles.tabScroll,
938
+ {
939
+ backgroundColor: isActive
940
+ ? colors.surface2
941
+ : pressed
942
+ ? alphaColor(colors.surface2, 0.5)
943
+ : "transparent",
944
+ paddingHorizontal: shouldFit ? spacing.sm : spacing.md,
945
+ paddingVertical: spacing.xs,
946
+ },
947
+ ]}
948
+ >
949
+ {tab.icon ? (
950
+ <Icon
951
+ name={tab.icon}
952
+ size={13}
953
+ color={isActive ? colors.foreground : colors.foregroundMuted}
954
+ />
955
+ ) : null}
956
+ <Text
957
+ numberOfLines={1}
958
+ style={[
959
+ styles.tabText,
960
+ {
961
+ color: isActive ? colors.foreground : colors.foregroundMuted,
962
+ fontWeight: isActive ? "600" : "500",
963
+ },
964
+ ]}
965
+ >
966
+ {tab.label}
967
+ </Text>
968
+ {tab.badge !== undefined ? (
969
+ <View
970
+ style={[
971
+ styles.tabBadge,
972
+ {
973
+ backgroundColor: isActive ? colors.accent : alphaColor(colors.foregroundMuted, 0.2),
974
+ },
975
+ ]}
976
+ >
977
+ <Text
978
+ style={[
979
+ styles.tabBadgeText,
980
+ {
981
+ color: isActive ? colors.accentForeground : colors.foregroundMuted,
982
+ },
983
+ ]}
984
+ >
985
+ {tab.badge}
986
+ </Text>
987
+ </View>
988
+ ) : null}
989
+ </Pressable>
990
+ );
991
+ };
992
+
993
+ if (shouldFit) {
994
+ return (
995
+ <View
996
+ style={[
997
+ styles.tabsFrame,
998
+ {
999
+ backgroundColor: colors.surface1,
1000
+ borderColor: colors.border,
1001
+ },
1002
+ style,
1003
+ ]}
1004
+ >
1005
+ <View style={styles.tabsTrackFit}>{tabs.map((tab) => renderTab(tab))}</View>
1006
+ </View>
1007
+ );
1008
+ }
1009
+
1010
+ return (
1011
+ <View
1012
+ onLayout={handleContainerLayout}
1013
+ style={[
1014
+ styles.tabsFrame,
1015
+ {
1016
+ backgroundColor: colors.surface1,
1017
+ borderColor: colors.border,
1018
+ },
1019
+ style,
1020
+ ]}
1021
+ >
1022
+ <FallbackScrollView
1023
+ ref={scrollRef}
1024
+ horizontal
1025
+ nestedScrollEnabled={true}
1026
+ directionalLockEnabled={true}
1027
+ keyboardShouldPersistTaps="handled"
1028
+ showsHorizontalScrollIndicator={true}
1029
+ style={styles.tabsScrollView}
1030
+ contentContainerStyle={styles.tabsScrollContent}
1031
+ >
1032
+ {tabs.map((tab) => renderTab(tab))}
1033
+ </FallbackScrollView>
1034
+ </View>
1035
+ );
1036
+ }
1037
+
1038
+ const styles = StyleSheet.create({
1039
+ card: {
1040
+ overflow: "hidden",
1041
+ width: "100%",
1042
+ borderWidth: 1,
1043
+ borderRadius: 12,
1044
+ gap: spacing.sm,
1045
+ },
1046
+ cardHeader: {
1047
+ flexDirection: "row",
1048
+ alignItems: "center",
1049
+ justifyContent: "space-between",
1050
+ flexWrap: "wrap",
1051
+ gap: spacing.sm,
1052
+ width: "100%",
1053
+ },
1054
+ cardHeaderLeft: {
1055
+ flexDirection: "row",
1056
+ alignItems: "center",
1057
+ gap: spacing.sm,
1058
+ flexGrow: 1,
1059
+ flexShrink: 1,
1060
+ flexBasis: 0,
1061
+ minWidth: 0,
1062
+ },
1063
+ titleColumn: {
1064
+ gap: 1,
1065
+ flexShrink: 1,
1066
+ },
1067
+ cardHeaderTitle: {
1068
+ fontWeight: "600",
1069
+ fontSize: 13,
1070
+ },
1071
+ cardHeaderSubtitle: {
1072
+ fontWeight: "400",
1073
+ fontSize: 11,
1074
+ },
1075
+ cardHeaderRight: {
1076
+ flexDirection: "row",
1077
+ alignItems: "center",
1078
+ gap: spacing.xs,
1079
+ flexShrink: 0,
1080
+ },
1081
+ cardHeaderValue: {
1082
+ fontWeight: "600",
1083
+ fontSize: 13,
1084
+ },
1085
+ sectionHeader: {
1086
+ flexDirection: "row",
1087
+ alignItems: "center",
1088
+ justifyContent: "space-between",
1089
+ gap: spacing.sm,
1090
+ width: "100%",
1091
+ },
1092
+ sectionTitle: {
1093
+ fontSize: 12,
1094
+ fontWeight: "600",
1095
+ letterSpacing: 0.4,
1096
+ textTransform: "uppercase",
1097
+ flexShrink: 1,
1098
+ },
1099
+ badge: {
1100
+ flexDirection: "row",
1101
+ alignItems: "center",
1102
+ alignSelf: "flex-start",
1103
+ borderWidth: 1,
1104
+ gap: 4,
1105
+ flexShrink: 1,
1106
+ maxWidth: "100%",
1107
+ },
1108
+ badgeText: {
1109
+ fontWeight: "600",
1110
+ flexShrink: 1,
1111
+ },
1112
+ dot: {
1113
+ width: 6,
1114
+ height: 6,
1115
+ borderRadius: 3,
1116
+ },
1117
+ statusDot: {
1118
+ borderRadius: 9999,
1119
+ },
1120
+ progressContainer: {
1121
+ gap: 4,
1122
+ width: "100%",
1123
+ },
1124
+ progressLabelRow: {
1125
+ flexDirection: "row",
1126
+ alignItems: "center",
1127
+ justifyContent: "space-between",
1128
+ },
1129
+ progressLabel: {
1130
+ fontSize: 11,
1131
+ },
1132
+ progressValue: {
1133
+ fontSize: 11,
1134
+ fontWeight: "600",
1135
+ },
1136
+ progressTrack: {
1137
+ width: "100%",
1138
+ overflow: "hidden",
1139
+ },
1140
+ progressFill: {
1141
+ height: "100%",
1142
+ },
1143
+ gaugeWrapper: {
1144
+ alignItems: "center",
1145
+ gap: 4,
1146
+ },
1147
+ gaugeBox: {
1148
+ alignItems: "center",
1149
+ justifyContent: "center",
1150
+ overflow: "hidden",
1151
+ },
1152
+ gaugeCenterHole: {
1153
+ alignItems: "center",
1154
+ justifyContent: "center",
1155
+ },
1156
+ gaugeFill: {
1157
+ position: "absolute",
1158
+ bottom: 0,
1159
+ left: 0,
1160
+ right: 0,
1161
+ },
1162
+ gaugePercent: {
1163
+ fontSize: 13,
1164
+ fontWeight: "700",
1165
+ },
1166
+ gaugeLabel: {
1167
+ fontSize: 11,
1168
+ },
1169
+ kvContainer: {
1170
+ gap: 2,
1171
+ width: "100%",
1172
+ },
1173
+ kvInline: {
1174
+ flexDirection: "row",
1175
+ alignItems: "center",
1176
+ justifyContent: "space-between",
1177
+ gap: spacing.sm,
1178
+ },
1179
+ kvLabel: {
1180
+ fontSize: 11,
1181
+ },
1182
+ kvInlineLabel: {
1183
+ fontSize: 11,
1184
+ flexShrink: 1,
1185
+ },
1186
+ kvValueRow: {
1187
+ flexDirection: "row",
1188
+ alignItems: "center",
1189
+ gap: spacing.xs,
1190
+ },
1191
+ kvInlineValue: {
1192
+ flexDirection: "row",
1193
+ alignItems: "center",
1194
+ gap: spacing.xs,
1195
+ flexShrink: 1,
1196
+ minWidth: 0,
1197
+ justifyContent: "flex-end",
1198
+ },
1199
+ kvValue: {
1200
+ fontSize: 12,
1201
+ flexShrink: 1,
1202
+ minWidth: 0,
1203
+ },
1204
+ kvSubValue: {
1205
+ fontSize: 11,
1206
+ },
1207
+ kvCopyBtn: {
1208
+ padding: 2,
1209
+ },
1210
+ emptyContainer: {
1211
+ alignItems: "center",
1212
+ justifyContent: "center",
1213
+ gap: spacing.sm,
1214
+ padding: spacing.xl,
1215
+ },
1216
+ emptyIconWrapper: {
1217
+ width: 56,
1218
+ height: 56,
1219
+ borderRadius: 28,
1220
+ alignItems: "center",
1221
+ justifyContent: "center",
1222
+ },
1223
+ emptyTitle: {
1224
+ fontSize: 14,
1225
+ fontWeight: "600",
1226
+ textAlign: "center",
1227
+ },
1228
+ emptyDescription: {
1229
+ fontSize: 12,
1230
+ textAlign: "center",
1231
+ },
1232
+ emptyActionRow: {
1233
+ marginTop: spacing.xs,
1234
+ },
1235
+ collapsibleContainer: {
1236
+ borderWidth: 1,
1237
+ borderRadius: 12,
1238
+ overflow: "hidden",
1239
+ width: "100%",
1240
+ },
1241
+ collapsibleHeader: {
1242
+ flexDirection: "row",
1243
+ alignItems: "center",
1244
+ gap: spacing.sm,
1245
+ padding: spacing.md,
1246
+ minHeight: 40,
1247
+ },
1248
+ collapsibleTitleColumn: {
1249
+ flex: 1,
1250
+ flexShrink: 1,
1251
+ gap: 1,
1252
+ },
1253
+ collapsibleTitle: {
1254
+ fontSize: 13,
1255
+ fontWeight: "600",
1256
+ },
1257
+ collapsibleSubtitle: {
1258
+ fontSize: 11,
1259
+ },
1260
+ collapsibleContent: {
1261
+ paddingHorizontal: spacing.md,
1262
+ paddingBottom: spacing.md,
1263
+ gap: spacing.sm,
1264
+ },
1265
+ tabsFrame: {
1266
+ width: "100%",
1267
+ maxWidth: "100%",
1268
+ borderWidth: 1,
1269
+ borderRadius: 10,
1270
+ overflow: "hidden",
1271
+ position: "relative",
1272
+ justifyContent: "center",
1273
+ },
1274
+ tabsTrackFit: {
1275
+ flexDirection: "row",
1276
+ flexWrap: "nowrap",
1277
+ alignItems: "center",
1278
+ width: "100%",
1279
+ padding: 3,
1280
+ gap: 2,
1281
+ },
1282
+ tabsScrollView: {
1283
+ width: "100%",
1284
+ maxWidth: "100%",
1285
+ },
1286
+ tabsScrollContent: {
1287
+ flexDirection: "row",
1288
+ alignItems: "center",
1289
+ padding: 3,
1290
+ gap: 4,
1291
+ },
1292
+ tab: {
1293
+ flexDirection: "row",
1294
+ alignItems: "center",
1295
+ justifyContent: "center",
1296
+ gap: 4,
1297
+ borderRadius: 8,
1298
+ overflow: "hidden",
1299
+ },
1300
+ tabFit: {
1301
+ flex: 1,
1302
+ flexShrink: 1,
1303
+ minWidth: 0,
1304
+ },
1305
+ tabScroll: {
1306
+ flexShrink: 0,
1307
+ },
1308
+ tabText: {
1309
+ textAlign: "center",
1310
+ fontSize: 12,
1311
+ flexShrink: 1,
1312
+ minWidth: 0,
1313
+ },
1314
+ tabBadge: {
1315
+ borderRadius: 9999,
1316
+ paddingHorizontal: 5,
1317
+ paddingVertical: 1,
1318
+ minWidth: 16,
1319
+ alignItems: "center",
1320
+ justifyContent: "center",
1321
+ },
1322
+ tabBadgeText: {
1323
+ fontSize: 10,
1324
+ fontWeight: "700",
1325
+ },
1326
+ });