@xpufx/paseo-top 0.4.4 → 0.4.7

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 +18 -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 +94 -1
  6. package/client/pill.tsx +692 -616
  7. package/client/resources-query.ts +1 -1
  8. package/client/settings-events.ts +1 -1
  9. package/client/settings-ui.tsx +86 -9
  10. package/client/surface.tsx +308 -276
  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} +9 -3
  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} +51 -18
  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/{surface.tsx → lifecycle/surface.tsx} +6 -6
  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} +50 -58
  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 -234
  48. package/npm-shrinkwrap.json +44 -0
  49. package/package.json +8 -4
  50. package/paseo-plugin.json +2 -0
  51. package/screenshots/1-paseo-top-modal-system.jpg +0 -0
  52. package/screenshots/2-paseo-top-modal-workspace.jpg +0 -0
  53. package/screenshots/3-paseo-top-modal-settings.jpg +0 -0
  54. package/screenshots/4-paseo-top-modal-about.jpg +0 -0
  55. package/screenshots/paseo-top-dashboard.jpg +0 -0
  56. package/screenshots/paseo-top-general-view.jpg +0 -0
  57. package/server/resources.ts +98 -0
  58. package/server/vendor/paseo-plugin-helper/custom-pills.ts +70 -0
  59. package/server/vendor/paseo-plugin-helper/index.ts +2 -0
  60. package/server/vendor/paseo-plugin-helper/logger.ts +5 -16
  61. package/server/vendor/paseo-plugin-helper/rpc-guard.ts +2 -2
  62. package/server/vendor/paseo-plugin-helper/storage-guard.ts +294 -0
  63. package/server/vendor/paseo-plugin-helper/storage.ts +36 -2
  64. package/server/vendor/paseo-plugin-helper/telemetry.ts +486 -0
  65. package/server/vendor/paseo-plugin-helper/tickets.ts +18 -67
  66. package/server/vendor/permission-audit/index.ts +1 -0
  67. package/server/vendor/permission-audit/server.ts +839 -0
  68. package/shared/helper-version.ts +1 -1
  69. package/shared/multi-host.ts +153 -159
  70. package/shared/resources.ts +145 -0
  71. package/shared/vendor/paseo-plugin-helper/async.ts +14 -7
  72. package/shared/vendor/paseo-plugin-helper/features.ts +244 -0
  73. package/shared/vendor/paseo-plugin-helper/forge.ts +27 -0
  74. package/shared/vendor/paseo-plugin-helper/index.ts +1 -0
  75. package/shared/vendor/paseo-plugin-helper/tickets.ts +122 -28
  76. package/shared/vendor/permission-audit/filter.ts +61 -0
  77. package/shared/vendor/permission-audit/index.ts +2 -0
  78. package/shared/vendor/permission-audit/shared.ts +119 -0
  79. package/shared/version.ts +1 -1
  80. package/client/vendor/paseo-plugin-helper/components/AboutSection.tsx +0 -493
  81. package/client/vendor/paseo-plugin-helper/components/AttentionBeacon.tsx +0 -250
  82. package/client/vendor/paseo-plugin-helper/components/Badge.tsx +0 -168
  83. package/client/vendor/paseo-plugin-helper/components/Button.tsx +0 -204
  84. package/client/vendor/paseo-plugin-helper/components/Card.tsx +0 -257
  85. package/client/vendor/paseo-plugin-helper/components/CodeBlock.tsx +0 -196
  86. package/client/vendor/paseo-plugin-helper/components/Collapsible.tsx +0 -277
  87. package/client/vendor/paseo-plugin-helper/components/CommandBox.tsx +0 -172
  88. package/client/vendor/paseo-plugin-helper/components/CopyButton.tsx +0 -180
  89. package/client/vendor/paseo-plugin-helper/components/EmptyState.tsx +0 -97
  90. package/client/vendor/paseo-plugin-helper/components/HighlightedText.tsx +0 -70
  91. package/client/vendor/paseo-plugin-helper/components/InlineButton.tsx +0 -88
  92. package/client/vendor/paseo-plugin-helper/components/InteractiveRow.tsx +0 -142
  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 -132
  98. package/client/vendor/paseo-plugin-helper/components/SectionHeader.tsx +0 -87
  99. package/client/vendor/paseo-plugin-helper/components/Select.tsx +0 -283
  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 -26
  106. package/client/vendor/paseo-plugin-helper/custom-pills.tsx +0 -225
  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 -30
  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 -368
  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/theme/flair.ts +0 -76
  121. package/client/vendor/paseo-plugin-helper/theme/host-variables.ts +0 -121
  122. package/client/vendor/paseo-plugin-helper/theme/index.ts +0 -7
  123. package/client/vendor/paseo-plugin-helper/theme/provider.tsx +0 -178
  124. package/client/vendor/paseo-plugin-helper/theme/responsive.ts +0 -213
  125. package/client/vendor/paseo-plugin-helper/theme/tokens.ts +0 -200
  126. package/client/vendor/paseo-plugin-helper/theme/useResponsive.ts +0 -57
  127. package/client/vendor/paseo-plugin-helper/tickets.tsx +0 -766
  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,821 @@
