@fungi.computer/bonsai 0.0.0-stage → 0.0.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 (132) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/LICENSE +21 -0
  3. package/README.md +196 -2
  4. package/backend/background.ts +259 -0
  5. package/backend/change-copy.ts +99 -0
  6. package/backend/changes.ts +530 -0
  7. package/backend/diff.ts +228 -0
  8. package/backend/file-edits.ts +850 -0
  9. package/backend/files-copy.ts +361 -0
  10. package/backend/gills.ts +125 -0
  11. package/backend/git.ts +123 -0
  12. package/backend/graph-commit-copies.ts +321 -0
  13. package/backend/history.ts +178 -0
  14. package/backend/index.ts +144 -0
  15. package/backend/insights.ts +256 -0
  16. package/backend/merge.ts +999 -0
  17. package/backend/methods.ts +835 -0
  18. package/backend/policy.ts +160 -0
  19. package/backend/read-copy-page.ts +77 -0
  20. package/backend/repository-copy.ts +70 -0
  21. package/backend/reviews.ts +317 -0
  22. package/backend/service.ts +892 -0
  23. package/backend/state.ts +914 -0
  24. package/demo/koi-pond/README.md +17 -0
  25. package/demo/koi-pond/index.html +11 -0
  26. package/demo/koi-pond/package.json +13 -0
  27. package/demo/koi-pond/pnpm-lock.yaml +450 -0
  28. package/demo/koi-pond/src/fish.ts +32 -0
  29. package/demo/koi-pond/src/main.ts +24 -0
  30. package/demo/koi-pond/src/pond.ts +27 -0
  31. package/docs/media/bonsai-insights.png +0 -0
  32. package/docs/media/overview-phone.png +0 -0
  33. package/docs/media/overview.png +0 -0
  34. package/package.json +152 -3
  35. package/scripts/agent.sh +55 -0
  36. package/scripts/demo.sh +233 -0
  37. package/scripts/rebuild-copies.mjs +34 -0
  38. package/scripts/release.mjs +52 -0
  39. package/scripts/runtime-proof.mjs +299 -0
  40. package/scripts/seed-demo.mjs +724 -0
  41. package/scripts/seed-step.mjs +37 -0
  42. package/scripts/shed-terminal-proof.mjs +774 -0
  43. package/shared/contract.ts +404 -0
  44. package/shared/hooks.ts +208 -0
  45. package/shared/insights.ts +90 -0
  46. package/shared/picker-options.ts +23 -0
  47. package/shared/presentation.ts +21 -0
  48. package/shared/repository-deletion.ts +4 -0
  49. package/test/apps-build-install.test.ts +302 -0
  50. package/test/background-runtime.test.ts +206 -0
  51. package/test/browser/board.browser.mjs +52 -0
  52. package/test/browser/capture.browser.mjs +1264 -0
  53. package/test/browser/edit-in-inkcap.browser.mjs +83 -0
  54. package/test/browser/repository-settings-fixture.tsx +108 -0
  55. package/test/browser/repository-settings.browser.mjs +136 -0
  56. package/test/browser/repository-settings.html +10 -0
  57. package/test/browser/visual-fixture.tsx +667 -0
  58. package/test/browser/visual.html +10 -0
  59. package/test/browser/vite.config.mjs +9 -0
  60. package/test/change-page.test.ts +916 -0
  61. package/test/changes.test.ts +1063 -0
  62. package/test/checks-workflow-template.test.mjs +234 -0
  63. package/test/diff.test.ts +70 -0
  64. package/test/edit-in-inkcap.test.ts +227 -0
  65. package/test/file-edits.test.ts +2714 -0
  66. package/test/files-read-copy-calls.test.ts +374 -0
  67. package/test/fork-adoption.test.ts +111 -0
  68. package/test/gills.test.ts +125 -0
  69. package/test/git-binding.fixture.ts +30 -0
  70. package/test/git-push.config.jsonc +9 -0
  71. package/test/git-push.native.mjs +413 -0
  72. package/test/git-push.worker.ts +280 -0
  73. package/test/graph-commit-read-copy-calls.test.ts +401 -0
  74. package/test/home-page.test.ts +170 -0
  75. package/test/hooks.test.ts +250 -0
  76. package/test/insight-data.test.ts +51 -0
  77. package/test/insight-layout.test.mjs +230 -0
  78. package/test/insights.test.ts +154 -0
  79. package/test/merge-background.test.ts +756 -0
  80. package/test/merge-conflict.test.ts +337 -0
  81. package/test/merge-declaration.test.ts +46 -0
  82. package/test/merge-repro.test.ts +561 -0
  83. package/test/merge-template-build.test.mjs +234 -0
  84. package/test/network-layout.test.ts +200 -0
  85. package/test/outbox.test.ts +424 -0
  86. package/test/parts.test.ts +86 -0
  87. package/test/pkt-line.test.ts +54 -0
  88. package/test/policy.test.ts +172 -0
  89. package/test/read-copy-calls.test.ts +691 -0
  90. package/test/read-copy-composer.test.ts +388 -0
  91. package/test/read-copy-lifetimes.test.ts +214 -0
  92. package/test/read-copy-rpc.config.jsonc +12 -0
  93. package/test/read-copy-rpc.native.mjs +47 -0
  94. package/test/read-copy-rpc.worker.ts +92 -0
  95. package/test/recent-activity.test.ts +62 -0
  96. package/test/repository-deletion.test.ts +500 -0
  97. package/test/seed-resume.test.mjs +91 -0
  98. package/test/storyboard-save-https-git.fixture.mjs +119 -0
  99. package/test/storyboard-save-template.test.mjs +545 -0
  100. package/test/ui-errors.test.ts +976 -0
  101. package/test/workflow-store.test.ts +115 -0
  102. package/tsconfig.json +36 -0
  103. package/turbo.jsonc +25 -0
  104. package/vite.config.ts +19 -0
  105. package/vitest.config.ts +5 -0
  106. package/web/api.ts +224 -0
  107. package/web/app-build-panel.tsx +171 -0
  108. package/web/board.tsx +259 -0
  109. package/web/bonsai.css +642 -0
  110. package/web/change-page.tsx +715 -0
  111. package/web/change-rows.tsx +135 -0
  112. package/web/code-tab.tsx +267 -0
  113. package/web/commit-tree.tsx +152 -0
  114. package/web/format.ts +54 -0
  115. package/web/home-page.tsx +274 -0
  116. package/web/index.html +17 -0
  117. package/web/insight-data.ts +76 -0
  118. package/web/insight-tiles.tsx +534 -0
  119. package/web/insights.tsx +250 -0
  120. package/web/issues-panel.tsx +305 -0
  121. package/web/main.tsx +72 -0
  122. package/web/network-graph.tsx +647 -0
  123. package/web/overview.tsx +339 -0
  124. package/web/pages.tsx +469 -0
  125. package/web/parts.tsx +381 -0
  126. package/web/public/favicon.svg +1 -0
  127. package/web/repo-navigation.tsx +256 -0
  128. package/web/repo-settings.tsx +145 -0
  129. package/web/router.tsx +151 -0
  130. package/web/transport.ts +91 -0
  131. package/web/views.tsx +151 -0
  132. package/web/working-now.tsx +98 -0
