@protoboardai/views 0.0.0-stage → 0.2.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 (56) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +13 -0
  3. package/README.md +21 -2
  4. package/package.json +46 -4
  5. package/src/applies.ts +54 -0
  6. package/src/builtins.tsx +89 -0
  7. package/src/drawing/BoardDrawingView.tsx +515 -0
  8. package/src/drawing/CircuitPcbView.tsx +494 -0
  9. package/src/drawing/DrawingControls.tsx +39 -0
  10. package/src/drawing/boards.ts +82 -0
  11. package/src/drawing/circuit.ts +413 -0
  12. package/src/drawing/kicadSchematic.ts +277 -0
  13. package/src/drawing/palettes.ts +150 -0
  14. package/src/drawing/svg.ts +445 -0
  15. package/src/drawing/usePanZoom.ts +400 -0
  16. package/src/icons.tsx +69 -0
  17. package/src/index.ts +33 -0
  18. package/src/inspector/DrcPanel.tsx +302 -0
  19. package/src/inspector/SelectionInspector.tsx +392 -0
  20. package/src/inspector/index.ts +9 -0
  21. package/src/inspector/model.ts +224 -0
  22. package/src/pair/PairView.tsx +314 -0
  23. package/src/pair/model.ts +206 -0
  24. package/src/parts/BomEditor.tsx +564 -0
  25. package/src/parts/CardPages.tsx +317 -0
  26. package/src/parts/CategoryList.tsx +72 -0
  27. package/src/parts/CloudPanel.tsx +160 -0
  28. package/src/parts/GlbPreview.tsx +116 -0
  29. package/src/parts/LibrariesManager.tsx +251 -0
  30. package/src/parts/LibraryBrowser.tsx +191 -0
  31. package/src/parts/LibraryPage.tsx +168 -0
  32. package/src/parts/PartCard.tsx +274 -0
  33. package/src/parts/PartDetail.tsx +613 -0
  34. package/src/parts/PartStatus.tsx +54 -0
  35. package/src/parts/Prices.tsx +241 -0
  36. package/src/parts/ProjectParts.tsx +174 -0
  37. package/src/parts/ProjectPartsPage.tsx +264 -0
  38. package/src/parts/SettingsPage.tsx +268 -0
  39. package/src/parts/Surface.tsx +51 -0
  40. package/src/parts/Windowed.tsx +70 -0
  41. package/src/parts/carousel.tsx +344 -0
  42. package/src/parts/index.ts +32 -0
  43. package/src/parts/library-data.ts +726 -0
  44. package/src/parts/matte.ts +69 -0
  45. package/src/parts/model.ts +590 -0
  46. package/src/parts/part-pages.ts +379 -0
  47. package/src/parts/part-status.ts +88 -0
  48. package/src/parts/revision.ts +11 -0
  49. package/src/parts/three.d.ts +5 -0
  50. package/src/parts/ui.tsx +335 -0
  51. package/src/parts/views.tsx +433 -0
  52. package/src/registry.ts +107 -0
  53. package/src/tabs.ts +118 -0
  54. package/src/theme.ts +225 -0
  55. package/src/types.ts +93 -0
  56. package/src/ui/Select.tsx +99 -0
