paseo-plugin-genui 0.0.0-stage → 0.1.0

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 +407 -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 +122 -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,53 @@
1
+ // Pure pieces of Environments (environments.tsx): which rows show, and which
2
+ // rows on screen need their git status and pull request (re)read.
3
+ import { defaultEnvironments, type EnvironmentInfo } from "../core/environments";
4
+
5
+ /** Environment details read at once. */
6
+ export const DETAIL_CONCURRENCY = 3;
7
+
8
+ /** How old the detail of an environment with a running thread may get before it is read again. */
9
+ export const LIVE_DETAIL_MS = 30_000;
10
+
11
+ /** Rows per page once every environment is shown. */
12
+ export const ENVIRONMENT_PAGE = 25;
13
+
14
+ /** What the view shows: the default (live environments) or all of them, a page at a time. */
15
+ export interface EnvironmentsShown {
16
+ readonly rows: readonly EnvironmentInfo[];
17
+ /** Environments the default view leaves out, for "Show all N". 0 once all are shown. */
18
+ readonly hidden: number;
19
+ /** Rows past the pages shown, for "Show N more". */
20
+ readonly more: number;
21
+ }
22
+
23
+ export function shownEnvironments(environments: readonly EnvironmentInfo[], all: boolean, pages: number): EnvironmentsShown {
24
+ const { shown, filtered } = defaultEnvironments(environments);
25
+ const pool = all || !filtered ? environments : shown;
26
+ const limit = Math.max(1, pages) * ENVIRONMENT_PAGE;
27
+ return {
28
+ rows: pool.slice(0, limit),
29
+ hidden: all || !filtered ? 0 : environments.length - shown.length,
30
+ more: Math.max(0, pool.length - limit),
31
+ };
32
+ }
33
+
34
+ /**
35
+ * The on-screen environments whose detail should be read now, in screen
36
+ * order: never read, or with a running thread and read more than
37
+ * LIVE_DETAIL_MS ago. Ones being read are skipped.
38
+ */
39
+ export function detailsDue(
40
+ onScreen: readonly string[],
41
+ environments: ReadonlyMap<string, EnvironmentInfo>,
42
+ readAt: ReadonlyMap<string, number>,
43
+ reading: ReadonlySet<string>,
44
+ now: number,
45
+ ): string[] {
46
+ return onScreen.filter((id) => {
47
+ const environment = environments.get(id);
48
+ if (environment === undefined || reading.has(id)) return false;
49
+ const at = readAt.get(id);
50
+ if (at === undefined) return true;
51
+ return environment.threadCounts.running > 0 && now - at > LIVE_DETAIL_MS;
52
+ });
53
+ }
@@ -0,0 +1,315 @@
1
+ // Pure logic behind the explaining components (Diagram details, Walkthrough,
2
+ // List, Calendar): no React, no DOM, so it is unit-testable without the host
3
+ // modules.
4
+ import type { Action, UiNode, Value } from "../core/types";
5
+ import { isAction, isNode, isObject, isScalar, list, optionalText, text, tone, type Tone } from "./values";
6
+
7
+ // ── Diagram details ─────────────────────────────────────────────────────
8
+
9
+ /** What opens under the diagram for one node: markdown and components, in order. */
10
+ export type DetailPart = string | UiNode;
11
+
12
+ export interface DiagramDetail {
13
+ readonly key: string;
14
+ readonly parts: readonly DetailPart[];
15
+ }
16
+
17
+ function detailParts(value: Value): DetailPart[] {
18
+ if (isNode(value)) return [value];
19
+ if (isScalar(value)) {
20
+ const body = text(value).trim();
21
+ return body === "" ? [] : [body];
22
+ }
23
+ if (Array.isArray(value)) return value.flatMap((item: Value) => detailParts(item));
24
+ return [];
25
+ }
26
+
27
+ /** `{nodeId: text or components}` as details in key order; null when `value` is not such an object. */
28
+ export function parseDetails(value: Value | undefined): DiagramDetail[] | null {
29
+ if (value === undefined || value === null) return [];
30
+ if (!isObject(value) || "kind" in value) return null;
31
+ return Object.entries(value as Readonly<Record<string, Value>>).flatMap(([key, item]) => {
32
+ const parts = detailParts(item);
33
+ return key.trim() === "" || parts.length === 0 ? [] : [{ key, parts }];
34
+ });
35
+ }
36
+
37
+ /**
38
+ * Mermaid ids its node groups as `<svg id>-<kind>-<node id>-<counter>`
39
+ * (flowchart, state, class and ER nodes) and subgraphs as `<svg id>-<id>`.
40
+ * Returns the node id the diagram's source used, or null.
41
+ */
42
+ export function mermaidNodeId(elementId: string, svgId: string, cluster: boolean): string | null {
43
+ const prefix = `${svgId}-`;
44
+ if (svgId === "" || !elementId.startsWith(prefix)) return null;
45
+ const rest = elementId.slice(prefix.length);
46
+ if (rest === "") return null;
47
+ if (cluster) return rest;
48
+ return /^(?:flowchart|state|classId|entity)-(.+)-\d+$/.exec(rest)?.[1] ?? null;
49
+ }
50
+
51
+ /**
52
+ * The detail a drawn node opens: the one keyed by its mermaid id, else (for
53
+ * diagrams without ids, like mindmaps) the one keyed by its visible label.
54
+ */
55
+ export function detailFor(details: readonly DiagramDetail[], id: string | null, label: string): DiagramDetail | null {
56
+ if (id !== null) {
57
+ const byId = details.find((detail) => detail.key === id);
58
+ if (byId !== undefined) return byId;
59
+ }
60
+ const normalize = (label: string) => label.replace(/\s+/g, " ").trim().toLowerCase();
61
+ const wanted = normalize(label);
62
+ if (wanted === "") return null;
63
+ return details.find((detail) => normalize(detail.key) === wanted) ?? null;
64
+ }
65
+
66
+ // ── Walkthrough ─────────────────────────────────────────────────────────
67
+
68
+ export interface WalkthroughStep {
69
+ readonly title: string;
70
+ readonly body: string;
71
+ readonly code: string | null;
72
+ readonly language: string | null;
73
+ }
74
+
75
+ /** Steps with a title, a body or code; a bare string is a step's body. Others are counted as skipped. */
76
+ export function parseSteps(value: Value | undefined): { steps: WalkthroughStep[]; skipped: number } {
77
+ const steps: WalkthroughStep[] = [];
78
+ let skipped = 0;
79
+ for (const item of list(value)) {
80
+ if (typeof item === "string" || typeof item === "number") {
81
+ const body = String(item).trim();
82
+ if (body === "") skipped += 1;
83
+ else steps.push({ title: "", body, code: null, language: null });
84
+ continue;
85
+ }
86
+ if (!isObject(item) || "kind" in item) {
87
+ skipped += 1;
88
+ continue;
89
+ }
90
+ const title = text(item.title).trim();
91
+ const body = text(item.body ?? item.text).trim();
92
+ const rawCode = optionalText(item.code);
93
+ const code = rawCode === null || rawCode.trim() === "" ? null : rawCode.replace(/\n+$/, "");
94
+ if (title === "" && body === "" && code === null) {
95
+ skipped += 1;
96
+ continue;
97
+ }
98
+ steps.push({ title, body, code, language: optionalText(item.language)?.trim() || null });
99
+ }
100
+ return { steps, skipped };
101
+ }
102
+
103
+ // ── List ────────────────────────────────────────────────────────────────
104
+
105
+ export interface ListRow {
106
+ readonly title: string;
107
+ readonly detail: string | null;
108
+ readonly icon: string | null;
109
+ /** A scalar, or a component such as a Badge. */
110
+ readonly value: Value | null;
111
+ readonly tone: Tone | null;
112
+ readonly actions: readonly Action[];
113
+ }
114
+
115
+ /** Rows with a title; a bare scalar is a row's title. Items that are neither are dropped. */
116
+ export function parseListRows(value: Value | undefined): ListRow[] {
117
+ return list(value).flatMap((item): ListRow[] => {
118
+ if (typeof item === "string" || typeof item === "number") {
119
+ const title = String(item).trim();
120
+ return title === "" ? [] : [{ title, detail: null, icon: null, value: null, tone: null, actions: [] }];
121
+ }
122
+ if (!isObject(item) || "kind" in item) return [];
123
+ const title = text(item.title ?? item.label).trim();
124
+ if (title === "") return [];
125
+ const rawValue = (item.value ?? null) as Value;
126
+ const shown = rawValue === null || (typeof rawValue === "string" && rawValue.trim() === "") ? null : rawValue;
127
+ return [
128
+ {
129
+ title,
130
+ detail: optionalText(item.detail)?.trim() || null,
131
+ icon: optionalText(item.icon)?.trim() || null,
132
+ value: shown,
133
+ tone: item.tone === undefined || item.tone === null ? null : tone(item.tone as Value),
134
+ actions: Array.isArray(item.action) ? item.action.filter(isAction) : isAction(item.action) ? [item.action] : [],
135
+ },
136
+ ];
137
+ });
138
+ }
139
+
140
+ /** The texts a row's Send actions send. */
141
+ export function sentTexts(actions: readonly Action[]): string[] {
142
+ return actions.flatMap((action) => (action.type === "send" ? [action.text] : []));
143
+ }
144
+
145
+ // ── Calendar ────────────────────────────────────────────────────────────
146
+ //
147
+ // Dates are plain calendar days: {year, month, day} integers, never Date
148
+ // objects in local time, so no timezone can move an event to another day.
149
+ // Where a Date is needed (weekday, names), it is built and read in UTC.
150
+
151
+ export interface Day {
152
+ readonly year: number;
153
+ /** 1-12. */
154
+ readonly month: number;
155
+ readonly day: number;
156
+ }
157
+
158
+ export interface Month {
159
+ readonly year: number;
160
+ /** 1-12. */
161
+ readonly month: number;
162
+ }
163
+
164
+ export function daysInMonth({ year, month }: Month): number {
165
+ return new Date(Date.UTC(year, month, 0)).getUTCDate();
166
+ }
167
+
168
+ /** `yyyy-mm-dd` (a trailing time is ignored) as a real calendar day, or null. */
169
+ export function parseDay(value: unknown): Day | null {
170
+ if (typeof value !== "string") return null;
171
+ const match = /^\s*(\d{4})-(\d{1,2})-(\d{1,2})(?:$|[T\s])/.exec(value);
172
+ if (match === null) return null;
173
+ const year = Number(match[1]);
174
+ const month = Number(match[2]);
175
+ const day = Number(match[3]);
176
+ if (month < 1 || month > 12 || day < 1 || day > daysInMonth({ year, month })) return null;
177
+ return { year, month, day };
178
+ }
179
+
180
+ /** `yyyy-mm` (or a full date) as a month, or null. */
181
+ export function parseMonth(value: unknown): Month | null {
182
+ if (typeof value !== "string") return null;
183
+ const match = /^\s*(\d{4})-(\d{1,2})(?:$|-|\s)/.exec(value);
184
+ if (match === null) return null;
185
+ const year = Number(match[1]);
186
+ const month = Number(match[2]);
187
+ return month >= 1 && month <= 12 ? { year, month } : null;
188
+ }
189
+
190
+ const pad = (n: number) => String(n).padStart(2, "0");
191
+
192
+ export function dayKey({ year, month, day }: Day): string {
193
+ return `${year}-${pad(month)}-${pad(day)}`;
194
+ }
195
+
196
+ export function monthKey({ year, month }: Month): string {
197
+ return `${year}-${pad(month)}`;
198
+ }
199
+
200
+ /** Months since year 0, for ordering and distances. */
201
+ export function monthIndex({ year, month }: Month): number {
202
+ return year * 12 + month - 1;
203
+ }
204
+
205
+ export function fromMonthIndex(index: number): Month {
206
+ return { year: Math.floor(index / 12), month: (((index % 12) + 12) % 12) + 1 };
207
+ }
208
+
209
+ export function addMonths(month: Month, delta: number): Month {
210
+ return fromMonthIndex(monthIndex(month) + delta);
211
+ }
212
+
213
+ /** 0 = Sunday … 6 = Saturday. */
214
+ export function weekday({ year, month, day }: Day): number {
215
+ return new Date(Date.UTC(year, month - 1, day)).getUTCDay();
216
+ }
217
+
218
+ /** Today on the user's wall clock. */
219
+ export function localToday(now: Date): Day {
220
+ return { year: now.getFullYear(), month: now.getMonth() + 1, day: now.getDate() };
221
+ }
222
+
223
+ /**
224
+ * The weeks of a month as rows of seven, `null` outside the month, starting
225
+ * on `firstDay` (0 = Sunday … 6 = Saturday).
226
+ */
227
+ export function monthWeeks(month: Month, firstDay: number): (number | null)[][] {
228
+ const lead = (weekday({ ...month, day: 1 }) - firstDay + 7) % 7;
229
+ const cells: (number | null)[] = [
230
+ ...Array.from({ length: lead }, () => null),
231
+ ...Array.from({ length: daysInMonth(month) }, (_, index) => index + 1),
232
+ ];
233
+ while (cells.length % 7 !== 0) cells.push(null);
234
+ return Array.from({ length: cells.length / 7 }, (_, week) => cells.slice(week * 7, week * 7 + 7));
235
+ }
236
+
237
+ /** CLDR regions whose week starts on Sunday or Saturday; everywhere else it is Monday. */
238
+ const SUNDAY_REGIONS =
239
+ "AG AS BD BR BS BT BW BZ CA CN CO DM DO ET GT GU HK HN ID IL IN JM JP KE KH KR LA MH MM MO MT MX MZ NI NP PA PE PH PK PR PT PY SA SG SV TH TT TW UM US VE VI WS YE ZA ZW".split(" ");
240
+ const SATURDAY_REGIONS = "AE AF BH DJ DZ EG IQ IR JO KW LY OM QA SD SY".split(" ");
241
+
242
+ /** Week data on Intl.Locale: a method in Chromium and newer WebKit, a getter in older WebKit; ISO days, 7 = Sunday. */
243
+ interface WeekInfo {
244
+ getWeekInfo?: () => { firstDay?: number };
245
+ readonly weekInfo?: { firstDay?: number };
246
+ }
247
+
248
+ /** The first day of the week (0 = Sunday … 6 = Saturday) for a BCP 47 locale. */
249
+ export function firstWeekday(locale: string): number {
250
+ let info: Intl.Locale;
251
+ try {
252
+ info = new Intl.Locale(locale);
253
+ } catch {
254
+ return 1;
255
+ }
256
+ try {
257
+ const week = info as Intl.Locale & WeekInfo;
258
+ const firstDay = (week.getWeekInfo?.() ?? week.weekInfo)?.firstDay;
259
+ if (typeof firstDay === "number" && firstDay >= 1 && firstDay <= 7) return firstDay % 7;
260
+ } catch {
261
+ // Fall through to the region table.
262
+ }
263
+ // A locale without a region ("en", "ja") takes its likeliest one.
264
+ const region = info.maximize().region ?? "";
265
+ if (SUNDAY_REGIONS.includes(region)) return 0;
266
+ if (SATURDAY_REGIONS.includes(region)) return 6;
267
+ return 1;
268
+ }
269
+
270
+ export interface CalendarEvent {
271
+ readonly day: Day;
272
+ readonly key: string;
273
+ readonly title: string;
274
+ readonly tone: Tone;
275
+ }
276
+
277
+ /** Events with a valid date, in date order (stable within a day); untoned events take the accent. */
278
+ export function parseEvents(value: Value | undefined): { events: CalendarEvent[]; skipped: number } {
279
+ const events: CalendarEvent[] = [];
280
+ let skipped = 0;
281
+ for (const item of list(value)) {
282
+ const day = isObject(item) && !("kind" in item) ? parseDay(item.date) : null;
283
+ if (day === null || !isObject(item) || "kind" in item) {
284
+ skipped += 1;
285
+ continue;
286
+ }
287
+ events.push({
288
+ day,
289
+ key: dayKey(day),
290
+ title: text(item.title ?? item.label).trim(),
291
+ tone: item.tone === undefined || item.tone === null ? "info" : tone(item.tone as Value),
292
+ });
293
+ }
294
+ events.sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0));
295
+ return { events, skipped };
296
+ }
297
+
298
+ /** The months the calendar can show: from the first to the last event, widened to `month` when given. */
299
+ export function monthRange(events: readonly CalendarEvent[], requested: Month | null, today: Day): { first: Month; last: Month } {
300
+ const indexes = events.map((event) => monthIndex(event.day));
301
+ if (requested !== null) indexes.push(monthIndex(requested));
302
+ if (indexes.length === 0) indexes.push(monthIndex(today));
303
+ return { first: fromMonthIndex(Math.min(...indexes)), last: fromMonthIndex(Math.max(...indexes)) };
304
+ }
305
+
306
+ /** The month shown first: the requested one, else today's when events fall in it, else the first upcoming event's, else the last. */
307
+ export function initialMonth(events: readonly CalendarEvent[], requested: Month | null, today: Day): Month {
308
+ if (requested !== null) return requested;
309
+ if (events.length === 0) return today;
310
+ const todayKey = dayKey(today);
311
+ const thisMonth = monthKey(today);
312
+ if (events.some((event) => monthKey(event.day) === thisMonth)) return today;
313
+ const upcoming = events.find((event) => event.key >= todayKey) ?? events[events.length - 1]!;
314
+ return upcoming.day;
315
+ }
@@ -0,0 +1,138 @@
1
+ // Pure rules behind the input fields in fields.tsx: stepping without float
2
+ // drift, option and list shapes, dates and ratings. No React, no DOM.
3
+ import type { Scalar, Value } from "../core/types";
4
+ import { finiteNumber, isObject, isScalar, list, optionalText } from "./values";
5
+
6
+ /** Decimal places a number is written with: 0.25 → 2, 1e-7 → 7, 3 → 0. */
7
+ export function decimals(value: number): number {
8
+ if (!Number.isFinite(value)) return 0;
9
+ const [mantissa = "", exponent = "0"] = String(value).split("e");
10
+ const fraction = mantissa.split(".")[1]?.length ?? 0;
11
+ return Math.max(0, fraction - Number(exponent));
12
+ }
13
+
14
+ /** Digits kept after a step; enough for any sane step, few enough to drop float noise. */
15
+ const MAX_DECIMALS = 10;
16
+
17
+ export interface StepRange {
18
+ readonly min: number | null;
19
+ readonly max: number | null;
20
+ /** Always positive. */
21
+ readonly step: number;
22
+ }
23
+
24
+ /** Normalizes Stepper params: a missing or non-positive step is 1; max never sits below min. */
25
+ export function stepRange(min: Value | undefined, max: Value | undefined, step: Value | undefined): StepRange {
26
+ const low = finiteNumber(min);
27
+ const high = finiteNumber(max);
28
+ const size = finiteNumber(step);
29
+ return {
30
+ min: low,
31
+ max: high === null || low === null ? high : Math.max(low, high),
32
+ step: size !== null && size > 0 ? size : 1,
33
+ };
34
+ }
35
+
36
+ export function clampToRange(value: number, range: StepRange): number {
37
+ const floor = range.min ?? -Infinity;
38
+ const ceiling = range.max ?? Infinity;
39
+ return Math.min(ceiling, Math.max(floor, value));
40
+ }
41
+
42
+ /** The precision a stepped value is shown and stored with. */
43
+ export function stepPrecision(value: number, range: StepRange): number {
44
+ return Math.min(
45
+ MAX_DECIMALS,
46
+ Math.max(decimals(range.step), decimals(range.min ?? 0), decimals(range.max ?? 0), decimals(value)),
47
+ );
48
+ }
49
+
50
+ /**
51
+ * `value` moved by `count` steps (negative steps down), rounded so 0.1 + 0.2
52
+ * lands on 0.3, then clamped. `±Infinity` jumps to a bound; with no such
53
+ * bound the value stays.
54
+ */
55
+ export function stepBy(value: number, count: number, range: StepRange): number {
56
+ const moved = clampToRange(Number((value + count * range.step).toFixed(stepPrecision(value, range))), range);
57
+ return Number.isFinite(moved) ? moved : value;
58
+ }
59
+
60
+ /** What the Stepper shows and steps from: the bound state, else min, else 0, inside the range. */
61
+ export function stepperValue(state: Value | undefined, range: StepRange): number {
62
+ return clampToRange(finiteNumber(state) ?? range.min ?? 0, range);
63
+ }
64
+
65
+ export interface RadioOption {
66
+ readonly value: Scalar;
67
+ readonly detail: string | null;
68
+ }
69
+
70
+ /** Radio options: scalars, or `{value, detail?}` objects with a scalar value; anything else is dropped. */
71
+ export function radioOptions(options: Value | undefined): RadioOption[] {
72
+ return list(options).flatMap((option: unknown): RadioOption[] => {
73
+ if (isScalar(option)) return [{ value: option, detail: null }];
74
+ if (!isObject(option) || !isScalar(option.value)) return [];
75
+ const detail = isScalar(option.detail) ? optionalText(option.detail) : null;
76
+ return [{ value: option.value, detail: detail === "" ? null : detail }];
77
+ });
78
+ }
79
+
80
+ /** How a list entry or option value reads on screen. */
81
+ export function itemText(value: Value): string {
82
+ if (value === null) return "None";
83
+ return isScalar(value) ? String(value) : JSON.stringify(value);
84
+ }
85
+
86
+ export type ItemProblem = "empty" | "duplicate";
87
+
88
+ /** Why `draft` cannot join `items` (ignoring the entry at `except`, the one being edited), or null if it can. */
89
+ export function itemProblem(items: readonly Value[], draft: string, except = -1): ItemProblem | null {
90
+ const item = draft.trim();
91
+ if (item === "") return "empty";
92
+ return items.some((existing, index) => index !== except && itemText(existing) === item) ? "duplicate" : null;
93
+ }
94
+
95
+ /** `items` with the trimmed draft appended, or null when it is empty or already listed. */
96
+ export function addItem(items: readonly Value[], draft: string): Value[] | null {
97
+ return itemProblem(items, draft) === null ? [...items, draft.trim()] : null;
98
+ }
99
+
100
+ /** `items` with entry `index` replaced by the trimmed draft; null when invalid or unchanged. */
101
+ export function replaceItem(items: readonly Value[], index: number, draft: string): Value[] | null {
102
+ if (index < 0 || index >= items.length || itemProblem(items, draft, index) !== null) return null;
103
+ const item = draft.trim();
104
+ if (items[index] === item) return null;
105
+ return items.map((existing, position) => (position === index ? item : existing));
106
+ }
107
+
108
+ /** A real calendar date in `yyyy-mm-dd`, else null (what a native date input accepts). */
109
+ export function isoDate(value: Value | undefined): string | null {
110
+ if (typeof value !== "string") return null;
111
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value);
112
+ if (match === null) return null;
113
+ const [year, month, day] = [Number(match[1]), Number(match[2]), Number(match[3])];
114
+ const date = new Date(Date.UTC(year, month - 1, day));
115
+ return date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day
116
+ ? value
117
+ : null;
118
+ }
119
+
120
+ export const RATING_MAX_DEFAULT = 5;
121
+ const RATING_MAX_LIMIT = 10;
122
+
123
+ /** Star count: a whole number from 1 to 10, default 5. */
124
+ export function ratingMax(max: Value | undefined): number {
125
+ const count = finiteNumber(max);
126
+ if (count === null) return RATING_MAX_DEFAULT;
127
+ return Math.min(RATING_MAX_LIMIT, Math.max(1, Math.round(count)));
128
+ }
129
+
130
+ /** The rating shown: the state inside 0..max (fractions kept, drawn as partial stars). */
131
+ export function ratingValue(state: Value | undefined, max: number): number {
132
+ return Math.min(max, Math.max(0, finiteNumber(state) ?? 0));
133
+ }
134
+
135
+ /** How much of star `index` (1-based) is filled at `value`: 0 to 1. */
136
+ export function starFill(value: number, index: number): number {
137
+ return Math.min(1, Math.max(0, value - (index - 1)));
138
+ }
@@ -0,0 +1,71 @@
1
+ // What the gallery's header lists and finds: the catalog's components group by
2
+ // group, and a query matched against their names, groups, summaries and
3
+ // example variants, best match first. Pure; GalleryHeader renders it.
4
+ import { COMPONENTS, type CatalogComponentSpec, type ComponentGroup, type ComponentName } from "../core/catalog";
5
+
6
+ /** The order groups appear in the gallery. */
7
+ export const GALLERY_GROUPS: readonly ComponentGroup[] = [
8
+ "Layout",
9
+ "Content",
10
+ "Media",
11
+ "Data",
12
+ "Inputs",
13
+ "Widgets",
14
+ "Project",
15
+ "Dev",
16
+ "Manage",
17
+ "Evidence",
18
+ ];
19
+
20
+ /** A group's components in catalog order. */
21
+ export function componentsIn(group: ComponentGroup): ComponentName[] {
22
+ return (Object.keys(COMPONENTS) as ComponentName[]).filter((name) => COMPONENTS[name].group === group);
23
+ }
24
+
25
+ /** Every component in gallery order: group by group, catalog order within each. */
26
+ export const GALLERY_ORDER: readonly ComponentName[] = GALLERY_GROUPS.flatMap(componentsIn);
27
+
28
+ /** Why a component matched, strongest first. */
29
+ export type MatchKind = "prefix" | "name" | "group" | "detail";
30
+
31
+ export interface ComponentMatch {
32
+ readonly name: ComponentName;
33
+ readonly kind: MatchKind;
34
+ /** Where the query starts in the name, for highlighting; null when it matched elsewhere. */
35
+ readonly at: number | null;
36
+ }
37
+
38
+ const RANK: Record<MatchKind, number> = { prefix: 0, name: 1, group: 2, detail: 3 };
39
+
40
+ /**
41
+ * The components a query finds, best first: the name starts with it, then
42
+ * contains it, then the group starts with it, then the summary or one of the
43
+ * component's example variants ("funnel" for Chart-funnel.ui) contains it;
44
+ * gallery order within each kind. Case is ignored. A blank query finds
45
+ * nothing: the caller shows everything, unfiltered.
46
+ */
47
+ export function searchComponents(
48
+ query: string,
49
+ variants: ReadonlyMap<ComponentName, readonly string[]> = new Map(),
50
+ ): ComponentMatch[] {
51
+ const needle = query.trim().toLowerCase();
52
+ if (needle === "") return [];
53
+ const found: ComponentMatch[] = [];
54
+ for (const name of GALLERY_ORDER) {
55
+ const spec: CatalogComponentSpec = COMPONENTS[name];
56
+ const at = name.toLowerCase().indexOf(needle);
57
+ let kind: MatchKind | null = null;
58
+ if (at === 0) kind = "prefix";
59
+ else if (at > 0) kind = "name";
60
+ else if (spec.group.toLowerCase().startsWith(needle)) kind = "group";
61
+ else if (
62
+ spec.summary?.toLowerCase().includes(needle) ||
63
+ (variants.get(name) ?? []).some((variant) => variant.toLowerCase().includes(needle))
64
+ ) {
65
+ kind = "detail";
66
+ }
67
+ if (kind !== null) found.push({ name, kind, at: at >= 0 ? at : null });
68
+ }
69
+ // Array.prototype.sort is stable, so gallery order holds within a kind.
70
+ return found.sort((a, b) => RANK[a.kind] - RANK[b.kind]);
71
+ }