@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,117 +0,0 @@
1
- import React from "react";
2
- import { 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 ProgressBarProps {
7
- value: number; // 0 to 100
8
- color?: string;
9
- autoStatusColor?: boolean;
10
- thresholds?: MetricThresholds;
11
- label?: string;
12
- showValueText?: boolean;
13
- height?: number;
14
- style?: StyleProp<ViewStyle>;
15
- }
16
-
17
- export function ProgressBar({
18
- value,
19
- color,
20
- autoStatusColor = true,
21
- thresholds,
22
- label,
23
- showValueText = false,
24
- height = 8,
25
- style,
26
- }: ProgressBarProps) {
27
- const { colors, resolveRadius, typography } = usePluginTheme();
28
-
29
- const clamped = Math.max(0, Math.min(100, Number.isFinite(value) ? value : 0));
30
- const radius = resolveRadius("pill");
31
-
32
- let barColor = color || colors.accent;
33
- if (!color && autoStatusColor) {
34
- const status = resolveMetricStatus(clamped, thresholds);
35
- if (status === "danger") {
36
- barColor = colors.statusDanger;
37
- } else if (status === "warning") {
38
- barColor = colors.statusWarning;
39
- } else {
40
- barColor = colors.statusSuccess;
41
- }
42
- }
43
-
44
- return (
45
- <View style={[styles.container, style]}>
46
- {(label || showValueText) && (
47
- <View style={styles.labelRow}>
48
- {label ? (
49
- <Text
50
- style={[
51
- styles.labelText,
52
- { color: colors.foregroundMuted, ...typography.caption },
53
- ]}
54
- >
55
- {label}
56
- </Text>
57
- ) : null}
58
- {showValueText ? (
59
- <Text
60
- style={[
61
- styles.valueText,
62
- { color: colors.foreground, ...typography.caption, fontWeight: "600" },
63
- ]}
64
- >
65
- {Math.round(clamped)}%
66
- </Text>
67
- ) : null}
68
- </View>
69
- )}
70
- <View
71
- style={[
72
- styles.track,
73
- {
74
- backgroundColor: colors.surface2,
75
- height,
76
- borderRadius: radius,
77
- },
78
- ]}
79
- >
80
- <View
81
- style={[
82
- styles.fill,
83
- {
84
- width: `${clamped}%`,
85
- backgroundColor: barColor,
86
- borderRadius: radius,
87
- },
88
- ]}
89
- />
90
- </View>
91
- </View>
92
- );
93
- }
94
-
95
- const styles = StyleSheet.create({
96
- container: {
97
- gap: 4,
98
- },
99
- labelRow: {
100
- flexDirection: "row",
101
- justifyContent: "space-between",
102
- alignItems: "center",
103
- },
104
- labelText: {
105
- fontWeight: "500",
106
- },
107
- valueText: {
108
- fontWeight: "600",
109
- },
110
- track: {
111
- width: "100%",
112
- overflow: "hidden",
113
- },
114
- fill: {
115
- height: "100%",
116
- },
117
- });
@@ -1,53 +0,0 @@
1
- import React, { type ReactNode } from "react";
2
- import { useResponsive, type UseResponsiveResult } from "../theme/useResponsive";
3
-
4
- export interface ResponsiveProps {
5
- /**
6
- * Content to render on desktop / wide viewports.
7
- */
8
- desktop?: ReactNode;
9
- /**
10
- * Content to render on mobile platforms ('ios' | 'android').
11
- */
12
- mobile?: ReactNode;
13
- /**
14
- * Content to render when in compact mode (narrow trackbar, mobile bottom sheet, or split view).
15
- */
16
- compact?: ReactNode;
17
- /**
18
- * Content to render when in wide mode (not compact).
19
- */
20
- wide?: ReactNode;
21
- /**
22
- * Render function taking `UseResponsiveResult` or children.
23
- */
24
- children?: ReactNode | ((responsive: UseResponsiveResult) => ReactNode);
25
- }
26
-
27
- /**
28
- * Declarative component for rendering different UI elements across desktop, mobile, and compact layouts.
29
- *
30
- * @example
31
- * ```tsx
32
- * <Responsive
33
- * desktop={<DataTable columns={["ID", "Name", "Status", "Latency"]} data={items} />}
34
- * mobile={<DataTable columns={["Name", "Status"]} data={items} />}
35
- * />
36
- * ```
37
- */
38
- export function Responsive({ desktop, mobile, compact, wide, children }: ResponsiveProps) {
39
- const responsive = useResponsive();
40
-
41
- if (typeof children === "function") {
42
- return <>{children(responsive)}</>;
43
- }
44
-
45
- const selected = responsive.select<ReactNode>({
46
- desktop,
47
- mobile,
48
- compact,
49
- wide,
50
- });
51
-
52
- return <>{selected ?? children ?? null}</>;
53
- }
@@ -1,118 +0,0 @@
1
- import React, { type ComponentType, type Ref } from "react";
2
- import {
3
- StyleSheet,
4
- TextInput as RNTextInput,
5
- View,
6
- Pressable,
7
- type StyleProp,
8
- type ViewStyle,
9
- type TextStyle,
10
- type TextInputProps as RNTextInputProps,
11
- type TextInput as RNTextInputInstance,
12
- } from "react-native";
13
- import { getClientHost, getOptionalClientHost } from "../host";
14
- import { usePluginTheme } from "../theme/provider";
15
-
16
- export interface SearchInputProps {
17
- value: string;
18
- onChangeText: (text: string) => void;
19
- placeholder?: string;
20
- onClear?: () => void;
21
- style?: StyleProp<ViewStyle>;
22
- inputStyle?: StyleProp<TextStyle>;
23
- testID?: string;
24
- }
25
-
26
- /**
27
- * Standardized search input with search icon, clear button, and theme support.
28
- */
29
- export function SearchInput({
30
- value,
31
- onChangeText,
32
- placeholder = "Search...",
33
- onClear,
34
- style,
35
- inputStyle,
36
- testID,
37
- }: SearchInputProps) {
38
- const { Icon } = getClientHost();
39
- const ResolvedInput = (getOptionalClientHost()?.TextInput ??
40
- RNTextInput) as ComponentType<RNTextInputProps & { ref?: Ref<RNTextInputInstance> }>;
41
- const { colors, resolveRadius, isCompact } = usePluginTheme();
42
- const radius = resolveRadius("sm");
43
-
44
- const handleClear = () => {
45
- onChangeText("");
46
- if (onClear) onClear();
47
- };
48
-
49
- return (
50
- <View
51
- style={[
52
- styles.container,
53
- {
54
- backgroundColor: colors.surface1,
55
- borderColor: colors.border,
56
- borderRadius: radius,
57
- height: isCompact ? 36 : 40,
58
- },
59
- style,
60
- ]}
61
- >
62
- <View style={styles.iconWrapper}>
63
- <Icon name="Search" size={16} color={colors.foregroundMuted} />
64
- </View>
65
- <ResolvedInput
66
- testID={testID}
67
- value={value}
68
- onChangeText={onChangeText}
69
- placeholder={placeholder}
70
- placeholderTextColor={colors.foregroundMuted}
71
- style={[
72
- styles.input,
73
- {
74
- color: colors.foreground,
75
- fontSize: isCompact ? 13 : 14,
76
- },
77
- inputStyle,
78
- ]}
79
- returnKeyType="search"
80
- autoCapitalize="none"
81
- autoCorrect={false}
82
- />
83
- {Boolean(value) && (
84
- <Pressable
85
- onPress={handleClear}
86
- style={styles.clearButton}
87
- hitSlop={8}
88
- accessibilityLabel="Clear search"
89
- >
90
- <Icon name="X" size={14} color={colors.foregroundMuted} />
91
- </Pressable>
92
- )}
93
- </View>
94
- );
95
- }
96
-
97
- const styles = StyleSheet.create({
98
- container: {
99
- flexDirection: "row",
100
- alignItems: "center",
101
- borderWidth: 1,
102
- paddingHorizontal: 10,
103
- },
104
- iconWrapper: {
105
- marginRight: 8,
106
- alignItems: "center",
107
- justifyContent: "center",
108
- },
109
- input: {
110
- flex: 1,
111
- paddingVertical: 0,
112
- outlineWidth: 0,
113
- },
114
- clearButton: {
115
- padding: 4,
116
- marginLeft: 4,
117
- },
118
- });
@@ -1,80 +0,0 @@
1
- import React from "react";
2
- import {
3
- StyleSheet,
4
- Text,
5
- View,
6
- type StyleProp,
7
- type TextStyle,
8
- type ViewStyle,
9
- } from "react-native";
10
- import { usePluginTheme } from "../theme/provider";
11
- import type { StatusVariant } from "../../../../shared/vendor/paseo-plugin-helper/types";
12
- import { Badge } from "./Badge";
13
-
14
- export interface SectionHeaderProps {
15
- title: string;
16
- /** Optional count — shown as a Badge (warning variant when > 0, neutral otherwise) */
17
- count?: number;
18
- /** Badge variant override */
19
- badgeVariant?: StatusVariant;
20
- style?: StyleProp<ViewStyle>;
21
- textStyle?: StyleProp<TextStyle>;
22
- }
23
-
24
- export function SectionHeader({
25
- title,
26
- count,
27
- badgeVariant,
28
- style,
29
- textStyle,
30
- }: SectionHeaderProps): React.ReactElement | null {
31
- const theme = usePluginTheme();
32
- const titleType = theme.typography?.bodyStrong ?? {
33
- fontSize: 13,
34
- lineHeight: 19,
35
- fontWeight: "600" as const,
36
- };
37
- const headingTransform = (theme.flair?.headingTransform ?? "none") === "uppercase"
38
- ? "uppercase"
39
- : "none";
40
-
41
- return (
42
- <View style={[styles.container, style]}>
43
- <Text
44
- style={[
45
- styles.title,
46
- {
47
- color: theme.colors.foregroundMuted,
48
- textTransform: headingTransform,
49
- fontSize: titleType.fontSize,
50
- lineHeight: titleType.lineHeight,
51
- fontWeight: titleType.fontWeight,
52
- },
53
- textStyle,
54
- ]}
55
- >
56
- {title}
57
- </Text>
58
- {count !== undefined ? (
59
- <Badge
60
- label={String(count)}
61
- variant={badgeVariant ?? (count > 0 ? "warning" : "neutral")}
62
- styleVariant={count > 0 ? "solid" : "tinted"}
63
- />
64
- ) : null}
65
- </View>
66
- );
67
- }
68
-
69
- const styles = StyleSheet.create({
70
- container: {
71
- flexDirection: "row",
72
- alignItems: "center",
73
- gap: 8,
74
- marginTop: 8,
75
- marginBottom: 2,
76
- },
77
- title: {
78
- letterSpacing: 0.8,
79
- },
80
- });
@@ -1,215 +0,0 @@
1
- import React, { useState } from "react";
2
- import {
3
- Pressable,
4
- ScrollView as FallbackScrollView,
5
- StyleSheet,
6
- Text,
7
- View,
8
- type StyleProp,
9
- type ViewStyle,
10
- } from "react-native";
11
- import { getClientHost } from "../host";
12
- import { usePluginTheme } from "../theme/provider";
13
- import { spacing } from "../theme/tokens";
14
- import type { BadgeSize } from "./Badge";
15
-
16
- export interface SelectOption {
17
- label: string;
18
- value: string;
19
- }
20
-
21
- export interface SelectProps {
22
- value: string;
23
- options: SelectOption[];
24
- onValueChange: (value: string) => void;
25
- label?: string;
26
- /** Compact/pill scale shared with {@link Badge}: `"md"` (default) or `"sm"`. */
27
- size?: BadgeSize;
28
- placeholder?: string;
29
- disabled?: boolean;
30
- style?: StyleProp<ViewStyle>;
31
- }
32
-
33
- const OPTION_LIST_MAX_HEIGHT = 216;
34
-
35
- /**
36
- * Compact single-choice picker sized to sit inside a {@link FormRow}. The
37
- * closed trigger stays one line tall; opening reveals a bounded, scrollable
38
- * option list, so a long list degrades to scrolling rather than overflow.
39
- */
40
- export function Select({
41
- value,
42
- options,
43
- onValueChange,
44
- label,
45
- size = "md",
46
- placeholder = "Select…",
47
- disabled = false,
48
- style,
49
- }: SelectProps) {
50
- const { Icon } = getClientHost();
51
- // The dropdown option list is a bounded box nested inside the host sheet,
52
- // so it scrolls with a plain React Native ScrollView — never the host
53
- // sheet-gesture scroller (#219).
54
- const { colors, resolveRadius, typography, isCompact, touchTargetMin, alpha } =
55
- usePluginTheme();
56
- const [open, setOpen] = useState(false);
57
-
58
- const caption = typography?.caption ?? {
59
- fontSize: 11,
60
- lineHeight: 15,
61
- fontWeight: "400" as const,
62
- };
63
- const fontSize = size === "sm" ? 10 : caption.fontSize;
64
- const lineHeight = size === "sm" ? 12 : caption.lineHeight;
65
- const paddingVertical = size === "sm" ? 3 : 6;
66
- const paddingHorizontal = size === "sm" ? 8 : 10;
67
- const iconSize = size === "sm" ? 12 : 14;
68
- const radius = resolveRadius("md");
69
-
70
- // A free-text value (advanced entry) may not be one of the options; surface
71
- // it on the trigger rather than masking it behind the placeholder.
72
- const selected = options.find((option) => option.value === value);
73
- const display = selected?.label ?? (value ? value : placeholder);
74
- const canOpen = !disabled && options.length > 0;
75
- const isOpen = open && canOpen;
76
-
77
- const triggerMinHeight = size === "sm" ? 28 : Math.max(34, touchTargetMin);
78
-
79
- return (
80
- <View style={[styles.container, style]}>
81
- <Pressable
82
- accessibilityRole="button"
83
- accessibilityLabel={label ? `${label}: ${display}` : display}
84
- accessibilityState={{ expanded: isOpen, disabled }}
85
- disabled={!canOpen}
86
- onPress={() => setOpen((prev) => !prev)}
87
- style={({ pressed }) => [
88
- styles.trigger,
89
- {
90
- minHeight: triggerMinHeight,
91
- borderRadius: radius,
92
- borderColor: isOpen ? colors.accent : colors.border,
93
- backgroundColor:
94
- disabled || !canOpen
95
- ? alpha(colors.surface1, 0.5)
96
- : pressed
97
- ? colors.surface2
98
- : colors.surface0,
99
- paddingVertical,
100
- paddingHorizontal,
101
- },
102
- ]}
103
- >
104
- <Text
105
- numberOfLines={1}
106
- style={[
107
- styles.triggerText,
108
- {
109
- color: disabled || !canOpen ? colors.foregroundMuted : colors.foreground,
110
- fontSize,
111
- lineHeight,
112
- fontWeight: selected ? "600" : "500",
113
- },
114
- ]}
115
- >
116
- {display}
117
- </Text>
118
- <Icon
119
- name={isOpen ? "ChevronUp" : "ChevronDown"}
120
- size={iconSize}
121
- color={colors.foregroundMuted}
122
- />
123
- </Pressable>
124
-
125
- {isOpen ? (
126
- <View
127
- style={[
128
- styles.optionList,
129
- {
130
- borderRadius: radius,
131
- borderColor: colors.border,
132
- backgroundColor: colors.surface0,
133
- marginTop: spacing.xs,
134
- },
135
- ]}
136
- >
137
- <FallbackScrollView
138
- nestedScrollEnabled
139
- keyboardShouldPersistTaps="handled"
140
- style={{ maxHeight: OPTION_LIST_MAX_HEIGHT }}
141
- >
142
- {options.map((option) => {
143
- const isSelected = option.value === value;
144
- return (
145
- <Pressable
146
- key={option.value}
147
- accessibilityRole="button"
148
- accessibilityLabel={option.label}
149
- accessibilityState={{ selected: isSelected }}
150
- onPress={() => {
151
- onValueChange(option.value);
152
- setOpen(false);
153
- }}
154
- style={({ pressed }) => [
155
- styles.option,
156
- {
157
- paddingVertical,
158
- paddingHorizontal,
159
- backgroundColor: isSelected
160
- ? colors.surface2
161
- : pressed
162
- ? alpha(colors.surface2, 0.5)
163
- : "transparent",
164
- },
165
- ]}
166
- >
167
- <Text
168
- numberOfLines={1}
169
- style={[
170
- styles.optionText,
171
- {
172
- color: isSelected ? colors.foreground : colors.foregroundMuted,
173
- fontSize: isCompact ? fontSize : fontSize + 1,
174
- lineHeight,
175
- fontWeight: isSelected ? "600" : "400",
176
- },
177
- ]}
178
- >
179
- {option.label}
180
- </Text>
181
- </Pressable>
182
- );
183
- })}
184
- </FallbackScrollView>
185
- </View>
186
- ) : null}
187
- </View>
188
- );
189
- }
190
-
191
- const styles = StyleSheet.create({
192
- container: {
193
- width: "100%",
194
- },
195
- trigger: {
196
- flexDirection: "row",
197
- alignItems: "center",
198
- justifyContent: "space-between",
199
- gap: spacing.sm,
200
- borderWidth: 1,
201
- },
202
- triggerText: {
203
- flexShrink: 1,
204
- minWidth: 0,
205
- },
206
- optionList: {
207
- borderWidth: 1,
208
- overflow: "hidden",
209
- },
210
- option: {},
211
- optionText: {
212
- flexShrink: 1,
213
- minWidth: 0,
214
- },
215
- });
@@ -1,80 +0,0 @@
1
- import React, { useEffect, useRef } from "react";
2
- import { Animated, StyleSheet, View, type StyleProp, type ViewStyle } from "react-native";
3
- import { usePluginTheme } from "../theme/provider";
4
- import type { StatusVariant } from "../../../../shared/vendor/paseo-plugin-helper/types";
5
-
6
- export interface StatusDotProps {
7
- variant?: StatusVariant;
8
- size?: "sm" | "md" | "lg";
9
- pulse?: boolean;
10
- style?: StyleProp<ViewStyle>;
11
- }
12
-
13
- export function StatusDot({ variant = "neutral", size = "md", pulse = false, style }: StatusDotProps) {
14
- const { getStatusColor, alpha } = usePluginTheme();
15
- const color = getStatusColor(variant);
16
- const pulseAnim = useRef(new Animated.Value(1)).current;
17
-
18
- useEffect(() => {
19
- if (!pulse) return;
20
-
21
- const loop = Animated.loop(
22
- Animated.sequence([
23
- Animated.timing(pulseAnim, {
24
- toValue: 0.35,
25
- duration: 900,
26
- useNativeDriver: true,
27
- }),
28
- Animated.timing(pulseAnim, {
29
- toValue: 1,
30
- duration: 900,
31
- useNativeDriver: true,
32
- }),
33
- ]),
34
- );
35
- loop.start();
36
-
37
- return () => loop.stop();
38
- }, [pulse, pulseAnim]);
39
-
40
- const dimension = size === "sm" ? 6 : size === "lg" ? 10 : 8;
41
-
42
- return (
43
- <View
44
- style={[
45
- styles.container,
46
- {
47
- width: dimension,
48
- height: dimension,
49
- },
50
- style,
51
- ]}
52
- >
53
- <Animated.View
54
- style={[
55
- styles.dot,
56
- {
57
- width: dimension,
58
- height: dimension,
59
- borderRadius: dimension / 2,
60
- backgroundColor: color,
61
- shadowColor: color,
62
- opacity: pulseAnim,
63
- },
64
- ]}
65
- />
66
- </View>
67
- );
68
- }
69
-
70
- const styles = StyleSheet.create({
71
- container: {
72
- alignItems: "center",
73
- justifyContent: "center",
74
- },
75
- dot: {
76
- shadowOffset: { width: 0, height: 0 },
77
- shadowOpacity: 0.4,
78
- shadowRadius: 3,
79
- },
80
- });