@principal-ai/subsystems-react 0.36.2 → 0.37.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.
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/pierre/PierreWalkthroughCodeView.d.ts +19 -1
- package/dist/pierre/PierreWalkthroughCodeView.d.ts.map +1 -1
- package/dist/pierre/PierreWalkthroughCodeView.js +53 -2
- package/dist/pierre/PierreWalkthroughCodeView.js.map +1 -1
- package/dist/subsystem/ComponentDeclaration.d.ts +13 -1
- package/dist/subsystem/ComponentDeclaration.d.ts.map +1 -1
- package/dist/subsystem/ComponentDeclaration.js +58 -7
- package/dist/subsystem/ComponentDeclaration.js.map +1 -1
- package/dist/subsystem/ConstructsCatalog.d.ts +6 -3
- package/dist/subsystem/ConstructsCatalog.d.ts.map +1 -1
- package/dist/subsystem/ConstructsCatalog.js +206 -217
- package/dist/subsystem/ConstructsCatalog.js.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.d.ts +10 -0
- package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
- package/dist/subsystem/SubsystemComponentGraph.js +108 -31
- package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
- package/dist/subsystem/SymbolInspectionCard.d.ts.map +1 -1
- package/dist/subsystem/SymbolInspectionCard.js +1 -1
- package/dist/subsystem/SymbolInspectionCard.js.map +1 -1
- package/dist/subsystem/model.d.ts.map +1 -1
- package/dist/subsystem/model.js +7 -1
- package/dist/subsystem/model.js.map +1 -1
- package/dist/utils/edgeLabel.d.ts +51 -0
- package/dist/utils/edgeLabel.d.ts.map +1 -0
- package/dist/utils/edgeLabel.js +81 -0
- package/dist/utils/edgeLabel.js.map +1 -0
- package/dist/utils/elkLayout.d.ts +6 -0
- package/dist/utils/elkLayout.d.ts.map +1 -1
- package/dist/utils/elkLayout.js +27 -6
- package/dist/utils/elkLayout.js.map +1 -1
- package/package.json +3 -2
- package/src/index.ts +7 -0
- package/src/pierre/PierreWalkthroughCodeView.tsx +75 -1
- package/src/stories/Subsystem/ComponentGraph/Flows.stories.tsx +222 -0
- package/src/stories/Subsystem/ConstructsCatalog.stories.tsx +443 -0
- package/src/subsystem/ComponentDeclaration.tsx +100 -7
- package/src/subsystem/ConstructsCatalog.tsx +291 -323
- package/src/subsystem/SubsystemComponentGraph.tsx +173 -30
- package/src/subsystem/SymbolInspectionCard.tsx +10 -9
- package/src/subsystem/model.ts +7 -1
- package/src/utils/edgeLabel.test.ts +23 -0
- package/src/utils/edgeLabel.ts +95 -0
- package/src/utils/elkLayout.ts +41 -6
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
3
|
+
import { ThemeProvider, defaultEditorTheme } from '@principal-ade/industry-theme';
|
|
4
|
+
import { ConstructsCatalog } from '../../subsystem/ConstructsCatalog';
|
|
5
|
+
import { PierreFileView, PierreSnippetView } from '../../pierre';
|
|
6
|
+
import type { SubsystemComponent } from '../../subsystem/model';
|
|
7
|
+
import type { SubsystemOpenFileOptions } from '../../subsystem/declarationRef';
|
|
8
|
+
import type { SymbolInspection } from '../../subsystem/symbolRefs';
|
|
9
|
+
import type { GraphifyComponentDetail } from '../../graphify';
|
|
10
|
+
|
|
11
|
+
const meta = {
|
|
12
|
+
title: 'Subsystem/ConstructsCatalog',
|
|
13
|
+
component: ConstructsCatalog,
|
|
14
|
+
parameters: { layout: 'fullscreen' },
|
|
15
|
+
tags: ['autodocs'],
|
|
16
|
+
decorators: [
|
|
17
|
+
(Story) => (
|
|
18
|
+
<ThemeProvider theme={defaultEditorTheme}>
|
|
19
|
+
<Story />
|
|
20
|
+
</ThemeProvider>
|
|
21
|
+
),
|
|
22
|
+
],
|
|
23
|
+
} satisfies Meta<typeof ConstructsCatalog>;
|
|
24
|
+
|
|
25
|
+
export default meta;
|
|
26
|
+
type Story = StoryObj<typeof meta>;
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Fixtures — two repos, two processes, one of each core construct kind, so the
|
|
30
|
+
// catalog exercises process sorting, purl grouping, and every badge/anatomy.
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
const INTAKE = 'pkg:github/you/notes-intake';
|
|
33
|
+
const UI = 'pkg:github/you/notes-ui';
|
|
34
|
+
|
|
35
|
+
const components: SubsystemComponent[] = [
|
|
36
|
+
{
|
|
37
|
+
alias: 'submit-note',
|
|
38
|
+
name: 'submitNote',
|
|
39
|
+
construct: 'function',
|
|
40
|
+
symbol: 'submitNote',
|
|
41
|
+
role: 'entry',
|
|
42
|
+
purl: INTAKE,
|
|
43
|
+
process: 'notes-api/server',
|
|
44
|
+
file: 'src/api/submitNote.ts',
|
|
45
|
+
purpose: 'HTTP/handler entry — validates and hands off to the service.',
|
|
46
|
+
declarationRef: {
|
|
47
|
+
file: 'src/api/submitNote.ts',
|
|
48
|
+
startLine: 7,
|
|
49
|
+
lineHash: 'storybook-placeholder',
|
|
50
|
+
capturedAt: new Date(0).toISOString(),
|
|
51
|
+
},
|
|
52
|
+
declaration: {
|
|
53
|
+
kind: 'function',
|
|
54
|
+
parameters: [
|
|
55
|
+
{ name: 'req', type: 'SubmitNoteRequest' },
|
|
56
|
+
{ name: 'ctx', type: 'RequestContext' },
|
|
57
|
+
],
|
|
58
|
+
returnType: 'Promise<NoteId>',
|
|
59
|
+
callers: [],
|
|
60
|
+
callees: [{ nodeId: 'note-service', name: 'NoteService' }],
|
|
61
|
+
} satisfies GraphifyComponentDetail,
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
alias: 'note-service',
|
|
65
|
+
name: 'NoteService',
|
|
66
|
+
construct: 'class',
|
|
67
|
+
symbol: 'NoteService',
|
|
68
|
+
purl: INTAKE,
|
|
69
|
+
process: 'notes-api/server',
|
|
70
|
+
file: 'src/NoteService.ts',
|
|
71
|
+
purpose: 'Owns intake rules; methods are separate nodes when they matter.',
|
|
72
|
+
declarationRef: {
|
|
73
|
+
file: 'src/NoteService.ts',
|
|
74
|
+
startLine: 3,
|
|
75
|
+
lineHash: 'storybook-placeholder',
|
|
76
|
+
capturedAt: new Date(0).toISOString(),
|
|
77
|
+
},
|
|
78
|
+
declaration: {
|
|
79
|
+
kind: 'class',
|
|
80
|
+
methods: [
|
|
81
|
+
{
|
|
82
|
+
nodeId: 'note-service-persist',
|
|
83
|
+
name: 'persist',
|
|
84
|
+
parameters: [{ name: 'draft', type: 'NoteDraft' }],
|
|
85
|
+
returnType: 'Promise<NoteId>',
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
properties: [{ name: 'repo', type: 'NoteRepository' }],
|
|
89
|
+
extends: [],
|
|
90
|
+
implements: ['NoteRepository'],
|
|
91
|
+
instantiations: [],
|
|
92
|
+
references: [],
|
|
93
|
+
} satisfies GraphifyComponentDetail,
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
alias: 'note-service-persist',
|
|
97
|
+
name: 'persist',
|
|
98
|
+
construct: 'method',
|
|
99
|
+
symbol: 'NoteService.persist',
|
|
100
|
+
purl: INTAKE,
|
|
101
|
+
process: 'notes-api/server',
|
|
102
|
+
file: 'src/NoteService.ts',
|
|
103
|
+
purpose: 'Instance method on NoteService — written as its own construct.',
|
|
104
|
+
declarationRef: {
|
|
105
|
+
file: 'src/NoteService.ts',
|
|
106
|
+
startLine: 14,
|
|
107
|
+
lineHash: 'storybook-placeholder',
|
|
108
|
+
capturedAt: new Date(0).toISOString(),
|
|
109
|
+
},
|
|
110
|
+
declaration: {
|
|
111
|
+
kind: 'method',
|
|
112
|
+
hostClass: 'NoteService',
|
|
113
|
+
parameters: [{ name: 'draft', type: 'NoteDraft' }],
|
|
114
|
+
returnType: 'Promise<NoteId>',
|
|
115
|
+
} satisfies GraphifyComponentDetail,
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
alias: 'note-repository',
|
|
119
|
+
name: 'NoteRepository',
|
|
120
|
+
construct: 'interface',
|
|
121
|
+
symbol: 'NoteRepository',
|
|
122
|
+
purl: INTAKE,
|
|
123
|
+
process: 'notes-api/server',
|
|
124
|
+
file: 'src/NoteRepository.ts',
|
|
125
|
+
purpose: 'Persistence contract the service depends on.',
|
|
126
|
+
declaration: {
|
|
127
|
+
kind: 'type',
|
|
128
|
+
properties: [
|
|
129
|
+
{ name: 'save', type: '(draft: NoteDraft) => Promise<NoteId>' },
|
|
130
|
+
{ name: 'findById', type: '(id: NoteId) => Promise<Note | null>' },
|
|
131
|
+
],
|
|
132
|
+
usedBy: [],
|
|
133
|
+
implementors: ['NoteService'],
|
|
134
|
+
} satisfies GraphifyComponentDetail,
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
alias: 'note-kind',
|
|
138
|
+
name: 'NoteKind',
|
|
139
|
+
construct: 'enum',
|
|
140
|
+
symbol: 'NoteKind',
|
|
141
|
+
purl: INTAKE,
|
|
142
|
+
process: 'notes-api/server',
|
|
143
|
+
file: 'src/types.ts',
|
|
144
|
+
purpose: 'Closed set of note kinds (idea, task, …).',
|
|
145
|
+
declaration: {
|
|
146
|
+
kind: 'type',
|
|
147
|
+
properties: [],
|
|
148
|
+
usedBy: [],
|
|
149
|
+
implementors: [],
|
|
150
|
+
enumMembers: [
|
|
151
|
+
{ name: 'Idea', value: "'idea'" },
|
|
152
|
+
{ name: 'Task', value: "'task'" },
|
|
153
|
+
],
|
|
154
|
+
} satisfies GraphifyComponentDetail,
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
alias: 'note-id',
|
|
158
|
+
name: 'NoteId',
|
|
159
|
+
construct: 'type_alias',
|
|
160
|
+
symbol: 'NoteId',
|
|
161
|
+
purl: INTAKE,
|
|
162
|
+
file: 'src/types.ts',
|
|
163
|
+
purpose: 'Branded id type — no process, so it sorts to the bottom.',
|
|
164
|
+
declaration: {
|
|
165
|
+
kind: 'type',
|
|
166
|
+
properties: [],
|
|
167
|
+
usedBy: [],
|
|
168
|
+
implementors: [],
|
|
169
|
+
aliasOf: "string & { readonly __brand: 'NoteId' }",
|
|
170
|
+
} satisfies GraphifyComponentDetail,
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
alias: 'note-editor',
|
|
174
|
+
name: 'NoteEditor',
|
|
175
|
+
construct: 'function',
|
|
176
|
+
framework: 'react',
|
|
177
|
+
stereotype: 'component',
|
|
178
|
+
symbol: 'NoteEditor',
|
|
179
|
+
purl: UI,
|
|
180
|
+
process: 'notes-web/client',
|
|
181
|
+
file: 'packages/ui/NoteEditor.tsx',
|
|
182
|
+
purpose: 'React component — a function playing the component stereotype.',
|
|
183
|
+
declaration: {
|
|
184
|
+
kind: 'function',
|
|
185
|
+
parameters: [{ name: 'props', type: 'NoteEditorProps' }],
|
|
186
|
+
returnType: 'JSX.Element',
|
|
187
|
+
callers: [],
|
|
188
|
+
callees: [],
|
|
189
|
+
} satisfies GraphifyComponentDetail,
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
alias: 'use-draft',
|
|
193
|
+
name: 'useDraft',
|
|
194
|
+
construct: 'function',
|
|
195
|
+
framework: 'react',
|
|
196
|
+
stereotype: 'hook',
|
|
197
|
+
symbol: 'useDraft',
|
|
198
|
+
purl: UI,
|
|
199
|
+
process: 'notes-web/client',
|
|
200
|
+
file: 'packages/ui/useDraft.ts',
|
|
201
|
+
purpose: 'React hook — draft state for the editor.',
|
|
202
|
+
declaration: {
|
|
203
|
+
kind: 'function',
|
|
204
|
+
parameters: [{ name: 'initial', type: 'NoteDraft' }],
|
|
205
|
+
returnType: '[NoteDraft, (next: NoteDraft) => void]',
|
|
206
|
+
callers: [],
|
|
207
|
+
callees: [],
|
|
208
|
+
} satisfies GraphifyComponentDetail,
|
|
209
|
+
},
|
|
210
|
+
];
|
|
211
|
+
|
|
212
|
+
/** Constructs with no `file` — the Files tab should not appear. */
|
|
213
|
+
const componentsWithoutFiles: SubsystemComponent[] = [
|
|
214
|
+
{
|
|
215
|
+
alias: 'remote-notes-service',
|
|
216
|
+
name: 'notes-service',
|
|
217
|
+
construct: 'external',
|
|
218
|
+
role: 'service',
|
|
219
|
+
symbol: '',
|
|
220
|
+
purl: 'pkg:npm/@you/notes-service',
|
|
221
|
+
file: '',
|
|
222
|
+
purpose: 'External package consumer — no source file in this repo.',
|
|
223
|
+
declaration: {
|
|
224
|
+
kind: 'external',
|
|
225
|
+
label: 'pkg:npm/@you/notes-service',
|
|
226
|
+
} satisfies GraphifyComponentDetail,
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
alias: 'remote-notes-client',
|
|
230
|
+
name: 'notes-client',
|
|
231
|
+
construct: 'external',
|
|
232
|
+
role: 'service',
|
|
233
|
+
symbol: '',
|
|
234
|
+
purl: 'pkg:npm/@you/notes-client',
|
|
235
|
+
file: '',
|
|
236
|
+
purpose: 'Second external dependency for the no-files case.',
|
|
237
|
+
declaration: {
|
|
238
|
+
kind: 'external',
|
|
239
|
+
label: 'pkg:npm/@you/notes-client',
|
|
240
|
+
} satisfies GraphifyComponentDetail,
|
|
241
|
+
},
|
|
242
|
+
];
|
|
243
|
+
|
|
244
|
+
// ---------------------------------------------------------------------------
|
|
245
|
+
// Inline source fixtures for the bottom FileDrawer (no fs in Storybook).
|
|
246
|
+
// ---------------------------------------------------------------------------
|
|
247
|
+
const storyFiles: Record<string, string> = {
|
|
248
|
+
'src/api/submitNote.ts': `import { NoteService } from '../NoteService';
|
|
249
|
+
import type { NoteId, SubmitNoteRequest, RequestContext } from '../types';
|
|
250
|
+
|
|
251
|
+
const service = new NoteService();
|
|
252
|
+
|
|
253
|
+
/** HTTP/handler entry — validates and hands off to the service. */
|
|
254
|
+
export async function submitNote(
|
|
255
|
+
req: SubmitNoteRequest,
|
|
256
|
+
ctx: RequestContext,
|
|
257
|
+
): Promise<NoteId> {
|
|
258
|
+
return service.persist({ ...req, actor: ctx.actor });
|
|
259
|
+
}
|
|
260
|
+
`,
|
|
261
|
+
'src/NoteService.ts': `import type { NoteDraft, NoteId, NoteRepository } from './types';
|
|
262
|
+
|
|
263
|
+
export class NoteService implements NoteRepository {
|
|
264
|
+
constructor(readonly repo: NoteRepository) {}
|
|
265
|
+
|
|
266
|
+
async save(draft: NoteDraft): Promise<NoteId> {
|
|
267
|
+
return this.persist(draft);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async findById(id: NoteId): Promise<Note | null> {
|
|
271
|
+
return this.repo.findById(id);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async persist(draft: NoteDraft): Promise<NoteId> {
|
|
275
|
+
return this.repo.save(draft);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
`,
|
|
279
|
+
'src/NoteRepository.ts': `import type { Note, NoteDraft, NoteId } from './types';
|
|
280
|
+
|
|
281
|
+
export interface NoteRepository {
|
|
282
|
+
save(draft: NoteDraft): Promise<NoteId>;
|
|
283
|
+
findById(id: NoteId): Promise<Note | null>;
|
|
284
|
+
}
|
|
285
|
+
`,
|
|
286
|
+
'src/types.ts': `export type NoteId = string & { readonly __brand: 'NoteId' };
|
|
287
|
+
|
|
288
|
+
export type NoteDraft = { title: string; kind: NoteKind };
|
|
289
|
+
|
|
290
|
+
export enum NoteKind {
|
|
291
|
+
Idea = 'idea',
|
|
292
|
+
Task = 'task',
|
|
293
|
+
}
|
|
294
|
+
`,
|
|
295
|
+
'packages/ui/NoteEditor.tsx': `import type { NoteDraft } from '../../src/types';
|
|
296
|
+
import { useDraft } from './useDraft';
|
|
297
|
+
|
|
298
|
+
export function NoteEditor(props: { initial: NoteDraft }) {
|
|
299
|
+
const [draft, setDraft] = useDraft(props.initial);
|
|
300
|
+
return <textarea value={draft.title} onChange={(e) => setDraft({ ...draft, title: e.target.value })} />;
|
|
301
|
+
}
|
|
302
|
+
`,
|
|
303
|
+
'packages/ui/useDraft.ts': `import { useState } from 'react';
|
|
304
|
+
import type { NoteDraft } from '../../src/types';
|
|
305
|
+
|
|
306
|
+
export function useDraft(initial: NoteDraft) {
|
|
307
|
+
return useState<NoteDraft>(initial);
|
|
308
|
+
}
|
|
309
|
+
`,
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
function readStoryFile(path: string): Promise<string> {
|
|
313
|
+
const content = storyFiles[path];
|
|
314
|
+
if (content == null) return Promise.reject(new Error(`No story fixture for ${path}`));
|
|
315
|
+
return Promise.resolve(content);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function renderFileViewer(file: string, opts?: SubsystemOpenFileOptions) {
|
|
319
|
+
const startLine = opts?.startLine;
|
|
320
|
+
if (startLine != null && !opts?.fullFile) {
|
|
321
|
+
return (
|
|
322
|
+
<PierreSnippetView
|
|
323
|
+
filePath={file}
|
|
324
|
+
fileName={file.split('/').pop() ?? file}
|
|
325
|
+
startLine={startLine}
|
|
326
|
+
endLine={startLine}
|
|
327
|
+
focusLine={startLine}
|
|
328
|
+
contextLines={12}
|
|
329
|
+
readFile={readStoryFile}
|
|
330
|
+
/>
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
return (
|
|
334
|
+
<PierreFileView
|
|
335
|
+
filePath={file}
|
|
336
|
+
fileName={file.split('/').pop() ?? file}
|
|
337
|
+
readFile={readStoryFile}
|
|
338
|
+
focusLine={opts?.fullFile ? startLine : undefined}
|
|
339
|
+
/>
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** Graphify lookups so referenced type tokens in declarations are clickable. */
|
|
344
|
+
const INSPECTIONS: Record<string, SymbolInspection> = {
|
|
345
|
+
NoteRepository: {
|
|
346
|
+
symbol: 'NoteRepository',
|
|
347
|
+
purl: INTAKE,
|
|
348
|
+
resolution: 'resolved',
|
|
349
|
+
node: {
|
|
350
|
+
nodeId: 'note-repository',
|
|
351
|
+
label: 'NoteRepository',
|
|
352
|
+
sourceFile: 'src/NoteRepository.ts',
|
|
353
|
+
sourceLocation: 'L3',
|
|
354
|
+
},
|
|
355
|
+
declaration: {
|
|
356
|
+
kind: 'type',
|
|
357
|
+
properties: [
|
|
358
|
+
{ name: 'save', type: '(draft: NoteDraft) => Promise<NoteId>' },
|
|
359
|
+
{ name: 'findById', type: '(id: NoteId) => Promise<Note | null>' },
|
|
360
|
+
],
|
|
361
|
+
usedBy: [],
|
|
362
|
+
implementors: ['NoteService'],
|
|
363
|
+
},
|
|
364
|
+
},
|
|
365
|
+
NoteDraft: {
|
|
366
|
+
symbol: 'NoteDraft',
|
|
367
|
+
purl: INTAKE,
|
|
368
|
+
resolution: 'resolved',
|
|
369
|
+
node: {
|
|
370
|
+
nodeId: 'note-draft',
|
|
371
|
+
label: 'NoteDraft',
|
|
372
|
+
sourceFile: 'src/types.ts',
|
|
373
|
+
sourceLocation: 'L3',
|
|
374
|
+
},
|
|
375
|
+
declaration: {
|
|
376
|
+
kind: 'type',
|
|
377
|
+
properties: [
|
|
378
|
+
{ name: 'title', type: 'string' },
|
|
379
|
+
{ name: 'kind', type: 'NoteKind' },
|
|
380
|
+
],
|
|
381
|
+
usedBy: [],
|
|
382
|
+
implementors: [],
|
|
383
|
+
},
|
|
384
|
+
},
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
function inspectSymbol(req: { symbol: string }): SymbolInspection | null {
|
|
388
|
+
return INSPECTIONS[req.symbol] ?? null;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function CatalogDemo({
|
|
392
|
+
comps = components,
|
|
393
|
+
hideSidebar = false,
|
|
394
|
+
}: {
|
|
395
|
+
comps?: SubsystemComponent[];
|
|
396
|
+
hideSidebar?: boolean;
|
|
397
|
+
}) {
|
|
398
|
+
const [selected, setSelected] = useState<string | null>(null);
|
|
399
|
+
return (
|
|
400
|
+
<div style={{ width: '100%', height: '100vh', display: 'flex', flexDirection: 'column' }}>
|
|
401
|
+
<div style={{ flex: 1, minHeight: 0 }}>
|
|
402
|
+
<ConstructsCatalog
|
|
403
|
+
components={comps}
|
|
404
|
+
title="Notes intake — constructs"
|
|
405
|
+
description="Nothing is selected, so every construct is listed and searchable; pick a file on the left to narrow. Every file stacks under one combined header (path + a description toggle for the whole stack), each declaration labeling the line that holds the construct in a file-like gutter. Click the header or a line to open the file."
|
|
406
|
+
hideSidebar={hideSidebar}
|
|
407
|
+
onSelect={(id) => setSelected(id)}
|
|
408
|
+
renderFileViewer={renderFileViewer}
|
|
409
|
+
onInspectSymbol={inspectSymbol}
|
|
410
|
+
/>
|
|
411
|
+
</div>
|
|
412
|
+
<div style={{ marginTop: 8, fontFamily: 'monospace', fontSize: 12, color: '#aaa' }}>
|
|
413
|
+
{selected
|
|
414
|
+
? `last toggled: ${selected}`
|
|
415
|
+
: 'click constructs to toggle their signatures'}
|
|
416
|
+
</div>
|
|
417
|
+
</div>
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/** Full catalog: title + description chrome, two repos, process-sorted rows. */
|
|
422
|
+
export const Catalog: Story = {
|
|
423
|
+
args: { components },
|
|
424
|
+
render: () => <CatalogDemo />,
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
/** Home-page embed shape — chrome suppressed via `hideSidebar`. */
|
|
428
|
+
export const Embedded: Story = {
|
|
429
|
+
args: { components },
|
|
430
|
+
render: () => <CatalogDemo hideSidebar />,
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
/** Constructs with no `file` — the Files tab is hidden, constructs-only list. */
|
|
434
|
+
export const NoFiles: Story = {
|
|
435
|
+
args: { components: componentsWithoutFiles },
|
|
436
|
+
render: () => <CatalogDemo comps={componentsWithoutFiles} />,
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
/** No components — the empty-state prompt in the detail pane. */
|
|
440
|
+
export const Empty: Story = {
|
|
441
|
+
args: { components: [] },
|
|
442
|
+
render: () => <CatalogDemo comps={[]} />,
|
|
443
|
+
};
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* comments. File content lives in the bottom FileDrawer, not here.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from 'react';
|
|
9
|
+
import { useCallback, useEffect, useMemo, useRef, useState, type CSSProperties, type ReactNode } from 'react';
|
|
10
10
|
import { createPortal } from 'react-dom';
|
|
11
11
|
import { AlignLeft, FileText } from 'lucide-react';
|
|
12
12
|
import { useTheme } from '@principal-ade/industry-theme';
|
|
@@ -138,6 +138,10 @@ function DetailLink({ children, onClick }: { children: ReactNode; onClick?: () =
|
|
|
138
138
|
);
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
+
/** Width of the `lineNumbers` gutter column and the gap after it. */
|
|
142
|
+
const GUTTER_NUMBER_WIDTH = 40;
|
|
143
|
+
const GUTTER_NUMBER_GAP = 8;
|
|
144
|
+
|
|
141
145
|
/** Panel props. Callbacks are wired by the graph; standalone usage without
|
|
142
146
|
* them renders the same panel with nothing clickable. */
|
|
143
147
|
export interface ComponentDeclarationProps {
|
|
@@ -174,6 +178,18 @@ export interface ComponentDeclarationProps {
|
|
|
174
178
|
* the file badge: drop the top-right `L#` (redundant) and put the
|
|
175
179
|
* description toggle in the badge after the line segment. */
|
|
176
180
|
fileBadgeChrome?: boolean;
|
|
181
|
+
/** Suppress the per-declaration action chrome entirely (file toggle, line
|
|
182
|
+
* label, description toggle). For hosts that surface those in a shared
|
|
183
|
+
* header instead. */
|
|
184
|
+
hideActions?: boolean;
|
|
185
|
+
/** Controlled purpose/description visibility. When provided it overrides the
|
|
186
|
+
* internal toggle, so a host can drive it from a shared header. */
|
|
187
|
+
showPurpose?: boolean;
|
|
188
|
+
/** Render a gutter beside the declaration body and label the construct's own
|
|
189
|
+
* line with its known start line — only that line is accurate, so the rest
|
|
190
|
+
* of the gutter stays blank and aligned. The label opens the file at that
|
|
191
|
+
* line when `onOpenFile` is set. */
|
|
192
|
+
lineNumbers?: boolean;
|
|
177
193
|
}
|
|
178
194
|
|
|
179
195
|
function verificationSummary(
|
|
@@ -318,6 +334,9 @@ export function ComponentDeclaration({
|
|
|
318
334
|
declarationOpen = false,
|
|
319
335
|
showRepoIdentity = true,
|
|
320
336
|
fileBadgeChrome = false,
|
|
337
|
+
hideActions = false,
|
|
338
|
+
showPurpose: showPurposeProp,
|
|
339
|
+
lineNumbers = false,
|
|
321
340
|
}: ComponentDeclarationProps) {
|
|
322
341
|
const { theme, mode } = useTheme();
|
|
323
342
|
const pierreSyntaxTheme = resolvePierreSyntaxThemeName(mode);
|
|
@@ -325,7 +344,8 @@ export function ComponentDeclaration({
|
|
|
325
344
|
'path' | 'line' | 'desc' | 'action' | null
|
|
326
345
|
>(null);
|
|
327
346
|
const [showFile, setShowFile] = useState(defaultShowFile);
|
|
328
|
-
const [
|
|
347
|
+
const [showPurposeState, setShowPurposeState] = useState(false);
|
|
348
|
+
const showPurpose = showPurposeProp ?? showPurposeState;
|
|
329
349
|
const muted = theme.colors.textMuted ?? theme.colors.textSecondary;
|
|
330
350
|
const color = componentColor(component, pierreSyntaxTheme);
|
|
331
351
|
const inlineChrome = fileBadgeChrome && showFile && !!component.file;
|
|
@@ -497,7 +517,7 @@ export function ComponentDeclaration({
|
|
|
497
517
|
{!inlineChrome && lineLocationLabel}
|
|
498
518
|
{!inlineChrome &&
|
|
499
519
|
component.purpose?.trim() &&
|
|
500
|
-
toggleBtn(showPurpose, () =>
|
|
520
|
+
toggleBtn(showPurpose, () => setShowPurposeState((v) => !v), 'Toggle description', AlignLeft)}
|
|
501
521
|
</span>
|
|
502
522
|
);
|
|
503
523
|
|
|
@@ -560,7 +580,7 @@ export function ComponentDeclaration({
|
|
|
560
580
|
'purl',
|
|
561
581
|
),
|
|
562
582
|
);
|
|
563
|
-
} else if (!showRepoIdentity && !inlineChrome) {
|
|
583
|
+
} else if (!showRepoIdentity && !inlineChrome && !hideActions) {
|
|
564
584
|
lines.push(line(headerActions, 'actions'));
|
|
565
585
|
}
|
|
566
586
|
|
|
@@ -695,7 +715,7 @@ export function ComponentDeclaration({
|
|
|
695
715
|
aria-label={showPurpose ? 'Hide description' : 'Show description'}
|
|
696
716
|
onClick={(e) => {
|
|
697
717
|
e.stopPropagation();
|
|
698
|
-
|
|
718
|
+
setShowPurposeState((v) => !v);
|
|
699
719
|
}}
|
|
700
720
|
{...segHover('desc')}
|
|
701
721
|
style={{
|
|
@@ -888,14 +908,17 @@ export function ComponentDeclaration({
|
|
|
888
908
|
);
|
|
889
909
|
|
|
890
910
|
const declLines: ReactNode[][] = [[]];
|
|
911
|
+
const declLineText: string[] = [''];
|
|
891
912
|
let di = 0;
|
|
892
913
|
let offset = 0;
|
|
893
914
|
for (const tok of tokens) {
|
|
894
915
|
if (tok.kind === 'newline') {
|
|
895
916
|
declLines.push([]);
|
|
917
|
+
declLineText.push('');
|
|
896
918
|
di++;
|
|
897
919
|
continue;
|
|
898
920
|
}
|
|
921
|
+
declLineText[di] += tok.text;
|
|
899
922
|
const color = tok.color ?? tokenColor[tok.kind] ?? declarationTextColor;
|
|
900
923
|
let i = 0;
|
|
901
924
|
while (i < tok.text.length) {
|
|
@@ -956,8 +979,78 @@ export function ComponentDeclaration({
|
|
|
956
979
|
}
|
|
957
980
|
offset += tok.text.length;
|
|
958
981
|
}
|
|
982
|
+
// The rendered line carrying the construct's own name gets the known line
|
|
983
|
+
// number — a method stub wraps it in `class Host {`, so the number belongs on
|
|
984
|
+
// the member line, not the synthetic first row. Falls back to the first line.
|
|
985
|
+
const constructName =
|
|
986
|
+
component.name || component.symbol?.split('.').pop() || '';
|
|
987
|
+
let labelDi = -1;
|
|
988
|
+
if (constructName) {
|
|
989
|
+
for (let li = 0; li < declLines.length; li++) {
|
|
990
|
+
if (declLines[li].length > 0 && declLineText[li].includes(constructName)) {
|
|
991
|
+
labelDi = li;
|
|
992
|
+
break;
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
if (labelDi === -1) {
|
|
997
|
+
labelDi = declLines.findIndex((l) => l.length > 0);
|
|
998
|
+
}
|
|
959
999
|
for (let li = 0; li < declLines.length; li++) {
|
|
960
1000
|
if (declLines[li].length === 0) continue;
|
|
1001
|
+
if (lineNumbers && declarationStartLine != null) {
|
|
1002
|
+
// Only the construct's own line is known; the rest of the gutter stays
|
|
1003
|
+
// blank (but aligned) since later rendered lines needn't map to source.
|
|
1004
|
+
const n = li === labelDi ? declarationStartLine : null;
|
|
1005
|
+
const interactive = n != null && !!onOpenFile && !!component.file;
|
|
1006
|
+
const gutterStyle: CSSProperties = {
|
|
1007
|
+
flexShrink: 0,
|
|
1008
|
+
width: GUTTER_NUMBER_WIDTH,
|
|
1009
|
+
paddingRight: GUTTER_NUMBER_GAP,
|
|
1010
|
+
border: 'none',
|
|
1011
|
+
background: 'transparent',
|
|
1012
|
+
textAlign: 'right',
|
|
1013
|
+
color: muted,
|
|
1014
|
+
fontFamily: 'inherit',
|
|
1015
|
+
fontSize: 'inherit',
|
|
1016
|
+
lineHeight: 'inherit',
|
|
1017
|
+
cursor: interactive ? 'pointer' : 'default',
|
|
1018
|
+
userSelect: 'none',
|
|
1019
|
+
};
|
|
1020
|
+
lines.push(
|
|
1021
|
+
<div
|
|
1022
|
+
key={`decl-${li}`}
|
|
1023
|
+
style={{ display: 'flex', alignItems: 'flex-start', minWidth: 0 }}
|
|
1024
|
+
>
|
|
1025
|
+
{n == null ? (
|
|
1026
|
+
<span style={gutterStyle} aria-hidden="true" />
|
|
1027
|
+
) : interactive ? (
|
|
1028
|
+
<button
|
|
1029
|
+
type="button"
|
|
1030
|
+
title={`Open line ${n}`}
|
|
1031
|
+
onClick={() => onOpenFile!(component.file, { startLine: n })}
|
|
1032
|
+
style={gutterStyle}
|
|
1033
|
+
>
|
|
1034
|
+
{n}
|
|
1035
|
+
</button>
|
|
1036
|
+
) : (
|
|
1037
|
+
<span style={gutterStyle}>{n}</span>
|
|
1038
|
+
)}
|
|
1039
|
+
<div
|
|
1040
|
+
style={{
|
|
1041
|
+
flex: 1,
|
|
1042
|
+
minWidth: 0,
|
|
1043
|
+
whiteSpace: 'pre-wrap',
|
|
1044
|
+
overflowWrap: 'anywhere',
|
|
1045
|
+
minHeight: 18,
|
|
1046
|
+
}}
|
|
1047
|
+
>
|
|
1048
|
+
{declLines[li]}
|
|
1049
|
+
</div>
|
|
1050
|
+
</div>,
|
|
1051
|
+
);
|
|
1052
|
+
continue;
|
|
1053
|
+
}
|
|
961
1054
|
lines.push(
|
|
962
1055
|
<div
|
|
963
1056
|
key={`decl-${li}`}
|
|
@@ -986,7 +1079,7 @@ export function ComponentDeclaration({
|
|
|
986
1079
|
{purposeLine.trim()}
|
|
987
1080
|
</span>,
|
|
988
1081
|
`purpose${i}`,
|
|
989
|
-
false,
|
|
1082
|
+
lineNumbers ? GUTTER_NUMBER_WIDTH + GUTTER_NUMBER_GAP : false,
|
|
990
1083
|
'normal',
|
|
991
1084
|
),
|
|
992
1085
|
),
|
|
@@ -1015,7 +1108,7 @@ export function ComponentDeclaration({
|
|
|
1015
1108
|
minWidth: 0,
|
|
1016
1109
|
boxSizing: 'border-box',
|
|
1017
1110
|
background: theme.colors.backgroundSecondary,
|
|
1018
|
-
padding: '10px 12px',
|
|
1111
|
+
padding: lineNumbers ? '10px 12px 10px 0' : '10px 12px',
|
|
1019
1112
|
fontFamily: theme.fonts.monospace,
|
|
1020
1113
|
fontSize: theme.fontSizes[1],
|
|
1021
1114
|
lineHeight: 1.7,
|