@xpufx/paseo-top 0.4.4 → 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 (129) 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 +94 -1
  6. package/client/pill.tsx +690 -614
  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/package.json +7 -4
  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 +2 -0
  59. package/server/vendor/paseo-plugin-helper/logger.ts +5 -16
  60. package/server/vendor/paseo-plugin-helper/rpc-guard.ts +2 -2
  61. package/server/vendor/paseo-plugin-helper/storage-guard.ts +294 -0
  62. package/server/vendor/paseo-plugin-helper/storage.ts +36 -2
  63. package/server/vendor/paseo-plugin-helper/telemetry.ts +486 -0
  64. package/server/vendor/paseo-plugin-helper/tickets.ts +18 -67
  65. package/server/vendor/permission-audit/index.ts +1 -0
  66. package/server/vendor/permission-audit/server.ts +839 -0
  67. package/shared/helper-version.ts +1 -1
  68. package/shared/multi-host.ts +153 -159
  69. package/shared/resources.ts +145 -0
  70. package/shared/vendor/paseo-plugin-helper/async.ts +14 -7
  71. package/shared/vendor/paseo-plugin-helper/features.ts +244 -0
  72. package/shared/vendor/paseo-plugin-helper/forge.ts +27 -0
  73. package/shared/vendor/paseo-plugin-helper/index.ts +1 -0
  74. package/shared/vendor/paseo-plugin-helper/tickets.ts +122 -28
  75. package/shared/vendor/permission-audit/filter.ts +61 -0
  76. package/shared/vendor/permission-audit/index.ts +2 -0
  77. package/shared/vendor/permission-audit/shared.ts +119 -0
  78. package/shared/version.ts +1 -1
  79. package/client/vendor/paseo-plugin-helper/components/AboutSection.tsx +0 -493
  80. package/client/vendor/paseo-plugin-helper/components/AttentionBeacon.tsx +0 -250
  81. package/client/vendor/paseo-plugin-helper/components/Badge.tsx +0 -168
  82. package/client/vendor/paseo-plugin-helper/components/Button.tsx +0 -204
  83. package/client/vendor/paseo-plugin-helper/components/Card.tsx +0 -257
  84. package/client/vendor/paseo-plugin-helper/components/CodeBlock.tsx +0 -196
  85. package/client/vendor/paseo-plugin-helper/components/Collapsible.tsx +0 -277
  86. package/client/vendor/paseo-plugin-helper/components/CommandBox.tsx +0 -172
  87. package/client/vendor/paseo-plugin-helper/components/CopyButton.tsx +0 -180
  88. package/client/vendor/paseo-plugin-helper/components/EmptyState.tsx +0 -97
  89. package/client/vendor/paseo-plugin-helper/components/HighlightedText.tsx +0 -70
  90. package/client/vendor/paseo-plugin-helper/components/InlineButton.tsx +0 -88
  91. package/client/vendor/paseo-plugin-helper/components/InteractiveRow.tsx +0 -142
  92. package/client/vendor/paseo-plugin-helper/components/KeyValue.tsx +0 -446
  93. package/client/vendor/paseo-plugin-helper/components/MetricGauge.tsx +0 -247
  94. package/client/vendor/paseo-plugin-helper/components/ProgressBar.tsx +0 -117
  95. package/client/vendor/paseo-plugin-helper/components/Responsive.tsx +0 -53
  96. package/client/vendor/paseo-plugin-helper/components/SearchInput.tsx +0 -132
  97. package/client/vendor/paseo-plugin-helper/components/SectionHeader.tsx +0 -87
  98. package/client/vendor/paseo-plugin-helper/components/Select.tsx +0 -283
  99. package/client/vendor/paseo-plugin-helper/components/StatusDot.tsx +0 -80
  100. package/client/vendor/paseo-plugin-helper/components/Tabs.tsx +0 -319
  101. package/client/vendor/paseo-plugin-helper/components/TextInput.tsx +0 -150
  102. package/client/vendor/paseo-plugin-helper/components/Toggle.tsx +0 -163
  103. package/client/vendor/paseo-plugin-helper/components/TruncatedText.tsx +0 -157
  104. package/client/vendor/paseo-plugin-helper/components/index.ts +0 -26
  105. package/client/vendor/paseo-plugin-helper/custom-pills.tsx +0 -225
  106. package/client/vendor/paseo-plugin-helper/forge-icon.tsx +0 -79
  107. package/client/vendor/paseo-plugin-helper/icon.tsx +0 -39
  108. package/client/vendor/paseo-plugin-helper/index.ts +0 -30
  109. package/client/vendor/paseo-plugin-helper/layout/ActionBar.tsx +0 -49
  110. package/client/vendor/paseo-plugin-helper/layout/FormRow.tsx +0 -103
  111. package/client/vendor/paseo-plugin-helper/layout/Grid.tsx +0 -65
  112. package/client/vendor/paseo-plugin-helper/layout/ModalBody.tsx +0 -368
  113. package/client/vendor/paseo-plugin-helper/layout/ModalContent.tsx +0 -51
  114. package/client/vendor/paseo-plugin-helper/layout/Row.tsx +0 -39
  115. package/client/vendor/paseo-plugin-helper/layout/Stack.tsx +0 -39
  116. package/client/vendor/paseo-plugin-helper/layout/index.ts +0 -7
  117. package/client/vendor/paseo-plugin-helper/styles/index.ts +0 -1
  118. package/client/vendor/paseo-plugin-helper/styles/recipes.ts +0 -550
  119. package/client/vendor/paseo-plugin-helper/theme/flair.ts +0 -76
  120. package/client/vendor/paseo-plugin-helper/theme/host-variables.ts +0 -121
  121. package/client/vendor/paseo-plugin-helper/theme/index.ts +0 -7
  122. package/client/vendor/paseo-plugin-helper/theme/provider.tsx +0 -178
  123. package/client/vendor/paseo-plugin-helper/theme/responsive.ts +0 -213
  124. package/client/vendor/paseo-plugin-helper/theme/tokens.ts +0 -200
  125. package/client/vendor/paseo-plugin-helper/theme/useResponsive.ts +0 -57
  126. package/client/vendor/paseo-plugin-helper/tickets.tsx +0 -766
  127. /package/client/vendor/paseo-plugin-helper/{command-center.ts → core/command-center.ts} +0 -0
  128. /package/client/vendor/paseo-plugin-helper/{snapshot.ts → core/snapshot.ts} +0 -0
  129. /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
+ });