paseo-plugin-genui 0.0.0-stage → 0.1.1

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 (375) hide show
  1. package/LICENSE +202 -0
  2. package/OVERVIEW.md +21 -0
  3. package/README.md +77 -2
  4. package/assets/fonts/Excalifont-OFL.txt +109 -0
  5. package/assets/fonts/Excalifont-Regular-latin.woff2 +0 -0
  6. package/assets/icon.png +0 -0
  7. package/assets/icon.svg +1 -0
  8. package/assets/screenshot-chat.png +0 -0
  9. package/assets/screenshot-gallery.png +0 -0
  10. package/assets/screenshot-playground.png +0 -0
  11. package/client/components/content.tsx +468 -0
  12. package/client/components/data.tsx +967 -0
  13. package/client/components/dev.tsx +998 -0
  14. package/client/components/diagram.tsx +513 -0
  15. package/client/components/inputs.tsx +994 -0
  16. package/client/components/layout.tsx +239 -0
  17. package/client/components/media.tsx +428 -0
  18. package/client/components/project.tsx +641 -0
  19. package/client/components/widgets.tsx +836 -0
  20. package/client/gallery.tsx +449 -0
  21. package/client/live-ui.tsx +525 -0
  22. package/client/loaders.ts +342 -0
  23. package/client/markdown.tsx +172 -0
  24. package/client/node-renderer.tsx +149 -0
  25. package/client/render.tsx +186 -0
  26. package/client/runtime.ts +103 -0
  27. package/client/settings.tsx +57 -0
  28. package/client/web.ts +186 -0
  29. package/gallery/Acceptance-baseline.ui +1 -0
  30. package/gallery/Acceptance-checklist.ui +14 -0
  31. package/gallery/Acceptance-evidence.ui +9 -0
  32. package/gallery/Acceptance.ui +1 -0
  33. package/gallery/Badge.ui +3 -0
  34. package/gallery/Benchmark-context.ui +8 -0
  35. package/gallery/Benchmark-hyperfine.ui +1 -0
  36. package/gallery/Benchmark-runs-list.ui +9 -0
  37. package/gallery/Benchmark-runs.ui +1 -0
  38. package/gallery/Benchmark-vitest.ui +1 -0
  39. package/gallery/Benchmark.ui +9 -0
  40. package/gallery/Board-bb-tasks.ui +1 -0
  41. package/gallery/Board-gh-issues.ui +1 -0
  42. package/gallery/Board-gh-prs.ui +1 -0
  43. package/gallery/Board.ui +12 -0
  44. package/gallery/Button-destructive.ui +4 -0
  45. package/gallery/Button.ui +5 -0
  46. package/gallery/Calendar.ui +15 -0
  47. package/gallery/CalendarHeatmap-short.ui +11 -0
  48. package/gallery/CalendarHeatmap.ui +4 -0
  49. package/gallery/Callout.ui +4 -0
  50. package/gallery/Card.ui +4 -0
  51. package/gallery/Carousel.ui +8 -0
  52. package/gallery/Changes-paths.ui +1 -0
  53. package/gallery/Changes-uncommitted.ui +1 -0
  54. package/gallery/Changes.ui +1 -0
  55. package/gallery/Chart-area.ui +3 -0
  56. package/gallery/Chart-funnel.ui +3 -0
  57. package/gallery/Chart-heatmap.ui +11 -0
  58. package/gallery/Chart-pie.ui +3 -0
  59. package/gallery/Chart-radar.ui +3 -0
  60. package/gallery/Chart-scatter.ui +4 -0
  61. package/gallery/Chart-stacked.ui +5 -0
  62. package/gallery/Chart.ui +3 -0
  63. package/gallery/Checklist.ui +6 -0
  64. package/gallery/Choice.ui +8 -0
  65. package/gallery/Code.ui +3 -0
  66. package/gallery/Commits-limit.ui +1 -0
  67. package/gallery/Commits.ui +1 -0
  68. package/gallery/Compare-conditions.ui +4 -0
  69. package/gallery/Compare-three.ui +7 -0
  70. package/gallery/Compare.ui +4 -0
  71. package/gallery/Coverage-go.ui +1 -0
  72. package/gallery/Coverage-lcov.ui +1 -0
  73. package/gallery/Coverage-python.ui +1 -0
  74. package/gallery/Coverage.ui +1 -0
  75. package/gallery/DateInput.ui +5 -0
  76. package/gallery/Decision-percent.ui +10 -0
  77. package/gallery/Decision.ui +12 -0
  78. package/gallery/Dependencies-audit.ui +1 -0
  79. package/gallery/Dependencies-both.ui +1 -0
  80. package/gallery/Dependencies-list.ui +7 -0
  81. package/gallery/Dependencies-pip.ui +1 -0
  82. package/gallery/Dependencies-pnpm.ui +1 -0
  83. package/gallery/Dependencies.ui +1 -0
  84. package/gallery/Details.ui +4 -0
  85. package/gallery/Diagram-flow.ui +16 -0
  86. package/gallery/Diagram-sequence.ui +15 -0
  87. package/gallery/Diagram.ui +16 -0
  88. package/gallery/Diff.ui +3 -0
  89. package/gallery/Divider.ui +5 -0
  90. package/gallery/EditableTable.ui +9 -0
  91. package/gallery/Entity-list.ui +6 -0
  92. package/gallery/Entity.ui +8 -0
  93. package/gallery/Environments.ui +1 -0
  94. package/gallery/Estimate-points.ui +6 -0
  95. package/gallery/Estimate.ui +8 -0
  96. package/gallery/File.ui +4 -0
  97. package/gallery/Findings-biome.ui +1 -0
  98. package/gallery/Findings-eslint.ui +1 -0
  99. package/gallery/Findings-oxlint.ui +1 -0
  100. package/gallery/Findings-review.ui +1 -0
  101. package/gallery/Findings-ruff.ui +1 -0
  102. package/gallery/Findings-sarif.ui +1 -0
  103. package/gallery/Findings-ship.ui +11 -0
  104. package/gallery/Findings-tsc.ui +1 -0
  105. package/gallery/Findings.ui +7 -0
  106. package/gallery/Flashcards.ui +3 -0
  107. package/gallery/FollowUps.ui +4 -0
  108. package/gallery/Form.ui +8 -0
  109. package/gallery/Gallery-links.ui +8 -0
  110. package/gallery/Gallery-strip.ui +14 -0
  111. package/gallery/Gallery.ui +10 -0
  112. package/gallery/Gauge.ui +7 -0
  113. package/gallery/Grid.ui +8 -0
  114. package/gallery/Hotspots-photo.ui +8 -0
  115. package/gallery/Hotspots.ui +11 -0
  116. package/gallery/Icon.ui +5 -0
  117. package/gallery/Image.ui +4 -0
  118. package/gallery/ImageSearch-aspects.ui +6 -0
  119. package/gallery/ImageSearch.ui +4 -0
  120. package/gallery/Input.ui +5 -0
  121. package/gallery/Json-api.ui +1 -0
  122. package/gallery/Json.ui +8 -0
  123. package/gallery/KeyValue.ui +3 -0
  124. package/gallery/List-dots.ui +8 -0
  125. package/gallery/List.ui +8 -0
  126. package/gallery/ListEditor.ui +5 -0
  127. package/gallery/Logs-jsonl.ui +1 -0
  128. package/gallery/Logs-lines.ui +11 -0
  129. package/gallery/Logs-python.ui +1 -0
  130. package/gallery/Logs.ui +1 -0
  131. package/gallery/Map-city.ui +8 -0
  132. package/gallery/Map.ui +8 -0
  133. package/gallery/Metric.ui +3 -0
  134. package/gallery/OptionCards-multi.ui +10 -0
  135. package/gallery/OptionCards.ui +10 -0
  136. package/gallery/Pipeline-checks.ui +1 -0
  137. package/gallery/Pipeline-passed.ui +1 -0
  138. package/gallery/Pipeline-running.ui +1 -0
  139. package/gallery/Pipeline-stages.ui +13 -0
  140. package/gallery/Pipeline.ui +1 -0
  141. package/gallery/Plan.ui +9 -0
  142. package/gallery/Progress.ui +4 -0
  143. package/gallery/Provenance-deploy.ui +6 -0
  144. package/gallery/Provenance-no-receipt.ui +1 -0
  145. package/gallery/Provenance.ui +1 -0
  146. package/gallery/PullRequest.ui +1 -0
  147. package/gallery/Quiz.ui +6 -0
  148. package/gallery/Radio.ui +5 -0
  149. package/gallery/Rating.ui +5 -0
  150. package/gallery/Roadmap-sprint.ui +8 -0
  151. package/gallery/Roadmap-year.ui +9 -0
  152. package/gallery/Roadmap.ui +12 -0
  153. package/gallery/Row.ui +3 -0
  154. package/gallery/SegmentedBar-status.ui +8 -0
  155. package/gallery/SegmentedBar.ui +8 -0
  156. package/gallery/Select.ui +5 -0
  157. package/gallery/Slider.ui +7 -0
  158. package/gallery/Snippet.ui +3 -0
  159. package/gallery/Sources-chips.ui +4 -0
  160. package/gallery/Sources.ui +8 -0
  161. package/gallery/Sparkline.ui +6 -0
  162. package/gallery/Stack.ui +7 -0
  163. package/gallery/StackTrace-aggregate.ui +1 -0
  164. package/gallery/StackTrace-browser.ui +1 -0
  165. package/gallery/StackTrace-go.ui +1 -0
  166. package/gallery/StackTrace-java.ui +1 -0
  167. package/gallery/StackTrace-python.ui +1 -0
  168. package/gallery/StackTrace-rust.ui +1 -0
  169. package/gallery/StackTrace.ui +1 -0
  170. package/gallery/Status.ui +9 -0
  171. package/gallery/Stepper.ui +5 -0
  172. package/gallery/Steps.ui +8 -0
  173. package/gallery/Swatches-long-names.ui +4 -0
  174. package/gallery/Swatches-picker.ui +5 -0
  175. package/gallery/Swatches.ui +10 -0
  176. package/gallery/Table.ui +7 -0
  177. package/gallery/Tabs.ui +8 -0
  178. package/gallery/Tests-junit.ui +1 -0
  179. package/gallery/Tests-vitest-junit.ui +1 -0
  180. package/gallery/Tests-vitest.ui +1 -0
  181. package/gallery/Tests.ui +9 -0
  182. package/gallery/Text.ui +4 -0
  183. package/gallery/TextArea.ui +5 -0
  184. package/gallery/Threads-ids.ui +1 -0
  185. package/gallery/Threads.ui +1 -0
  186. package/gallery/Timeline.ui +8 -0
  187. package/gallery/Timer.ui +4 -0
  188. package/gallery/Toggle.ui +5 -0
  189. package/gallery/Transcript-text.ui +1 -0
  190. package/gallery/Transcript-untimed.ui +1 -0
  191. package/gallery/Transcript-whisper.ui +1 -0
  192. package/gallery/Transcript.ui +1 -0
  193. package/gallery/Tree.ui +3 -0
  194. package/gallery/Treemap-du.ui +1 -0
  195. package/gallery/Treemap-esbuild.ui +1 -0
  196. package/gallery/Treemap-tree.ui +7 -0
  197. package/gallery/Treemap.ui +18 -0
  198. package/gallery/Walkthrough.ui +9 -0
  199. package/gallery/Waterfall-har.ui +1 -0
  200. package/gallery/Waterfall-trace.ui +1 -0
  201. package/gallery/Waterfall.ui +11 -0
  202. package/gallery/data/acceptance/coil/viewport-lifecycle.json +25 -0
  203. package/gallery/data/acceptance/coil-gate.json +153 -0
  204. package/gallery/data/acceptance/orbit-baseline.json +163 -0
  205. package/gallery/data/acceptance/shots/gate-cell-opened.png +0 -0
  206. package/gallery/data/acceptance/shots/gate-light.png +0 -0
  207. package/gallery/data/acceptance/shots/gate-phone-dark.png +0 -0
  208. package/gallery/data/activity.json +1 -0
  209. package/gallery/data/bb-tasks-orb.json +420 -0
  210. package/gallery/data/biome.json +229 -0
  211. package/gallery/data/compare/status-after.png +0 -0
  212. package/gallery/data/compare/status-before.png +0 -0
  213. package/gallery/data/compare/status-dark-2x.png +0 -0
  214. package/gallery/data/compare/status-rerender.png +0 -0
  215. package/gallery/data/coverage-py.json +1 -0
  216. package/gallery/data/coverage-summary.json +1 -0
  217. package/gallery/data/coverage.out +23 -0
  218. package/gallery/data/deploys.json +1 -0
  219. package/gallery/data/device-runs.json +285 -0
  220. package/gallery/data/du-node-modules.txt +359 -0
  221. package/gallery/data/esbuild-meta.json +292 -0
  222. package/gallery/data/eslint.json +266 -0
  223. package/gallery/data/eslint.sarif +343 -0
  224. package/gallery/data/findings-review.json +76 -0
  225. package/gallery/data/gh-issues-nextjs.json +293 -0
  226. package/gallery/data/gh-pr-checks.json +272 -0
  227. package/gallery/data/gh-prs-nextjs.json +419 -0
  228. package/gallery/data/gh-run-failed.json +1 -0
  229. package/gallery/data/gh-run-passed.json +1 -0
  230. package/gallery/data/gh-run-running.json +1 -0
  231. package/gallery/data/github-release.json +57 -0
  232. package/gallery/data/hyperfine-startup.json +187 -0
  233. package/gallery/data/lcov.info +234 -0
  234. package/gallery/data/logs/media-sync.log +127 -0
  235. package/gallery/data/logs/thumbnailer.jsonl +17 -0
  236. package/gallery/data/logs/vitest-run.log +89 -0
  237. package/gallery/data/npm-audit.json +672 -0
  238. package/gallery/data/npm-outdated.json +179 -0
  239. package/gallery/data/oxlint.json +156 -0
  240. package/gallery/data/pip-outdated.json +68 -0
  241. package/gallery/data/pnpm-outdated.json +58 -0
  242. package/gallery/data/provenance/devicectl-install.json +35 -0
  243. package/gallery/data/provenance/install-timeout.log +4 -0
  244. package/gallery/data/provenance/iphone-install.json +72 -0
  245. package/gallery/data/provenance/iphone-no-receipt.json +57 -0
  246. package/gallery/data/provenance/live-home.jpg +0 -0
  247. package/gallery/data/provenance/live-probe.txt +10 -0
  248. package/gallery/data/provenance/xcodebuild-2811.log +19 -0
  249. package/gallery/data/provenance/xcodebuild-4712.log +19 -0
  250. package/gallery/data/pytest-junit.xml +64 -0
  251. package/gallery/data/python-org-trace.json +35 -0
  252. package/gallery/data/python-org.har +31 -0
  253. package/gallery/data/retry.ts +13 -0
  254. package/gallery/data/ruff.json +261 -0
  255. package/gallery/data/src/durations.py +63 -0
  256. package/gallery/data/src/json-logic.ts +156 -0
  257. package/gallery/data/src/mapMath.ts +135 -0
  258. package/gallery/data/src/semver/bump.go +73 -0
  259. package/gallery/data/traces/browser-error.json +5 -0
  260. package/gallery/data/traces/go/main.go +27 -0
  261. package/gallery/data/traces/go-panic.txt +16 -0
  262. package/gallery/data/traces/java-caused-by.txt +15 -0
  263. package/gallery/data/traces/node-aggregate.txt +30 -0
  264. package/gallery/data/traces/node-cause.txt +21 -0
  265. package/gallery/data/traces/py/deploy.json +5 -0
  266. package/gallery/data/traces/py/deploy.py +17 -0
  267. package/gallery/data/traces/py/settings.py +19 -0
  268. package/gallery/data/traces/python-chain.txt +30 -0
  269. package/gallery/data/traces/rust/main.rs +11 -0
  270. package/gallery/data/traces/rust-panic.txt +16 -0
  271. package/gallery/data/traces/shop/cart.mjs +19 -0
  272. package/gallery/data/traces/shop/pricing.mjs +12 -0
  273. package/gallery/data/traces/shop/status.mjs +9 -0
  274. package/gallery/data/traces/web/app.js +11 -0
  275. package/gallery/data/traces/web/index.html +1 -0
  276. package/gallery/data/traces/web/search.js +10 -0
  277. package/gallery/data/transcript/call-noisy.wav +0 -0
  278. package/gallery/data/transcript/call.json +362 -0
  279. package/gallery/data/transcript/call.wav +0 -0
  280. package/gallery/data/transcript/mlx-whisper-tiny.en-noisy.json +1 -0
  281. package/gallery/data/transcript/whisper-cpp-base.en-noisy.json +658 -0
  282. package/gallery/data/transcript/whisper-cpp-base.en.json +658 -0
  283. package/gallery/data/transcript/whisper-cpp-base.en.srt +16 -0
  284. package/gallery/data/transcript/whisper-cpp-tiny.en-noisy.json +619 -0
  285. package/gallery/data/tsc.txt +4 -0
  286. package/gallery/data/vitest-bench.json +119 -0
  287. package/gallery/data/vitest-junit.xml +133 -0
  288. package/gallery/data/vitest-report.json +612 -0
  289. package/gallery/images/bell.svg +10 -0
  290. package/gallery/images/bike.svg +47 -0
  291. package/gallery/images/footer-planet.webp +0 -0
  292. package/gallery/images/front-light.svg +13 -0
  293. package/gallery/images/golden-hour.webp +0 -0
  294. package/gallery/images/option-flowers.webp +0 -0
  295. package/gallery/images/option-hilltop.webp +0 -0
  296. package/gallery/images/option-meadow.webp +0 -0
  297. package/gallery/images/option-sky.webp +0 -0
  298. package/gallery/images/u-lock.svg +10 -0
  299. package/index.client.tsx +164 -0
  300. package/index.server.ts +216 -0
  301. package/package.json +50 -4
  302. package/paseo-plugin.json +14 -0
  303. package/server/data-source.ts +461 -0
  304. package/server/project.ts +981 -0
  305. package/server/web/address.ts +57 -0
  306. package/server/web/cache.ts +85 -0
  307. package/server/web/commons.ts +126 -0
  308. package/server/web/geocode.ts +40 -0
  309. package/server/web/image.ts +35 -0
  310. package/server/web/lookups.ts +248 -0
  311. package/server/web/net.ts +210 -0
  312. package/shared/core/acceptance.ts +744 -0
  313. package/shared/core/ast.ts +76 -0
  314. package/shared/core/benchmark.ts +743 -0
  315. package/shared/core/catalog.ts +733 -0
  316. package/shared/core/compare.ts +225 -0
  317. package/shared/core/coverage.ts +456 -0
  318. package/shared/core/data.ts +334 -0
  319. package/shared/core/decisions.ts +527 -0
  320. package/shared/core/dependencies.ts +604 -0
  321. package/shared/core/diagnostics.ts +24 -0
  322. package/shared/core/environments.ts +212 -0
  323. package/shared/core/evaluate.ts +737 -0
  324. package/shared/core/fields.ts +66 -0
  325. package/shared/core/findings.ts +1172 -0
  326. package/shared/core/form.ts +69 -0
  327. package/shared/core/index.ts +44 -0
  328. package/shared/core/junit.ts +252 -0
  329. package/shared/core/lexer.ts +275 -0
  330. package/shared/core/logs.ts +496 -0
  331. package/shared/core/markdown.ts +2374 -0
  332. package/shared/core/parser.ts +355 -0
  333. package/shared/core/perf-units.ts +228 -0
  334. package/shared/core/pipeline.ts +679 -0
  335. package/shared/core/project.ts +472 -0
  336. package/shared/core/provenance.ts +725 -0
  337. package/shared/core/stack-trace.ts +718 -0
  338. package/shared/core/streaming.ts +136 -0
  339. package/shared/core/test-results.ts +562 -0
  340. package/shared/core/thread-activity.ts +203 -0
  341. package/shared/core/tracking.ts +887 -0
  342. package/shared/core/transcript.ts +1364 -0
  343. package/shared/core/treemap.ts +446 -0
  344. package/shared/core/types.ts +70 -0
  345. package/shared/core/validate.ts +574 -0
  346. package/shared/core/waterfall.ts +536 -0
  347. package/shared/diagram/flowchart.ts +205 -0
  348. package/shared/diagram/layout.ts +440 -0
  349. package/shared/diagram/markdown.ts +135 -0
  350. package/shared/diagram/sequence.ts +122 -0
  351. package/shared/diagram/zoom.ts +114 -0
  352. package/shared/fallback.ts +120 -0
  353. package/shared/genui.test.ts +182 -0
  354. package/shared/instructions.ts +144 -0
  355. package/shared/logic/calendarMath.ts +130 -0
  356. package/shared/logic/chartMath.ts +289 -0
  357. package/shared/logic/choose-logic.ts +196 -0
  358. package/shared/logic/compare-logic.ts +270 -0
  359. package/shared/logic/content-model.ts +227 -0
  360. package/shared/logic/environments-logic.ts +53 -0
  361. package/shared/logic/explain-logic.ts +315 -0
  362. package/shared/logic/fieldLogic.ts +138 -0
  363. package/shared/logic/gallery-search.ts +71 -0
  364. package/shared/logic/icons.ts +322 -0
  365. package/shared/logic/json-logic.ts +153 -0
  366. package/shared/logic/mapMath.ts +133 -0
  367. package/shared/logic/project-logic.ts +39 -0
  368. package/shared/logic/references-model.ts +201 -0
  369. package/shared/logic/sketch.ts +165 -0
  370. package/shared/logic/supersession.ts +77 -0
  371. package/shared/logic/values.ts +133 -0
  372. package/shared/logic/widget-logic.ts +163 -0
  373. package/shared/rpc.ts +252 -0
  374. package/shared/segment.ts +81 -0
  375. package/shared/settings.ts +12 -0
