@petrarca/sonnet-playground 0.4.2 → 0.5.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.
@@ -0,0 +1,1058 @@
1
+ // src/pages/PlayGraph.tsx
2
+ import { useCallback, useMemo, useRef, useState } from "react";
3
+ import {
4
+ GraphVisualizer,
5
+ ActionPanel,
6
+ GraphA11yList,
7
+ GraphRendererProvider,
8
+ GraphStoresContext,
9
+ GraphEditToolbar,
10
+ createGraphDataStore,
11
+ createGraphFilterStore,
12
+ createGraphExplorationStore,
13
+ createSelectionStore,
14
+ createGraphFocusStore,
15
+ enrichNodes,
16
+ enrichEdges,
17
+ buildColorPalette,
18
+ GraphModelEditor
19
+ } from "@petrarca/sonnet-graph";
20
+ import { DEFAULT_WIDGETS } from "@petrarca/sonnet-forms";
21
+
22
+ // src/pages/graph/inMemoryGraphEditing.ts
23
+ function createIdAllocator(store) {
24
+ const allIds = [
25
+ ...store.nodes.map((n) => n.id_ ?? 0),
26
+ ...store.edges.map((e) => e.id_ ?? 0)
27
+ ];
28
+ const max = allIds.reduce((m, id) => id > m ? id : m, 0);
29
+ let next = max + 1;
30
+ return () => next++;
31
+ }
32
+ function edgesForNode(store, id) {
33
+ return {
34
+ inbound: store.edges.filter((e) => e.end_id_ === id),
35
+ outbound: store.edges.filter((e) => e.start_id_ === id)
36
+ };
37
+ }
38
+ function diffRelations(store, nodeId, relations, nextId) {
39
+ const created = [];
40
+ const deleted = [];
41
+ for (const [relType, targetIds] of Object.entries(relations)) {
42
+ const existing = store.edges.filter(
43
+ (e) => e.label_ === relType && e.start_id_ === nodeId
44
+ );
45
+ const desired = new Set(targetIds);
46
+ for (const edge of existing) {
47
+ if (!desired.has(edge.end_id_) && edge.id_ != null) {
48
+ deleted.push(edge.id_);
49
+ }
50
+ }
51
+ const present = new Set(existing.map((e) => e.end_id_));
52
+ for (const targetId of targetIds) {
53
+ if (!present.has(targetId)) {
54
+ const edge = {
55
+ id_: nextId(),
56
+ label_: relType,
57
+ start_id_: nodeId,
58
+ end_id_: targetId,
59
+ properties_: {}
60
+ };
61
+ created.push(edge);
62
+ }
63
+ }
64
+ }
65
+ store.edges = store.edges.filter((e) => e.id_ == null || !deleted.includes(e.id_)).concat(created);
66
+ return { created, deleted };
67
+ }
68
+ function createInMemoryPort(store, onChange) {
69
+ const nextId = createIdAllocator(store);
70
+ const createNode = (input) => {
71
+ const node = {
72
+ id_: nextId(),
73
+ label_: input.label_,
74
+ properties_: input.properties_
75
+ };
76
+ store.nodes = [...store.nodes, node];
77
+ return node;
78
+ };
79
+ const updateNode = (input) => {
80
+ const node = {
81
+ id_: input.id_,
82
+ label_: input.label_,
83
+ properties_: input.properties_
84
+ };
85
+ store.nodes = store.nodes.map((n) => n.id_ === input.id_ ? node : n);
86
+ return node;
87
+ };
88
+ const createEdge = (input) => {
89
+ const edge = {
90
+ id_: nextId(),
91
+ label_: input.label_,
92
+ start_id_: input.start_id_,
93
+ end_id_: input.end_id_,
94
+ properties_: input.properties_ ?? {}
95
+ };
96
+ store.edges = [...store.edges, edge];
97
+ return edge;
98
+ };
99
+ const updateEdge = (input) => {
100
+ let updated;
101
+ store.edges = store.edges.map((e) => {
102
+ if (e.id_ !== input.id_) return e;
103
+ updated = { ...e, properties_: input.properties_ };
104
+ return updated;
105
+ });
106
+ if (!updated) throw new Error(`Edge ${input.id_} not found`);
107
+ return updated;
108
+ };
109
+ const withRefresh = (fn) => {
110
+ return (...args) => {
111
+ const result = fn(...args);
112
+ onChange();
113
+ return Promise.resolve(result);
114
+ };
115
+ };
116
+ return {
117
+ createNode: withRefresh(createNode),
118
+ updateNode: withRefresh(updateNode),
119
+ deleteNode: withRefresh((id) => {
120
+ store.nodes = store.nodes.filter((n) => n.id_ !== id);
121
+ store.edges = store.edges.filter(
122
+ (e) => e.start_id_ !== id && e.end_id_ !== id
123
+ );
124
+ }),
125
+ createEdge: withRefresh(createEdge),
126
+ updateEdge: withRefresh(updateEdge),
127
+ deleteEdge: withRefresh((id) => {
128
+ store.edges = store.edges.filter((e) => e.id_ !== id);
129
+ }),
130
+ createNodeWithRelations: withRefresh(
131
+ (input) => {
132
+ const node = createNode(input);
133
+ const { created } = diffRelations(
134
+ store,
135
+ node.id_,
136
+ input.relations,
137
+ nextId
138
+ );
139
+ return { node, edges: created };
140
+ }
141
+ ),
142
+ updateNodeWithRelations: withRefresh(
143
+ (input) => {
144
+ const node = updateNode(input);
145
+ const { created, deleted } = diffRelations(
146
+ store,
147
+ input.id_,
148
+ input.relations,
149
+ nextId
150
+ );
151
+ return { node, edges: { created, deleted } };
152
+ }
153
+ ),
154
+ getInboundEdges: (id) => Promise.resolve(edgesForNode(store, id).inbound),
155
+ getOutboundEdges: (id) => Promise.resolve(edgesForNode(store, id).outbound),
156
+ getPairEdges: (a, b) => Promise.resolve(
157
+ store.edges.filter(
158
+ (e) => e.start_id_ === a && e.end_id_ === b || e.start_id_ === b && e.end_id_ === a
159
+ )
160
+ ),
161
+ getNodes: (ids) => Promise.resolve(
162
+ store.nodes.filter((n) => n.id_ != null && ids.includes(n.id_))
163
+ )
164
+ };
165
+ }
166
+ function toPropertySchema(type) {
167
+ if (!type) return null;
168
+ return {
169
+ type: "object",
170
+ properties: type.properties ?? {},
171
+ required: type.required ?? []
172
+ };
173
+ }
174
+ function createModelSchemaProvider(model) {
175
+ const byLabel = new Map(model.map((t) => [t["x-graph"].label, t]));
176
+ const nodeLabels = model.filter((t) => t["x-graph"].type !== "EDGE").map((t) => t["x-graph"].label);
177
+ return {
178
+ getNodeSchema: (label) => Promise.resolve(toPropertySchema(byLabel.get(label))),
179
+ getEdgeSchema: (type) => Promise.resolve(toPropertySchema(byLabel.get(type))),
180
+ listNodeLabels: () => Promise.resolve(nodeLabels),
181
+ relationsFor: (label) => {
182
+ const relations = byLabel.get(label)?.["x-graph"].relations ?? [];
183
+ return Promise.resolve(
184
+ relations.map((r) => ({
185
+ rel_type_name: r.rel_type_name,
186
+ to_type_name: r.to_type_name,
187
+ form_field: r.form_field,
188
+ // cardinality drives single- vs multi-select in the form.
189
+ cardinality: r.cardinality
190
+ }))
191
+ );
192
+ }
193
+ };
194
+ }
195
+
196
+ // src/pages/graph/relationWidget.tsx
197
+ import {
198
+ FormSelect,
199
+ FormMultiSelect
200
+ } from "@petrarca/sonnet-forms";
201
+ import { extractDisplayLabel } from "@petrarca/sonnet-graph";
202
+ import { jsx } from "react/jsx-runtime";
203
+ function optionsForType(store, targetType) {
204
+ return store.nodes.filter((n) => n.label_ === targetType && n.id_ != null).map((n) => ({ value: String(n.id_), label: extractDisplayLabel(n) }));
205
+ }
206
+ function toStringArray(value) {
207
+ if (Array.isArray(value)) return value.map(String);
208
+ if (value == null || value === "") return [];
209
+ return [String(value)];
210
+ }
211
+ function createRelationWidget(store) {
212
+ return function RelationWidget({
213
+ value,
214
+ onChange,
215
+ label,
216
+ options,
217
+ placeholder
218
+ }) {
219
+ const targetType = String(options?.targetType ?? "");
220
+ const multiple = options?.multiple !== false;
221
+ const selectOptions = optionsForType(store, targetType);
222
+ if (multiple) {
223
+ return /* @__PURE__ */ jsx(
224
+ FormMultiSelect,
225
+ {
226
+ label: label || false,
227
+ options: selectOptions,
228
+ value: toStringArray(value),
229
+ onChange: (v) => onChange(v),
230
+ placeholder: placeholder ?? `Select ${targetType}...`
231
+ }
232
+ );
233
+ }
234
+ const single = toStringArray(value)[0] ?? null;
235
+ return /* @__PURE__ */ jsx(
236
+ FormSelect,
237
+ {
238
+ label: label || false,
239
+ options: selectOptions,
240
+ value: single,
241
+ onChange: (v) => onChange(v ?? ""),
242
+ placeholder: placeholder ?? `Select ${targetType}...`
243
+ }
244
+ );
245
+ };
246
+ }
247
+
248
+ // src/pages/PlayGraph.tsx
249
+ import {
250
+ Button,
251
+ Alert,
252
+ AlertDescription,
253
+ Tabs,
254
+ TabsContent,
255
+ TabsList,
256
+ TabsTrigger,
257
+ ResizeHandle
258
+ } from "@petrarca/sonnet-ui";
259
+ import { FormSelect as FormSelect2 } from "@petrarca/sonnet-forms";
260
+ import { Play, RotateCcw } from "lucide-react";
261
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
262
+ var TECH_NODES = [
263
+ {
264
+ id_: 1,
265
+ label_: "Product",
266
+ properties_: { key: "product-a", name: "Product A" }
267
+ },
268
+ {
269
+ id_: 2,
270
+ label_: "Product",
271
+ properties_: { key: "product-b", name: "Product B" }
272
+ },
273
+ {
274
+ id_: 3,
275
+ label_: "TechnologyStack",
276
+ properties_: { key: "product-a-stack", name: "Product A stack" }
277
+ },
278
+ {
279
+ id_: 4,
280
+ label_: "TechnologyStack",
281
+ properties_: { key: "product-b-stack", name: "Product B stack" }
282
+ },
283
+ {
284
+ id_: 5,
285
+ label_: "Technology",
286
+ properties_: { key: "react", name: "React" }
287
+ },
288
+ {
289
+ id_: 6,
290
+ label_: "Technology",
291
+ properties_: { key: "postgresql", name: "PostgreSQL" }
292
+ },
293
+ {
294
+ id_: 7,
295
+ label_: "Technology",
296
+ properties_: { key: "kafka", name: "Kafka" }
297
+ },
298
+ {
299
+ id_: 8,
300
+ label_: "Technology",
301
+ properties_: { key: "go", name: "Go" }
302
+ },
303
+ {
304
+ id_: 9,
305
+ label_: "TechnologyCategory",
306
+ properties_: { key: "frontend-framework", name: "Frontend Framework" }
307
+ },
308
+ {
309
+ id_: 10,
310
+ label_: "TechnologyCategory",
311
+ properties_: { key: "database", name: "Database" }
312
+ },
313
+ {
314
+ id_: 11,
315
+ label_: "TechnologyCategory",
316
+ properties_: { key: "messaging", name: "Messaging" }
317
+ },
318
+ {
319
+ id_: 12,
320
+ label_: "TechnologyCategory",
321
+ properties_: { key: "language", name: "Language" }
322
+ }
323
+ ];
324
+ var TECH_EDGES = [
325
+ // Two products, each with its own stack; both stacks share PostgreSQL.
326
+ {
327
+ id_: 101,
328
+ label_: "HAS_TECH_STACK",
329
+ start_id_: 1,
330
+ end_id_: 3,
331
+ properties_: {}
332
+ },
333
+ {
334
+ id_: 102,
335
+ label_: "HAS_TECH_STACK",
336
+ start_id_: 2,
337
+ end_id_: 4,
338
+ properties_: {}
339
+ },
340
+ {
341
+ id_: 103,
342
+ label_: "INTEGRATES_WITH",
343
+ start_id_: 1,
344
+ end_id_: 2,
345
+ properties_: {}
346
+ },
347
+ // Product A stack: React, PostgreSQL, Kafka.
348
+ {
349
+ id_: 104,
350
+ label_: "USES_TECHNOLOGY",
351
+ start_id_: 3,
352
+ end_id_: 5,
353
+ properties_: {}
354
+ },
355
+ {
356
+ id_: 105,
357
+ label_: "USES_TECHNOLOGY",
358
+ start_id_: 3,
359
+ end_id_: 6,
360
+ properties_: {}
361
+ },
362
+ {
363
+ id_: 106,
364
+ label_: "USES_TECHNOLOGY",
365
+ start_id_: 3,
366
+ end_id_: 7,
367
+ properties_: {}
368
+ },
369
+ // Product B stack: Go + the shared PostgreSQL.
370
+ {
371
+ id_: 107,
372
+ label_: "USES_TECHNOLOGY",
373
+ start_id_: 4,
374
+ end_id_: 6,
375
+ properties_: {}
376
+ },
377
+ {
378
+ id_: 108,
379
+ label_: "USES_TECHNOLOGY",
380
+ start_id_: 4,
381
+ end_id_: 8,
382
+ properties_: {}
383
+ },
384
+ // Technology -> category.
385
+ { id_: 109, label_: "BELONGS_TO", start_id_: 5, end_id_: 9, properties_: {} },
386
+ {
387
+ id_: 110,
388
+ label_: "BELONGS_TO",
389
+ start_id_: 6,
390
+ end_id_: 10,
391
+ properties_: {}
392
+ },
393
+ {
394
+ id_: 111,
395
+ label_: "BELONGS_TO",
396
+ start_id_: 7,
397
+ end_id_: 11,
398
+ properties_: {}
399
+ },
400
+ {
401
+ id_: 112,
402
+ label_: "BELONGS_TO",
403
+ start_id_: 8,
404
+ end_id_: 12,
405
+ properties_: {}
406
+ }
407
+ ];
408
+ function createPlaygroundStores() {
409
+ const graphFilter = createGraphFilterStore();
410
+ const graphData = createGraphDataStore();
411
+ const selection = createSelectionStore();
412
+ const graphFocus = createGraphFocusStore();
413
+ const graphExploration = createGraphExplorationStore({
414
+ graphData,
415
+ selection,
416
+ graphFocus,
417
+ graphFilter,
418
+ // Static data -- no server expansion available in the playground
419
+ expandNode: async () => ({ nodes: [], edges: [], has_more: false }),
420
+ getNodes: async () => []
421
+ });
422
+ return { graphFilter, graphData, graphExploration, selection };
423
+ }
424
+ var PLAYGROUND_STORES = createPlaygroundStores();
425
+ var EMPTY_DATA = { nodes: [], edges: [] };
426
+ function serializeGraphData(data) {
427
+ return JSON.stringify(data, null, 2);
428
+ }
429
+ function validateNode(n, i) {
430
+ if (!n || typeof n !== "object") return `nodes[${i}] must be an object`;
431
+ const node = n;
432
+ if (node.id_ === void 0) return `nodes[${i}] is missing id_`;
433
+ if (typeof node.label_ !== "string") return `nodes[${i}] is missing label_`;
434
+ return null;
435
+ }
436
+ function validateEdge(e, i) {
437
+ if (!e || typeof e !== "object") return `edges[${i}] must be an object`;
438
+ const edge = e;
439
+ if (edge.id_ === void 0) return `edges[${i}] is missing id_`;
440
+ if (typeof edge.label_ !== "string") return `edges[${i}] is missing label_`;
441
+ if (edge.start_id_ === void 0) return `edges[${i}] is missing start_id_`;
442
+ if (edge.end_id_ === void 0) return `edges[${i}] is missing end_id_`;
443
+ return null;
444
+ }
445
+ function validateGraphInput(value) {
446
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
447
+ return "JSON must be an object with nodes and edges arrays";
448
+ }
449
+ const obj = value;
450
+ if (!Array.isArray(obj.nodes)) return '"nodes" must be an array';
451
+ if (!Array.isArray(obj.edges)) return '"edges" must be an array';
452
+ for (const [i, n] of obj.nodes.entries()) {
453
+ const err = validateNode(n, i);
454
+ if (err) return err;
455
+ }
456
+ for (const [i, e] of obj.edges.entries()) {
457
+ const err = validateEdge(e, i);
458
+ if (err) return err;
459
+ }
460
+ return null;
461
+ }
462
+ function loadIntoStore(stores, data) {
463
+ const labelCounts = {};
464
+ for (const n of data.nodes) {
465
+ labelCounts[n.label_] = (labelCounts[n.label_] ?? 0) + 1;
466
+ }
467
+ const palette = buildColorPalette(labelCounts);
468
+ const nodes = enrichNodes(data.nodes, palette);
469
+ const edges = enrichEdges(data.edges, palette);
470
+ stores.graphFilter.getState().resetFilters();
471
+ stores.graphData.getState().setQueryData(nodes, edges, {
472
+ nodes: nodes.length,
473
+ edges: edges.length
474
+ });
475
+ stores.graphFilter.getState().notifyDataChange();
476
+ }
477
+ var LIVE_STORE = { nodes: [], edges: [] };
478
+ function setLiveData(stores, data) {
479
+ LIVE_STORE.nodes = data.nodes.map((n) => ({ ...n }));
480
+ LIVE_STORE.edges = data.edges.map((e) => ({ ...e }));
481
+ loadIntoStore(stores, LIVE_STORE);
482
+ }
483
+ setLiveData(PLAYGROUND_STORES, { nodes: TECH_NODES, edges: TECH_EDGES });
484
+ var TECH_MODEL = [
485
+ {
486
+ title: "Product",
487
+ type: "object",
488
+ properties: { key: { type: "string" }, name: { type: "string" } },
489
+ required: ["key", "name"],
490
+ "x-graph": {
491
+ type: "NODE",
492
+ label: "Product",
493
+ relations: [
494
+ {
495
+ rel_type_name: "HAS_TECH_STACK",
496
+ to_type_name: "TechnologyStack",
497
+ cardinality: "ONE_TO_ONE",
498
+ form_field: true
499
+ },
500
+ {
501
+ // Fictitious second Product -> TechnologyStack relation: a product
502
+ // may also provision a stack it does not own (one-to-many).
503
+ rel_type_name: "PROVIDES_STACK_FOR",
504
+ to_type_name: "TechnologyStack",
505
+ cardinality: "ONE_TO_MANY",
506
+ form_field: true
507
+ },
508
+ {
509
+ rel_type_name: "INTEGRATES_WITH",
510
+ to_type_name: "Product",
511
+ cardinality: "ONE_TO_MANY",
512
+ form_field: true
513
+ }
514
+ ]
515
+ }
516
+ },
517
+ {
518
+ title: "TechnologyStack",
519
+ type: "object",
520
+ properties: { key: { type: "string" }, name: { type: "string" } },
521
+ "x-graph": {
522
+ type: "NODE",
523
+ label: "TechnologyStack",
524
+ relations: [
525
+ {
526
+ rel_type_name: "USES_TECHNOLOGY",
527
+ to_type_name: "Technology",
528
+ cardinality: "ONE_TO_MANY",
529
+ form_field: true
530
+ }
531
+ ]
532
+ }
533
+ },
534
+ {
535
+ title: "Technology",
536
+ type: "object",
537
+ properties: { key: { type: "string" }, name: { type: "string" } },
538
+ required: ["key", "name"],
539
+ "x-graph": {
540
+ type: "NODE",
541
+ label: "Technology",
542
+ relations: [
543
+ {
544
+ rel_type_name: "BELONGS_TO",
545
+ to_type_name: "TechnologyCategory",
546
+ cardinality: "ONE_TO_ONE",
547
+ form_field: true
548
+ }
549
+ ]
550
+ }
551
+ },
552
+ {
553
+ title: "TechnologyCategory",
554
+ type: "object",
555
+ properties: { key: { type: "string" }, name: { type: "string" } },
556
+ required: ["key", "name"],
557
+ "x-graph": { type: "NODE", label: "TechnologyCategory" }
558
+ }
559
+ ];
560
+ var ORG_MODEL = [
561
+ {
562
+ title: "Network",
563
+ type: "object",
564
+ "x-graph": { type: "NODE", label: "Network" }
565
+ },
566
+ {
567
+ title: "Hospital",
568
+ type: "object",
569
+ "x-graph": {
570
+ type: "NODE",
571
+ label: "Hospital",
572
+ relations: [
573
+ {
574
+ rel_type_name: "partOf",
575
+ to_type_name: "Network",
576
+ cardinality: "ONE_TO_ONE"
577
+ }
578
+ ]
579
+ }
580
+ },
581
+ {
582
+ title: "Department",
583
+ type: "object",
584
+ "x-graph": {
585
+ type: "NODE",
586
+ label: "Department",
587
+ relations: [
588
+ {
589
+ rel_type_name: "partOf",
590
+ to_type_name: "Hospital",
591
+ cardinality: "ONE_TO_ONE"
592
+ }
593
+ ]
594
+ }
595
+ },
596
+ {
597
+ title: "Practice",
598
+ type: "object",
599
+ "x-graph": {
600
+ type: "NODE",
601
+ label: "Practice",
602
+ relations: [
603
+ {
604
+ rel_type_name: "partOf",
605
+ to_type_name: "Network",
606
+ cardinality: "ONE_TO_ONE"
607
+ },
608
+ {
609
+ rel_type_name: "affiliation",
610
+ to_type_name: "Hospital",
611
+ cardinality: "ONE_TO_MANY"
612
+ }
613
+ ]
614
+ }
615
+ },
616
+ // Pre-defined relation-type catalog. With edgeTypes "catalog", relations may
617
+ // only use these types; no new edge types can be created.
618
+ {
619
+ title: "partOf",
620
+ type: "object",
621
+ "x-graph": { type: "EDGE", label: "partOf" }
622
+ },
623
+ {
624
+ title: "affiliation",
625
+ type: "object",
626
+ "x-graph": { type: "EDGE", label: "affiliation" }
627
+ }
628
+ ];
629
+ var ORG_NODES = [
630
+ {
631
+ id_: 1,
632
+ label_: "Network",
633
+ properties_: { name: "Northwind Care Network" }
634
+ },
635
+ { id_: 2, label_: "Hospital", properties_: { name: "Riverside General" } },
636
+ { id_: 3, label_: "Hospital", properties_: { name: "Lakeside Clinic" } },
637
+ { id_: 4, label_: "Department", properties_: { name: "Cardiology" } },
638
+ { id_: 5, label_: "Department", properties_: { name: "Radiology" } },
639
+ { id_: 6, label_: "Practice", properties_: { name: "Maple Street GP" } }
640
+ ];
641
+ var ORG_EDGES = [
642
+ // partOf: child -> parent (containment).
643
+ { id_: 201, label_: "partOf", start_id_: 2, end_id_: 1, properties_: {} },
644
+ { id_: 202, label_: "partOf", start_id_: 3, end_id_: 1, properties_: {} },
645
+ { id_: 203, label_: "partOf", start_id_: 4, end_id_: 2, properties_: {} },
646
+ { id_: 204, label_: "partOf", start_id_: 5, end_id_: 2, properties_: {} },
647
+ // affiliation: a practice affiliated with a hospital (non-hierarchical).
648
+ {
649
+ id_: 205,
650
+ label_: "affiliation",
651
+ start_id_: 6,
652
+ end_id_: 2,
653
+ properties_: {}
654
+ }
655
+ ];
656
+ var DATASETS = {
657
+ empty: {
658
+ label: "Empty",
659
+ data: EMPTY_DATA,
660
+ model: [],
661
+ capabilities: { readOnly: false, capabilities: {} },
662
+ // No model to drive forms, so editing stays off.
663
+ editCapabilities: {}
664
+ },
665
+ techStack: {
666
+ label: "Technology Stack",
667
+ data: { nodes: TECH_NODES, edges: TECH_EDGES },
668
+ model: TECH_MODEL,
669
+ capabilities: { readOnly: false, capabilities: {} },
670
+ // Full instance editing: create / edit / delete nodes and edges.
671
+ editCapabilities: {
672
+ createNode: "edit",
673
+ updateNode: "edit",
674
+ deleteNode: "edit",
675
+ createEdge: "edit",
676
+ updateEdge: "edit",
677
+ deleteEdge: "edit"
678
+ }
679
+ },
680
+ organization: {
681
+ label: "Organization model",
682
+ data: { nodes: ORG_NODES, edges: ORG_EDGES },
683
+ model: ORG_MODEL,
684
+ // Simulates the planned Sestina org editor: fixed edge-type catalog (no new
685
+ // edge types) and no property editing (node/edge properties hidden).
686
+ capabilities: {
687
+ readOnly: false,
688
+ capabilities: {
689
+ edgeTypes: "catalog",
690
+ nodeProperties: "hidden",
691
+ edgeProperties: "hidden"
692
+ }
693
+ },
694
+ // A constrained instance-editing profile: edges may be linked / unlinked
695
+ // (partOf / affiliation), but nodes are not created or deleted here.
696
+ editCapabilities: {
697
+ updateNode: "edit",
698
+ createEdge: "edit",
699
+ deleteEdge: "edit"
700
+ }
701
+ }
702
+ };
703
+ var DATASET_OPTIONS = Object.entries(DATASETS).map(([value, d]) => ({
704
+ value,
705
+ label: d.label
706
+ }));
707
+ var DEFAULT_DATASET = "techStack";
708
+ function useModelState(initialModel) {
709
+ const [modelText, setModelText] = useState(
710
+ () => JSON.stringify(initialModel, null, 2)
711
+ );
712
+ const [model, setModel] = useState(initialModel);
713
+ const [modelError, setModelError] = useState(null);
714
+ const applyModel = useCallback(() => {
715
+ let parsed;
716
+ try {
717
+ parsed = JSON.parse(modelText);
718
+ } catch (e) {
719
+ setModelError(e instanceof Error ? e.message : "Invalid JSON");
720
+ return;
721
+ }
722
+ if (!Array.isArray(parsed)) {
723
+ setModelError("A graph model must be a JSON array of type schemas.");
724
+ return;
725
+ }
726
+ setModelError(null);
727
+ setModel(parsed);
728
+ }, [modelText]);
729
+ const loadModel = useCallback((next) => {
730
+ setModelText(JSON.stringify(next, null, 2));
731
+ setModel(next);
732
+ setModelError(null);
733
+ }, []);
734
+ const updateModel = useCallback((next) => {
735
+ setModel(next);
736
+ setModelText(JSON.stringify(next, null, 2));
737
+ setModelError(null);
738
+ }, []);
739
+ return {
740
+ model,
741
+ modelText,
742
+ modelError,
743
+ setModelText,
744
+ applyModel,
745
+ loadModel,
746
+ updateModel
747
+ };
748
+ }
749
+ function ModelGraphTab({
750
+ model,
751
+ onChange,
752
+ readOnly,
753
+ capabilities
754
+ }) {
755
+ return /* @__PURE__ */ jsx2("div", { className: "h-[640px] rounded-lg border overflow-hidden", children: /* @__PURE__ */ jsx2(
756
+ GraphModelEditor,
757
+ {
758
+ value: model,
759
+ onChange,
760
+ readOnly,
761
+ capabilities,
762
+ widgetOptions: ["entity-select", "coded-value", "textarea"]
763
+ }
764
+ ) });
765
+ }
766
+ function CapabilitiesTab({
767
+ config,
768
+ onApply
769
+ }) {
770
+ const [text, setText] = useState(() => JSON.stringify(config, null, 2));
771
+ const [error, setError] = useState(null);
772
+ const apply = useCallback(() => {
773
+ let parsed;
774
+ try {
775
+ parsed = JSON.parse(text);
776
+ } catch (e) {
777
+ setError(e instanceof Error ? e.message : "Invalid JSON");
778
+ return;
779
+ }
780
+ setError(null);
781
+ const obj = parsed;
782
+ onApply({
783
+ readOnly: Boolean(obj.readOnly),
784
+ capabilities: obj.capabilities ?? {}
785
+ });
786
+ }, [text, onApply]);
787
+ const reset = useCallback(() => {
788
+ const fresh = { readOnly: false, capabilities: {} };
789
+ setText(JSON.stringify(fresh, null, 2));
790
+ setError(null);
791
+ onApply(fresh);
792
+ }, [onApply]);
793
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
794
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
795
+ /* @__PURE__ */ jsxs(Button, { size: "sm", onClick: apply, children: [
796
+ /* @__PURE__ */ jsx2(Play, { className: "h-4 w-4 mr-1" }),
797
+ "Apply"
798
+ ] }),
799
+ /* @__PURE__ */ jsxs(Button, { size: "sm", variant: "outline", onClick: reset, children: [
800
+ /* @__PURE__ */ jsx2(RotateCcw, { className: "h-4 w-4 mr-1" }),
801
+ "Reset"
802
+ ] }),
803
+ /* @__PURE__ */ jsx2("span", { className: "text-xs text-muted-foreground ml-2", children: "Edit, Apply, then open the Model Editor tab and select a node." })
804
+ ] }),
805
+ error && /* @__PURE__ */ jsx2(Alert, { variant: "destructive", children: /* @__PURE__ */ jsx2(AlertDescription, { children: error }) }),
806
+ /* @__PURE__ */ jsx2(
807
+ "textarea",
808
+ {
809
+ value: text,
810
+ onChange: (e) => setText(e.target.value),
811
+ spellCheck: false,
812
+ className: "w-full h-[280px] font-mono text-xs p-3 border rounded-lg bg-muted/30 resize-y focus:outline-none focus:ring-2 focus:ring-ring"
813
+ }
814
+ ),
815
+ /* @__PURE__ */ jsxs("div", { children: [
816
+ /* @__PURE__ */ jsx2("p", { className: "text-xs font-medium text-muted-foreground mb-1", children: "Effective config (applied to GraphModelEditor)" }),
817
+ /* @__PURE__ */ jsx2("pre", { className: "rounded-md bg-muted px-3 py-2 text-xs overflow-auto max-h-40", children: JSON.stringify(config, null, 2) }),
818
+ /* @__PURE__ */ jsx2("p", { className: "mt-2 text-xs text-muted-foreground", children: "readOnly = lock everything. capabilities: nodeTypes / relations (edit | readonly), edgeTypes (edit = select or create | catalog = select only | readonly), nodeProperties / edgeProperties (edit | readonly | hidden), showEdgeTypes (bool, default false starts edge-type nodes deselected; true shows them). More fields are added as the editor grows." })
819
+ ] })
820
+ ] });
821
+ }
822
+ function ModelJsonTab({
823
+ state,
824
+ onReset
825
+ }) {
826
+ const { modelText, modelError, setModelText, applyModel } = state;
827
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
828
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
829
+ /* @__PURE__ */ jsxs(Button, { size: "sm", onClick: applyModel, children: [
830
+ /* @__PURE__ */ jsx2(Play, { className: "h-4 w-4 mr-1" }),
831
+ "Apply"
832
+ ] }),
833
+ /* @__PURE__ */ jsxs(Button, { size: "sm", variant: "outline", onClick: onReset, children: [
834
+ /* @__PURE__ */ jsx2(RotateCcw, { className: "h-4 w-4 mr-1" }),
835
+ "Reset"
836
+ ] }),
837
+ /* @__PURE__ */ jsx2("span", { className: "text-xs text-muted-foreground ml-2", children: "Edit the model, Apply, then switch to the Model tab" })
838
+ ] }),
839
+ modelError && /* @__PURE__ */ jsx2(Alert, { variant: "destructive", children: /* @__PURE__ */ jsx2(AlertDescription, { children: modelError }) }),
840
+ /* @__PURE__ */ jsx2(
841
+ "textarea",
842
+ {
843
+ value: modelText,
844
+ onChange: (e) => setModelText(e.target.value),
845
+ spellCheck: false,
846
+ className: "w-full h-[600px] font-mono text-xs p-3 border rounded-lg bg-muted/30 resize-y focus:outline-none focus:ring-2 focus:ring-ring"
847
+ }
848
+ )
849
+ ] });
850
+ }
851
+ function PlayGraph() {
852
+ const [datasetKey, setDatasetKey] = useState(DEFAULT_DATASET);
853
+ const [jsonText, setJsonText] = useState(
854
+ () => serializeGraphData(DATASETS[DEFAULT_DATASET].data)
855
+ );
856
+ const [parseError, setParseError] = useState(null);
857
+ const modelState = useModelState(DATASETS[DEFAULT_DATASET].model);
858
+ const { loadModel } = modelState;
859
+ const [capsConfig, setCapsConfig] = useState(
860
+ DATASETS[DEFAULT_DATASET].capabilities
861
+ );
862
+ const editPort = useMemo(
863
+ () => createInMemoryPort(
864
+ LIVE_STORE,
865
+ () => loadIntoStore(PLAYGROUND_STORES, LIVE_STORE)
866
+ ),
867
+ []
868
+ );
869
+ const editSchemaProvider = useMemo(
870
+ () => createModelSchemaProvider(modelState.model),
871
+ [modelState.model]
872
+ );
873
+ const handleMutated = useCallback(() => {
874
+ setJsonText(serializeGraphData(LIVE_STORE));
875
+ }, []);
876
+ const editWidgets = useMemo(
877
+ () => ({
878
+ ...DEFAULT_WIDGETS,
879
+ "entity-select": createRelationWidget(LIVE_STORE)
880
+ }),
881
+ []
882
+ );
883
+ const loadDataset = useCallback(
884
+ (key) => {
885
+ const ds = DATASETS[key];
886
+ if (!ds) return;
887
+ setDatasetKey(key);
888
+ setJsonText(serializeGraphData(ds.data));
889
+ setParseError(null);
890
+ setLiveData(PLAYGROUND_STORES, ds.data);
891
+ loadModel(ds.model);
892
+ setCapsConfig(ds.capabilities);
893
+ },
894
+ [loadModel]
895
+ );
896
+ const [panelWidth, setPanelWidth] = useState(300);
897
+ const rowRef = useRef(null);
898
+ const startResize = useCallback((e) => {
899
+ e.preventDefault();
900
+ const target = e.currentTarget;
901
+ target.setPointerCapture(e.pointerId);
902
+ const move = (ev) => {
903
+ const right = rowRef.current?.getBoundingClientRect().right ?? 0;
904
+ setPanelWidth(Math.min(560, Math.max(200, right - ev.clientX)));
905
+ };
906
+ const up = () => {
907
+ window.removeEventListener("pointermove", move);
908
+ window.removeEventListener("pointerup", up);
909
+ };
910
+ window.addEventListener("pointermove", move);
911
+ window.addEventListener("pointerup", up);
912
+ }, []);
913
+ const handleApply = useCallback(() => {
914
+ let parsed;
915
+ try {
916
+ parsed = JSON.parse(jsonText);
917
+ } catch (e) {
918
+ setParseError(e instanceof Error ? e.message : "Invalid JSON");
919
+ return;
920
+ }
921
+ const validationError = validateGraphInput(parsed);
922
+ if (validationError) {
923
+ setParseError(validationError);
924
+ return;
925
+ }
926
+ setParseError(null);
927
+ setLiveData(PLAYGROUND_STORES, parsed);
928
+ }, [jsonText]);
929
+ const handleReset = useCallback(() => {
930
+ const data = DATASETS[datasetKey].data;
931
+ setJsonText(serializeGraphData(data));
932
+ setParseError(null);
933
+ setLiveData(PLAYGROUND_STORES, data);
934
+ }, [datasetKey]);
935
+ const handleResetModel = useCallback(() => {
936
+ loadModel(DATASETS[datasetKey].model);
937
+ }, [datasetKey, loadModel]);
938
+ const handleKeyDown = useCallback(
939
+ (e) => {
940
+ if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
941
+ e.preventDefault();
942
+ handleApply();
943
+ }
944
+ },
945
+ [handleApply]
946
+ );
947
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-6", children: [
948
+ /* @__PURE__ */ jsxs("div", { children: [
949
+ /* @__PURE__ */ jsx2("h1", { children: "Graph Visualization" }),
950
+ /* @__PURE__ */ jsx2("p", { className: "text-sm text-muted-foreground mt-1", children: "Interactive property graph with dual renderer support. Edit the JSON data and apply to re-render. The Model tab renders a graph model (metamodel) with GraphModelEditor." })
951
+ ] }),
952
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
953
+ /* @__PURE__ */ jsx2(
954
+ FormSelect2,
955
+ {
956
+ label: "Dataset",
957
+ options: DATASET_OPTIONS,
958
+ value: datasetKey,
959
+ onChange: (v) => v && loadDataset(v),
960
+ clearable: false,
961
+ wrapperClassName: "w-64"
962
+ }
963
+ ),
964
+ /* @__PURE__ */ jsx2("span", { className: "text-xs text-muted-foreground mt-5", children: "A dataset bundles graph data + model + capability profile." })
965
+ ] }),
966
+ /* @__PURE__ */ jsx2(GraphStoresContext.Provider, { value: PLAYGROUND_STORES, children: /* @__PURE__ */ jsxs(Tabs, { defaultValue: "graph", className: "space-y-3", children: [
967
+ /* @__PURE__ */ jsxs(TabsList, { children: [
968
+ /* @__PURE__ */ jsx2(TabsTrigger, { value: "graph", children: "Graph" }),
969
+ /* @__PURE__ */ jsx2(TabsTrigger, { value: "data", children: "Data JSON" }),
970
+ /* @__PURE__ */ jsx2(TabsTrigger, { value: "model", children: "Model Editor" }),
971
+ /* @__PURE__ */ jsx2(TabsTrigger, { value: "model-json", children: "Model JSON" }),
972
+ /* @__PURE__ */ jsx2(TabsTrigger, { value: "model-capabilities", children: "Model Editor Capabilities" })
973
+ ] }),
974
+ /* @__PURE__ */ jsx2(TabsContent, { value: "graph", className: "mt-0", children: /* @__PURE__ */ jsx2(GraphRendererProvider, { defaultRenderer: "cytoscape", children: /* @__PURE__ */ jsxs("div", { ref: rowRef, className: "flex overflow-hidden h-[600px]", children: [
975
+ /* @__PURE__ */ jsx2("div", { className: "flex-1 min-w-0", children: /* @__PURE__ */ jsx2(GraphVisualizer, {}) }),
976
+ /* @__PURE__ */ jsx2(
977
+ ResizeHandle,
978
+ {
979
+ orientation: "vertical",
980
+ "aria-valuenow": panelWidth,
981
+ "aria-valuemin": 200,
982
+ "aria-valuemax": 560,
983
+ onPointerDown: startResize,
984
+ onDoubleClick: () => setPanelWidth(300)
985
+ }
986
+ ),
987
+ /* @__PURE__ */ jsxs(
988
+ "div",
989
+ {
990
+ style: { width: panelWidth },
991
+ className: "flex-shrink-0 flex flex-col min-h-0",
992
+ children: [
993
+ /* @__PURE__ */ jsx2("div", { className: "flex-1 min-h-0 overflow-hidden", children: /* @__PURE__ */ jsx2(
994
+ ActionPanel,
995
+ {
996
+ actions: /* @__PURE__ */ jsx2(
997
+ GraphEditToolbar,
998
+ {
999
+ port: editPort,
1000
+ schemaProvider: editSchemaProvider,
1001
+ capabilities: DATASETS[datasetKey].editCapabilities,
1002
+ widgets: editWidgets,
1003
+ onMutated: handleMutated
1004
+ }
1005
+ )
1006
+ }
1007
+ ) }),
1008
+ /* @__PURE__ */ jsxs("div", { className: "shrink-0 px-4 pb-4 pt-2", children: [
1009
+ /* @__PURE__ */ jsx2("p", { className: "text-xs font-medium text-muted-foreground uppercase tracking-wide mb-2", children: "Accessible list (demo)" }),
1010
+ /* @__PURE__ */ jsx2(GraphA11yList, { visible: true, maxHeight: 160 })
1011
+ ] })
1012
+ ]
1013
+ }
1014
+ )
1015
+ ] }) }) }),
1016
+ /* @__PURE__ */ jsx2(TabsContent, { value: "data", className: "mt-0", children: /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
1017
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
1018
+ /* @__PURE__ */ jsxs(Button, { size: "sm", onClick: handleApply, children: [
1019
+ /* @__PURE__ */ jsx2(Play, { className: "h-4 w-4 mr-1" }),
1020
+ "Apply"
1021
+ ] }),
1022
+ /* @__PURE__ */ jsxs(Button, { size: "sm", variant: "outline", onClick: handleReset, children: [
1023
+ /* @__PURE__ */ jsx2(RotateCcw, { className: "h-4 w-4 mr-1" }),
1024
+ "Reset"
1025
+ ] }),
1026
+ /* @__PURE__ */ jsx2("span", { className: "text-xs text-muted-foreground ml-2", children: "Cmd+Enter to apply" })
1027
+ ] }),
1028
+ parseError && /* @__PURE__ */ jsx2(Alert, { variant: "destructive", children: /* @__PURE__ */ jsx2(AlertDescription, { children: parseError }) }),
1029
+ /* @__PURE__ */ jsx2(
1030
+ "textarea",
1031
+ {
1032
+ value: jsonText,
1033
+ onChange: (e) => setJsonText(e.target.value),
1034
+ onKeyDown: handleKeyDown,
1035
+ spellCheck: false,
1036
+ className: "w-full h-[600px] font-mono text-xs p-3 border rounded-lg bg-muted/30 resize-y focus:outline-none focus:ring-2 focus:ring-ring"
1037
+ }
1038
+ )
1039
+ ] }) }),
1040
+ /* @__PURE__ */ jsx2(TabsContent, { value: "model", className: "mt-0", children: /* @__PURE__ */ jsx2(
1041
+ ModelGraphTab,
1042
+ {
1043
+ model: modelState.model,
1044
+ onChange: modelState.updateModel,
1045
+ readOnly: capsConfig.readOnly,
1046
+ capabilities: capsConfig.capabilities
1047
+ }
1048
+ ) }),
1049
+ /* @__PURE__ */ jsx2(TabsContent, { value: "model-json", className: "mt-0", children: /* @__PURE__ */ jsx2(ModelJsonTab, { state: modelState, onReset: handleResetModel }) }),
1050
+ /* @__PURE__ */ jsx2(TabsContent, { value: "model-capabilities", className: "mt-0", children: /* @__PURE__ */ jsx2(CapabilitiesTab, { config: capsConfig, onApply: setCapsConfig }) })
1051
+ ] }) })
1052
+ ] });
1053
+ }
1054
+
1055
+ export {
1056
+ PlayGraph
1057
+ };
1058
+ //# sourceMappingURL=chunk-C2P3UQMZ.js.map