@@ -0,0 +1,69 @@
1
+ /**
2
+ * What a part picture is drawn on (PB-917, the 2026-10-05 review: "dark if
3
+ * we can, a light background at worst"). Most library thumbnails are renders
4
+ * with a transparent background: they sit on the theme's own surface. A photo
5
+ * shot on white keeps a light tile, and the white melts into the tile
6
+ * (`mix-blend-mode: multiply`), so in a dark theme the tile is a soft grey
7
+ * rather than a white square. Other photos keep their own background on the
8
+ * tile. The picture itself is never changed (no re-rendering, no keying).
9
+ *
10
+ * The kind is read from the picture's border once it has loaded (a 32 × 32
11
+ * sample on a canvas). When the canvas cannot be read (a picture from
12
+ * another origin without CORS), the picture gets the light tile: what every
13
+ * picture had before.
14
+ */
15
+ export type Matte = "clear" | "white" | "photo";
16
+
17
+ /** The border of an RGBA sample (`size` × `size`), as the share of pixels that are transparent and that are near-white. */
18
+ export function borderStats(data: ArrayLike<number>, size: number): { clear: number; white: number } {
19
+ let n = 0;
20
+ let clear = 0;
21
+ let white = 0;
22
+ const at = (x: number, y: number) => {
23
+ const i = (y * size + x) * 4;
24
+ n++;
25
+ const a = data[i + 3];
26
+ if (a < 24) clear++;
27
+ else if (a > 232 && Math.min(data[i], data[i + 1], data[i + 2]) >= 236) white++;
28
+ };
29
+ for (let x = 0; x < size; x++) {
30
+ at(x, 0);
31
+ at(x, size - 1);
32
+ }
33
+ for (let y = 1; y < size - 1; y++) {
34
+ at(0, y);
35
+ at(size - 1, y);
36
+ }
37
+ return { clear: clear / n, white: white / n };
38
+ }
39
+
40
+ /** The matte for a picture's border: mostly transparent → clear; mostly near-white → white; else a photo. */
41
+ export function matteOf(stats: { clear: number; white: number }): Matte {
42
+ if (stats.clear >= 0.6) return "clear";
43
+ if (stats.white + stats.clear >= 0.6) return "white";
44
+ return "photo";
45
+ }
46
+
47
+ const known = new Map<string, Matte>();
48
+
49
+ /** The matte of a loaded picture (cached by source), or undefined when its pixels cannot be read. */
50
+ export function readMatte(img: HTMLImageElement): Matte | undefined {
51
+ const cached = known.get(img.currentSrc || img.src);
52
+ if (cached) return cached;
53
+ const size = 32;
54
+ try {
55
+ const canvas = document.createElement("canvas");
56
+ canvas.width = canvas.height = size;
57
+ const ctx = canvas.getContext("2d", { willReadFrequently: true });
58
+ if (!ctx) return undefined;
59
+ ctx.drawImage(img, 0, 0, size, size);
60
+ const m = matteOf(borderStats(ctx.getImageData(0, 0, size, size).data, size));
61
+ known.set(img.currentSrc || img.src, m);
62
+ return m;
63
+ } catch {
64
+ return undefined;
65
+ }
66
+ }
67
+
68
+ /** A matte read before (the same picture in another card). */
69
+ export const knownMatte = (src: string | undefined): Matte | undefined => (src ? known.get(src) : undefined);
@@ -0,0 +1,590 @@
1
+ /**
2
+ * The data the parts views draw (PB-902, PB-903, PB-904, PB-912): the
3
+ * library browser and its part cards, a part's detail, the project's parts,
4
+ * the libraries manager, the bill of materials and the Cloud panel. Plain
5
+ * data, computed by the shell (the VS Code extension from the CLI and the
6
+ * library client; the web shell from `protoboard dev`), so the views run the
7
+ * same in every host. Actions go back through the `*Actions` callbacks.
8
+ */
9
+
10
+ import type { LibrarySort } from "@protoboardai/core/library-sort";
11
+ import type { Bom, BomGroup, BomLine } from "@protoboardai/core/bom";
12
+ import type { PartTrust } from "@protoboardai/core/part-trust";
13
+
14
+ export type { LibrarySort, PartTrust };
15
+
16
+ // ---------------------------------------------------------------- library
17
+
18
+ export interface LibraryQuery {
19
+ text: string;
20
+ /** Taxonomy path ("" for every category). */
21
+ category: string;
22
+ domain: string;
23
+ protocol: string;
24
+ /** The order asked for (the `sort` request parameter of a library that sorts). */
25
+ sort: LibrarySort;
26
+ }
27
+
28
+ /** The order of the results and what the control may offer (library-data.ts `sortState`). */
29
+ export interface LibrarySortModel {
30
+ /** The order shown: the one asked for when it can be made, else the nearest that can. */
31
+ value: LibrarySort;
32
+ /** Every order that works here: those the libraries sort by, and those made from the loaded parts. */
33
+ options: LibrarySort[];
34
+ /** The order was made here, over the parts loaded so far (the libraries do not sort by it). */
35
+ local: boolean;
36
+ }
37
+
38
+ /** One part as a card or a row of results. */
39
+ export interface PartCardModel {
40
+ /** Unique in the list (library and part id). */
41
+ key: string;
42
+ partId: string;
43
+ /** The short name ("Adafruit BME688"). */
44
+ name: string;
45
+ /** What it is, for the second half of a compact title ("Temp, Humidity, Pressure, Gas"). */
46
+ summary?: string;
47
+ manufacturer?: string;
48
+ mpn?: string;
49
+ /** The category trail's last names ("Environmental · Gas"). */
50
+ category?: string;
51
+ /** One short sentence that tells it apart from its neighbours. */
52
+ description?: string;
53
+ /** Protocols, packages and the like, as small tags. */
54
+ tags: string[];
55
+ library: string;
56
+ /** The revision Add takes. */
57
+ revision: number;
58
+ /** A thumbnail URL, once the host has it. */
59
+ thumbnail?: string;
60
+ /** The project's copy: its pinned revision (absent when the part is not in the project). */
61
+ used?: { revision?: number; elsewhere?: boolean };
62
+ /** A newer revision than the project's pin. */
63
+ update?: number;
64
+ deprecated?: boolean;
65
+ /** An earlier library provides the same id. */
66
+ shadowedBy?: string;
67
+ /** Adding or updating now ("Adding"). */
68
+ busy?: string;
69
+ /** Flags the "Show only" filters use, when the library says. */
70
+ hasModel?: boolean;
71
+ audited?: boolean;
72
+ /** The part's relevance in its library, when the library ranks. */
73
+ relevance?: number;
74
+ /**
75
+ * How the card asks for its pages (PB-918): the library part and revision
76
+ * whose specifications, dimensions, interfaces, CAD and artifacts it shows.
77
+ * Absent: the card has only its overview (a part no library holds).
78
+ */
79
+ pages?: PartPagesRef;
80
+ }
81
+
82
+ // ---------------------------------------------------------------- a part's pages (PB-918)
83
+
84
+ /** The pages a card's text carousel turns through, in order; a part shows only those it has data for. */
85
+ export const CARD_PAGES = ["overview", "specs", "dimensions", "interfaces", "cad", "artifacts"] as const;
86
+ export type CardPageId = (typeof CARD_PAGES)[number];
87
+
88
+ /** Which part's pages: `<library>/<part id>` and the revision (the recommended one when absent). */
89
+ export interface PartPagesRef {
90
+ key: string;
91
+ revision?: number;
92
+ }
93
+
94
+ export interface PartImage {
95
+ url: string;
96
+ /** What the picture is ("Photo", "CAD render"), for its accessible name. */
97
+ label: string;
98
+ }
99
+
100
+ /** A part's outline, in millimetres, as far as the definition states it. */
101
+ export interface PartOutline {
102
+ length?: number;
103
+ width?: number;
104
+ height?: number;
105
+ diameter?: number;
106
+ }
107
+
108
+ export interface PartCadInfo {
109
+ /** A still picture of the model (a list never loads the model itself). */
110
+ preview?: string;
111
+ /** "STEP · 0.85 MB". */
112
+ file?: string;
113
+ /** "Vendor CAD · MIT", "Generated · KCL". */
114
+ origin?: string;
115
+ /** Named mate features (one per interface with geometry). */
116
+ features: number;
117
+ /** The revision carries a mesh the detail can orbit, and its size in bytes. */
118
+ mesh?: number;
119
+ }
120
+
121
+ export interface PartArtifactItem {
122
+ name: string;
123
+ /** Its format or size: "PDF", "web", "0.85 MB". */
124
+ note?: string;
125
+ url?: string;
126
+ license?: string;
127
+ }
128
+
129
+ /** A part's artifacts of one kind: datasheets, CAD models, CAD sources. */
130
+ export interface PartArtifactGroup {
131
+ kind: string;
132
+ label: string;
133
+ icon: string;
134
+ items: PartArtifactItem[];
135
+ /** One line for the group as a row: its only item's name, or its formats ("STEP · 3MF · F3D"). */
136
+ summary: string;
137
+ /** The row's right-hand note: the only item's format, or the count ("3 files"). */
138
+ note: string;
139
+ }
140
+
141
+ /** What a part's pages show, from its definition and its revision's files. Fetched when a page is first shown. */
142
+ export interface PartPages {
143
+ revision: number;
144
+ /** Its pictures: the catalog picture first, then the others the revision carries. */
145
+ images: PartImage[];
146
+ specs: PartFact[];
147
+ /** Remarks the definition makes about its figures, under the specifications. */
148
+ specNotes: string[];
149
+ dimensions?: { outline?: PartOutline; rows: PartFact[] };
150
+ /** One chip per interface or bus, the same ones folded ("STEMMA QT ×2"); `count` is every interface. */
151
+ interfaces: { chips: string[]; count: number };
152
+ cad?: PartCadInfo;
153
+ artifacts: PartArtifactGroup[];
154
+ }
155
+
156
+ // ---------------------------------------------------------------- prices by source
157
+
158
+ /** One price a supplier lists, as the library serves it. */
159
+ export interface PartPriceOffer {
160
+ mpn: string;
161
+ sku?: string;
162
+ url?: string;
163
+ unitPrice?: number;
164
+ currency?: string;
165
+ priceBreaks?: { quantity: number; unitPrice: number }[];
166
+ minQuantity?: number;
167
+ stock?: number;
168
+ leadTimeWeeks?: number;
169
+ packaging?: string;
170
+ market?: string;
171
+ authorized?: boolean;
172
+ observedAt: string;
173
+ stale?: boolean;
174
+ }
175
+
176
+ export interface PartPriceSource {
177
+ supplier: string;
178
+ authorized?: boolean;
179
+ from?: { unitPrice: number; currency: string };
180
+ observedAt: string;
181
+ offers: PartPriceOffer[];
182
+ }
183
+
184
+ /** A part's prices by source (`partPrices` of @protoboardai/core/library-client). */
185
+ export interface PartPricesModel {
186
+ partId: string;
187
+ sources: PartPriceSource[];
188
+ offers: number;
189
+ /** Why there are none to show, and what to do about it. */
190
+ unavailable?: { code: string; message: string };
191
+ }
192
+
193
+ export interface CategoryRow {
194
+ path: string;
195
+ name: string;
196
+ count: number;
197
+ /** A Lucide icon name; the views pick one by name when absent. */
198
+ icon?: string;
199
+ /** It has sub-categories with parts: choosing it drills in. */
200
+ more?: boolean;
201
+ description?: string;
202
+ }
203
+
204
+ export interface LibrarySource {
205
+ id: string;
206
+ /** The library's own name, when its discovery document gives one. */
207
+ name?: string;
208
+ /** How many parts matched there, or why it did not answer. */
209
+ total?: number;
210
+ error?: string;
211
+ }
212
+
213
+ export interface LibraryModel {
214
+ query: LibraryQuery;
215
+ /** From the top ("All") to the current category. */
216
+ crumbs: { path: string; name: string }[];
217
+ /** The current category's sub-categories: the rows of the category list at this level. */
218
+ categories: CategoryRow[];
219
+ /** The category whose sub-categories `categories` are: the current one, or its parent when the current one has none. */
220
+ level: { path: string; name: string; count: number };
221
+ /** The order of `items`, and the orders the control offers. */
222
+ sort: LibrarySortModel;
223
+ /** The top level (the full page's category list). */
224
+ topCategories: CategoryRow[];
225
+ total: number;
226
+ /** Every part in the libraries searched (the unfiltered search's total), when known. */
227
+ catalogTotal?: number;
228
+ libraries: LibrarySource[];
229
+ items: PartCardModel[];
230
+ more: boolean;
231
+ loading: boolean;
232
+ loadingMore?: boolean;
233
+ error?: string;
234
+ /** The libraries could not be listed at all. */
235
+ setupError?: string;
236
+ /** No project is open: browsing works, adding does not. */
237
+ noProject?: boolean;
238
+ }
239
+
240
+ export interface LibraryActions {
241
+ query(q: Partial<LibraryQuery>): void;
242
+ open(key: string, tab?: PartDetailTab): void;
243
+ add(key: string): void;
244
+ update(key: string): void;
245
+ loadMore(): void;
246
+ retry?(): void;
247
+ /** The part's other actions (copy its id, open its definition): the host's menu. */
248
+ more?(key: string): void;
249
+ /** Open the full-page library (from the sidebar). */
250
+ openPage?(): void;
251
+ /** Open the libraries manager. */
252
+ manage?(): void;
253
+ /** A part's pages, for the cards' text carousel (PB-918); without it the cards keep to their overview. */
254
+ pages?(ref: PartPagesRef): Promise<PartPages>;
255
+ }
256
+
257
+ // ---------------------------------------------------------------- part detail
258
+
259
+ export type PartDetailTab = "overview" | "specs" | "dimensions" | "interfaces" | "cad" | "artifacts" | "prices" | "revisions";
260
+
261
+ export interface PartFact {
262
+ label: string;
263
+ value: string;
264
+ /** A Lucide icon name; picked from the label when absent. */
265
+ icon?: string;
266
+ }
267
+
268
+ export interface PartDetailModel {
269
+ key: string;
270
+ partId: string;
271
+ name: string;
272
+ /** Category trail, top first. */
273
+ crumbs: string[];
274
+ manufacturer?: string;
275
+ mpn?: string;
276
+ /** More identifiers for the sub line (a breakout's chip, a connector system). */
277
+ identifiers: string[];
278
+ description?: string;
279
+ library: string;
280
+ libraryUrl?: string;
281
+ revision: number;
282
+ recommended?: number;
283
+ latest: number;
284
+ thumbnail?: string;
285
+ /** The body model to orbit, when the revision has one. */
286
+ model?: string;
287
+ modelError?: string;
288
+ facts: PartFact[];
289
+ interfaces: { id: string; name: string; domain?: string; protocols: string; count: number }[];
290
+ interfaceCount: number;
291
+ datasheet?: { label: string; url: string };
292
+ links: { label: string; url: string }[];
293
+ revisions: { revision: number; createdAt: string; publisher: string; deprecated?: string; recommended: boolean; current: boolean; pinned: boolean }[];
294
+ evidence: { kind: string; title?: string }[];
295
+ dependencies: string[];
296
+ used?: { revision?: number; elsewhere?: boolean };
297
+ /** What the main action does. */
298
+ action: { kind: "add" | "update" | "switch" | "pinned" | "elsewhere"; revision: number };
299
+ busy?: string;
300
+ /** No project is open: the part can be browsed, not added. */
301
+ noProject?: boolean;
302
+ /** A CAD licence or vendor note ("vendor · MIT"). */
303
+ cad?: string;
304
+ /** The part's status (PB-932) and why, when the revision's evidence could be read. */
305
+ trust?: PartTrust;
306
+ trustBasis?: string;
307
+ /** Where the back control returns to ("Library", "Project parts"); none when the detail was not opened from a list. */
308
+ back?: string;
309
+ /** The back control's keyboard shortcut, handled by the page itself ("Alt+←": the web shell). */
310
+ backShortcut?: string;
311
+ /** A shortcut the host itself binds to the back control (VS Code's keybinding): only shown, in the tooltip. */
312
+ backShortcutLabel?: string;
313
+ /** Its pages (PB-918): pictures, specifications, dimensions, interface chips, CAD and artifacts. */
314
+ pages?: PartPages;
315
+ /** The body model is fetched when the CAD section is first shown (`actions.loadModel`), not with the detail. */
316
+ modelLazy?: boolean;
317
+ /** The library can be asked for prices (`actions.prices`). */
318
+ hasPrices?: boolean;
319
+ /** Changes when the prices should be asked for again (the sign-in changed). */
320
+ pricesAs?: string;
321
+ }
322
+
323
+ export interface PartDetailActions {
324
+ act(kind: "add" | "update" | "switch", revision: number): void;
325
+ revision(revision: number): void;
326
+ /** Back to the list the part was opened from (its scroll position kept). */
327
+ back?(): void;
328
+ /** Compare the shown revision with the project's (or another). */
329
+ compare?(): void;
330
+ /** Open the definition source (the vendored copy, or the library's). */
331
+ openSource?(): void;
332
+ openLink(url: string): void;
333
+ openInViewer?(): void;
334
+ copy?(text: string): void;
335
+ /** Fetch the body model (the CAD section was shown); it arrives in `model.model`. */
336
+ loadModel?(): void;
337
+ /** The part's prices by source, from its library. */
338
+ prices?(): Promise<PartPricesModel>;
339
+ /** Sign in to the library's service (the prices need an account). */
340
+ signIn?(): void;
341
+ }
342
+
343
+ // ---------------------------------------------------------------- project parts
344
+
345
+ export type ProjectPartStatus = "pinned" | "up-to-date" | "update-available" | "diverged" | "local" | "designed";
346
+
347
+ /** One of the project's parts (`protoboard parts list`). */
348
+ export interface ProjectPartModel {
349
+ id: string;
350
+ name: string;
351
+ tier: "catalog" | "project-researched" | "project-specific";
352
+ status: ProjectPartStatus;
353
+ file: string;
354
+ line?: number;
355
+ library?: string;
356
+ revision?: number | string;
357
+ recommended?: number;
358
+ edited?: string[];
359
+ instances: number;
360
+ capability?: { capability: string; provider: string };
361
+ published?: { revision: number; service: string };
362
+ manufacturer?: string;
363
+ partNumber?: string;
364
+ category?: string;
365
+ description?: string;
366
+ /** The part's status (PB-932): local, unverified, verified or official, and why. */
367
+ trust?: PartTrust;
368
+ trustBasis?: string;
369
+ /** A picture, once the host has one (the library's thumbnail of a library part). */
370
+ thumbnail?: string;
371
+ /** Updating or restoring now ("Updating"). */
372
+ busy?: string;
373
+ }
374
+
375
+ export interface ProjectPartsModel {
376
+ parts: ProjectPartModel[];
377
+ counts: { total: number; library: number; researched: number; designed: number; updates: number; diverged: number };
378
+ checked?: { at: string; problems: string[] };
379
+ loading?: boolean;
380
+ error?: string;
381
+ /** Why there is no list yet (B12: signed out, or no project set up): said in place of "no parts". */
382
+ waiting?: string;
383
+ }
384
+
385
+ export interface ProjectPartsActions {
386
+ /** A part's detail (a library part), or where it is written (the project's own). */
387
+ open(id: string): void;
388
+ /** Where the part is written, whatever its tier. */
389
+ openSource?(id: string): void;
390
+ /** The Project Parts page, in the editor area (from the side bar). */
391
+ openPage?(): void;
392
+ /** The library, to add parts from. */
393
+ openLibrary?(): void;
394
+ update?(id: string): void;
395
+ /** Restore a diverged part to its pinned revision. */
396
+ restore?(id: string): void;
397
+ /** Compare a part's copy with its library revision. */
398
+ compare?(id: string): void;
399
+ publish?(id: string): void;
400
+ check?(): void;
401
+ /** A library part's pages, for the cards' text carousel (PB-918). */
402
+ pages?(ref: PartPagesRef): Promise<PartPages>;
403
+ }
404
+
405
+ // ---------------------------------------------------------------- account and libraries
406
+
407
+ export interface AccountModel {
408
+ state: "unknown" | "signed-out" | "open" | "signed-in" | "expired";
409
+ service?: string;
410
+ email?: string;
411
+ /** A second line: the organisation and plan when known, else where the sign-in is kept. */
412
+ detail?: string;
413
+ publisher?: boolean;
414
+ problem?: string;
415
+ }
416
+
417
+ export interface ManagedLibraryModel {
418
+ id: string;
419
+ name?: string;
420
+ url: string;
421
+ scope: "default" | "project" | "personal";
422
+ /** Parts it holds, when it says. */
423
+ parts?: number;
424
+ access: "signed-in" | "token" | "token-missing" | "public" | "sign-in" | "error" | "checking";
425
+ authEnv?: string;
426
+ error?: string;
427
+ /** Vendored parts pinned to it. */
428
+ pinned: number;
429
+ position: number;
430
+ }
431
+
432
+ export interface LibrariesModel {
433
+ account: AccountModel;
434
+ libraries: ManagedLibraryModel[];
435
+ problems: string[];
436
+ working: boolean;
437
+ noProject?: boolean;
438
+ }
439
+
440
+ export interface LibrariesActions {
441
+ add(url: string, scope: "project" | "personal", authEnv?: string): void;
442
+ remove(id: string, scope: ManagedLibraryModel["scope"]): void;
443
+ /** Move a library `steps` places (default 1) within its scope's search order. */
444
+ move(id: string, scope: ManagedLibraryModel["scope"], direction: "up" | "down", steps?: number): void;
445
+ check?(): void;
446
+ signIn?(): void;
447
+ signOut?(): void;
448
+ }
449
+
450
+ // ---------------------------------------------------------------- settings
451
+
452
+ /** An agent on this machine and whether Protoboard's MCP server is in its configuration (PB-930). */
453
+ export interface AgentRowModel {
454
+ id: string;
455
+ name: string;
456
+ /** Connected; an older entry of ours; installed but not connected; its configuration does not parse. */
457
+ status: "connected" | "outdated" | "not-connected" | "unreadable";
458
+ /** Its configuration file, as shown (the home folder as `~`). */
459
+ file: string;
460
+ /** A second line: what went wrong, what was just written, or that another server holds the name. */
461
+ detail?: string;
462
+ detailTone?: "neutral" | "ok" | "warning" | "error";
463
+ /** A change to this agent is running. */
464
+ working?: boolean;
465
+ /** The command the user runs in a terminal to connect it, when Protoboard does not make the change itself: the row offers it to copy in place of Connect. */
466
+ manual?: string;
467
+ }
468
+
469
+ /** The entry for an MCP client Protoboard does not set up itself, in each form. */
470
+ export interface OtherClientsModel {
471
+ steps: string[];
472
+ forms: { id: string; label: string; text: string }[];
473
+ }
474
+
475
+ export interface SettingsModel {
476
+ account: AccountModel;
477
+ about: {
478
+ /** The version the extension and the stack share. */
479
+ version: string;
480
+ /** Which CLI runs, and where it was found. */
481
+ cli?: string;
482
+ cliFrom?: string;
483
+ /** Why no CLI runs. */
484
+ problem?: string;
485
+ };
486
+ agents: {
487
+ state: "loading" | "ready" | "failed";
488
+ rows: AgentRowModel[];
489
+ /** Known agents that were not found here, by name. */
490
+ notFound: string[];
491
+ problem?: string;
492
+ };
493
+ other?: OtherClientsModel;
494
+ /** What else of Protoboard is reached from here: the stack's folder, a new project, the bill of materials, the first steps. */
495
+ links?: SettingsLinkModel[];
496
+ }
497
+
498
+ /** A row of the settings page that opens something of the host's. */
499
+ export interface SettingsLinkModel {
500
+ id: string;
501
+ label: string;
502
+ note: string;
503
+ /** A lucide icon name. */
504
+ icon: string;
505
+ /** The button's word: "Open", "Show Folder", "Create". */
506
+ action: string;
507
+ }
508
+
509
+ export interface SettingsActions {
510
+ signIn?(): void;
511
+ signOut?(): void;
512
+ connect(id: string): void;
513
+ disconnect(id: string): void;
514
+ refresh(): void;
515
+ openFile(id: string): void;
516
+ copy(text: string, what: string): void;
517
+ openVsCodeSettings(): void;
518
+ openLibraries?(): void;
519
+ showLog?(): void;
520
+ /** One of `links`, by id. */
521
+ run?(id: string): void;
522
+ }
523
+
524
+ // ---------------------------------------------------------------- bill of materials
525
+
526
+ /** `protoboard bom --format json`: core's structured bill of materials (`@protoboardai/core/bom`). */
527
+ export type BomModel = Bom;
528
+ export type BomRowModel = BomLine;
529
+ export type BomGroupModel = BomGroup;
530
+
531
+ export interface BomActions {
532
+ /** Open the part of a line (its page in the library, or its definition). */
533
+ openPart?(line: BomLine): void;
534
+ /** Open a line where it is written, when it comes from a file. */
535
+ openRow?(line: BomLine): void;
536
+ /** Show the line's instances in the viewer. */
537
+ focus?(line: BomLine): void;
538
+ /** Keep the units to build (the view has already re-priced). */
539
+ setUnits?(units: number): void;
540
+ /** Keep the offer chosen for a line; `null` goes back to the best offer. */
541
+ chooseOffer?(line: string, offer: string | null): void;
542
+ refreshOffers?(): void;
543
+ /** Export the bill as shown (units and offers included) as CSV. */
544
+ exportCsv?(bom: Bom): void;
545
+ openLink?(url: string): void;
546
+ }
547
+
548
+ // ---------------------------------------------------------------- cloud
549
+
550
+ export interface CloudModel {
551
+ account: AccountModel;
552
+ project: {
553
+ name: string;
554
+ description?: string;
555
+ author?: string;
556
+ libraries: number;
557
+ parts?: number;
558
+ published: false | { at: string };
559
+ };
560
+ release: { state: "idle" | "running" | "failed" | "refused" | "done"; message?: string; step?: string; problems?: { code: string; message: string }[] };
561
+ releases: { version: string; date: string; units?: number; files?: number; index: boolean }[];
562
+ cost?: {
563
+ /** What the parts of one unit cost, in dollars ("$299.81"). */
564
+ total: string;
565
+ /** What the order costs for the units built, when packs and minimum orders make it more ("$412.10 to order 1"). */
566
+ order?: string;
567
+ /** Notes under the total: converted prices and their rate date, stale prices. */
568
+ others: string[];
569
+ bought: number;
570
+ priced: number;
571
+ unpriced: number;
572
+ made: number;
573
+ /** The largest lines, most first. */
574
+ top: { item: string; price: string }[];
575
+ source: string;
576
+ };
577
+ costMissing?: string;
578
+ quotes: { id: string; title: string; state: string }[];
579
+ }
580
+
581
+ export interface CloudActions {
582
+ signIn?(): void;
583
+ signOut?(): void;
584
+ generate?(): void;
585
+ openRelease?(version: string): void;
586
+ openBom?(): void;
587
+ publish?(): void;
588
+ requestQuote?(): void;
589
+ fetchOffers?(): void;
590
+ }