package/web/board.tsx ADDED
@@ -0,0 +1,259 @@
1
+ import {
2
+ useLayoutEffect,
3
+ useMemo,
4
+ useRef,
5
+ useState,
6
+ type ReactNode,
7
+ } from "react";
8
+ import { Screen, ScreenHeader } from "@fungi.computer/caps/components/surface";
9
+ import { Button } from "@fungi.computer/caps/components/button";
10
+ import { Grip, Maximize2, X } from "lucide-react";
11
+ import { Link } from "./router.js";
12
+ import {
13
+ arrangeBento,
14
+ applyBentoOps,
15
+ useMaosaicoBento,
16
+ type BentoPlacement,
17
+ type BentoColumns,
18
+ } from "@fungi.computer/maosaico";
19
+
20
+ export type BoardTile = {
21
+ id: string;
22
+ title: string;
23
+ status?: ReactNode;
24
+ body: ReactNode;
25
+ to?: string;
26
+ /** Long documents grow their rows instead of acquiring a nested scroller. */
27
+ autoHeight?: boolean;
28
+ };
29
+
30
+ function BonsaiTile({
31
+ title,
32
+ status,
33
+ children,
34
+ footer,
35
+ autoHeight = false,
36
+ to,
37
+ }: {
38
+ title: ReactNode;
39
+ status?: ReactNode;
40
+ children: ReactNode;
41
+ footer?: ReactNode;
42
+ autoHeight?: boolean;
43
+ to: string | undefined;
44
+ }) {
45
+ const surface = (
46
+ <Screen className={autoHeight ? "min-w-0" : "flex h-full min-w-0 flex-col"}>
47
+ <ScreenHeader title={<h2>{title}</h2>} status={status} />
48
+ <div
49
+ className={
50
+ autoHeight ? "min-w-0 p-3" : "min-h-0 flex-1 overflow-auto p-3"
51
+ }
52
+ >
53
+ {children}
54
+ </div>
55
+ {footer ? (
56
+ <footer className="flex justify-between gap-2 p-3">{footer}</footer>
57
+ ) : null}
58
+ </Screen>
59
+ );
60
+ return to ? (
61
+ <Link to={to} className="block">
62
+ {surface}
63
+ </Link>
64
+ ) : (
65
+ surface
66
+ );
67
+ }
68
+
69
+ /** App content and order around Maosaico's measured geometry and interaction. */
70
+ export function BonsaiBoard({
71
+ placements,
72
+ tiles,
73
+ phoneOrder,
74
+ rowHeight = 168,
75
+ columns: fixedColumns,
76
+ editable = false,
77
+ onChange,
78
+ onRemove,
79
+ }: {
80
+ placements: readonly BentoPlacement[];
81
+ tiles: readonly BoardTile[];
82
+ phoneOrder?: readonly string[];
83
+ /** Minimum row height; zero packs fixed, measured content. */
84
+ rowHeight?: number;
85
+ columns?: BentoColumns;
86
+ editable?: boolean;
87
+ onChange?: (placements: readonly BentoPlacement[]) => void;
88
+ onRemove?: (id: string) => void;
89
+ }) {
90
+ const container = useRef<HTMLDivElement>(null);
91
+ const [width, setWidth] = useState(0);
92
+ const [heights, setHeights] = useState<ReadonlyMap<string, number>>(
93
+ new Map(),
94
+ );
95
+ useLayoutEffect(() => {
96
+ const element = container.current;
97
+ if (!element) return;
98
+ const observer = new ResizeObserver(([entry]) => {
99
+ if (entry) setWidth(entry.contentRect.width);
100
+ });
101
+ observer.observe(element);
102
+ return () => {
103
+ observer.disconnect();
104
+ };
105
+ }, []);
106
+ const columns = fixedColumns ?? (width < 560 ? 1 : width < 920 ? 2 : 4);
107
+ const shown = useMemo(() => {
108
+ const active = placements.filter((placement) =>
109
+ tiles.some((tile) => tile.id === placement.id),
110
+ );
111
+ if (columns === 1 && phoneOrder?.length) {
112
+ let row = 0;
113
+ return [...active]
114
+ .sort((a, b) => phoneOrder.indexOf(a.id) - phoneOrder.indexOf(b.id))
115
+ .map((tile) => {
116
+ const h = tile.h;
117
+ const next = { ...tile, col: 0, row, w: 1, h };
118
+ row += h;
119
+ return next;
120
+ });
121
+ }
122
+ return active.some((tile) => tile.col + tile.w > columns)
123
+ ? arrangeBento(active, columns)
124
+ : active;
125
+ }, [placements, tiles, columns, phoneOrder]);
126
+ const rowHeights = useMemo(() => {
127
+ const rows: number[] = [];
128
+ for (const tile of shown) {
129
+ if (!tiles.find((item) => item.id === tile.id)?.autoHeight) continue;
130
+ const measured = heights.get(tile.id);
131
+ if (measured === undefined) continue;
132
+ const last = tile.row + tile.h - 1;
133
+ const earlier = Array.from(
134
+ { length: tile.h - 1 },
135
+ (_, i) => (rows[tile.row + i] ?? rowHeight) + 12,
136
+ ).reduce((sum, h) => sum + h, 0);
137
+ while (rows.length <= last) rows.push(rowHeight);
138
+ rows[last] = Math.max(rows[last] ?? rowHeight, measured - earlier);
139
+ }
140
+ return rows;
141
+ }, [shown, tiles, heights, rowHeight]);
142
+ const binding = useMaosaicoBento({
143
+ placements: shown,
144
+ columns,
145
+ width,
146
+ rowHeight,
147
+ rowHeights,
148
+ gap: 12,
149
+ enabled: editable,
150
+ onChange: (ops) => {
151
+ onChange?.(applyBentoOps(shown, ops, columns));
152
+ },
153
+ });
154
+ useLayoutEffect(() => {
155
+ const element = container.current;
156
+ if (!element) return;
157
+ const observer = new ResizeObserver((entries) => {
158
+ setHeights((current) => {
159
+ const next = new Map(current);
160
+ let changed = false;
161
+ for (const entry of entries) {
162
+ const id = entry.target.parentElement?.dataset.tile;
163
+ if (!id) continue;
164
+ const height = Math.ceil(entry.target.getBoundingClientRect().height);
165
+ if (next.get(id) !== height) {
166
+ next.set(id, height);
167
+ changed = true;
168
+ }
169
+ }
170
+ return changed ? next : current;
171
+ });
172
+ });
173
+ for (const child of element.children) {
174
+ if (
175
+ child instanceof HTMLElement &&
176
+ tiles.find((tile) => tile.id === child.dataset.tile)?.autoHeight &&
177
+ child.firstElementChild
178
+ )
179
+ observer.observe(child.firstElementChild);
180
+ }
181
+ return () => {
182
+ observer.disconnect();
183
+ };
184
+ }, [tiles, width]);
185
+ const height = Math.max(
186
+ 0,
187
+ ...[...binding.frames.values()].map((frame) => frame.y + frame.height),
188
+ );
189
+ return (
190
+ <div ref={container} className="relative min-w-0" style={{ height }}>
191
+ {shown.map((placement) => {
192
+ const tile = tiles.find((item) => item.id === placement.id);
193
+ const frame = binding.frames.get(placement.id);
194
+ if (!tile || !frame) return null;
195
+ return (
196
+ <div
197
+ key={tile.id}
198
+ data-tile={tile.id}
199
+ className="absolute min-w-0"
200
+ style={{
201
+ left: frame.x,
202
+ top: frame.y,
203
+ width: frame.width,
204
+ height: frame.height,
205
+ }}
206
+ >
207
+ <BonsaiTile
208
+ title={tile.title}
209
+ to={editable ? undefined : tile.to}
210
+ autoHeight={tile.autoHeight ?? false}
211
+ status={
212
+ editable ? (
213
+ <Button
214
+ size="xs"
215
+ variant="ghost"
216
+ aria-label={`Remove ${tile.title}`}
217
+ onClick={() => onRemove?.(tile.id)}
218
+ >
219
+ <X size={14} />
220
+ </Button>
221
+ ) : (
222
+ tile.status
223
+ )
224
+ }
225
+ footer={
226
+ editable ? (
227
+ <>
228
+ <Button
229
+ size="xs"
230
+ variant="outline"
231
+ className="touch-none"
232
+ aria-label={`Move ${tile.title}`}
233
+ {...binding.bind(tile.id, "move")}
234
+ >
235
+ <Grip size={12} />
236
+ Move
237
+ </Button>
238
+ <Button
239
+ size="xs"
240
+ variant="outline"
241
+ className="touch-none"
242
+ aria-label={`Resize ${tile.title}`}
243
+ {...binding.bind(tile.id, "resize")}
244
+ >
245
+ <Maximize2 size={12} />
246
+ Resize
247
+ </Button>
248
+ </>
249
+ ) : null
250
+ }
251
+ >
252
+ {tile.body}
253
+ </BonsaiTile>
254
+ </div>
255
+ );
256
+ })}
257
+ </div>
258
+ );
259
+ }