@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
@@ -1,446 +0,0 @@
1
- import React, { useState, type ReactNode } from "react";
2
- import {
3
- Platform,
4
- Pressable,
5
- StyleSheet,
6
- Text,
7
- View,
8
- type StyleProp,
9
- type TextStyle,
10
- type ViewStyle,
11
- } from "react-native";
12
- import { getClientHost } from "../host";
13
- import { usePluginTheme } from "../theme/provider";
14
- import { resolveGridColumns } from "../theme/responsive";
15
- import { spacing } from "../theme/tokens";
16
- import { copyToClipboard } from "../utils/clipboard";
17
- import {
18
- truncate,
19
- truncateMiddle,
20
- truncatePath,
21
- TruncatePathOptions,
22
- } from "../../../../shared/vendor/paseo-plugin-helper/formatters";
23
-
24
- export type KeyValueTruncateMode = "end" | "middle" | "path";
25
-
26
- export interface KeyValueProps {
27
- label: string;
28
- value: string | number | null | undefined;
29
- subValue?: string;
30
- mono?: boolean;
31
- copyable?: boolean;
32
- /** Truncate long value: "middle" (UUIDs/hashes), "path" (filepaths), or "end" (standard) */
33
- truncate?: boolean | KeyValueTruncateMode;
34
- /** Maximum length before truncation applies. Default: 32 */
35
- truncateMaxLength?: number;
36
- /** Custom options when truncate="path" */
37
- truncatePathOptions?: TruncatePathOptions;
38
- /**
39
- * "stacked" (default) keeps the existing label-above-value layout.
40
- * "inline" renders label and value on one line, with the value truncating
41
- * middle so the row stays a single text line.
42
- */
43
- layout?: "stacked" | "inline";
44
- stackOnCompact?: boolean;
45
- style?: StyleProp<ViewStyle>;
46
- labelStyle?: StyleProp<TextStyle>;
47
- valueStyle?: StyleProp<TextStyle>;
48
- }
49
-
50
- export function KeyValue({
51
- label,
52
- value,
53
- subValue,
54
- mono = false,
55
- copyable = false,
56
- truncate: truncateProp = false,
57
- truncateMaxLength = 32,
58
- truncatePathOptions,
59
- layout = "stacked",
60
- stackOnCompact = true,
61
- style,
62
- labelStyle,
63
- valueStyle,
64
- }: KeyValueProps) {
65
- const { Icon, useToast } = getClientHost();
66
- const { colors, flair, isCompact, touchTargetMin, fonts, typography } = usePluginTheme();
67
- const toast = useToast();
68
- const [copied, setCopied] = useState(false);
69
-
70
- const rawString = value === null || value === undefined ? "" : String(value);
71
-
72
- let displayValue = rawString || "-";
73
- if (truncateProp && rawString.length > truncateMaxLength) {
74
- const mode: KeyValueTruncateMode =
75
- typeof truncateProp === "string" ? truncateProp : "middle";
76
- switch (mode) {
77
- case "path":
78
- displayValue = truncatePath(rawString, truncateMaxLength, truncatePathOptions);
79
- break;
80
- case "end":
81
- displayValue = truncate(rawString, truncateMaxLength);
82
- break;
83
- case "middle":
84
- default:
85
- displayValue = truncateMiddle(rawString, truncateMaxLength);
86
- break;
87
- }
88
- }
89
-
90
- const handleCopy = async () => {
91
- if (!copyable || !rawString) return;
92
- const ok = await copyToClipboard(rawString, {
93
- toast,
94
- toastMessage: label,
95
- });
96
- if (ok) {
97
- setCopied(true);
98
- setTimeout(() => setCopied(false), 2000);
99
- }
100
- };
101
-
102
- const fontFamily = mono
103
- ? fonts.mono ?? Platform.select({ ios: "Menlo", android: "monospace", default: "monospace" })
104
- : undefined;
105
-
106
- const shouldStack = stackOnCompact && isCompact;
107
-
108
- const copyButton = copyable && value ? (
109
- <Pressable
110
- onPress={handleCopy}
111
- hitSlop={Math.max(8, (touchTargetMin - 20) / 2)}
112
- style={styles.copyBtn}
113
- accessibilityRole="button"
114
- accessibilityLabel={`Copy ${label}`}
115
- >
116
- <Icon
117
- name={copied ? "Check" : "Copy"}
118
- size={isCompact ? 12 : 13}
119
- color={copied ? colors.statusSuccess : colors.foregroundMuted}
120
- />
121
- </Pressable>
122
- ) : null;
123
-
124
- if (layout === "inline") {
125
- return (
126
- <View
127
- style={[
128
- styles.container,
129
- styles.inlineContainer,
130
- { paddingVertical: isCompact ? spacing.xs : spacing.sm },
131
- style,
132
- ]}
133
- >
134
- <Text
135
- style={[
136
- styles.inlineLabel,
137
- {
138
- color: colors.foregroundMuted,
139
- ...typography.label,
140
- textTransform: flair.headingTransform === "uppercase" ? "uppercase" : "none",
141
- },
142
- labelStyle,
143
- ]}
144
- >
145
- {label}
146
- </Text>
147
-
148
- <Text
149
- selectable
150
- numberOfLines={1}
151
- ellipsizeMode="middle"
152
- style={[
153
- styles.inlineValueText,
154
- {
155
- color: colors.foreground,
156
- ...typography.bodySmall,
157
- fontFamily,
158
- },
159
- valueStyle,
160
- ]}
161
- >
162
- {displayValue}
163
- </Text>
164
-
165
- {subValue ? (
166
- <Text
167
- selectable
168
- numberOfLines={1}
169
- style={[
170
- styles.inlineSubValue,
171
- {
172
- color: colors.foregroundMuted,
173
- ...typography.caption,
174
- },
175
- ]}
176
- >
177
- {subValue}
178
- </Text>
179
- ) : null}
180
-
181
- {copyButton}
182
- </View>
183
- );
184
- }
185
-
186
- if (shouldStack) {
187
- return (
188
- <View
189
- style={[
190
- styles.container,
191
- styles.stackedContainer,
192
- { paddingVertical: isCompact ? spacing.xs : spacing.sm },
193
- style,
194
- ]}
195
- >
196
- <View style={styles.stackedHeaderRow}>
197
- <Text
198
- style={[
199
- styles.label,
200
- {
201
- color: colors.foregroundMuted,
202
- ...typography.label,
203
- textTransform: flair.headingTransform === "uppercase" ? "uppercase" : "none",
204
- },
205
- labelStyle,
206
- ]}
207
- >
208
- {label}
209
- </Text>
210
- {copyButton}
211
- </View>
212
-
213
- <Text
214
- selectable
215
- style={[
216
- styles.stackedValueText,
217
- {
218
- color: colors.foreground,
219
- ...typography.bodySmall,
220
- fontFamily,
221
- },
222
- valueStyle,
223
- ]}
224
- >
225
- {displayValue}
226
- </Text>
227
-
228
- {subValue ? (
229
- <Text
230
- style={[
231
- styles.subValue,
232
- {
233
- color: colors.foregroundMuted,
234
- ...typography.caption,
235
- },
236
- ]}
237
- >
238
- {subValue}
239
- </Text>
240
- ) : null}
241
- </View>
242
- );
243
- }
244
-
245
- // Horizontal layout for Desktop / Wide screens
246
- return (
247
- <View
248
- style={[
249
- styles.container,
250
- styles.rowContainer,
251
- { paddingVertical: isCompact ? spacing.xs : spacing.sm },
252
- style,
253
- ]}
254
- >
255
- <Text
256
- style={[
257
- styles.label,
258
- {
259
- color: colors.foregroundMuted,
260
- ...typography.label,
261
- textTransform: flair.headingTransform === "uppercase" ? "uppercase" : "none",
262
- },
263
- labelStyle,
264
- ]}
265
- >
266
- {label}
267
- </Text>
268
-
269
- <View style={styles.rowValueWrapper}>
270
- <View style={styles.rowValueLine}>
271
- <Text
272
- selectable
273
- style={[
274
- styles.rowValueText,
275
- {
276
- color: colors.foreground,
277
- ...typography.bodySmall,
278
- fontFamily,
279
- },
280
- valueStyle,
281
- ]}
282
- >
283
- {displayValue}
284
- </Text>
285
-
286
- {copyButton}
287
- </View>
288
-
289
- {subValue ? (
290
- <Text
291
- style={[
292
- styles.subValue,
293
- {
294
- color: colors.foregroundMuted,
295
- ...typography.caption,
296
- },
297
- ]}
298
- >
299
- {subValue}
300
- </Text>
301
- ) : null}
302
- </View>
303
- </View>
304
- );
305
- }
306
-
307
- export interface KeyValueGroupProps {
308
- children: ReactNode;
309
- columns?: 1 | 2 | 3 | 4;
310
- gap?: number;
311
- /**
312
- * How a compact surface treats the column count.
313
- * - "compact" (default): collapse to a single column on a compact surface —
314
- * the historical behavior.
315
- * - "never": keep the requested column count on a compact surface.
316
- */
317
- collapse?: "compact" | "never";
318
- /**
319
- * Minimum width a column should keep. When set and the container width is
320
- * known, the effective column count is capped so each column stays at least
321
- * this wide, wrapping to fewer columns rather than collapsing to one.
322
- * `columns` remains the upper bound.
323
- */
324
- minColumnWidth?: number;
325
- style?: StyleProp<ViewStyle>;
326
- }
327
-
328
- export function KeyValueGroup({
329
- children,
330
- columns = 2,
331
- gap = spacing.md,
332
- collapse = "compact",
333
- minColumnWidth,
334
- style,
335
- }: KeyValueGroupProps) {
336
- const { isCompact, layout } = usePluginTheme();
337
- const effectiveColumns = resolveGridColumns({
338
- columns,
339
- gap,
340
- width: layout.width,
341
- minColumnWidth,
342
- collapse,
343
- isCompact,
344
- });
345
-
346
- const childArray = React.Children.toArray(children).filter(Boolean);
347
-
348
- return (
349
- <View style={[styles.groupContainer, { gap }, style]}>
350
- {childArray.map((child, index) => (
351
- <View
352
- key={index}
353
- style={{
354
- flexGrow: 1,
355
- flexShrink: 1,
356
- flexBasis: `${Math.floor(100 / effectiveColumns) - 2}%`,
357
- }}
358
- >
359
- {child}
360
- </View>
361
- ))}
362
- </View>
363
- );
364
- }
365
-
366
- const styles = StyleSheet.create({
367
- container: {
368
- width: "100%",
369
- },
370
- rowContainer: {
371
- flexDirection: "row",
372
- alignItems: "flex-start",
373
- justifyContent: "space-between",
374
- gap: 12,
375
- },
376
- stackedContainer: {
377
- flexDirection: "column",
378
- gap: 3,
379
- width: "100%",
380
- },
381
- stackedHeaderRow: {
382
- flexDirection: "row",
383
- alignItems: "center",
384
- justifyContent: "space-between",
385
- width: "100%",
386
- },
387
- stackedValueText: {
388
- width: "100%",
389
- },
390
- inlineContainer: {
391
- flexDirection: "row",
392
- alignItems: "center",
393
- gap: spacing.xs,
394
- minWidth: 0,
395
- },
396
- inlineLabel: {
397
- flexShrink: 0,
398
- },
399
- inlineValueText: {
400
- flexShrink: 1,
401
- minWidth: 0,
402
- },
403
- inlineSubValue: {
404
- flexShrink: 0,
405
- },
406
- rowValueWrapper: {
407
- flexDirection: "column",
408
- alignItems: "flex-end",
409
- flexGrow: 1,
410
- flexShrink: 1,
411
- minWidth: 0,
412
- gap: 2,
413
- },
414
- rowValueLine: {
415
- flexDirection: "row",
416
- alignItems: "center",
417
- justifyContent: "flex-end",
418
- gap: 6,
419
- flexShrink: 1,
420
- minWidth: 0,
421
- maxWidth: "100%",
422
- },
423
- rowValueText: {
424
- flexShrink: 1,
425
- minWidth: 0,
426
- textAlign: "right",
427
- },
428
- groupContainer: {
429
- flexDirection: "row",
430
- flexWrap: "wrap",
431
- width: "100%",
432
- },
433
- label: {
434
- flexShrink: 1,
435
- minWidth: 0,
436
- },
437
- subValue: {
438
- flexShrink: 1,
439
- minWidth: 0,
440
- },
441
- copyBtn: {
442
- padding: 3,
443
- alignItems: "center",
444
- justifyContent: "center",
445
- },
446
- });
@@ -1,247 +0,0 @@
1
- import React, { type ReactNode } from "react";
2
- import { Platform, StyleSheet, Text, View, type StyleProp, type ViewStyle } from "react-native";
3
- import { usePluginTheme } from "../theme/provider";
4
- import { resolveMetricStatus, type MetricThresholds } from "../../../../shared/vendor/paseo-plugin-helper/formatters";
5
-
6
- export interface MetricGaugeProps {
7
- value: number; // 0 - 100
8
- size?: number;
9
- strokeWidth?: number;
10
- thresholds?: MetricThresholds;
11
- color?: string;
12
- autoStatusColor?: boolean;
13
- label?: string;
14
- showPercent?: boolean;
15
- centerSlot?: ReactNode;
16
- style?: StyleProp<ViewStyle>;
17
- }
18
-
19
- /**
20
- * Clean circular metric gauge.
21
- * Displays a proportional percentage ring with automated threshold coloring and center slot.
22
- */
23
- export function MetricGauge({
24
- value,
25
- size = 76,
26
- strokeWidth = 7,
27
- thresholds,
28
- color,
29
- autoStatusColor = true,
30
- label,
31
- showPercent = true,
32
- centerSlot,
33
- style,
34
- }: MetricGaugeProps) {
35
- const { colors } = usePluginTheme();
36
-
37
- const clamped = Math.max(0, Math.min(100, Number.isFinite(value) ? value : 0));
38
-
39
- let gaugeColor = color || colors.accent;
40
- if (!color && autoStatusColor) {
41
- const status = resolveMetricStatus(clamped, thresholds);
42
- if (status === "danger") {
43
- gaugeColor = colors.statusDanger;
44
- } else if (status === "warning") {
45
- gaugeColor = colors.statusWarning;
46
- } else {
47
- gaugeColor = colors.statusSuccess;
48
- }
49
- }
50
-
51
- const radius = size / 2;
52
- const innerSize = Math.max(0, size - strokeWidth * 2);
53
- const innerRadius = innerSize / 2;
54
- const trackColor = colors.surface2;
55
-
56
- // Web & React Native Web: Conic gradient provides a pixel-perfect proportional ring arc
57
- if (Platform.OS === "web") {
58
- const webBackground = `conic-gradient(${gaugeColor} 0% ${clamped}%, ${trackColor} ${clamped}% 100%)`;
59
-
60
- return (
61
- <View style={[styles.wrapper, style]}>
62
- <View
63
- style={[
64
- styles.gaugeBox,
65
- {
66
- width: size,
67
- height: size,
68
- borderRadius: radius,
69
- },
70
- ({ background: webBackground } as any),
71
- ]}
72
- >
73
- {/* Inner cutout mask */}
74
- <View
75
- style={[
76
- styles.centerHole,
77
- {
78
- width: innerSize,
79
- height: innerSize,
80
- borderRadius: innerRadius,
81
- backgroundColor: colors.surface0,
82
- },
83
- ]}
84
- >
85
- {centerSlot ? (
86
- centerSlot
87
- ) : showPercent ? (
88
- <Text style={[styles.percentText, { color: colors.foreground }]}>
89
- {Math.round(clamped)}%
90
- </Text>
91
- ) : null}
92
- </View>
93
- </View>
94
-
95
- {label ? (
96
- <Text style={[styles.labelText, { color: colors.foregroundMuted }]}>
97
- {label}
98
- </Text>
99
- ) : null}
100
- </View>
101
- );
102
- }
103
-
104
- // Native (iOS/Android): Two-semicircle clipping approach
105
- const firstHalfRotation = Math.min(180, clamped * 3.6);
106
- const secondHalfRotation = clamped > 50 ? (clamped - 50) * 3.6 : 0;
107
-
108
- return (
109
- <View style={[styles.wrapper, style]}>
110
- <View style={[styles.gaugeBox, { width: size, height: size }]}>
111
- {/* Background track circle */}
112
- <View
113
- style={[
114
- StyleSheet.absoluteFillObject,
115
- {
116
- borderRadius: radius,
117
- borderWidth: strokeWidth,
118
- borderColor: trackColor,
119
- },
120
- ]}
121
- />
122
-
123
- {/* First 180 degrees */}
124
- <View
125
- style={[
126
- styles.halfCircleContainer,
127
- {
128
- width: size,
129
- height: size,
130
- transform: [{ rotate: `${firstHalfRotation}deg` }],
131
- },
132
- ]}
133
- >
134
- <View
135
- style={[
136
- styles.halfCircle,
137
- {
138
- width: size,
139
- height: size,
140
- borderRadius: radius,
141
- borderWidth: strokeWidth,
142
- borderColor: gaugeColor,
143
- borderBottomColor: "transparent",
144
- borderLeftColor: "transparent",
145
- },
146
- ]}
147
- />
148
- </View>
149
-
150
- {/* Second 180 degrees */}
151
- {clamped > 50 ? (
152
- <View
153
- style={[
154
- styles.halfCircleContainer,
155
- {
156
- width: size,
157
- height: size,
158
- transform: [{ rotate: `${secondHalfRotation + 180}deg` }],
159
- },
160
- ]}
161
- >
162
- <View
163
- style={[
164
- styles.halfCircle,
165
- {
166
- width: size,
167
- height: size,
168
- borderRadius: radius,
169
- borderWidth: strokeWidth,
170
- borderColor: gaugeColor,
171
- borderBottomColor: "transparent",
172
- borderLeftColor: "transparent",
173
- },
174
- ]}
175
- />
176
- </View>
177
- ) : null}
178
-
179
- {/* Center Content Slot */}
180
- <View
181
- style={[
182
- styles.centerHole,
183
- {
184
- width: innerSize,
185
- height: innerSize,
186
- borderRadius: innerRadius,
187
- backgroundColor: colors.surface0,
188
- },
189
- ]}
190
- >
191
- {centerSlot ? (
192
- centerSlot
193
- ) : showPercent ? (
194
- <Text style={[styles.percentText, { color: colors.foreground }]}>
195
- {Math.round(clamped)}%
196
- </Text>
197
- ) : null}
198
- </View>
199
- </View>
200
-
201
- {label ? (
202
- <Text style={[styles.labelText, { color: colors.foregroundMuted }]}>
203
- {label}
204
- </Text>
205
- ) : null}
206
- </View>
207
- );
208
- }
209
-
210
- const styles = StyleSheet.create({
211
- wrapper: {
212
- alignItems: "center",
213
- justifyContent: "center",
214
- gap: 6,
215
- },
216
- gaugeBox: {
217
- position: "relative",
218
- alignItems: "center",
219
- justifyContent: "center",
220
- overflow: "hidden",
221
- },
222
- centerHole: {
223
- position: "absolute",
224
- alignItems: "center",
225
- justifyContent: "center",
226
- zIndex: 10,
227
- },
228
- halfCircleContainer: {
229
- position: "absolute",
230
- top: 0,
231
- left: 0,
232
- overflow: "hidden",
233
- },
234
- halfCircle: {
235
- position: "absolute",
236
- top: 0,
237
- left: 0,
238
- },
239
- percentText: {
240
- fontSize: 13,
241
- fontWeight: "700",
242
- },
243
- labelText: {
244
- fontSize: 11,
245
- fontWeight: "500",
246
- },
247
- });