@@ -0,0 +1,239 @@
1
+ import { Icon } from "@getpaseo/plugin/client/react-native";
2
+ import React, { useContext, useState } from "react";
3
+ import { Pressable, ScrollView, StyleSheet, Text, View } from "react-native";
4
+ import { finiteNumber, list, nodes, optionalText, text, type Props } from "../../shared/logic/values";
5
+ import { RenderNode, RenderNodes } from "../node-renderer";
6
+ import { CardDepthContext, propsKey, usePersistedState, useUiRuntime } from "../runtime";
7
+
8
+ export function Card({ props }: { props: Props }) {
9
+ const { theme, compact } = useUiRuntime();
10
+ const depth = useContext(CardDepthContext);
11
+ const title = optionalText(props.title);
12
+ const children = nodes(props.children);
13
+
14
+ return (
15
+ <CardDepthContext.Provider value={depth + 1}>
16
+ <View
17
+ style={{
18
+ borderRadius: 12,
19
+ borderWidth: StyleSheet.hairlineWidth,
20
+ borderColor: theme.colors.border,
21
+ backgroundColor: depth === 0 ? theme.colors.surface1 : theme.colors.surface2,
22
+ padding: compact ? 12 : 16,
23
+ gap: compact ? 10 : 12,
24
+ }}
25
+ >
26
+ {title !== null && title.trim() !== "" ? (
27
+ <Text
28
+ selectable
29
+ style={{
30
+ color: theme.colors.foreground,
31
+ fontSize: depth === 0 ? 15 : 13.5,
32
+ fontWeight: "600",
33
+ }}
34
+ >
35
+ {title}
36
+ </Text>
37
+ ) : null}
38
+ <View style={{ gap: compact ? 8 : 10 }}>
39
+ <RenderNodes nodes={children} />
40
+ </View>
41
+ </View>
42
+ </CardDepthContext.Provider>
43
+ );
44
+ }
45
+
46
+ export function Row({ props }: { props: Props }) {
47
+ const { compact } = useUiRuntime();
48
+ const children = nodes(props.children);
49
+ return (
50
+ <View
51
+ style={{
52
+ flexDirection: compact ? "column" : "row",
53
+ flexWrap: "wrap",
54
+ gap: compact ? 8 : 12,
55
+ alignItems: "stretch",
56
+ }}
57
+ >
58
+ {children.map((child, idx) => (
59
+ <View key={`${child.key}:${idx}`} style={compact ? { width: "100%" } : { flex: 1, minWidth: 160 }}>
60
+ <RenderNode node={child} />
61
+ </View>
62
+ ))}
63
+ </View>
64
+ );
65
+ }
66
+
67
+ export function Stack({ props }: { props: Props }) {
68
+ const { compact } = useUiRuntime();
69
+ const children = nodes(props.children);
70
+ return (
71
+ <View style={{ gap: compact ? 8 : 10 }}>
72
+ <RenderNodes nodes={children} />
73
+ </View>
74
+ );
75
+ }
76
+
77
+ export function Grid({ props }: { props: Props }) {
78
+ const { compact } = useUiRuntime();
79
+ const children = nodes(props.children);
80
+ const requestedCols = finiteNumber(props.columns);
81
+ const cols = compact ? 1 : Math.max(1, Math.min(4, requestedCols ? Math.round(requestedCols) : 2));
82
+ const rows: typeof children[] = [];
83
+ for (let i = 0; i < children.length; i += cols) {
84
+ rows.push(children.slice(i, i + cols));
85
+ }
86
+ return (
87
+ <View style={{ gap: compact ? 8 : 10 }}>
88
+ {rows.map((row, rIdx) => (
89
+ <View key={rIdx} style={{ flexDirection: compact ? "column" : "row", gap: compact ? 8 : 10 }}>
90
+ {row.map((child, cIdx) => (
91
+ <View key={`${child.key}:${cIdx}`} style={{ flex: 1 }}>
92
+ <RenderNode node={child} />
93
+ </View>
94
+ ))}
95
+ </View>
96
+ ))}
97
+ </View>
98
+ );
99
+ }
100
+
101
+ export function Tabs({ props }: { props: Props }) {
102
+ const { theme } = useUiRuntime();
103
+ const labels = list(props.labels).map((item) => text(item));
104
+ const children = nodes(props.children);
105
+ const [selected, setSelected] = usePersistedState<number>(
106
+ `tabs:${propsKey(labels)}`,
107
+ 0,
108
+ (v) => (typeof v === "number" && v >= 0 && v < labels.length ? v : null),
109
+ );
110
+ const activeIdx = Math.min(Math.max(0, selected), Math.max(0, labels.length - 1));
111
+ const activeChild = children[activeIdx];
112
+
113
+ return (
114
+ <View style={{ gap: 10 }}>
115
+ <ScrollView horizontal showsHorizontalScrollIndicator={false}>
116
+ <View
117
+ style={{
118
+ flexDirection: "row",
119
+ gap: 4,
120
+ borderBottomWidth: StyleSheet.hairlineWidth,
121
+ borderBottomColor: theme.colors.border,
122
+ paddingBottom: 6,
123
+ }}
124
+ >
125
+ {labels.map((label, index) => {
126
+ const active = index === activeIdx;
127
+ return (
128
+ <Pressable
129
+ key={`${label}:${index}`}
130
+ accessibilityRole="tab"
131
+ onPress={() => setSelected(index)}
132
+ style={{
133
+ paddingHorizontal: 10,
134
+ paddingVertical: 5,
135
+ borderRadius: 6,
136
+ backgroundColor: active ? theme.colors.surface2 : "transparent",
137
+ }}
138
+ >
139
+ <Text
140
+ style={{
141
+ color: active ? theme.colors.foreground : theme.colors.foregroundMuted,
142
+ fontSize: 12.5,
143
+ fontWeight: active ? "600" : "400",
144
+ }}
145
+ >
146
+ {label}
147
+ </Text>
148
+ </Pressable>
149
+ );
150
+ })}
151
+ </View>
152
+ </ScrollView>
153
+ {activeChild ? <RenderNode node={activeChild} /> : null}
154
+ </View>
155
+ );
156
+ }
157
+
158
+ export function Details({ props }: { props: Props }) {
159
+ const { theme } = useUiRuntime();
160
+ const title = text(props.title);
161
+ const children = nodes(props.children);
162
+ const initialOpen = props.open === true;
163
+ const [open, setOpen] = useState(initialOpen);
164
+
165
+ return (
166
+ <View
167
+ style={{
168
+ borderRadius: 8,
169
+ borderWidth: StyleSheet.hairlineWidth,
170
+ borderColor: theme.colors.border,
171
+ backgroundColor: theme.colors.surface2,
172
+ overflow: "hidden",
173
+ }}
174
+ >
175
+ <Pressable
176
+ accessibilityRole="button"
177
+ onPress={() => setOpen((prev) => !prev)}
178
+ style={{
179
+ flexDirection: "row",
180
+ alignItems: "center",
181
+ justifyContent: "space-between",
182
+ paddingHorizontal: 12,
183
+ paddingVertical: 9,
184
+ gap: 8,
185
+ }}
186
+ >
187
+ <Text style={{ color: theme.colors.foreground, fontSize: 13, fontWeight: "500", flex: 1 }}>
188
+ {title}
189
+ </Text>
190
+ <Icon
191
+ name={open ? "ChevronDown" : "ChevronRight"}
192
+ size={14}
193
+ color={theme.colors.foregroundMuted}
194
+ />
195
+ </Pressable>
196
+ {open ? (
197
+ <View
198
+ style={{
199
+ borderTopWidth: StyleSheet.hairlineWidth,
200
+ borderTopColor: theme.colors.border,
201
+ padding: 12,
202
+ gap: 8,
203
+ }}
204
+ >
205
+ <RenderNodes nodes={children} />
206
+ </View>
207
+ ) : null}
208
+ </View>
209
+ );
210
+ }
211
+
212
+ export function Divider() {
213
+ const { theme } = useUiRuntime();
214
+ return (
215
+ <View
216
+ style={{
217
+ height: StyleSheet.hairlineWidth,
218
+ backgroundColor: theme.colors.border,
219
+ marginVertical: 4,
220
+ }}
221
+ />
222
+ );
223
+ }
224
+
225
+ export function Carousel({ props }: { props: Props }) {
226
+ const { compact } = useUiRuntime();
227
+ const children = nodes(props.children);
228
+ return (
229
+ <ScrollView horizontal showsHorizontalScrollIndicator={!compact}>
230
+ <View style={{ flexDirection: "row", gap: 12, paddingVertical: 2 }}>
231
+ {children.map((child, idx) => (
232
+ <View key={`${child.key}:${idx}`} style={{ width: compact ? 260 : 320 }}>
233
+ <RenderNode node={child} />
234
+ </View>
235
+ ))}
236
+ </View>
237
+ </ScrollView>
238
+ );
239
+ }
@@ -0,0 +1,428 @@
1
+ import { Icon, Modal } from "@getpaseo/plugin/client/react-native";
2
+ import { ExternalLink } from "@getpaseo/plugin/client/ui";
3
+ import React, { useEffect, useMemo, useState } from "react";
4
+ import { Image as RNImage, Pressable, StyleSheet, Text, View } from "react-native";
5
+ import { fitView, project, tileZoom, toScreen, visibleTiles, zoomAround } from "../../shared/logic/mapMath";
6
+ import { hotspotPoints, imageRef } from "../../shared/logic/references-model";
7
+ import { asRecord, finiteNumber, list, optionalText, text, type Props } from "../../shared/logic/values";
8
+ import { useGeocodePlace, useImageSearch, useMapTileLoader } from "../loaders";
9
+ import { ErrorLine } from "../node-renderer";
10
+ import { useUiRuntime } from "../runtime";
11
+ import { LoadedImageView } from "./content";
12
+
13
+ export function ImageSearch({ props }: { props: Props }) {
14
+ const { theme } = useUiRuntime();
15
+ const query = text(props.query);
16
+ const alt = optionalText(props.alt) ?? query;
17
+ const aspect = optionalText(props.aspect) ?? "wide";
18
+ const found = useImageSearch(query);
19
+ const height = aspect === "tall" ? 260 : aspect === "square" ? 220 : 180;
20
+
21
+ if (!query.trim()) return <ErrorLine message="ImageSearch query is empty" />;
22
+ if (found === null) {
23
+ return (
24
+ <View
25
+ style={{
26
+ height,
27
+ borderRadius: 8,
28
+ backgroundColor: theme.colors.surface2,
29
+ alignItems: "center",
30
+ justifyContent: "center",
31
+ }}
32
+ >
33
+ <Text style={{ color: theme.colors.foregroundMuted, fontSize: 12 }}>
34
+ Searching photo for “{query}”…
35
+ </Text>
36
+ </View>
37
+ );
38
+ }
39
+ if (!found.ok) {
40
+ return (
41
+ <View
42
+ style={{
43
+ borderRadius: 8,
44
+ borderWidth: StyleSheet.hairlineWidth,
45
+ borderColor: theme.colors.border,
46
+ backgroundColor: theme.colors.surface2,
47
+ padding: 10,
48
+ }}
49
+ >
50
+ <Text style={{ color: theme.colors.foregroundMuted, fontSize: 12 }}>{found.error}</Text>
51
+ </View>
52
+ );
53
+ }
54
+
55
+ return (
56
+ <View style={{ gap: 4 }}>
57
+ <LoadedImageView src={found.image.url} alt={alt} height={height} />
58
+ <View style={{ flexDirection: "row", flexWrap: "wrap", gap: 6 }}>
59
+ <ExternalLink href={found.image.sourceUrl}>
60
+ <Text style={{ color: theme.colors.foregroundMuted, fontSize: 11 }}>
61
+ {found.image.title}
62
+ </Text>
63
+ </ExternalLink>
64
+ {found.image.creator || found.image.license ? (
65
+ <Text style={{ color: theme.colors.foregroundMuted, fontSize: 11 }}>
66
+ · {[found.image.creator, found.image.license].filter(Boolean).join(" · ")}
67
+ </Text>
68
+ ) : null}
69
+ </View>
70
+ </View>
71
+ );
72
+ }
73
+
74
+ export function Gallery({ props }: { props: Props }) {
75
+ const { theme, compact } = useUiRuntime();
76
+ const rawItems = list(props.images);
77
+ const cols = compact ? 1 : Math.max(1, Math.min(4, finiteNumber(props.columns) ?? 2));
78
+ const [selected, setSelected] = useState<{ src?: string; query?: string; caption: string | null } | null>(
79
+ null,
80
+ );
81
+
82
+ const items = rawItems.map((item) => {
83
+ if (typeof item === "string") return { src: item, query: undefined, caption: null };
84
+ const rec = asRecord(item);
85
+ if (rec) {
86
+ return {
87
+ src: optionalText(rec.src) ?? undefined,
88
+ query: optionalText(rec.query) ?? undefined,
89
+ caption: optionalText(rec.caption),
90
+ };
91
+ }
92
+ return { src: undefined, query: undefined, caption: null };
93
+ });
94
+
95
+ const rows: (typeof items)[] = [];
96
+ for (let i = 0; i < items.length; i += cols) rows.push(items.slice(i, i + cols));
97
+
98
+ return (
99
+ <View style={{ gap: 8 }}>
100
+ {rows.map((row, rIdx) => (
101
+ <View key={rIdx} style={{ flexDirection: compact ? "column" : "row", gap: 8 }}>
102
+ {row.map((item, cIdx) => (
103
+ <Pressable
104
+ key={cIdx}
105
+ onPress={() => setSelected(item)}
106
+ style={{
107
+ flex: 1,
108
+ borderRadius: 8,
109
+ borderWidth: StyleSheet.hairlineWidth,
110
+ borderColor: theme.colors.border,
111
+ backgroundColor: theme.colors.surface2,
112
+ padding: 8,
113
+ gap: 4,
114
+ }}
115
+ >
116
+ {item.query ? (
117
+ <ImageSearch props={{ query: item.query, alt: item.caption ?? item.query }} />
118
+ ) : item.src ? (
119
+ <LoadedImageView src={item.src} alt={item.caption} height={150} />
120
+ ) : null}
121
+ {item.caption ? (
122
+ <Text style={{ color: theme.colors.foregroundMuted, fontSize: 11.5 }}>
123
+ {item.caption}
124
+ </Text>
125
+ ) : null}
126
+ </Pressable>
127
+ ))}
128
+ </View>
129
+ ))}
130
+ {selected ? (
131
+ <Modal
132
+ title={selected.caption ?? "Image"}
133
+ open={true}
134
+ onOpenChange={(open) => {
135
+ if (!open) setSelected(null);
136
+ }}
137
+ >
138
+ <Modal.Content>
139
+ {selected.query ? (
140
+ <ImageSearch props={{ query: selected.query, alt: selected.caption ?? selected.query }} />
141
+ ) : selected.src ? (
142
+ <LoadedImageView src={selected.src} alt={selected.caption} height={360} />
143
+ ) : null}
144
+ </Modal.Content>
145
+ </Modal>
146
+ ) : null}
147
+ </View>
148
+ );
149
+ }
150
+
151
+ interface PlaceSpec {
152
+ name: string;
153
+ lat: number | null;
154
+ lng: number | null;
155
+ note: string | null;
156
+ }
157
+
158
+ export function MapView({ props }: { props: Props }) {
159
+ const { theme } = useUiRuntime();
160
+ const geocode = useGeocodePlace();
161
+ const loadTile = useMapTileLoader();
162
+ const route = props.route === true;
163
+
164
+ const places: PlaceSpec[] = useMemo(
165
+ () =>
166
+ list(props.places).flatMap((item): PlaceSpec[] => {
167
+ if (typeof item === "string") return [{ name: item, lat: null, lng: null, note: null }];
168
+ const rec = asRecord(item);
169
+ if (!rec) return [];
170
+ const name = text(rec.name ?? rec.title);
171
+ if (!name) return [];
172
+ return [
173
+ {
174
+ name,
175
+ lat: finiteNumber(rec.lat),
176
+ lng: finiteNumber(rec.lng),
177
+ note: optionalText(rec.note),
178
+ },
179
+ ];
180
+ }),
181
+ [props.places],
182
+ );
183
+
184
+ const [resolved, setResolved] = useState<
185
+ ReadonlyArray<{ name: string; lat: number; lng: number; note: string | null }>
186
+ >([]);
187
+ const [geoError, setGeoError] = useState<string | null>(null);
188
+
189
+ useEffect(() => {
190
+ let live = true;
191
+ async function run() {
192
+ const out: { name: string; lat: number; lng: number; note: string | null }[] = [];
193
+ let near: { lat: number; lng: number } | null = null;
194
+ for (const place of places) {
195
+ if (place.lat !== null && place.lng !== null) {
196
+ near = { lat: place.lat, lng: place.lng };
197
+ out.push({ name: place.name, lat: place.lat, lng: place.lng, note: place.note });
198
+ } else {
199
+ const found = await geocode(place.name, near);
200
+ if (!live) return;
201
+ if (found.ok) {
202
+ near = { lat: found.lat, lng: found.lng };
203
+ out.push({ name: place.name, lat: found.lat, lng: found.lng, note: place.note });
204
+ } else {
205
+ setGeoError(found.error);
206
+ }
207
+ }
208
+ }
209
+ if (live) setResolved(out);
210
+ }
211
+ void run();
212
+ return () => {
213
+ live = false;
214
+ };
215
+ }, [geocode, places]);
216
+
217
+ const mapSize = { width: 520, height: 220 };
218
+ const worldPoints = useMemo(() => resolved.map((p) => project(p.lat, p.lng)), [resolved]);
219
+ const fitted = useMemo(() => fitView(worldPoints, mapSize, 36, 15), [worldPoints]);
220
+ const [zoomDelta, setZoomDelta] = useState(0);
221
+ const view = useMemo(
222
+ () => zoomAround(fitted, fitted.zoom + zoomDelta, { left: mapSize.width / 2, top: mapSize.height / 2 }, mapSize),
223
+ [fitted, zoomDelta],
224
+ );
225
+ const tiles = useMemo(() => visibleTiles(view, mapSize, tileZoom(view)), [view]);
226
+ const [tileData, setTileData] = useState<ReadonlyMap<string, string>>(new Map());
227
+
228
+ useEffect(() => {
229
+ let live = true;
230
+ for (const tile of tiles) {
231
+ const k = `${tile.z}/${tile.x}/${tile.y}`;
232
+ if (tileData.has(k)) continue;
233
+ void loadTile(tile.z, tile.x, tile.y).then((res) => {
234
+ if (live && res.ok) {
235
+ setTileData((prev) => new Map(prev).set(k, res.dataUrl));
236
+ }
237
+ });
238
+ }
239
+ return () => {
240
+ live = false;
241
+ };
242
+ }, [loadTile, tiles, tileData]);
243
+
244
+ return (
245
+ <View
246
+ style={{
247
+ borderRadius: 10,
248
+ borderWidth: StyleSheet.hairlineWidth,
249
+ borderColor: theme.colors.border,
250
+ backgroundColor: theme.colors.surface2,
251
+ overflow: "hidden",
252
+ gap: 8,
253
+ }}
254
+ >
255
+ {resolved.length > 0 ? (
256
+ <View style={{ width: "100%", height: mapSize.height, backgroundColor: theme.colors.surface1, overflow: "hidden" }}>
257
+ {tiles.map((tile) => {
258
+ const uri = tileData.get(`${tile.z}/${tile.x}/${tile.y}`);
259
+ return uri ? (
260
+ <RNImage
261
+ key={tile.key}
262
+ source={{ uri }}
263
+ style={{
264
+ position: "absolute",
265
+ left: tile.left,
266
+ top: tile.top,
267
+ width: tile.size,
268
+ height: tile.size,
269
+ }}
270
+ />
271
+ ) : null;
272
+ })}
273
+ {resolved.map((place, idx) => {
274
+ const pt = toScreen(project(place.lat, place.lng), view, mapSize);
275
+ return (
276
+ <View
277
+ key={`${place.name}:${idx}`}
278
+ style={{
279
+ position: "absolute",
280
+ left: pt.left - 11,
281
+ top: pt.top - 11,
282
+ width: 22,
283
+ height: 22,
284
+ borderRadius: 11,
285
+ backgroundColor: theme.colors.accent,
286
+ borderWidth: 2,
287
+ borderColor: theme.colors.surface0,
288
+ alignItems: "center",
289
+ justifyContent: "center",
290
+ }}
291
+ >
292
+ <Text style={{ color: theme.colors.accentForeground, fontSize: 10, fontWeight: "700" }}>
293
+ {idx + 1}
294
+ </Text>
295
+ </View>
296
+ );
297
+ })}
298
+ <View style={{ position: "absolute", right: 8, top: 8, gap: 4 }}>
299
+ <Pressable
300
+ onPress={() => setZoomDelta((d) => d + 1)}
301
+ style={{
302
+ width: 26,
303
+ height: 26,
304
+ borderRadius: 6,
305
+ backgroundColor: theme.colors.surface0,
306
+ alignItems: "center",
307
+ justifyContent: "center",
308
+ }}
309
+ >
310
+ <Text style={{ color: theme.colors.foreground, fontSize: 14, fontWeight: "700" }}>+</Text>
311
+ </Pressable>
312
+ <Pressable
313
+ onPress={() => setZoomDelta((d) => d - 1)}
314
+ style={{
315
+ width: 26,
316
+ height: 26,
317
+ borderRadius: 6,
318
+ backgroundColor: theme.colors.surface0,
319
+ alignItems: "center",
320
+ justifyContent: "center",
321
+ }}
322
+ >
323
+ <Text style={{ color: theme.colors.foreground, fontSize: 14, fontWeight: "700" }}>−</Text>
324
+ </Pressable>
325
+ </View>
326
+ </View>
327
+ ) : null}
328
+ <View style={{ padding: 10, gap: 6 }}>
329
+ {places.map((p, idx) => (
330
+ <View key={`${p.name}:${idx}`} style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
331
+ <View
332
+ style={{
333
+ width: 20,
334
+ height: 20,
335
+ borderRadius: 10,
336
+ backgroundColor: theme.colors.accent,
337
+ alignItems: "center",
338
+ justifyContent: "center",
339
+ }}
340
+ >
341
+ <Text style={{ color: theme.colors.accentForeground, fontSize: 10.5, fontWeight: "700" }}>
342
+ {idx + 1}
343
+ </Text>
344
+ </View>
345
+ <View style={{ flex: 1 }}>
346
+ <Text style={{ color: theme.colors.foreground, fontSize: 12.5, fontWeight: "600" }}>
347
+ {p.name}
348
+ {route && idx < places.length - 1 ? " →" : ""}
349
+ </Text>
350
+ {p.note ? (
351
+ <Text style={{ color: theme.colors.foregroundMuted, fontSize: 11.5 }}>{p.note}</Text>
352
+ ) : null}
353
+ </View>
354
+ </View>
355
+ ))}
356
+ {geoError ? (
357
+ <Text style={{ color: theme.colors.foregroundMuted, fontSize: 11 }}>{geoError}</Text>
358
+ ) : null}
359
+ </View>
360
+ </View>
361
+ );
362
+ }
363
+
364
+ export function Hotspots({ props }: { props: Props }) {
365
+ const { theme } = useUiRuntime();
366
+ const img = imageRef(text(props.image));
367
+ const { points, problems } = hotspotPoints(list(props.points));
368
+ const [active, setActive] = useState<number>(points[0]?.number ?? 1);
369
+
370
+ return (
371
+ <View style={{ gap: 8 }}>
372
+ {img.kind === "load" ? <LoadedImageView src={img.src} height={220} /> : null}
373
+ <View style={{ gap: 6 }}>
374
+ {points.map((pt) => {
375
+ const isSelected = active === pt.number;
376
+ return (
377
+ <Pressable
378
+ key={pt.number}
379
+ onPress={() => setActive(pt.number)}
380
+ style={{
381
+ flexDirection: "row",
382
+ alignItems: "flex-start",
383
+ gap: 8,
384
+ padding: 8,
385
+ borderRadius: 8,
386
+ borderWidth: StyleSheet.hairlineWidth,
387
+ borderColor: isSelected ? theme.colors.accent : theme.colors.border,
388
+ backgroundColor: theme.colors.surface2,
389
+ }}
390
+ >
391
+ <View
392
+ style={{
393
+ width: 20,
394
+ height: 20,
395
+ borderRadius: 10,
396
+ backgroundColor: isSelected ? theme.colors.accent : theme.colors.surface1,
397
+ alignItems: "center",
398
+ justifyContent: "center",
399
+ }}
400
+ >
401
+ <Text
402
+ style={{
403
+ color: isSelected ? theme.colors.accentForeground : theme.colors.foreground,
404
+ fontSize: 11,
405
+ fontWeight: "700",
406
+ }}
407
+ >
408
+ {pt.number}
409
+ </Text>
410
+ </View>
411
+ <View style={{ flex: 1, gap: 2 }}>
412
+ <Text style={{ color: theme.colors.foreground, fontSize: 12.5, fontWeight: "600" }}>
413
+ {pt.title}
414
+ </Text>
415
+ {pt.text ? (
416
+ <Text style={{ color: theme.colors.foregroundMuted, fontSize: 12 }}>{pt.text}</Text>
417
+ ) : null}
418
+ </View>
419
+ </Pressable>
420
+ );
421
+ })}
422
+ {problems.map((p, i) => (
423
+ <ErrorLine key={i} message={p} />
424
+ ))}
425
+ </View>
426
+ </View>
427
+ );
428
+ }