@protoboardai/views 0.0.0-stage → 0.2.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 (56) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +13 -0
  3. package/README.md +21 -2
  4. package/package.json +57 -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,392 @@
1
+ /**
2
+ * The Inspector's selection (PB-940; design A "Linear-tight", PB-1042, with
3
+ * expandable interface cards): one column, no boxes. The
4
+ * header is the shell's: the kind's icon, the name, what it is and which
5
+ * part, the part's status, and a summary row (the findings, the price, how
6
+ * many modules are below, the quick actions). Then sections, each a 28 px
7
+ * header row (chevron, title, count or summary) over its body, separated by
8
+ * 1 px dividers: the viewer's own (`SelectionDetails` of
9
+ * @deltarobotics/uhd-viewer: Parameters, Interfaces as slot cards that
10
+ * expand, Artifacts, and the secondary ones closed), then the shell's
11
+ * (Checks, Children, Links, Sourcing, Used in…). Arrow keys move through the
12
+ * section headers and the cards.
13
+ *
14
+ * The shell gives the card, the project's bundle and the files the page may
15
+ * load. Nothing is loaded that the card does not name: pictures are lazy, a
16
+ * model is drawn only when the shell marks it small enough.
17
+ */
18
+ import { useCallback, useMemo, type CSSProperties } from "react";
19
+ import {
20
+ ArtifactGrid,
21
+ DetailsSection,
22
+ HostContext,
23
+ Icon,
24
+ IconButton,
25
+ ScrollArea,
26
+ SECTION_LOOK,
27
+ SelectionDetails,
28
+ Sections,
29
+ cx,
30
+ useShowAll,
31
+ useTheme,
32
+ type ArtifactCardModel,
33
+ type HostActions,
34
+ } from "@deltarobotics/uhd-viewer/components";
35
+ import { PartStatusDot } from "../parts/PartStatus.js";
36
+ import { PartsSurface } from "../parts/Surface.js";
37
+ import { RowAction } from "../parts/ui.js";
38
+ import type { InspectorActions, InspectorCard, InspectorChip, InspectorContent, InspectorRow, InspectorSection } from "./model.js";
39
+
40
+ type Bundle = NonNullable<Parameters<typeof SelectionDetails>[0]["bundle"]>;
41
+ type Definitions = NonNullable<Parameters<typeof SelectionDetails>[0]["definitions"]>;
42
+
43
+ const TONE: Record<string, string> = { error: "text-error", warning: "text-warning", info: "text-info", ok: "text-ok" };
44
+ const SEVERITY_ICON: Record<string, string> = { error: "circle-x", warning: "triangle-alert", info: "info" };
45
+
46
+ /** The header's look (design A), in one place. */
47
+ export const INSPECTOR_HEADER_LOOK = {
48
+ header: "border-b border-line-subtle px-3.5 pt-3 pb-2.5",
49
+ tile: "grid size-9 shrink-0 place-items-center rounded-[8px] border border-line bg-[color-mix(in_oklab,var(--uhd-text)_6%,transparent)] text-fg",
50
+ title: "truncate text-[14px] leading-[1.3] font-semibold text-fg",
51
+ subtitle: "truncate text-[11px] leading-[1.4] text-fg-muted",
52
+ status: "inline-flex h-[22px] shrink-0 items-center gap-1.5 rounded-full bg-[color-mix(in_oklab,var(--uhd-text)_8%,transparent)] px-2 text-[11px] leading-none text-fg",
53
+ summary: "mt-2.5 flex min-h-6 flex-wrap items-center gap-x-2.5 gap-y-1 text-[11.5px] leading-none text-fg-muted",
54
+ divider: "h-3.5 w-px shrink-0 bg-line",
55
+ } as const;
56
+
57
+ /** A small outlined label: the part's tier, its sync state, a quantity, stale outputs. */
58
+ function Chip({ chip }: { chip: InspectorChip }) {
59
+ return (
60
+ <span
61
+ data-tooltip={chip.title}
62
+ data-tooltip-clamp={chip.title && chip.title.length > 40 ? "4" : undefined}
63
+ data-blank={chip.blank ? "" : undefined}
64
+ className={cx(
65
+ "inline-flex h-[18px] max-w-full shrink-0 items-center gap-1.5 rounded-full border px-1.5 text-[10.5px] leading-none whitespace-nowrap",
66
+ chip.tone && TONE[chip.tone] ? cx(TONE[chip.tone], "border-current") : "border-[var(--pb-control-border)] text-fg-muted",
67
+ chip.blank && "border-dashed border-fg-muted",
68
+ "outline-1 -outline-offset-1 outline-contrast",
69
+ )}
70
+ >
71
+ {chip.status && <PartStatusDot trust={chip.status} blank={chip.blank} size={7} />}
72
+ <span className="truncate">{chip.text}</span>
73
+ </span>
74
+ );
75
+ }
76
+
77
+ /** The part's status, at the title's right: its dot and its name (Local, Unverified, Verified, Official). */
78
+ function StatusChip({ chip }: { chip: InspectorChip }) {
79
+ return (
80
+ <span data-status-chip="" data-tooltip={chip.title} data-tooltip-clamp="4" className={cx(INSPECTOR_HEADER_LOOK.status, chip.blank && "border border-dashed border-fg-muted bg-transparent")}>
81
+ {chip.status && <PartStatusDot trust={chip.status} blank={chip.blank} size={7} />}
82
+ <span className="truncate">{chip.text}</span>
83
+ </span>
84
+ );
85
+ }
86
+
87
+ /** The findings here and below, in the summary row: a count per severity with its icon, or "No findings". */
88
+ function Counts({ counts }: { counts: InspectorCard["counts"] }) {
89
+ const total = counts.error + counts.warning + counts.info;
90
+ const label = `${counts.error} blocking, ${counts.warning} warnings, ${counts.info} info`;
91
+ if (!total)
92
+ return (
93
+ <span aria-label="No findings" className="inline-flex items-center gap-1 text-ok">
94
+ <Icon name="circle-check" size={13} strokeWidth={1.75} aria-hidden />
95
+ <span className="text-fg-muted">No findings</span>
96
+ </span>
97
+ );
98
+ return (
99
+ <span role="img" aria-label={`Findings here and below: ${label}`} data-tooltip={`Findings here and below: ${label}`} className="inline-flex items-center gap-2">
100
+ {(["error", "warning", "info"] as const).map((sev) =>
101
+ counts[sev] ? (
102
+ <span key={sev} className={cx("inline-flex items-center gap-1 tabular-nums", TONE[sev])}>
103
+ <Icon name={SEVERITY_ICON[sev]} size={13} strokeWidth={1.75} aria-hidden />
104
+ <span className="text-fg">{counts[sev]}</span>
105
+ </span>
106
+ ) : null,
107
+ )}
108
+ </span>
109
+ );
110
+ }
111
+
112
+ /** A row of a section: a module, a link's end, a finding (its message and its code under it), a fact. */
113
+ function Row({ row, actions }: { row: InspectorRow; actions: InspectorActions }) {
114
+ const act = row.action;
115
+ const go = act ? () => (act.type === "focus" ? actions.focus(act.path) : actions.finding(act.key)) : undefined;
116
+ const finding = act?.type === "finding" || row.code;
117
+ return (
118
+ <div
119
+ role={go ? "button" : undefined}
120
+ tabIndex={go ? 0 : undefined}
121
+ data-tooltip={`${row.label}${row.detail ? ` · ${row.detail}` : ""}`}
122
+ data-tooltip-clamp="3"
123
+ data-tooltip-delay="600"
124
+ data-row-finding={row.code}
125
+ onClick={go}
126
+ onKeyDown={
127
+ go &&
128
+ ((e) => {
129
+ if (e.target === e.currentTarget && (e.key === "Enter" || e.key === " ")) {
130
+ e.preventDefault();
131
+ go();
132
+ }
133
+ })
134
+ }
135
+ className={cx(
136
+ "group/row -mx-1 flex gap-2 rounded-[4px] px-1 text-[11.5px] text-fg",
137
+ finding ? "items-start py-1" : "min-h-[24px] items-center",
138
+ go && "cursor-pointer hover:bg-hover focus-visible:outline focus-visible:outline-1 focus-visible:-outline-offset-1 focus-visible:outline-focus",
139
+ )}
140
+ >
141
+ <Icon name={row.icon} size={13} strokeWidth={1.75} className={cx("shrink-0", finding && "mt-px", row.tone && TONE[row.tone] ? TONE[row.tone] : "text-fg-muted")} aria-hidden />
142
+ {finding ? (
143
+ <span className="flex min-w-0 flex-1 flex-col gap-0.5">
144
+ <span className="leading-[1.4]">{row.label}</span>
145
+ {row.code && <span className="font-code text-[9.5px] leading-none text-fg-faint">{row.code}</span>}
146
+ </span>
147
+ ) : (
148
+ <>
149
+ <span className="min-w-0 flex-1 truncate">{row.label}</span>
150
+ {row.detail && <span className="max-w-[50%] min-w-0 shrink truncate text-[11px] text-fg-muted">{row.detail}</span>}
151
+ </>
152
+ )}
153
+ {(row.chips ?? []).map((c) => (
154
+ <Chip key={c.text} chip={c} />
155
+ ))}
156
+ {act?.type === "finding" && (
157
+ <span className="flex shrink-0 opacity-0 group-focus-within/row:opacity-100 group-hover/row:opacity-100">
158
+ <RowAction icon="circle-help" label={`Explain ${row.code ?? "this finding"}`} onClick={() => actions.explain(act.key)} />
159
+ </span>
160
+ )}
161
+ </div>
162
+ );
163
+ }
164
+
165
+ /** One of the shell's sections, in the details' look: a header row, closed when secondary, "Show all N" past its limit. */
166
+ function HostSection({ section, actions }: { section: InspectorSection; actions: InspectorActions }) {
167
+ const showAll = useShowAll(section.rows.length, section.limit ?? Number.POSITIVE_INFINITY);
168
+ const rows = showAll.limit !== undefined ? section.rows.slice(0, showAll.limit) : section.rows;
169
+ return (
170
+ <DetailsSection id={`host:${section.title}`} title={section.title} count={section.count} summary={section.summary} aside={showAll.aside} defaultOpen={!section.collapsed}>
171
+ {rows.map((r, i) => (
172
+ <Row key={`${r.label}-${i}`} row={r} actions={actions} />
173
+ ))}
174
+ </DetailsSection>
175
+ );
176
+ }
177
+
178
+ /**
179
+ * The way to the code (the 2026-10-08 first builds, D7): the open-source
180
+ * action drawn with the name of the file it opens, so it is seen without
181
+ * hovering; the tooltip says the whole path. It comes first among the actions.
182
+ */
183
+ function SourceAction({ source, onOpen }: { source: { file: string; line?: number }; onOpen: () => void }) {
184
+ const name = source.file.split("/").pop() ?? source.file;
185
+ const at = `${source.file}${source.line ? `:${source.line}` : ""}`;
186
+ return (
187
+ <button
188
+ type="button"
189
+ data-action="source"
190
+ aria-label={`Open source: ${at}`}
191
+ data-tooltip={`Open source: ${at}`}
192
+ data-tooltip-clamp={at.length > 40 ? "3" : undefined}
193
+ onClick={onOpen}
194
+ className={cx(
195
+ "inline-flex h-6 max-w-full min-w-0 cursor-pointer items-center gap-1.5 rounded-[4px] px-1.5 text-[11px] leading-none text-fg",
196
+ "hover:bg-hover focus-visible:outline focus-visible:outline-1 focus-visible:-outline-offset-1 focus-visible:outline-focus",
197
+ )}
198
+ >
199
+ <Icon name="file-code" size={14} strokeWidth={1.75} className="shrink-0 text-link" aria-hidden />
200
+ <span className="min-w-0 truncate font-code">
201
+ {name}
202
+ {source.line ? <span className="text-fg-muted">:{source.line}</span> : null}
203
+ </span>
204
+ </button>
205
+ );
206
+ }
207
+
208
+ /** Cards outside the viewer's details open through the same host actions. */
209
+ function HostCards({ host, children }: { host: HostActions; children: React.ReactNode }) {
210
+ return <HostContext.Provider value={host}>{children}</HostContext.Provider>;
211
+ }
212
+
213
+ /**
214
+ * On a light page the viewer's details write in the link-state hues (a part's audit, its assumptions, a link's
215
+ * diagnostics), which are bright for dark pages: yellow on white is 1.4:1. They take the deeper shades the parts
216
+ * views' surface gives the severities there (PB-900), the same hue for the same meaning; dark pages keep the hues.
217
+ */
218
+ const LIGHT_STATES = {
219
+ "--uhd-drc-configured": "var(--uhd-severity-ok)",
220
+ "--uhd-drc-partial": "var(--uhd-severity-warning)",
221
+ "--uhd-drc-incompatible": "var(--uhd-severity-error)",
222
+ // the grey of "not configured" as text: the muted text colour
223
+ "--uhd-drc-unconfigured": "var(--uhd-text-muted)",
224
+ } as CSSProperties;
225
+
226
+ export interface SelectionInspectorProps {
227
+ content: InspectorContent | undefined;
228
+ /** The project's bundle (loaded by the shell from `content.bundle`). */
229
+ bundle?: unknown;
230
+ actions: InspectorActions;
231
+ }
232
+
233
+ /** The header (design A): icon, name, what it is, the status at the right, then the summary row with the quick actions. */
234
+ function Header({ card, actions }: { card: InspectorCard; actions: InspectorActions }) {
235
+ const a = card.actions;
236
+ const status = card.chips.find((c) => c.status);
237
+ // the kind is said in the subtitle; the other chips (tier, sync, quantity, stale outputs) stay small under it
238
+ const kindChip = !card.kindLabel ? card.chips.find((c) => !c.status && !c.tone && c === card.chips[0]) : undefined;
239
+ const kind = card.kindLabel ?? kindChip?.text;
240
+ const rest = card.chips.filter((c) => c !== status && c !== kindChip);
241
+ const subtitle = [kind, card.subtitle].filter(Boolean).join(" · ");
242
+ const findingsShown = (card.kind !== "part" && card.kind !== "interface") || card.counts.error + card.counts.warning + card.counts.info > 0;
243
+ const facts = [
244
+ ...(card.summary?.price ? [<span key="price" className="tabular-nums text-fg" data-summary="price">{card.summary.price}</span>] : []),
245
+ ...(card.summary?.children ? [<span key="children" data-summary="children">{`${card.summary.children} ${card.summary.children === 1 ? "child" : "children"}`}</span>] : []),
246
+ ];
247
+ return (
248
+ <header className={INSPECTOR_HEADER_LOOK.header}>
249
+ <div className="flex items-start gap-2.5">
250
+ <span aria-hidden className={INSPECTOR_HEADER_LOOK.tile}>
251
+ <Icon name={card.icon} size={18} strokeWidth={1.6} />
252
+ </span>
253
+ <div className="min-w-0 flex-1 pt-px">
254
+ <h1 data-tooltip={card.title} data-tooltip-clamp="3" className={cx("m-0", INSPECTOR_HEADER_LOOK.title)}>
255
+ {card.title}
256
+ </h1>
257
+ {subtitle && (
258
+ <div data-tooltip={subtitle} data-tooltip-clamp="3" className={INSPECTOR_HEADER_LOOK.subtitle}>
259
+ {subtitle}
260
+ </div>
261
+ )}
262
+ </div>
263
+ {status && <StatusChip chip={status} />}
264
+ </div>
265
+ {rest.length > 0 && (
266
+ <div className="mt-2 flex flex-wrap gap-1">
267
+ {rest.map((c) => (
268
+ <Chip key={c.text} chip={c} />
269
+ ))}
270
+ </div>
271
+ )}
272
+ <div className={INSPECTOR_HEADER_LOOK.summary}>
273
+ {findingsShown && <Counts counts={card.counts} />}
274
+ {facts.map((f, i) => (
275
+ <span key={i} className="inline-flex items-center gap-2.5">
276
+ {(findingsShown || i > 0) && <span aria-hidden className={INSPECTOR_HEADER_LOOK.divider} />}
277
+ {f}
278
+ </span>
279
+ ))}
280
+ {/* the quick actions, at the right; they wrap under the facts in a narrow side bar */}
281
+ <div role="toolbar" aria-label="Selection actions" className="ml-auto flex min-w-0 flex-wrap items-center justify-end gap-0.5 empty:hidden">
282
+ {a.source && card.source && <SourceAction source={card.source} onOpen={() => actions.action("source")} />}
283
+ {a.focus && <IconButton size="sm" icon="crosshair" label={card.kind === "link" ? "Show the link in the viewer" : "Focus in the viewer"} onClick={() => actions.action("focus")} />}
284
+ {a.pair && <IconButton size="sm" icon="git-compare-arrows" label="Open the pair view in a tab" onClick={() => actions.action("pair")} />}
285
+ {a.detail && <IconButton size="sm" icon="panel-top-open" label="Open the part's page" onClick={() => actions.action("detail")} />}
286
+ {a.source && !card.source && <IconButton size="sm" icon="file-code" label="Open source" onClick={() => actions.action("source")} />}
287
+ {a.artifacts && <IconButton size="sm" icon="folder-open" label="Open artifact…" onClick={() => actions.action("artifacts")} />}
288
+ {a.operation && <IconButton size="sm" icon="play" label="Run operation…" onClick={() => actions.action("operation")} />}
289
+ </div>
290
+ </div>
291
+ </header>
292
+ );
293
+ }
294
+
295
+ export function SelectionInspector({ content, bundle, actions }: SelectionInspectorProps) {
296
+ const card = content && "card" in content ? content.card : undefined;
297
+ const details = card?.details;
298
+ const light = useTheme().theme.scheme.includes("light");
299
+ const resolve = useCallback((defId: string, filePath: string) => details?.files[`${defId}\n${filePath}`], [details]);
300
+ const host = useMemo<HostActions>(
301
+ () => ({
302
+ openArtifact: actions.openArtifact,
303
+ openLink: actions.openLink,
304
+ // nothing is loaded for a 3D preview here: a model is a card, drawn small only when the shell says it may be
305
+ artifactModels: "cards",
306
+ artifactPreview: (def, artifact) => details?.previews[`${def.id}#${artifact.id}`],
307
+ ...(actions.drawModel ? { drawModel: actions.drawModel } : {}),
308
+ // pictures come from the shell's own files only
309
+ remoteImages: false,
310
+ evidence: (defId) => details?.evidence?.[defId] as ReturnType<NonNullable<HostActions["evidence"]>>,
311
+ }),
312
+ [actions, details],
313
+ );
314
+ if (!card)
315
+ return (
316
+ <PartsSurface place="sidebar" className="bg-transparent!">
317
+ <div className="grid size-full place-items-center px-6" data-inspector-empty="">
318
+ <div className="flex max-w-[260px] flex-col items-center gap-2 text-center font-ui">
319
+ <span aria-hidden className={cx(INSPECTOR_HEADER_LOOK.tile, "text-fg-muted")}>
320
+ <Icon name="mouse-pointer-click" size={18} strokeWidth={1.6} />
321
+ </span>
322
+ <h1 className="m-0 text-[13px] font-semibold text-fg">Nothing selected</h1>
323
+ <p className="m-0 text-[11.5px] leading-[1.5] text-fg-muted">{content && "empty" in content ? content.empty : "Open a Protoboard project."}</p>
324
+ </div>
325
+ </div>
326
+ </PartsSurface>
327
+ );
328
+ // a part's picture that is not an artifact of its definition (a library's thumbnail)
329
+ const picture: ArtifactCardModel[] = details?.picture
330
+ ? [{ key: "picture", defId: card.part?.partId ?? "", title: card.title, kind: "image", group: "images", kindLabel: "Picture", source: card.part?.library, icon: "image", preview: { kind: "image", src: details.picture }, remote: false }]
331
+ : [];
332
+ const own: ArtifactCardModel[] = (card.artifacts ?? []).map((x) => ({
333
+ key: `${x.defId}#${x.artifactId}`,
334
+ defId: x.defId,
335
+ artifactId: x.artifactId,
336
+ title: x.title,
337
+ kind: "model",
338
+ group: "data",
339
+ kindLabel: x.kindLabel,
340
+ ...(x.format ? { format: x.format } : {}),
341
+ ...(x.source ? { source: x.source } : {}),
342
+ icon: x.icon,
343
+ ...(x.preview ? { preview: x.preview } : {}),
344
+ remote: false,
345
+ }));
346
+ return (
347
+ <PartsSurface place="sidebar" className="bg-transparent!">
348
+ <div className="contents" style={light ? LIGHT_STATES : undefined}>
349
+ {/* a new selection starts at its top (the scroll is the selection's, not the view's) */}
350
+ <ScrollArea key={`${card.kind}:${card.path}:${card.linkId ?? card.part?.partId ?? ""}:${card.title}`} className="size-full" contentClassName="min-w-[188px] pb-4 font-ui">
351
+ <Header card={card} actions={actions} />
352
+ <Sections label={`${card.title}: details`} className="pb-inspector-sections">
353
+ {picture.length > 0 && (
354
+ <HostCards host={host}>
355
+ <DetailsSection id="picture" title="Picture">
356
+ <ArtifactGrid cards={picture} />
357
+ </DetailsSection>
358
+ </HostCards>
359
+ )}
360
+ {details && (
361
+ <SelectionDetails
362
+ className="pb-inspector-details"
363
+ bundle={bundle as Bundle | undefined}
364
+ definitions={details.definitions as Definitions | undefined}
365
+ target={details.target}
366
+ resolveArtifact={resolve}
367
+ host={host}
368
+ onSelect={actions.select}
369
+ onPair={() => actions.action("pair")}
370
+ {...(details.findings ? { findings: details.findings } : {})}
371
+ onFinding={actions.finding}
372
+ {...(actions.interfaceSource ? { onInterfaceSource: actions.interfaceSource } : {})}
373
+ {...(actions.connect ? { onConnect: actions.connect } : {})}
374
+ missing={bundle || details.target.kind === "definition" ? null : <p className={cx(SECTION_LOOK.body, "m-0 py-2 text-[11.5px] text-fg-muted")}>Reading the build…</p>}
375
+ />
376
+ )}
377
+ {own.length > 0 && (
378
+ <HostCards host={host}>
379
+ <DetailsSection id="host:artifacts" title="Artifacts" count={own.length}>
380
+ <ArtifactGrid cards={own} />
381
+ </DetailsSection>
382
+ </HostCards>
383
+ )}
384
+ {card.sections.map((s) => (
385
+ <HostSection key={s.title} section={s} actions={actions} />
386
+ ))}
387
+ </Sections>
388
+ </ScrollArea>
389
+ </div>
390
+ </PartsSurface>
391
+ );
392
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The Inspector's selection and the DRC panel as components (PB-940, PB-901):
3
+ * data in (model.ts), actions out. The VS Code shell draws them in native
4
+ * views; the selection is built on the viewer's own details
5
+ * (@deltarobotics/uhd-viewer `SelectionDetails`).
6
+ */
7
+ export type * from "./model.js";
8
+ export { SelectionInspector, type SelectionInspectorProps } from "./SelectionInspector.js";
9
+ export { DrcPanel, type DrcPanelProps } from "./DrcPanel.js";
@@ -0,0 +1,224 @@
1
+ /**
2
+ * What the Inspector and the DRC panel draw (PB-940, PB-901): plain data a
3
+ * shell computes and hands to the components, and the actions they call back
4
+ * with. No React and no host API here: the VS Code extension's Node side
5
+ * builds these models and imports only this file's types.
6
+ */
7
+ import type { PartTrust } from "../parts/part-status.js";
8
+
9
+ /** A small outlined label: the kind, the part's tier, its status, a sync state. */
10
+ export interface InspectorChip {
11
+ text: string;
12
+ /** A part status (PB-932): the chip starts with the state's dot. */
13
+ status?: PartTrust;
14
+ /** The part is a blank block (no part chosen yet): the dot is a dashed ring and the chip stands out with a dashed outline. */
15
+ blank?: boolean;
16
+ /** A severity or "ok": the chip takes its colour. */
17
+ tone?: string;
18
+ title?: string;
19
+ }
20
+
21
+ export interface InspectorRow {
22
+ /** Lucide icon name. */
23
+ icon: string;
24
+ label: string;
25
+ detail?: string;
26
+ chips?: InspectorChip[];
27
+ /** A dot in the severity colour. */
28
+ tone?: string;
29
+ /** What a click does. */
30
+ action?: { type: "focus"; path: string } | { type: "finding"; key: string };
31
+ /** A finding's code, under its message. */
32
+ code?: string;
33
+ }
34
+
35
+ export interface InspectorSection {
36
+ title: string;
37
+ count?: number;
38
+ rows: InspectorRow[];
39
+ /** What it holds, said in its header, shown open or closed ("FC · ESC · cable", "DolphinRC · $69.99"). */
40
+ summary?: string;
41
+ /** A secondary section: it starts closed (design A, PB-1042). */
42
+ collapsed?: boolean;
43
+ /** Rows shown before "Show all N" (the rest wait behind it). */
44
+ limit?: number;
45
+ }
46
+
47
+ /** A finding the interface cards show (on an interface or its links), with the canonical ids it is about. */
48
+ export interface InspectorFinding {
49
+ key: string;
50
+ code: string;
51
+ message: string;
52
+ severity: "error" | "warning" | "info";
53
+ /** Links as the viewer selects them (`owner/local`, `local` at the root; a derived link by its own id), interfaces as `path:id`. */
54
+ refs: string[];
55
+ }
56
+
57
+ /** An interface card's request: the module, the interface, and the links that end on it as their module declares them. */
58
+ export interface InspectorInterfaceRequest {
59
+ path: string;
60
+ interfaceId: string;
61
+ owner: string;
62
+ links: string[];
63
+ }
64
+
65
+ /** A picture a card may show: an image file, or a model small enough to draw. `F` is how a file is named: a path on the host's side, a URL on the page's. */
66
+ export interface InspectorPreview<F = string> {
67
+ kind: "image" | "model";
68
+ src: F;
69
+ }
70
+
71
+ /**
72
+ * What the viewer's own details show for the selection, and the files they
73
+ * may load. `target` is uhd-viewer's `DetailsTarget`.
74
+ */
75
+ export interface InspectorDetails<F = string> {
76
+ target: { kind: "module"; path: string } | { kind: "link"; owner: string; ids: string[] } | { kind: "definition"; id: string };
77
+ /** Artifact files the page may load, by `<definition id>\n<file path>`. */
78
+ files: Record<string, F>;
79
+ /** Pictures for artifacts that are not images themselves (a model's exported preview, or a small model to draw), by `<definition id>#<artifact id>`. */
80
+ previews: Record<string, InspectorPreview<F>>;
81
+ /** Evidence files by definition id (sources.json, verification.json), as read. */
82
+ evidence?: Record<string, { sources?: unknown[]; verification?: unknown }>;
83
+ /** Definitions that are not in the project's bundle (a library part shown on its own). */
84
+ definitions?: unknown[];
85
+ /** A picture of the part that is not one of its definition's artifacts (a library's thumbnail). */
86
+ picture?: F;
87
+ /** Web addresses the page may ask the host to open (artifact and evidence links of what is shown). */
88
+ links?: string[];
89
+ /** The findings the interface cards show, with the keys the DRC panel knows them by. */
90
+ findings?: InspectorFinding[];
91
+ }
92
+
93
+ /** An artifact card the shell builds itself (an interface's geometry), drawn like the details' cards. */
94
+ export interface InspectorArtifact<F = string> {
95
+ defId: string;
96
+ artifactId: string;
97
+ title: string;
98
+ /** "Interface geometry", "3D model". */
99
+ kindLabel: string;
100
+ format?: string;
101
+ /** Where it comes from: a file's name. */
102
+ source?: string;
103
+ /** Lucide icon name. */
104
+ icon: string;
105
+ preview?: InspectorPreview<F>;
106
+ }
107
+
108
+ export type InspectorAction = "focus" | "source" | "artifacts" | "operation" | "pair" | "detail";
109
+
110
+ /** The selection as a card: its header, state and sections; the viewer's details fill the middle. */
111
+ export interface InspectorCard<F = string> {
112
+ kind: "module" | "link" | "level" | "part" | "interface";
113
+ /** Lucide icon name. */
114
+ icon: string;
115
+ title: string;
116
+ subtitle?: string;
117
+ /** What it is, before the subtitle: "Module", "Assembly", "Part", "Link" (design A: "Module · DolphinRC F405 V3 stack"). */
118
+ kindLabel?: string;
119
+ /** The header's summary row (design A): its price and how many modules are below it. */
120
+ summary?: { price?: string; children?: number };
121
+ chips: InspectorChip[];
122
+ /** Findings here (and below a module): the worst first. */
123
+ counts: { error: number; warning: number; info: number };
124
+ /** Sections of rows after the details: where a part is used, the modules below, the links it declares, its findings. */
125
+ sections: InspectorSection[];
126
+ /** Which actions apply (icon buttons in the header). */
127
+ actions: { focus: boolean; source: boolean; artifacts: boolean; operation: boolean; pair?: boolean; detail?: boolean };
128
+ /** The file the source action opens (project-relative), and the line when it is known: the action is drawn with the file's name. */
129
+ source?: { file: string; line?: number };
130
+ /** The instance path the actions are about ("" the root), and a link id for a link. */
131
+ path: string;
132
+ linkId?: string;
133
+ /** A part's card: the part and the library it comes from (its page opens from the card). */
134
+ part?: { partId: string; library?: string };
135
+ details?: InspectorDetails<F>;
136
+ /** Artifact cards of the card's own (an interface's geometry), before the sections. */
137
+ artifacts?: InspectorArtifact<F>[];
138
+ }
139
+
140
+ /** The project's UHD bundle as the page loads it: a URL, and a version that changes with every build. */
141
+ export interface InspectorBundle {
142
+ src: string;
143
+ version: string;
144
+ }
145
+
146
+ export type InspectorContent<F = string> = { card: InspectorCard<F>; project: string; bundle?: InspectorBundle } | { empty: string; project?: string };
147
+
148
+ export interface InspectorActions {
149
+ action(action: InspectorAction): void;
150
+ /** Show a module in the viewer. */
151
+ focus(path: string): void;
152
+ /** Something picked inside the details: canonical ids from the root. */
153
+ select(selection: { kind: "node" | "link"; id: string }): void;
154
+ finding(key: string): void;
155
+ explain(key: string): void;
156
+ openArtifact(defId: string, artifactId: string): void;
157
+ openLink(url: string): void;
158
+ /** Draws a model small, as a picture's URL (the shell's three.js). */
159
+ drawModel?(src: string): Promise<string>;
160
+ /** An interface card's Open in source: the line of its link, else of the interface. */
161
+ interfaceSource?(request: InspectorInterfaceRequest): void;
162
+ /** An open slot's Connect…: where a link from it is written. */
163
+ connect?(request: InspectorInterfaceRequest): void;
164
+ }
165
+
166
+ // ---- the DRC panel
167
+
168
+ export interface DrcRowModel {
169
+ key: string;
170
+ code: string;
171
+ summary: string;
172
+ message: string;
173
+ reasons: string[];
174
+ module: string;
175
+ means?: string;
176
+ todo?: string;
177
+ validator: string;
178
+ sourceKind: string;
179
+ sourceLabel?: string;
180
+ hasSource: boolean;
181
+ /** A fix the extension can run for this finding (B6: Fetch Parts for a library part that was not fetched). */
182
+ fix?: { label: string; icon?: string };
183
+ }
184
+
185
+ export interface DrcLaneModel {
186
+ id: "blocking" | "warnings" | "info";
187
+ title: string;
188
+ total: number;
189
+ shown: number;
190
+ groups: { key: string; label: string; items: DrcRowModel[] }[];
191
+ }
192
+
193
+ export interface DrcQueryModel {
194
+ groupBy: "module" | "rule" | "source";
195
+ text: string;
196
+ severity: "all" | "error" | "warning";
197
+ info: boolean;
198
+ }
199
+
200
+ export interface DrcPanelModel {
201
+ state: "none" | "idle" | "running" | "failed";
202
+ project: string;
203
+ /** When the findings were produced, as a time of day. */
204
+ at?: string;
205
+ query: DrcQueryModel;
206
+ model?: {
207
+ lanes: DrcLaneModel[];
208
+ counts: { error: number; warning: number; info: number };
209
+ configured: { links: number; linksTotal: number; checksRan: number; checksSkipped: number };
210
+ };
211
+ }
212
+
213
+ export interface DrcPanelActions {
214
+ query(query: DrcQueryModel): void;
215
+ locate(key: string): void;
216
+ openSource(key: string): void;
217
+ explain(key: string): void;
218
+ /** Run the row's fix (`DrcRowModel.fix`). */
219
+ fix?(key: string): void;
220
+ refresh(): void;
221
+ /** Which rows are open and which groups are closed, kept by the shell across reloads. */
222
+ saved?: { expanded?: string[]; collapsed?: string[] };
223
+ save?(state: { expanded: string[]; collapsed: string[] }): void;
224
+ }