1
+ import React, { useState, type ReactNode } from "react";
2
+ import {
3
+ Platform,
4
+ Pressable,
5
+ ScrollView as FallbackScrollView,
6
+ StyleSheet,
7
+ Text,
8
+ View,
9
+ type AccessibilityRole,
10
+ type GestureResponderEvent,
11
+ type StyleProp,
12
+ type TextStyle,
13
+ type ViewStyle,
14
+ } from "react-native";
15
+ import { getClientHost } from "../core/host";
16
+ import { copyToClipboard } from "../lifecycle/clipboard";
17
+ import { useHostTheme, useHostLayout } from "./theme";
18
+ import { alpha } from "./color";
19
+ import { spacing } from "./layout";
20
+ import { HostBadge, HostKeyValue } from "./content";
21
+ import { HostButton } from "./controls";
22
+
23
+ /**
24
+ * Paseo Plugin Helper — UI text & misc adapters (`paseo-plugin-helper/ui`).
25
+ *
26
+ * Thin, host-delegating text adapters
27
+ * (CodeBlock, CommandBox, TruncatedText, …). Same contract as the rest of ui/:
28
+ * host theme colors only, no scroll ownership, no design-system machinery.
29
+ */
30
+
31
+ // --- Code block ------------------------------------------------------------
32
+
33
+ export interface HostCodeBlockProps {
34
+ code: string;
35
+ language?: string;
36
+ title?: string;
37
+ maxHeight?: number;
38
+ copyable?: boolean;
39
+ style?: StyleProp<ViewStyle>;
40
+ textStyle?: StyleProp<TextStyle>;
41
+ }
42
+
43
+ /** Bounded code block with optional copy button. */
44
+ export function HostCodeBlock({
45
+ code,
46
+ language,
47
+ title,
48
+ maxHeight = 320,
49
+ copyable = true,
50
+ style,
51
+ textStyle,
52
+ }: HostCodeBlockProps) {
53
+ const { Icon, useToast } = getClientHost();
54
+ const { colors } = useHostTheme();
55
+ const toast = useToast();
56
+ const [copied, setCopied] = useState(false);
57
+
58
+ const handleCopy = async () => {
59
+ const ok = await copyToClipboard(code, { toast, toastMessage: title || "Code" });
60
+ if (ok) {
61
+ setCopied(true);
62
+ setTimeout(() => setCopied(false), 2000);
63
+ }
64
+ };
65
+
66
+ const fontFamily = Platform.select({
67
+ ios: "Menlo",
68
+ android: "monospace",
69
+ default: "monospace",
70
+ });
71
+
72
+ return (
73
+ <View
74
+ style={[
75
+ styles.codeContainer,
76
+ {
77
+ backgroundColor: colors.surface0,
78
+ borderColor: colors.border,
79
+ },
80
+ style,
81
+ ]}
82
+ >
83
+ {(title || language || copyable) && (
84
+ <View style={[styles.codeHeader, { borderBottomColor: alpha(colors.border, 0.7) }]}>
85
+ <View style={styles.codeHeaderLeft}>
86
+ {title ? (
87
+ <Text style={[styles.codeTitle, { color: colors.foreground }]}>{title}</Text>
88
+ ) : language ? (
89
+ <Text style={[styles.codeLanguage, { color: colors.foregroundMuted }]}>
90
+ {language.toUpperCase()}
91
+ </Text>
92
+ ) : null}
93
+ </View>
94
+ {copyable ? (
95
+ <Pressable
96
+ onPress={handleCopy}
97
+ accessibilityRole="button"
98
+ accessibilityLabel="Copy code"
99
+ hitSlop={4}
100
+ style={({ pressed }) => [
101
+ styles.codeCopyButton,
102
+ {
103
+ backgroundColor: pressed ? colors.surface2 : colors.surface1,
104
+ borderColor: colors.border,
105
+ },
106
+ ]}
107
+ >
108
+ <Icon
109
+ name={copied ? "Check" : "Copy"}
110
+ size={12}
111
+ color={copied ? colors.statusSuccess : colors.foregroundMuted}
112
+ />
113
+ </Pressable>
114
+ ) : null}
115
+ </View>
116
+ )}
117
+ {/* The snippet scroller is a plain React Native ScrollView: the host
118
+ ScrollView is a sheet-gesture pan controller and collapses/crashes
119
+ when nested inside the host sheet (#219). */}
120
+ <FallbackScrollView
121
+ style={{ maxHeight }}
122
+ contentContainerStyle={styles.codeScrollContent}
123
+ horizontal={false}
124
+ >
125
+ <Text
126
+ selectable
127
+ style={[
128
+ styles.codeText,
129
+ {
130
+ color: colors.foreground,
131
+ fontFamily,
132
+ },
133
+ textStyle,
134
+ ]}
135
+ >
136
+ {code}
137
+ </Text>
138
+ </FallbackScrollView>
139
+ </View>
140
+ );
141
+ }
142
+
143
+ // --- Command box -----------------------------------------------------------
144
+
145
+ export interface HostCommandBoxProps {
146
+ argv?: string[];
147
+ command?: string;
148
+ style?: StyleProp<ViewStyle>;
149
+ textStyle?: StyleProp<TextStyle>;
150
+ copyLabel?: string;
151
+ }
152
+
153
+ /** Formats an argv array into a shell command line. */
154
+ export function formatCommandLine(argv: string[]): string {
155
+ return argv.map((arg) => (arg.includes(" ") ? JSON.stringify(arg) : arg)).join(" ");
156
+ }
157
+
158
+ /** Shell command display with copy-to-clipboard. */
159
+ export function HostCommandBox({
160
+ argv = [],
161
+ command,
162
+ style,
163
+ textStyle,
164
+ copyLabel = "Copy command",
165
+ }: HostCommandBoxProps): React.ReactElement | null {
166
+ const { colors } = useHostTheme();
167
+ const { Icon, useToast } = getClientHost();
168
+ const toast = useToast();
169
+ const [copied, setCopied] = useState(false);
170
+
171
+ const fullCommand = command ?? formatCommandLine(argv);
172
+ const [prog, ...rest] = argv;
173
+
174
+ const handleCopy = async () => {
175
+ if (!fullCommand) return;
176
+ const ok = await copyToClipboard(fullCommand, { toast, toastMessage: "Command" });
177
+ if (ok) {
178
+ setCopied(true);
179
+ setTimeout(() => setCopied(false), 2000);
180
+ }
181
+ };
182
+
183
+ const fontFamily = Platform.select({
184
+ ios: "Menlo",
185
+ android: "monospace",
186
+ default: "monospace",
187
+ });
188
+
189
+ return (
190
+ <View
191
+ style={[
192
+ styles.commandContainer,
193
+ {
194
+ backgroundColor: colors.surface2,
195
+ borderColor: colors.border,
196
+ },
197
+ style,
198
+ ]}
199
+ >
200
+ <View style={styles.commandTextContainer}>
201
+ <Text style={[styles.commandPrompt, { color: colors.statusWarning, fontFamily }]}>
202
+ {prog ? `$ ${prog}` : "$"}
203
+ </Text>
204
+ {rest.length > 0 ? (
205
+ <Text style={[styles.commandArgs, { color: colors.foreground, fontFamily }]}>
206
+ {" "}
207
+ {rest.join(" ")}
208
+ </Text>
209
+ ) : null}
210
+ </View>
211
+ <Pressable
212
+ onPress={handleCopy}
213
+ accessibilityRole="button"
214
+ accessibilityLabel={copyLabel}
215
+ hitSlop={4}
216
+ style={({ pressed }) => [
217
+ styles.commandCopyButton,
218
+ {
219
+ backgroundColor: pressed ? colors.surface1 : "transparent",
220
+ opacity: pressed ? 0.7 : 1,
221
+ },
222
+ ]}
223
+ >
224
+ <Icon
225
+ name={copied ? "Check" : "Copy"}
226
+ size={12}
227
+ color={copied ? colors.statusSuccess : colors.foregroundMuted}
228
+ />
229
+ </Pressable>
230
+ </View>
231
+ );
232
+ }
233
+
234
+ // --- Truncated text --------------------------------------------------------
235
+
236
+ export type HostTruncateMode = "end" | "middle" | "path";
237
+
238
+ export interface HostTruncatedTextProps {
239
+ text: string;
240
+ maxLength?: number;
241
+ mode?: HostTruncateMode;
242
+ copyable?: boolean;
243
+ mono?: boolean;
244
+ toastMessage?: string;
245
+ style?: StyleProp<ViewStyle>;
246
+ textStyle?: StyleProp<TextStyle>;
247
+ }
248
+
249
+ /** Single-line text with truncation and optional copy-to-clipboard. */
250
+ export function HostTruncatedText({
251
+ text,
252
+ maxLength = 32,
253
+ mode = "middle",
254
+ copyable = true,
255
+ mono = true,
256
+ toastMessage = "Copied",
257
+ style,
258
+ textStyle,
259
+ }: HostTruncatedTextProps) {
260
+ const { colors } = useHostTheme();
261
+ const { Icon, useToast } = getClientHost();
262
+ const toast = useToast();
263
+ const [copied, setCopied] = useState(false);
264
+
265
+ if (!text) {
266
+ return <Text style={[{ color: colors.foregroundMuted }, textStyle]}>-</Text>;
267
+ }
268
+
269
+ let formattedText = text;
270
+ if (text.length > maxLength) {
271
+ switch (mode) {
272
+ case "path":
273
+ formattedText = `…${text.slice(-maxLength)}`;
274
+ break;
275
+ case "end":
276
+ formattedText = `${text.slice(0, maxLength)}…`;
277
+ break;
278
+ case "middle":
279
+ default: {
280
+ const half = Math.floor(maxLength / 2);
281
+ formattedText = `${text.slice(0, half)}…${text.slice(-half)}`;
282
+ break;
283
+ }
284
+ }
285
+ }
286
+
287
+ const handleCopy = async () => {
288
+ if (!copyable || !text) return;
289
+ const ok = await copyToClipboard(text, { toast, toastMessage });
290
+ if (ok) {
291
+ setCopied(true);
292
+ setTimeout(() => setCopied(false), 2000);
293
+ }
294
+ };
295
+
296
+ const fontFamily = mono
297
+ ? Platform.select({ ios: "Menlo", android: "monospace", default: "monospace" })
298
+ : undefined;
299
+
300
+ return (
301
+ <View style={[styles.truncatedContainer, style]}>
302
+ <Text
303
+ selectable
304
+ numberOfLines={1}
305
+ ellipsizeMode="clip"
306
+ accessibilityLabel={text}
307
+ style={[
308
+ styles.truncatedText,
309
+ {
310
+ color: colors.foreground,
311
+ fontFamily,
312
+ },
313
+ textStyle,
314
+ ]}
315
+ >
316
+ {formattedText}
317
+ </Text>
318
+ {copyable ? (
319
+ <Pressable
320
+ onPress={handleCopy}
321
+ hitSlop={8}
322
+ style={styles.truncatedCopyBtn}
323
+ accessibilityRole="button"
324
+ accessibilityLabel={`Copy ${text}`}
325
+ >
326
+ <Icon
327
+ name={copied ? "Check" : "Copy"}
328
+ size={13}
329
+ color={copied ? colors.statusSuccess : colors.foregroundMuted}
330
+ />
331
+ </Pressable>
332
+ ) : null}
333
+ </View>
334
+ );
335
+ }
336
+
337
+ // --- Highlighted text ------------------------------------------------------
338
+
339
+ export interface HostHighlightedTextProps {
340
+ text: string;
341
+ query: string;
342
+ style?: StyleProp<TextStyle>;
343
+ highlightStyle?: StyleProp<TextStyle>;
344
+ numberOfLines?: number;
345
+ selectable?: boolean;
346
+ fuzzyFallback?: boolean;
347
+ }
348
+
349
+ interface TextPart {
350
+ text: string;
351
+ matched: boolean;
352
+ }
353
+
354
+ /**
355
+ * Renders text with case-insensitive query matches highlighted using the host
356
+ * accent. The query is matched literally, never as a regex.
357
+ */
358
+ export function HostHighlightedText({
359
+ text,
360
+ query,
361
+ style,
362
+ highlightStyle,
363
+ numberOfLines,
364
+ selectable = true,
365
+ fuzzyFallback = false,
366
+ }: HostHighlightedTextProps) {
367
+ const { colors } = useHostTheme();
368
+
369
+ const parts = splitHighlightParts(text, query, fuzzyFallback);
370
+ const matchStyle: StyleProp<TextStyle> = [
371
+ { backgroundColor: colors.accent, color: colors.accentForeground },
372
+ highlightStyle,
373
+ ];
374
+ return (
375
+ <Text style={style} numberOfLines={numberOfLines} selectable={selectable}>
376
+ {parts.map((part, index) =>
377
+ part.matched ? (
378
+ <Text key={`m${index}`} style={matchStyle}>
379
+ {part.text}
380
+ </Text>
381
+ ) : (
382
+ part.text
383
+ ),
384
+ )}
385
+ </Text>
386
+ );
387
+ }
388
+
389
+ function splitHighlightParts(text: string, query: string, fuzzyFallback: boolean): TextPart[] {
390
+ if (!query) return [{ text, matched: false }];
391
+ const lowerText = text.toLowerCase();
392
+ const lowerQuery = query.toLowerCase();
393
+ const parts: TextPart[] = [];
394
+ let cursor = 0;
395
+ let hit = lowerText.indexOf(lowerQuery, cursor);
396
+ while (hit !== -1) {
397
+ if (hit > cursor) parts.push({ text: text.slice(cursor, hit), matched: false });
398
+ parts.push({ text: text.slice(hit, hit + query.length), matched: true });
399
+ cursor = hit + query.length;
400
+ hit = lowerText.indexOf(lowerQuery, cursor);
401
+ }
402
+ if (cursor < text.length) parts.push({ text: text.slice(cursor), matched: false });
403
+ if (parts.length === 0 || !parts.some((p) => p.matched)) {
404
+ if (fuzzyFallback) return [{ text, matched: true }];
405
+ return [{ text, matched: false }];
406
+ }
407
+ return parts;
408
+ }
409
+
410
+ // --- Responsive ------------------------------------------------------------
411
+
412
+ export interface HostResponsiveProps {
413
+ desktop?: ReactNode;
414
+ mobile?: ReactNode;
415
+ compact?: ReactNode;
416
+ wide?: ReactNode;
417
+ children?: ReactNode | ((responsive: HostResponsiveLayout) => ReactNode);
418
+ }
419
+
420
+ export interface HostResponsiveLayout {
421
+ isCompact: boolean;
422
+ isMobile: boolean;
423
+ isWide: boolean;
424
+ select<T>(options: { desktop?: T; mobile?: T; compact?: T; wide?: T } & Record<string, T>): T | undefined;
425
+ }
426
+
427
+ /** Picks children by host layout (compact / mobile / wide). */
428
+ export function HostResponsive({ desktop, mobile, compact, wide, children }: HostResponsiveProps) {
429
+ const layout = useHostLayout();
430
+ const isMobile = layout.platform === "ios" || layout.platform === "android";
431
+ const isCompact = layout.compact || isMobile;
432
+ const isWide = !isCompact;
433
+
434
+ const responsive: HostResponsiveLayout = {
435
+ isCompact,
436
+ isMobile,
437
+ isWide,
438
+ select: <T,>(options: { desktop?: T; mobile?: T; compact?: T; wide?: T } & Record<string, T>): T | undefined =>
439
+ options.compact ?? options.mobile ?? options.wide ?? options.desktop ?? options.default,
440
+ };
441
+
442
+ if (typeof children === "function") {
443
+ return <>{children(responsive)}</>;
444
+ }
445
+
446
+ const selected = responsive.select<ReactNode>({ desktop, mobile, compact, wide });
447
+ return <>{selected ?? children ?? null}</>;
448
+ }
449
+
450
+ // --- Interactive row -------------------------------------------------------
451
+
452
+ export interface HostInteractiveRowProps {
453
+ children?: ReactNode;
454
+ onPress?: (event: GestureResponderEvent) => void;
455
+ title?: string;
456
+ disabled?: boolean;
457
+ accessibilityRole?: AccessibilityRole;
458
+ accessibilityLabel?: string;
459
+ accessibilityHint?: string;
460
+ testID?: string;
461
+ style?: StyleProp<ViewStyle>;
462
+ pressedOpacity?: number;
463
+ disabledOpacity?: number;
464
+ hitSlop?: number;
465
+ }
466
+
467
+ /** Pressable row with pressed feedback. */
468
+ export function HostInteractiveRow({
469
+ children,
470
+ onPress,
471
+ title,
472
+ disabled = false,
473
+ accessibilityRole,
474
+ accessibilityLabel,
475
+ accessibilityHint,
476
+ testID,
477
+ style,
478
+ pressedOpacity = 0.7,
479
+ disabledOpacity = 0.45,
480
+ hitSlop,
481
+ }: HostInteractiveRowProps) {
482
+ const { colors, alpha: alphaColor } = useHostTheme();
483
+ const [pressed, setPressed] = useState(false);
484
+
485
+ const webProps = {
486
+ onMouseEnter: () => {},
487
+ onMouseLeave: () => {},
488
+ ...(title ? { title } : {}),
489
+ } as any;
490
+
491
+ return (
492
+ <Pressable
493
+ onPress={onPress}
494
+ disabled={disabled}
495
+ accessibilityRole={accessibilityRole}
496
+ accessibilityLabel={accessibilityLabel}
497
+ accessibilityHint={accessibilityHint}
498
+ testID={testID}
499
+ hitSlop={hitSlop}
500
+ onPressIn={() => setPressed(true)}
501
+ onPressOut={() => setPressed(false)}
502
+ style={({ pressed: isPressed }) => [
503
+ styles.interactiveRow,
504
+ {
505
+ backgroundColor: pressed || isPressed ? alphaColor(colors.surface1, 0.5) : "transparent",
506
+ opacity: disabled ? disabledOpacity : pressed || isPressed ? pressedOpacity : 1,
507
+ },
508
+ style,
509
+ ]}
510
+ {...webProps}
511
+ >
512
+ {children}
513
+ </Pressable>
514
+ );
515
+ }
516
+
517
+ // --- About section ---------------------------------------------------------
518
+
519
+ export interface HostAboutLink {
520
+ label: string;
521
+ url: string;
522
+ icon?: string;
523
+ }
524
+
525
+ export interface HostAboutSectionProps {
526
+ name: string;
527
+ description?: string;
528
+ version: string;
529
+ author?: string;
530
+ repository?: string;
531
+ issues?: string;
532
+ homepage?: string;
533
+ license?: string;
534
+ links?: HostAboutLink[];
535
+ extraItems?: Array<{
536
+ label: string;
537
+ value: string;
538
+ subValue?: string;
539
+ copyable?: boolean;
540
+ }>;
541
+ style?: StyleProp<ViewStyle>;
542
+ }
543
+
544
+ /** Plugin identity card: name, version, author, links, and extra key-values. */
545
+ export function HostAboutSection({
546
+ name,
547
+ description,
548
+ version,
549
+ author,
550
+ repository,
551
+ issues,
552
+ homepage,
553
+ license = "MIT",
554
+ links = [],
555
+ extraItems = [],
556
+ style,
557
+ }: HostAboutSectionProps) {
558
+ const { colors } = useHostTheme();
559
+ return (
560
+ <View style={[styles.aboutContainer, style]}>
561
+ <Text style={[styles.aboutName, { color: colors.foreground }]}>{name}</Text>
562
+ {description ? (
563
+ <Text style={[styles.aboutDescription, { color: colors.foregroundMuted }]}>
564
+ {description}
565
+ </Text>
566
+ ) : null}
567
+ <View style={styles.aboutMeta}>
568
+ <HostBadge label={`v${version}`} variant="accent" size="sm" />
569
+ {author ? (
570
+ <Text style={[styles.aboutMetaText, { color: colors.foregroundMuted }]}>{author}</Text>
571
+ ) : null}
572
+ <Text style={[styles.aboutMetaText, { color: colors.foregroundMuted }]}>{license}</Text>
573
+ </View>
574
+ {extraItems.length > 0 ? (
575
+ <View style={styles.aboutItems}>
576
+ {extraItems.map((item) => (
577
+ <HostKeyValue
578
+ key={item.label}
579
+ label={item.label}
580
+ value={item.value}
581
+ subValue={item.subValue}
582
+ copyable={item.copyable}
583
+ />
584
+ ))}
585
+ </View>
586
+ ) : null}
587
+ {links.length > 0 ? (
588
+ <View style={styles.aboutLinks}>
589
+ {links.map((link) => (
590
+ <HostButton
591
+ key={link.label}
592
+ label={link.label}
593
+ variant="ghost"
594
+ size="sm"
595
+ icon={link.icon}
596
+ onPress={() => {
597
+ // External URLs open via the host's Linking when available.
598
+ import("react-native").then(({ Linking }) => {
599
+ Linking?.openURL?.(link.url).catch(() => {});
600
+ });
601
+ }}
602
+ />
603
+ ))}
604
+ </View>
605
+ ) : null}
606
+ {repository ? (
607
+ <Text style={[styles.aboutRepo, { color: colors.foregroundMuted }]}>{repository}</Text>
608
+ ) : null}
609
+ </View>
610
+ );
611
+ }
612
+
613
+ // --- Attention beacon ------------------------------------------------------
614
+
615
+ export type HostAttentionBeaconMode = "radar" | "glow" | "pulse" | "badge";
616
+ export type HostAttentionBeaconTone = "warning" | "accent" | "danger";
617
+
618
+ export interface HostAttentionBeaconProps {
619
+ children: ReactNode;
620
+ mode?: HostAttentionBeaconMode;
621
+ tone?: HostAttentionBeaconTone;
622
+ color?: string;
623
+ active?: boolean;
624
+ style?: StyleProp<ViewStyle>;
625
+ accessibilityLabel?: string;
626
+ testID?: string;
627
+ badgeIcon?: string | ReactNode;
628
+ }
629
+
630
+ /**
631
+ * Attention beacon: a host-themed dot/badge overlay that pulses while active.
632
+ * Thin by design — no halo/radar animation machinery; the host owns motion.
633
+ */
634
+ export function HostAttentionBeacon({
635
+ children,
636
+ mode = "pulse",
637
+ tone = "warning",
638
+ color,
639
+ active = true,
640
+ style,
641
+ accessibilityLabel,
642
+ testID,
643
+ badgeIcon,
644
+ }: HostAttentionBeaconProps) {
645
+ const { Icon } = getClientHost();
646
+ const { colors, getStatusColor } = useHostTheme();
647
+ const beaconColor = color ?? getStatusColor(tone === "warning" ? "warning" : tone === "danger" ? "danger" : "accent");
648
+
649
+ const showDot = active && (mode === "pulse" || mode === "radar" || mode === "glow");
650
+ const showBadge = active && mode === "badge";
651
+
652
+ return (
653
+ <View style={[styles.beaconContainer, style]}>
654
+ {children}
655
+ {showDot ? (
656
+ <View
657
+ testID={testID}
658
+ accessibilityLabel={accessibilityLabel}
659
+ style={[
660
+ styles.beaconDot,
661
+ {
662
+ backgroundColor: beaconColor,
663
+ opacity: mode === "pulse" ? 0.75 : 1,
664
+ },
665
+ ]}
666
+ />
667
+ ) : null}
668
+ {showBadge ? (
669
+ <View style={styles.beaconBadge}>
670
+ {badgeIcon ? (
671
+ typeof badgeIcon === "string" ? (
672
+ <Icon name={badgeIcon} size={10} color={colors.accentForeground} />
673
+ ) : (
674
+ badgeIcon
675
+ )
676
+ ) : (
677
+ <View style={[styles.beaconDot, { backgroundColor: beaconColor }]} />
678
+ )}
679
+ </View>
680
+ ) : null}
681
+ </View>
682
+ );
683
+ }
684
+
685
+ const styles = StyleSheet.create({
686
+ codeContainer: {
687
+ borderWidth: 1,
688
+ borderRadius: 10,
689
+ overflow: "hidden",
690
+ },
691
+ codeHeader: {
692
+ flexDirection: "row",
693
+ alignItems: "center",
694
+ justifyContent: "space-between",
695
+ paddingHorizontal: spacing.md,
696
+ paddingVertical: spacing.xs,
697
+ borderBottomWidth: 1,
698
+ },
699
+ codeHeaderLeft: {
700
+ flex: 1,
701
+ },
702
+ codeTitle: {
703
+ fontSize: 12,
704
+ fontWeight: "600",
705
+ },
706
+ codeLanguage: {
707
+ fontSize: 10,
708
+ fontWeight: "600",
709
+ letterSpacing: 0.5,
710
+ },
711
+ codeCopyButton: {
712
+ borderWidth: 1,
713
+ borderRadius: 6,
714
+ padding: 4,
715
+ },
716
+ codeScrollContent: {
717
+ padding: spacing.md,
718
+ },
719
+ codeText: {
720
+ fontSize: 12,
721
+ lineHeight: 18,
722
+ },
723
+ commandContainer: {
724
+ flexDirection: "row",
725
+ alignItems: "center",
726
+ justifyContent: "space-between",
727
+ gap: spacing.sm,
728
+ borderWidth: 1,
729
+ borderRadius: 8,
730
+ paddingHorizontal: spacing.md,
731
+ paddingVertical: spacing.sm,
732
+ },
733
+ commandTextContainer: {
734
+ flexDirection: "row",
735
+ flex: 1,
736
+ flexShrink: 1,
737
+ },
738
+ commandPrompt: {
739
+ fontSize: 12,
740
+ fontWeight: "700",
741
+ },
742
+ commandArgs: {
743
+ fontSize: 12,
744
+ },
745
+ commandCopyButton: {
746
+ padding: 4,
747
+ },
748
+ truncatedContainer: {
749
+ flexDirection: "row",
750
+ alignItems: "center",
751
+ gap: spacing.xs,
752
+ },
753
+ truncatedText: {
754
+ flex: 1,
755
+ flexShrink: 1,
756
+ fontSize: 12,
757
+ },
758
+ truncatedCopyBtn: {
759
+ padding: 2,
760
+ },
761
+ interactiveRow: {
762
+ flexDirection: "row",
763
+ alignItems: "center",
764
+ borderRadius: 8,
765
+ paddingVertical: spacing.sm,
766
+ paddingHorizontal: spacing.md,
767
+ },
768
+ aboutContainer: {
769
+ gap: spacing.sm,
770
+ alignItems: "flex-start",
771
+ },
772
+ aboutName: {
773
+ fontSize: 16,
774
+ fontWeight: "700",
775
+ },
776
+ aboutDescription: {
777
+ fontSize: 12,
778
+ },
779
+ aboutMeta: {
780
+ flexDirection: "row",
781
+ alignItems: "center",
782
+ gap: spacing.sm,
783
+ },
784
+ aboutMetaText: {
785
+ fontSize: 11,
786
+ },
787
+ aboutItems: {
788
+ gap: spacing.xs,
789
+ width: "100%",
790
+ },
791
+ aboutLinks: {
792
+ flexDirection: "row",
793
+ flexWrap: "wrap",
794
+ gap: spacing.xs,
795
+ },
796
+ aboutRepo: {
797
+ fontSize: 10,
798
+ },
799
+ beaconContainer: {
800
+ position: "relative",
801
+ },
802
+ beaconDot: {
803
+ position: "absolute",
804
+ top: -2,
805
+ right: -2,
806
+ width: 8,
807
+ height: 8,
808
+ borderRadius: 4,
809
+ },
810
+ beaconBadge: {
811
+ position: "absolute",
812
+ top: -6,
813
+ right: -6,
814
+ minWidth: 16,
815
+ height: 16,
816
+ borderRadius: 8,
817
+ alignItems: "center",
818
+ justifyContent: "center",
819
+ backgroundColor: "transparent",
820
+ },
821
+ });