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,967 @@
1
+ import { Icon, copyText, useToast } from "@getpaseo/plugin/client/react-native";
2
+ import React, { useMemo, useState } from "react";
3
+ import { Pressable, ScrollView, StyleSheet, Text, View } from "react-native";
4
+ import { calendarLayout, calendarStats, dayLabel, readDays } from "../../shared/logic/calendarMath";
5
+ import { percentages, pieSlices } from "../../shared/logic/chartMath";
6
+ import { buildTree, defaultExpanded, timelineEntries, visibleRows } from "../../shared/logic/content-model";
7
+ import {
8
+ addMonths,
9
+ dayKey,
10
+ initialMonth,
11
+ localToday,
12
+ monthKey,
13
+ monthWeeks,
14
+ parseEvents,
15
+ parseMonth,
16
+ } from "../../shared/logic/explain-logic";
17
+ import {
18
+ asRecord,
19
+ finiteNumber,
20
+ formatNumber,
21
+ isNode,
22
+ isObject,
23
+ list,
24
+ optionalText,
25
+ text,
26
+ tone,
27
+ type Props,
28
+ type Tone,
29
+ } from "../../shared/logic/values";
30
+ import { ErrorLine, RenderNode } from "../node-renderer";
31
+ import { InTableContext, propsKey, toneColor, usePersistedState, useUiRuntime } from "../runtime";
32
+
33
+ export function Table({ props }: { props: Props }) {
34
+ const { theme, compact } = useUiRuntime();
35
+ const columns = list(props.columns).map((col) => {
36
+ const rec = asRecord(col);
37
+ return rec ? text(rec.label ?? rec.key) : text(col);
38
+ });
39
+ const rows = list(props.rows);
40
+
41
+ return (
42
+ <InTableContext.Provider value={true}>
43
+ <ScrollView horizontal showsHorizontalScrollIndicator={!compact}>
44
+ <View
45
+ style={{
46
+ borderRadius: 8,
47
+ borderWidth: StyleSheet.hairlineWidth,
48
+ borderColor: theme.colors.border,
49
+ overflow: "hidden",
50
+ minWidth: "100%",
51
+ }}
52
+ >
53
+ <View
54
+ style={{
55
+ flexDirection: "row",
56
+ backgroundColor: theme.colors.surface2,
57
+ borderBottomWidth: StyleSheet.hairlineWidth,
58
+ borderBottomColor: theme.colors.border,
59
+ }}
60
+ >
61
+ {columns.map((col, idx) => (
62
+ <View
63
+ key={`${col}:${idx}`}
64
+ style={{ flex: 1, minWidth: 110, paddingHorizontal: 10, paddingVertical: 8 }}
65
+ >
66
+ <Text style={{ color: theme.colors.foregroundMuted, fontSize: 12, fontWeight: "600" }}>
67
+ {col}
68
+ </Text>
69
+ </View>
70
+ ))}
71
+ </View>
72
+ {rows.map((row, rIdx) => {
73
+ const rec = asRecord(row);
74
+ const cells = Array.isArray(row)
75
+ ? row
76
+ : rec
77
+ ? columns.map((c) => rec[c] ?? null)
78
+ : [row];
79
+ return (
80
+ <View
81
+ key={rIdx}
82
+ style={{
83
+ flexDirection: "row",
84
+ alignItems: "center",
85
+ borderTopWidth: rIdx === 0 ? 0 : StyleSheet.hairlineWidth,
86
+ borderTopColor: theme.colors.border,
87
+ }}
88
+ >
89
+ {columns.map((_, cIdx) => {
90
+ const cell = cells[cIdx];
91
+ return (
92
+ <View
93
+ key={cIdx}
94
+ style={{ flex: 1, minWidth: 110, paddingHorizontal: 10, paddingVertical: 8 }}
95
+ >
96
+ {isNode(cell) ? (
97
+ <RenderNode node={cell} />
98
+ ) : (
99
+ <Text selectable style={{ color: theme.colors.foreground, fontSize: 12.5 }}>
100
+ {text(cell)}
101
+ </Text>
102
+ )}
103
+ </View>
104
+ );
105
+ })}
106
+ </View>
107
+ );
108
+ })}
109
+ </View>
110
+ </ScrollView>
111
+ </InTableContext.Provider>
112
+ );
113
+ }
114
+
115
+ interface ParsedSeries {
116
+ name: string;
117
+ values: (number | null)[];
118
+ tone: Tone;
119
+ }
120
+
121
+ function parseChartSeries(raw: unknown): ParsedSeries[] {
122
+ if (Array.isArray(raw)) {
123
+ return [
124
+ {
125
+ name: "Value",
126
+ values: raw.map((v) => finiteNumber(v)),
127
+ tone: "info",
128
+ },
129
+ ];
130
+ }
131
+ if (isObject(raw)) {
132
+ const tones: Tone[] = ["info", "good", "warn", "bad", "neutral"];
133
+ return Object.entries(raw).map(([name, entry], idx) => {
134
+ if (Array.isArray(entry)) {
135
+ return {
136
+ name,
137
+ values: entry.map((v) => finiteNumber(v)),
138
+ tone: tones[idx % tones.length]!,
139
+ };
140
+ }
141
+ if (isObject(entry) && Array.isArray(entry.values)) {
142
+ return {
143
+ name,
144
+ values: entry.values.map((v) => finiteNumber(v)),
145
+ tone: tone(entry.tone as never),
146
+ };
147
+ }
148
+ return { name, values: [], tone: tones[idx % tones.length]! };
149
+ });
150
+ }
151
+ return [];
152
+ }
153
+
154
+ export function Chart({ props }: { props: Props }) {
155
+ const { theme } = useUiRuntime();
156
+ const chartType = text(props.type) || "bar";
157
+ const labels = list(props.labels).map((l) => text(l));
158
+ const series = useMemo(() => parseChartSeries(props.series), [props.series]);
159
+ const unit = optionalText(props.unit) ?? "";
160
+ const fmtVal = (v: number) => (unit ? `${formatNumber(v)} ${unit}`.trim() : formatNumber(v));
161
+
162
+ if (series.length === 0) return <ErrorLine message="Chart series is empty" />;
163
+
164
+ if (chartType === "pie" || chartType === "funnel") {
165
+ const first = series[0]!;
166
+ const slices = pieSlices(labels, first.values, 8);
167
+ const shares = percentages(slices.map((s) => s.value));
168
+ const maxVal = Math.max(1, ...slices.map((s) => s.value));
169
+ return (
170
+ <View style={{ gap: 8 }}>
171
+ {slices.map((slice, idx) => {
172
+ const pct = chartType === "funnel" ? Math.round((slice.value / maxVal) * 100) : shares[idx] ?? 0;
173
+ return (
174
+ <View key={`${slice.label}:${idx}`} style={{ gap: 4 }}>
175
+ <View style={{ flexDirection: "row", justifyContent: "space-between" }}>
176
+ <Text style={{ color: theme.colors.foreground, fontSize: 12.5 }}>{slice.label}</Text>
177
+ <Text style={{ color: theme.colors.foregroundMuted, fontSize: 12 }}>
178
+ {fmtVal(slice.value)} ({pct}%)
179
+ </Text>
180
+ </View>
181
+ <View
182
+ style={{
183
+ height: 10,
184
+ borderRadius: 5,
185
+ backgroundColor: theme.colors.surface2,
186
+ overflow: "hidden",
187
+ }}
188
+ >
189
+ <View
190
+ style={{
191
+ width: `${Math.max(2, Math.min(100, pct))}%`,
192
+ height: "100%",
193
+ backgroundColor: toneColor(theme, first.tone),
194
+ borderRadius: 5,
195
+ }}
196
+ />
197
+ </View>
198
+ </View>
199
+ );
200
+ })}
201
+ </View>
202
+ );
203
+ }
204
+
205
+ if (chartType === "heatmap") {
206
+ const allNums = series.flatMap((s) => s.values.filter((v): v is number => v !== null));
207
+ const maxVal = Math.max(1, ...allNums);
208
+ return (
209
+ <ScrollView horizontal>
210
+ <View style={{ gap: 4 }}>
211
+ <View style={{ flexDirection: "row", gap: 4, paddingLeft: 80 }}>
212
+ {labels.map((l, idx) => (
213
+ <View key={idx} style={{ width: 54, alignItems: "center" }}>
214
+ <Text numberOfLines={1} style={{ color: theme.colors.foregroundMuted, fontSize: 11 }}>
215
+ {l}
216
+ </Text>
217
+ </View>
218
+ ))}
219
+ </View>
220
+ {series.map((row, rIdx) => (
221
+ <View key={rIdx} style={{ flexDirection: "row", alignItems: "center", gap: 4 }}>
222
+ <Text
223
+ numberOfLines={1}
224
+ style={{ width: 76, color: theme.colors.foregroundMuted, fontSize: 11.5 }}
225
+ >
226
+ {row.name}
227
+ </Text>
228
+ {labels.map((_, cIdx) => {
229
+ const v = row.values[cIdx] ?? 0;
230
+ const intensity = Math.max(0.12, Math.min(1, v / maxVal));
231
+ return (
232
+ <View
233
+ key={cIdx}
234
+ style={{
235
+ width: 54,
236
+ height: 28,
237
+ borderRadius: 4,
238
+ backgroundColor: theme.colors.surface2,
239
+ borderWidth: StyleSheet.hairlineWidth,
240
+ borderColor: v > 0 ? theme.colors.accent : theme.colors.border,
241
+ alignItems: "center",
242
+ justifyContent: "center",
243
+ opacity: v > 0 ? intensity : 0.4,
244
+ }}
245
+ >
246
+ <Text style={{ color: theme.colors.foreground, fontSize: 11 }}>{formatNumber(v)}</Text>
247
+ </View>
248
+ );
249
+ })}
250
+ </View>
251
+ ))}
252
+ </View>
253
+ </ScrollView>
254
+ );
255
+ }
256
+
257
+ const allValues = series.flatMap((s) => s.values.filter((v): v is number => v !== null));
258
+ const maxAbs = Math.max(1, ...allValues.map((v) => Math.abs(v)));
259
+
260
+ return (
261
+ <View style={{ gap: 10 }}>
262
+ {series.length > 1 ? (
263
+ <View style={{ flexDirection: "row", flexWrap: "wrap", gap: 12 }}>
264
+ {series.map((s, idx) => (
265
+ <View key={idx} style={{ flexDirection: "row", alignItems: "center", gap: 6 }}>
266
+ <View
267
+ style={{
268
+ width: 8,
269
+ height: 8,
270
+ borderRadius: 4,
271
+ backgroundColor: toneColor(theme, s.tone),
272
+ }}
273
+ />
274
+ <Text style={{ color: theme.colors.foregroundMuted, fontSize: 12 }}>{s.name}</Text>
275
+ </View>
276
+ ))}
277
+ </View>
278
+ ) : null}
279
+ <View style={{ gap: 8 }}>
280
+ {labels.map((label, lIdx) => (
281
+ <View key={`${label}:${lIdx}`} style={{ gap: 4 }}>
282
+ <View style={{ flexDirection: "row", justifyContent: "space-between", gap: 8 }}>
283
+ <Text style={{ color: theme.colors.foreground, fontSize: 12.5 }}>{label}</Text>
284
+ <Text style={{ color: theme.colors.foregroundMuted, fontSize: 12 }}>
285
+ {series
286
+ .map((s) => {
287
+ const v = s.values[lIdx];
288
+ return v === null || v === undefined ? "—" : fmtVal(v);
289
+ })
290
+ .join(" / ")}
291
+ </Text>
292
+ </View>
293
+ <View style={{ gap: 3 }}>
294
+ {series.map((s, sIdx) => {
295
+ const v = s.values[lIdx] ?? 0;
296
+ const pct = Math.min(100, Math.max(2, Math.round((Math.abs(v) / maxAbs) * 100)));
297
+ return (
298
+ <View
299
+ key={sIdx}
300
+ style={{
301
+ height: series.length > 1 ? 7 : 9,
302
+ borderRadius: 4,
303
+ backgroundColor: theme.colors.surface2,
304
+ overflow: "hidden",
305
+ }}
306
+ >
307
+ <View
308
+ style={{
309
+ width: `${pct}%`,
310
+ height: "100%",
311
+ backgroundColor: toneColor(theme, s.tone),
312
+ borderRadius: 4,
313
+ }}
314
+ />
315
+ </View>
316
+ );
317
+ })}
318
+ </View>
319
+ </View>
320
+ ))}
321
+ </View>
322
+ </View>
323
+ );
324
+ }
325
+
326
+ export function Sparkline({ props }: { props: Props }) {
327
+ const { theme } = useUiRuntime();
328
+ const values = list(props.values)
329
+ .map((v) => finiteNumber(v))
330
+ .filter((v): v is number => v !== null);
331
+ const t = tone(props.tone);
332
+ const color = toneColor(theme, t === "neutral" ? "info" : t);
333
+ if (values.length === 0) return null;
334
+ const min = Math.min(...values);
335
+ const max = Math.max(...values);
336
+ const span = Math.max(1, max - min);
337
+
338
+ return (
339
+ <View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
340
+ <View style={{ flexDirection: "row", alignItems: "flex-end", gap: 2, height: 24 }}>
341
+ {values.map((v, idx) => {
342
+ const h = Math.max(3, Math.round(((v - min) / span) * 22));
343
+ return (
344
+ <View
345
+ key={idx}
346
+ style={{
347
+ width: 4,
348
+ height: h,
349
+ borderRadius: 2,
350
+ backgroundColor: color,
351
+ }}
352
+ />
353
+ );
354
+ })}
355
+ </View>
356
+ <Text style={{ color: theme.colors.foregroundMuted, fontSize: 11.5 }}>
357
+ {formatNumber(values[values.length - 1]!)}
358
+ </Text>
359
+ </View>
360
+ );
361
+ }
362
+
363
+ export function Progress({ props }: { props: Props }) {
364
+ const { theme } = useUiRuntime();
365
+ const value = finiteNumber(props.value) ?? 0;
366
+ const max = finiteNumber(props.max) ?? 100;
367
+ const label = optionalText(props.label);
368
+ const t = tone(props.tone);
369
+ const pct = max > 0 ? Math.min(100, Math.max(0, Math.round((value / max) * 100))) : 0;
370
+ const color = toneColor(theme, t === "neutral" ? "info" : t);
371
+
372
+ return (
373
+ <View style={{ gap: 4 }}>
374
+ <View style={{ flexDirection: "row", justifyContent: "space-between" }}>
375
+ {label ? <Text style={{ color: theme.colors.foreground, fontSize: 12.5 }}>{label}</Text> : <View />}
376
+ <Text style={{ color: theme.colors.foregroundMuted, fontSize: 12 }}>{pct}%</Text>
377
+ </View>
378
+ <View
379
+ style={{
380
+ height: 8,
381
+ borderRadius: 4,
382
+ backgroundColor: theme.colors.surface2,
383
+ overflow: "hidden",
384
+ }}
385
+ >
386
+ <View style={{ width: `${pct}%`, height: "100%", backgroundColor: color, borderRadius: 4 }} />
387
+ </View>
388
+ </View>
389
+ );
390
+ }
391
+
392
+ export function Gauge({ props }: { props: Props }) {
393
+ const { theme } = useUiRuntime();
394
+ const value = finiteNumber(props.value) ?? 0;
395
+ const min = finiteNumber(props.min) ?? 0;
396
+ const max = finiteNumber(props.max) ?? 100;
397
+ const label = optionalText(props.label);
398
+ const t = tone(props.tone);
399
+ const span = Math.max(1, max - min);
400
+ const pct = Math.min(100, Math.max(0, Math.round(((value - min) / span) * 100)));
401
+ const color = toneColor(theme, t === "neutral" ? "info" : t);
402
+
403
+ return (
404
+ <View
405
+ style={{
406
+ borderRadius: 10,
407
+ borderWidth: StyleSheet.hairlineWidth,
408
+ borderColor: theme.colors.border,
409
+ backgroundColor: theme.colors.surface2,
410
+ padding: 12,
411
+ gap: 6,
412
+ }}
413
+ >
414
+ <View style={{ flexDirection: "row", justifyContent: "space-between", alignItems: "baseline" }}>
415
+ <Text style={{ color: theme.colors.foregroundMuted, fontSize: 12 }}>{label ?? "Gauge"}</Text>
416
+ <Text style={{ color, fontSize: 18, fontWeight: "700" }}>
417
+ {formatNumber(value)} <Text style={{ fontSize: 11, fontWeight: "400" }}>/ {formatNumber(max)}</Text>
418
+ </Text>
419
+ </View>
420
+ <View style={{ height: 10, borderRadius: 5, backgroundColor: theme.colors.surface1, overflow: "hidden" }}>
421
+ <View style={{ width: `${pct}%`, height: "100%", backgroundColor: color, borderRadius: 5 }} />
422
+ </View>
423
+ </View>
424
+ );
425
+ }
426
+
427
+ export function SegmentedBar({ props }: { props: Props }) {
428
+ const { theme } = useUiRuntime();
429
+ const unit = optionalText(props.unit) ?? "";
430
+ const parts = list(props.parts).flatMap((p) => {
431
+ const rec = asRecord(p);
432
+ if (!rec) return [];
433
+ const val = finiteNumber(rec.value);
434
+ if (val === null || val <= 0) return [];
435
+ return [{ label: text(rec.label), value: val, tone: tone(rec.tone) }];
436
+ });
437
+ const shares = percentages(parts.map((p) => p.value));
438
+ const tones: Tone[] = ["info", "good", "warn", "bad", "neutral"];
439
+
440
+ return (
441
+ <View style={{ gap: 8 }}>
442
+ <View
443
+ style={{
444
+ flexDirection: "row",
445
+ height: 12,
446
+ borderRadius: 6,
447
+ overflow: "hidden",
448
+ backgroundColor: theme.colors.surface2,
449
+ gap: 2,
450
+ }}
451
+ >
452
+ {parts.map((part, idx) => {
453
+ const t = part.tone === "neutral" ? tones[idx % tones.length]! : part.tone;
454
+ return (
455
+ <View
456
+ key={idx}
457
+ style={{
458
+ flex: Math.max(1, shares[idx] ?? 1),
459
+ backgroundColor: toneColor(theme, t),
460
+ }}
461
+ />
462
+ );
463
+ })}
464
+ </View>
465
+ <View style={{ flexDirection: "row", flexWrap: "wrap", gap: 12 }}>
466
+ {parts.map((part, idx) => {
467
+ const t = part.tone === "neutral" ? tones[idx % tones.length]! : part.tone;
468
+ return (
469
+ <View key={idx} style={{ flexDirection: "row", alignItems: "center", gap: 6 }}>
470
+ <View style={{ width: 8, height: 8, borderRadius: 4, backgroundColor: toneColor(theme, t) }} />
471
+ <Text style={{ color: theme.colors.foreground, fontSize: 12 }}>
472
+ {part.label}:{" "}
473
+ <Text style={{ color: theme.colors.foregroundMuted }}>
474
+ {formatNumber(part.value)}
475
+ {unit ? ` ${unit}` : ""} ({shares[idx] ?? 0}%)
476
+ </Text>
477
+ </Text>
478
+ </View>
479
+ );
480
+ })}
481
+ </View>
482
+ </View>
483
+ );
484
+ }
485
+
486
+ export function Steps({ props }: { props: Props }) {
487
+ const { theme } = useUiRuntime();
488
+ const items = list(props.items);
489
+
490
+ return (
491
+ <View style={{ gap: 8 }}>
492
+ {items.map((item, idx) => {
493
+ const obj = asRecord(item) ?? { title: text(item) };
494
+ const title = text(obj.title ?? obj.label);
495
+ const detail = optionalText(obj.detail);
496
+ const status = text(obj.status);
497
+ const color =
498
+ status === "done"
499
+ ? theme.colors.statusSuccess
500
+ : status === "blocked"
501
+ ? theme.colors.statusDanger
502
+ : status === "current"
503
+ ? theme.colors.accent
504
+ : theme.colors.foregroundMuted;
505
+ return (
506
+ <View key={idx} style={{ flexDirection: "row", alignItems: "flex-start", gap: 10 }}>
507
+ <View
508
+ style={{
509
+ width: 22,
510
+ height: 22,
511
+ borderRadius: 11,
512
+ borderWidth: 1.5,
513
+ borderColor: color,
514
+ backgroundColor: theme.colors.surface2,
515
+ alignItems: "center",
516
+ justifyContent: "center",
517
+ marginTop: 1,
518
+ }}
519
+ >
520
+ <Text style={{ color, fontSize: 11, fontWeight: "700" }}>{idx + 1}</Text>
521
+ </View>
522
+ <View style={{ flex: 1, gap: 2 }}>
523
+ <Text selectable style={{ color: theme.colors.foreground, fontSize: 13, fontWeight: "600" }}>
524
+ {title}
525
+ </Text>
526
+ {detail ? (
527
+ <Text selectable style={{ color: theme.colors.foregroundMuted, fontSize: 12 }}>
528
+ {detail}
529
+ </Text>
530
+ ) : null}
531
+ </View>
532
+ </View>
533
+ );
534
+ })}
535
+ </View>
536
+ );
537
+ }
538
+
539
+ export function Timeline({ props }: { props: Props }) {
540
+ const { theme } = useUiRuntime();
541
+ const entries = timelineEntries(list(props.items));
542
+
543
+ return (
544
+ <View style={{ gap: 10 }}>
545
+ {entries.map((entry, idx) => (
546
+ <View key={idx} style={{ flexDirection: "row", alignItems: "flex-start", gap: 10 }}>
547
+ <View
548
+ style={{
549
+ width: 8,
550
+ height: 8,
551
+ borderRadius: 4,
552
+ backgroundColor: toneColor(theme, entry.tone),
553
+ marginTop: 6,
554
+ }}
555
+ />
556
+ <View style={{ flex: 1, gap: 2 }}>
557
+ <View style={{ flexDirection: "row", justifyContent: "space-between", gap: 8 }}>
558
+ <Text selectable style={{ color: theme.colors.foreground, fontSize: 13, fontWeight: "600", flex: 1 }}>
559
+ {entry.title}
560
+ </Text>
561
+ {entry.time ? (
562
+ <Text style={{ color: theme.colors.foregroundMuted, fontSize: 11.5 }}>{entry.time}</Text>
563
+ ) : null}
564
+ </View>
565
+ {entry.detail ? (
566
+ <Text selectable style={{ color: theme.colors.foregroundMuted, fontSize: 12 }}>
567
+ {entry.detail}
568
+ </Text>
569
+ ) : null}
570
+ </View>
571
+ </View>
572
+ ))}
573
+ </View>
574
+ );
575
+ }
576
+
577
+ export function Calendar({ props }: { props: Props }) {
578
+ const { theme } = useUiRuntime();
579
+ const { events } = parseEvents(props.events);
580
+ const requested = parseMonth(props.month);
581
+ const today = useMemo(() => localToday(new Date()), []);
582
+ const startMonth = useMemo(() => initialMonth(events, requested, today), [events, requested, today]);
583
+ const [month, setMonth] = useState(startMonth);
584
+
585
+ const weeks = useMemo(() => monthWeeks(month, 1), [month]);
586
+ const byDay = useMemo(() => {
587
+ const map = new Map<string, typeof events>();
588
+ for (const ev of events) {
589
+ map.set(ev.key, [...(map.get(ev.key) ?? []), ev]);
590
+ }
591
+ return map;
592
+ }, [events]);
593
+
594
+ return (
595
+ <View
596
+ style={{
597
+ borderRadius: 10,
598
+ borderWidth: StyleSheet.hairlineWidth,
599
+ borderColor: theme.colors.border,
600
+ backgroundColor: theme.colors.surface2,
601
+ padding: 12,
602
+ gap: 8,
603
+ }}
604
+ >
605
+ <View style={{ flexDirection: "row", alignItems: "center", justifyContent: "space-between" }}>
606
+ <Text style={{ color: theme.colors.foreground, fontSize: 13.5, fontWeight: "600" }}>
607
+ {monthKey(month)}
608
+ </Text>
609
+ <View style={{ flexDirection: "row", gap: 6 }}>
610
+ <Pressable
611
+ onPress={() => setMonth((m) => addMonths(m, -1))}
612
+ style={{ padding: 4, borderRadius: 4, backgroundColor: theme.colors.surface1 }}
613
+ >
614
+ <Icon name="ChevronLeft" size={14} color={theme.colors.foreground} />
615
+ </Pressable>
616
+ <Pressable
617
+ onPress={() => setMonth((m) => addMonths(m, 1))}
618
+ style={{ padding: 4, borderRadius: 4, backgroundColor: theme.colors.surface1 }}
619
+ >
620
+ <Icon name="ChevronRight" size={14} color={theme.colors.foreground} />
621
+ </Pressable>
622
+ </View>
623
+ </View>
624
+ {weeks.map((week, wIdx) => (
625
+ <View key={wIdx} style={{ flexDirection: "row", gap: 4 }}>
626
+ {week.map((day, dIdx) => {
627
+ const key = day === null ? "" : dayKey({ ...month, day });
628
+ const dayEvents = key ? byDay.get(key) ?? [] : [];
629
+ return (
630
+ <View
631
+ key={dIdx}
632
+ style={{
633
+ flex: 1,
634
+ minHeight: 44,
635
+ borderRadius: 6,
636
+ padding: 4,
637
+ backgroundColor: day === null ? "transparent" : theme.colors.surface1,
638
+ borderWidth: dayEvents.length > 0 ? 1 : StyleSheet.hairlineWidth,
639
+ borderColor: dayEvents.length > 0 ? theme.colors.accent : theme.colors.border,
640
+ gap: 2,
641
+ }}
642
+ >
643
+ {day !== null ? (
644
+ <Text style={{ color: theme.colors.foregroundMuted, fontSize: 10.5 }}>{day}</Text>
645
+ ) : null}
646
+ {dayEvents.slice(0, 2).map((ev, eIdx) => (
647
+ <Text
648
+ key={eIdx}
649
+ numberOfLines={1}
650
+ style={{ color: toneColor(theme, ev.tone), fontSize: 10, fontWeight: "500" }}
651
+ >
652
+ {ev.title}
653
+ </Text>
654
+ ))}
655
+ </View>
656
+ );
657
+ })}
658
+ </View>
659
+ ))}
660
+ </View>
661
+ );
662
+ }
663
+
664
+ export function CalendarHeatmap({ props }: { props: Props }) {
665
+ const { theme } = useUiRuntime();
666
+ const { values } = useMemo(() => readDays(props.days), [props.days]);
667
+ const label = optionalText(props.label);
668
+ const stats = useMemo(() => calendarStats(values), [values]);
669
+ const days = [...values.keys()].sort((a, b) => a - b);
670
+ if (days.length === 0) return <ErrorLine message="CalendarHeatmap has no valid dates" />;
671
+ const first = days[0]!;
672
+ const last = Math.max(first + 28, days[days.length - 1]!);
673
+ const layout = calendarLayout(first, last);
674
+ const peakVal = stats.peak?.value ?? 1;
675
+
676
+ return (
677
+ <View style={{ gap: 8 }}>
678
+ <View style={{ flexDirection: "row", justifyContent: "space-between" }}>
679
+ <Text style={{ color: theme.colors.foreground, fontSize: 12.5, fontWeight: "500" }}>
680
+ {label ?? "Activity"}
681
+ </Text>
682
+ <Text style={{ color: theme.colors.foregroundMuted, fontSize: 11.5 }}>
683
+ {formatNumber(stats.total)} total · {stats.active} active days
684
+ {stats.peak ? ` · peak ${formatNumber(stats.peak.value)} (${dayLabel(stats.peak.day)})` : ""}
685
+ </Text>
686
+ </View>
687
+ <ScrollView horizontal>
688
+ <View style={{ flexDirection: "row", gap: 3 }}>
689
+ {Array.from({ length: Math.min(53, layout.weeks) }, (_, w) => (
690
+ <View key={w} style={{ gap: 3 }}>
691
+ {Array.from({ length: 7 }, (__, d) => {
692
+ const day = layout.start + w * 7 + d;
693
+ const val = values.get(day) ?? 0;
694
+ const active = val > 0;
695
+ return (
696
+ <View
697
+ key={d}
698
+ style={{
699
+ width: 11,
700
+ height: 11,
701
+ borderRadius: 2,
702
+ backgroundColor: active ? theme.colors.accent : theme.colors.surface2,
703
+ opacity: active ? Math.max(0.25, Math.min(1, val / peakVal)) : 0.4,
704
+ }}
705
+ />
706
+ );
707
+ })}
708
+ </View>
709
+ ))}
710
+ </View>
711
+ </ScrollView>
712
+ </View>
713
+ );
714
+ }
715
+
716
+ export function Tree({ props }: { props: Props }) {
717
+ const { theme } = useUiRuntime();
718
+ const roots = useMemo(() => buildTree(list(props.items)), [props.items]);
719
+ const initialOpen = useMemo(() => [...defaultExpanded(roots)], [roots]);
720
+ const [openIds, setOpenIds] = usePersistedState<string[]>(
721
+ `tree:${propsKey(props.items)}`,
722
+ initialOpen,
723
+ (v) => (Array.isArray(v) ? v.filter((x): x is string => typeof x === "string") : null),
724
+ );
725
+ const openSet = useMemo(() => new Set(openIds), [openIds]);
726
+ const rows = useMemo(() => visibleRows(roots, (id) => openSet.has(id)), [roots, openSet]);
727
+
728
+ return (
729
+ <View
730
+ style={{
731
+ borderRadius: 8,
732
+ borderWidth: StyleSheet.hairlineWidth,
733
+ borderColor: theme.colors.border,
734
+ backgroundColor: theme.colors.surface2,
735
+ paddingVertical: 6,
736
+ }}
737
+ >
738
+ {rows.map(({ item, depth }) => {
739
+ const isOpen = openSet.has(item.id);
740
+ return (
741
+ <Pressable
742
+ key={item.id}
743
+ onPress={() => {
744
+ if (!item.folder) return;
745
+ setOpenIds((prev) =>
746
+ prev.includes(item.id) ? prev.filter((x) => x !== item.id) : [...prev, item.id],
747
+ );
748
+ }}
749
+ style={{
750
+ flexDirection: "row",
751
+ alignItems: "center",
752
+ justifyContent: "space-between",
753
+ paddingLeft: 10 + depth * 16,
754
+ paddingRight: 10,
755
+ paddingVertical: 5,
756
+ gap: 8,
757
+ }}
758
+ >
759
+ <View style={{ flexDirection: "row", alignItems: "center", gap: 6, flex: 1 }}>
760
+ <Icon
761
+ name={item.folder ? (isOpen ? "FolderOpen" : "Folder") : "FileText"}
762
+ size={13}
763
+ color={item.folder ? theme.colors.accent : theme.colors.foregroundMuted}
764
+ />
765
+ <Text selectable style={{ color: theme.colors.foreground, fontFamily: "monospace", fontSize: 12 }}>
766
+ {item.label}
767
+ </Text>
768
+ </View>
769
+ {item.note !== null ? (
770
+ isNode(item.note) ? (
771
+ <RenderNode node={item.note} />
772
+ ) : (
773
+ <Text style={{ color: theme.colors.foregroundMuted, fontSize: 11.5 }}>
774
+ {text(item.note)}
775
+ </Text>
776
+ )
777
+ ) : null}
778
+ </Pressable>
779
+ );
780
+ })}
781
+ </View>
782
+ );
783
+ }
784
+
785
+ export function Code({ props }: { props: Props }) {
786
+ const { theme } = useUiRuntime();
787
+ const toast = useToast();
788
+ const code = text(props.code);
789
+ const language = optionalText(props.language);
790
+
791
+ return (
792
+ <View
793
+ style={{
794
+ borderRadius: 8,
795
+ borderWidth: StyleSheet.hairlineWidth,
796
+ borderColor: theme.colors.border,
797
+ backgroundColor: theme.colors.surface2,
798
+ overflow: "hidden",
799
+ }}
800
+ >
801
+ <View
802
+ style={{
803
+ flexDirection: "row",
804
+ justifyContent: "space-between",
805
+ alignItems: "center",
806
+ paddingHorizontal: 10,
807
+ paddingVertical: 6,
808
+ borderBottomWidth: StyleSheet.hairlineWidth,
809
+ borderBottomColor: theme.colors.border,
810
+ }}
811
+ >
812
+ <Text style={{ color: theme.colors.foregroundMuted, fontSize: 11, fontFamily: "monospace" }}>
813
+ {language ?? "code"}
814
+ </Text>
815
+ <Pressable
816
+ onPress={() => {
817
+ void copyText(code).then(
818
+ () => toast.show("Copied", { variant: "success" }),
819
+ () => toast.error("Could not copy"),
820
+ );
821
+ }}
822
+ >
823
+ <Icon name="Copy" size={13} color={theme.colors.foregroundMuted} />
824
+ </Pressable>
825
+ </View>
826
+ <ScrollView horizontal>
827
+ <View style={{ padding: 10 }}>
828
+ <Text
829
+ selectable
830
+ style={{
831
+ color: theme.colors.foreground,
832
+ fontFamily: "monospace",
833
+ fontSize: 12,
834
+ lineHeight: 18,
835
+ }}
836
+ >
837
+ {code}
838
+ </Text>
839
+ </View>
840
+ </ScrollView>
841
+ </View>
842
+ );
843
+ }
844
+
845
+ export function Snippet({ props }: { props: Props }) {
846
+ const { theme, openFile } = useUiRuntime();
847
+ const filePath = text(props.path);
848
+ const from = finiteNumber(props.from) ?? 1;
849
+ const code = text(props.code);
850
+ const lines = code.split("\n");
851
+
852
+ return (
853
+ <View
854
+ style={{
855
+ borderRadius: 8,
856
+ borderWidth: StyleSheet.hairlineWidth,
857
+ borderColor: theme.colors.border,
858
+ backgroundColor: theme.colors.surface2,
859
+ overflow: "hidden",
860
+ }}
861
+ >
862
+ <Pressable
863
+ onPress={() => {
864
+ if (openFile) void openFile(filePath, Math.round(from));
865
+ }}
866
+ style={{
867
+ flexDirection: "row",
868
+ justifyContent: "space-between",
869
+ alignItems: "center",
870
+ paddingHorizontal: 10,
871
+ paddingVertical: 6,
872
+ borderBottomWidth: StyleSheet.hairlineWidth,
873
+ borderBottomColor: theme.colors.border,
874
+ }}
875
+ >
876
+ <Text style={{ color: theme.colors.accent, fontSize: 11.5, fontFamily: "monospace" }}>
877
+ {filePath}:{Math.round(from)}
878
+ </Text>
879
+ </Pressable>
880
+ <ScrollView horizontal>
881
+ <View style={{ padding: 10, gap: 1 }}>
882
+ {lines.map((line, idx) => (
883
+ <View key={idx} style={{ flexDirection: "row", gap: 10 }}>
884
+ <Text
885
+ style={{
886
+ width: 34,
887
+ textAlign: "right",
888
+ color: theme.colors.foregroundMuted,
889
+ fontFamily: "monospace",
890
+ fontSize: 11.5,
891
+ }}
892
+ >
893
+ {Math.round(from) + idx}
894
+ </Text>
895
+ <Text
896
+ selectable
897
+ style={{
898
+ color: theme.colors.foreground,
899
+ fontFamily: "monospace",
900
+ fontSize: 12,
901
+ }}
902
+ >
903
+ {line}
904
+ </Text>
905
+ </View>
906
+ ))}
907
+ </View>
908
+ </ScrollView>
909
+ </View>
910
+ );
911
+ }
912
+
913
+ export function Diff({ props }: { props: Props }) {
914
+ const { theme } = useUiRuntime();
915
+ const patch = text(props.patch);
916
+ const lines = patch.split("\n");
917
+
918
+ return (
919
+ <View
920
+ style={{
921
+ borderRadius: 8,
922
+ borderWidth: StyleSheet.hairlineWidth,
923
+ borderColor: theme.colors.border,
924
+ backgroundColor: theme.colors.surface2,
925
+ overflow: "hidden",
926
+ }}
927
+ >
928
+ <ScrollView horizontal>
929
+ <View style={{ paddingVertical: 8, minWidth: "100%" }}>
930
+ {lines.map((line, idx) => {
931
+ const added = line.startsWith("+") && !line.startsWith("+++");
932
+ const removed = line.startsWith("-") && !line.startsWith("---");
933
+ const hunk = line.startsWith("@@");
934
+ const color = added
935
+ ? theme.colors.statusSuccess
936
+ : removed
937
+ ? theme.colors.statusDanger
938
+ : hunk
939
+ ? theme.colors.accent
940
+ : theme.colors.foreground;
941
+ return (
942
+ <View
943
+ key={idx}
944
+ style={{
945
+ paddingHorizontal: 10,
946
+ paddingVertical: 1,
947
+ }}
948
+ >
949
+ <Text
950
+ selectable
951
+ style={{
952
+ color,
953
+ fontFamily: "monospace",
954
+ fontSize: 12,
955
+ lineHeight: 17,
956
+ }}
957
+ >
958
+ {line}
959
+ </Text>
960
+ </View>
961
+ );
962
+ })}
963
+ </View>
964
+ </ScrollView>
965
+ </View>
966
+ );
967
+ }