@macrostrat/feedback-components 1.0.1 → 1.1.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 (105) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/esm/feedback-components.3b3a5357.js +137 -0
  3. package/dist/esm/feedback-components.3b3a5357.js.map +1 -0
  4. package/dist/esm/feedback-components.46a7a347.js +269 -0
  5. package/dist/esm/feedback-components.46a7a347.js.map +1 -0
  6. package/dist/esm/{node.28634e40.js → feedback-components.5509fab3.js} +11 -6
  7. package/dist/esm/feedback-components.5509fab3.js.map +1 -0
  8. package/dist/esm/feedback-components.586103e8.js +578 -0
  9. package/dist/esm/feedback-components.586103e8.js.map +1 -0
  10. package/dist/esm/{extractions.65bb73cc.js → feedback-components.5df2a926.js} +46 -18
  11. package/dist/esm/feedback-components.5df2a926.js.map +1 -0
  12. package/dist/esm/{main.module.cd706d67.js → feedback-components.6d32ee91.js} +1 -1
  13. package/dist/esm/{main.module.cd706d67.js.map → feedback-components.6d32ee91.js.map} +1 -1
  14. package/dist/esm/feedback-components.95dbe7d7.js +82 -0
  15. package/dist/esm/feedback-components.95dbe7d7.js.map +1 -0
  16. package/dist/esm/{type-selector.6e8952d6.js → feedback-components.ad9f284e.js} +6 -5
  17. package/dist/esm/feedback-components.ad9f284e.js.map +1 -0
  18. package/dist/esm/{main.module.8d366b6e.css → feedback-components.bf93773c.css} +1 -1
  19. package/dist/esm/{main.module.8d366b6e.css.map → feedback-components.bf93773c.css.map} +1 -1
  20. package/dist/esm/{main.module.2f2972c8.css → feedback-components.e273ed5b.css} +1 -1
  21. package/dist/esm/{main.module.2f2972c8.css.map → feedback-components.e273ed5b.css.map} +1 -1
  22. package/dist/esm/{main.module.d2fbdf09.js → feedback-components.f9850d85.js} +1 -1
  23. package/dist/esm/{main.module.d2fbdf09.js.map → feedback-components.f9850d85.js.map} +1 -1
  24. package/dist/esm/{edit-state.c39d8466.js → feedback-components.fa1d3641.js} +125 -7
  25. package/dist/esm/feedback-components.fa1d3641.js.map +1 -0
  26. package/dist/esm/feedback-components.fb60c70d.css +180 -0
  27. package/dist/esm/feedback-components.fb60c70d.css.map +1 -0
  28. package/dist/esm/index.d.ts +10 -64
  29. package/dist/esm/index.d.ts.map +1 -1
  30. package/dist/esm/index.js +2 -2
  31. package/dist/node/feedback-components.2f391fa4.js +2 -0
  32. package/dist/node/feedback-components.2f391fa4.js.map +1 -0
  33. package/dist/node/feedback-components.561466ac.js +2 -0
  34. package/dist/node/feedback-components.561466ac.js.map +1 -0
  35. package/dist/node/feedback-components.571ee23c.js +2 -0
  36. package/dist/node/feedback-components.571ee23c.js.map +1 -0
  37. package/dist/node/{main.module.ebdf985b.js → feedback-components.794f429b.js} +2 -2
  38. package/dist/node/feedback-components.794f429b.js.map +1 -0
  39. package/dist/node/{main.module.1fdfe813.css → feedback-components.83c21466.css} +1 -1
  40. package/dist/node/feedback-components.83c21466.css.map +1 -0
  41. package/dist/node/feedback-components.8b03e8be.js +2 -0
  42. package/dist/node/feedback-components.8b03e8be.js.map +1 -0
  43. package/dist/node/{main.module.6bc7d51b.css → feedback-components.9eb1d41a.css} +1 -1
  44. package/dist/node/feedback-components.9eb1d41a.css.map +1 -0
  45. package/dist/node/feedback-components.a39f7653.js +2 -0
  46. package/dist/node/feedback-components.a39f7653.js.map +1 -0
  47. package/dist/node/feedback-components.acac789b.js +2 -0
  48. package/dist/node/feedback-components.acac789b.js.map +1 -0
  49. package/dist/node/feedback-components.b7946db4.js +2 -0
  50. package/dist/node/feedback-components.b7946db4.js.map +1 -0
  51. package/dist/node/feedback-components.c459cc27.js +2 -0
  52. package/dist/node/feedback-components.c459cc27.js.map +1 -0
  53. package/dist/node/feedback-components.c88cb37f.css +2 -0
  54. package/dist/node/feedback-components.c88cb37f.css.map +1 -0
  55. package/dist/node/feedback-components.ec54a1e7.js +2 -0
  56. package/dist/node/feedback-components.ec54a1e7.js.map +1 -0
  57. package/dist/node/index.js +1 -1
  58. package/dist/node/index.js.map +1 -1
  59. package/package.json +7 -6
  60. package/src/extractions/index.ts +76 -21
  61. package/src/extractions/types.ts +6 -1
  62. package/src/feedback/edit-state.ts +184 -16
  63. package/src/feedback/feedback.module.sass +121 -9
  64. package/src/feedback/graph.ts +90 -32
  65. package/src/feedback/index.ts +553 -146
  66. package/src/feedback/node.ts +7 -1
  67. package/src/feedback/text-visualizer.ts +286 -49
  68. package/src/feedback/type-selector/index.ts +4 -2
  69. package/dist/esm/edit-state.c39d8466.js.map +0 -1
  70. package/dist/esm/extractions.65bb73cc.js.map +0 -1
  71. package/dist/esm/feedback.5c86878e.js +0 -252
  72. package/dist/esm/feedback.5c86878e.js.map +0 -1
  73. package/dist/esm/feedback.module.55921afe.css +0 -44
  74. package/dist/esm/feedback.module.55921afe.css.map +0 -1
  75. package/dist/esm/feedback.module.765b1e58.js +0 -28
  76. package/dist/esm/feedback.module.765b1e58.js.map +0 -1
  77. package/dist/esm/graph.f4f65d79.js +0 -83
  78. package/dist/esm/graph.f4f65d79.js.map +0 -1
  79. package/dist/esm/node.28634e40.js.map +0 -1
  80. package/dist/esm/text-visualizer.198e27ff.js +0 -101
  81. package/dist/esm/text-visualizer.198e27ff.js.map +0 -1
  82. package/dist/esm/type-selector.6e8952d6.js.map +0 -1
  83. package/dist/node/edit-state.f50ca728.js +0 -2
  84. package/dist/node/edit-state.f50ca728.js.map +0 -1
  85. package/dist/node/extractions.e6ea2eb9.js +0 -2
  86. package/dist/node/extractions.e6ea2eb9.js.map +0 -1
  87. package/dist/node/feedback.8d3d1219.js +0 -2
  88. package/dist/node/feedback.8d3d1219.js.map +0 -1
  89. package/dist/node/feedback.module.a8744203.js +0 -2
  90. package/dist/node/feedback.module.a8744203.js.map +0 -1
  91. package/dist/node/feedback.module.c4eab97d.css +0 -2
  92. package/dist/node/feedback.module.c4eab97d.css.map +0 -1
  93. package/dist/node/graph.ca5b649f.js +0 -2
  94. package/dist/node/graph.ca5b649f.js.map +0 -1
  95. package/dist/node/main.module.1857be22.js +0 -2
  96. package/dist/node/main.module.1857be22.js.map +0 -1
  97. package/dist/node/main.module.1fdfe813.css.map +0 -1
  98. package/dist/node/main.module.6bc7d51b.css.map +0 -1
  99. package/dist/node/main.module.ebdf985b.js.map +0 -1
  100. package/dist/node/node.33108ccc.js +0 -2
  101. package/dist/node/node.33108ccc.js.map +0 -1
  102. package/dist/node/text-visualizer.1e770afa.js +0 -2
  103. package/dist/node/text-visualizer.1e770afa.js.map +0 -1
  104. package/dist/node/type-selector.0035ef7d.js +0 -2
  105. package/dist/node/type-selector.0035ef7d.js.map +0 -1
@@ -6,25 +6,37 @@ import Node from "./node";
6
6
  import { FeedbackText } from "./text-visualizer";
7
7
  import type { InternalEntity, TreeData } from "./types";
8
8
  import type { Entity } from "../extractions";
9
- import { ModelInfo } from "../extractions";
9
+ import { getTagStyle, ModelInfo } from "../extractions";
10
10
  import {
11
11
  TreeDispatchContext,
12
12
  treeToGraph,
13
13
  useUpdatableTree,
14
14
  ViewMode,
15
15
  } from "./edit-state";
16
- import { useCallback, useEffect, useRef } from "react";
17
- import { ButtonGroup, Card, SegmentedControl } from "@blueprintjs/core";
16
+ import { useCallback, useEffect, useRef, useState } from "react";
17
+ import {
18
+ ButtonGroup,
19
+ Card,
20
+ SegmentedControl,
21
+ Icon,
22
+ Popover,
23
+ Divider,
24
+ Overlay2,
25
+ } from "@blueprintjs/core";
18
26
  import { OmniboxSelector } from "./type-selector";
19
27
  import {
20
28
  CancelButton,
21
29
  DataField,
30
+ ErrorBoundary,
22
31
  FlexBox,
23
32
  FlexRow,
24
33
  SaveButton,
25
34
  } from "@macrostrat/ui-components";
26
35
  import useElementDimensions from "use-element-dimensions";
27
36
  import { GraphView } from "./graph";
37
+ import { useInDarkMode } from "@macrostrat/ui-components";
38
+ import { asChromaColor } from "@macrostrat/color-utils";
39
+ import { ColorPicker } from "@macrostrat/data-sheet";
28
40
 
29
41
  export type { GraphData } from "./edit-state";
30
42
  export { treeToGraph } from "./edit-state";
@@ -47,110 +59,140 @@ export function FeedbackComponent({
47
59
  entityTypes,
48
60
  matchComponent,
49
61
  onSave,
62
+ allowOverlap,
50
63
  }) {
51
64
  // Get the input arguments
52
-
53
65
  const [state, dispatch] = useUpdatableTree(
54
66
  entities.map(processEntity) as any,
55
- entityTypes
67
+ entityTypes,
56
68
  );
57
69
 
58
- const { selectedNodes, tree, selectedEntityType, isSelectingEntityType } =
59
- state;
70
+ const {
71
+ selectedNodes,
72
+ tree,
73
+ selectedEntityType,
74
+ isSelectingEntityType,
75
+ entityTypesMap,
76
+ } = state;
60
77
 
61
78
  const [{ width, height }, ref] = useElementDimensions();
62
79
 
63
- return h(TreeDispatchContext.Provider, { value: dispatch }, [
64
- h(FeedbackText, {
65
- text,
66
- dispatch,
67
- // @ts-ignore
68
- nodes: tree,
69
- selectedNodes,
70
- }),
71
- h(FlexRow, { alignItems: "baseline", justifyContent: "space-between" }, [
72
- h(ModelInfo, { data: model }),
73
- h(SegmentedControl, {
74
- options: [
75
- { label: "Tree", value: "tree" },
76
- { label: "Graph", value: "graph" },
77
- ],
78
- value: state.viewMode,
79
- small: true,
80
- onValueChange(value: ViewMode) {
81
- console.log("Setting view mode", value);
82
- dispatch({ type: "set-view-mode", payload: value });
83
- },
84
- }),
85
- ]),
80
+ return h("div.page-wrapper", [
86
81
  h(
87
- "div.entity-panel",
88
- {
89
- ref,
90
- },
91
- [
92
- h(Card, { className: "control-panel" }, [
93
- h(
94
- ButtonGroup,
95
- {
96
- vertical: true,
97
- fill: true,
98
- minimal: true,
99
- alignText: "left",
100
- },
101
- [
102
- h(
103
- CancelButton,
104
- {
105
- icon: "trash",
106
- disabled: state.initialTree == state.tree,
107
- onClick() {
108
- dispatch({ type: "reset" });
109
- },
82
+ "div.feedback-container",
83
+ h(TreeDispatchContext.Provider, { value: dispatch }, [
84
+ h(
85
+ ErrorBoundary,
86
+ {
87
+ description:
88
+ "An error occurred while rendering the feedback text component.",
89
+ },
90
+ h(FeedbackText, {
91
+ text,
92
+ dispatch,
93
+ // @ts-ignore
94
+ nodes: tree,
95
+ selectedNodes,
96
+ allowOverlap,
97
+ }),
98
+ ),
99
+ h(
100
+ FlexRow,
101
+ { alignItems: "baseline", justifyContent: "space-between" },
102
+ [
103
+ h(ModelInfo, { data: model }),
104
+ h(SegmentedControl, {
105
+ options: [
106
+ { label: "Tree", value: "tree" },
107
+ { label: "Graph", value: "graph" },
108
+ ],
109
+ value: state.viewMode,
110
+ small: true,
111
+ onValueChange(value: ViewMode) {
112
+ console.log("Setting view mode", value);
113
+ dispatch({ type: "set-view-mode", payload: value });
114
+ },
115
+ }),
116
+ ],
117
+ ),
118
+ h(
119
+ "div.entity-panel",
120
+ {
121
+ ref,
122
+ },
123
+ [
124
+ h.if(state.viewMode == "tree")(ManagedSelectionTree, {
125
+ selectedNodes,
126
+ dispatch,
127
+ tree,
128
+ width,
129
+ height,
130
+ matchComponent,
131
+ }),
132
+ h.if(state.viewMode == "graph")(GraphView, {
133
+ tree,
134
+ width,
135
+ height,
136
+ dispatch,
137
+ selectedNodes,
138
+ }),
139
+ ],
140
+ ),
141
+ ]),
142
+ ),
143
+ h(Card, { className: "control-panel" }, [
144
+ h("div.control-content", [
145
+ h(
146
+ ButtonGroup,
147
+ {
148
+ vertical: true,
149
+ fill: true,
150
+ minimal: true,
151
+ alignText: "left",
152
+ },
153
+ [
154
+ h(
155
+ CancelButton,
156
+ {
157
+ icon: "trash",
158
+ disabled: state.initialTree == state.tree,
159
+ onClick() {
160
+ dispatch({ type: "reset" });
110
161
  },
111
- "Reset"
112
- ),
113
- h(
114
- SaveButton,
115
- {
116
- onClick() {
117
- onSave(state.tree);
118
- },
119
- disabled: state.initialTree == state.tree,
162
+ },
163
+ "Reset",
164
+ ),
165
+ h(
166
+ SaveButton,
167
+ {
168
+ onClick() {
169
+ onSave(state.tree);
120
170
  },
121
- "Save"
122
- ),
123
- ]
124
- ),
125
- h(EntityTypeSelector, {
126
- entityTypes,
127
- selected: selectedEntityType,
128
- onChange(payload) {
129
- dispatch({ type: "select-entity-type", payload });
130
- },
131
- isOpen: isSelectingEntityType,
132
- setOpen: (isOpen: boolean) =>
133
- dispatch({
134
- type: "toggle-entity-type-selector",
135
- payload: isOpen,
136
- }),
137
- }),
138
- ]),
139
- h.if(state.viewMode == "tree")(ManagedSelectionTree, {
140
- selectedNodes,
171
+ disabled: state.initialTree == state.tree,
172
+ },
173
+ "Save",
174
+ ),
175
+ ],
176
+ ),
177
+ h(Divider),
178
+ h(EntityTypeSelector, {
179
+ entityTypes: entityTypesMap,
180
+ selected: selectedEntityType,
181
+ onChange(payload) {
182
+ dispatch({ type: "select-entity-type", payload });
183
+ },
141
184
  dispatch,
142
185
  tree,
143
- width,
144
- height,
145
- matchComponent,
146
- }),
147
- h.if(state.viewMode == "graph")(GraphView, {
148
- tree,
149
- width,
150
- height,
186
+ selectedNodes,
187
+ isOpen: isSelectingEntityType,
188
+ setOpen: (isOpen: boolean) =>
189
+ dispatch({
190
+ type: "toggle-entity-type-selector",
191
+ payload: isOpen,
192
+ }),
151
193
  }),
152
- ]
153
- ),
194
+ ]),
195
+ ]),
154
196
  ]);
155
197
  }
156
198
 
@@ -171,27 +213,41 @@ function EntityTypeSelector({
171
213
  isOpen,
172
214
  setOpen,
173
215
  onChange,
216
+ tree,
217
+ dispatch,
218
+ selectedNodes = [],
174
219
  }) {
175
220
  // Show all entity types when selected is null
176
221
  const _selected = selected != null ? selected : undefined;
177
- return h(DataField, { label: "Entity type", inline: true }, [
178
- h(
179
- "code.bp5-code",
180
- {
181
- onClick() {
182
- setOpen((d) => !d);
183
- },
184
- },
185
- selected.name
186
- ),
222
+ const [inputValue, setInputValue] = useState("");
223
+ const types = Array.from(entityTypes.values());
224
+
225
+ const items =
226
+ inputValue !== ""
227
+ ? types.filter((d) =>
228
+ d.name.toLowerCase().includes(inputValue.toLowerCase()),
229
+ )
230
+ : types;
231
+
232
+ return h("div.entity-type-selector", [
233
+ h(TypeList, {
234
+ types: entityTypes,
235
+ selected: _selected,
236
+ dispatch,
237
+ selectedNodes,
238
+ tree,
239
+ }),
187
240
  h(OmniboxSelector, {
188
241
  isOpen,
189
- items: Array.from(entityTypes.values()),
242
+ items,
190
243
  selectedItem: _selected,
191
244
  onSelectItem(item) {
192
245
  setOpen(false);
193
246
  onChange(item);
194
247
  },
248
+ onQueryChange(query) {
249
+ setInputValue(query);
250
+ },
195
251
  onClose() {
196
252
  setOpen(false);
197
253
  },
@@ -199,33 +255,44 @@ function EntityTypeSelector({
199
255
  ]);
200
256
  }
201
257
 
258
+ function countNodes(tree) {
259
+ if (!tree) return 0;
260
+ let count = 0;
261
+
262
+ function recurse(nodes) {
263
+ for (const node of nodes) {
264
+ count++;
265
+ if (node.children && Array.isArray(node.children)) {
266
+ recurse(node.children);
267
+ }
268
+ }
269
+ }
270
+
271
+ recurse(tree);
272
+ return count;
273
+ }
274
+
202
275
  function ManagedSelectionTree(props) {
203
- const {
204
- selectedNodes,
205
- dispatch,
206
- tree,
207
- height,
208
- width,
209
- matchComponent,
210
- ...rest
211
- } = props;
276
+ const { selectedNodes, dispatch, tree, height, width, matchComponent } =
277
+ props;
212
278
 
213
279
  const ref = useRef<TreeApi<TreeData>>();
280
+ // Use a ref to track clicks (won't cause rerender)
281
+ const clickedRef = useRef(false);
214
282
 
215
283
  const _Node = useCallback(
216
284
  (props) => h(Node, { ...props, matchComponent }),
217
- [matchComponent]
285
+ [matchComponent],
218
286
  );
219
287
 
288
+ // Update Tree selection when selectedNodes change
220
289
  useEffect(() => {
221
290
  if (ref.current == null) return;
222
- // Check if selection matches current
291
+
223
292
  const selection = new Set(selectedNodes.map((d) => d.toString()));
224
293
  const currentSelection = ref.current.selectedIds;
225
294
  if (setsAreTheSame(selection, currentSelection)) return;
226
- // If the selection is the same, do nothing
227
295
 
228
- // Set selection
229
296
  ref.current.setSelection({
230
297
  ids: selectedNodes.map((d) => d.toString()),
231
298
  anchor: null,
@@ -233,39 +300,379 @@ function ManagedSelectionTree(props) {
233
300
  });
234
301
  }, [selectedNodes]);
235
302
 
236
- return h(Tree, {
237
- className: "selection-tree",
238
- height,
239
- width,
240
- ref,
241
- data: tree,
242
- onMove({ dragIds, parentId, index }) {
243
- dispatch({
244
- type: "move-node",
245
- payload: {
246
- dragIds: dragIds.map((d) => parseInt(d)),
247
- parentId: parentId ? parseInt(parentId) : null,
248
- index,
249
- },
250
- });
251
- },
252
- onDelete({ ids }) {
253
- dispatch({
254
- type: "delete-node",
255
- payload: { ids: ids.map((d) => parseInt(d)) },
256
- });
257
- },
258
- onSelect(nodes) {
303
+ // Mark clicked when user clicks inside the tree container
304
+ function handleClick() {
305
+ clickedRef.current = true;
306
+ }
307
+
308
+ const ctrlPressedRef = useRef(false);
309
+
310
+ useEffect(() => {
311
+ const down = (e) => {
312
+ if (e.ctrlKey || e.metaKey) ctrlPressedRef.current = true;
313
+ };
314
+ const up = () => (ctrlPressedRef.current = false);
315
+
316
+ window.addEventListener("keydown", down);
317
+ window.addEventListener("keyup", up);
318
+ return () => {
319
+ window.removeEventListener("keydown", down);
320
+ window.removeEventListener("keyup", up);
321
+ };
322
+ }, []);
323
+
324
+ const handleSelect = useCallback(
325
+ (nodes) => {
326
+ if (!clickedRef.current) return;
327
+ clickedRef.current = false;
328
+ const isMultiSelect = ctrlPressedRef.current;
329
+
259
330
  let ids = nodes.map((d) => parseInt(d.id));
260
- if (ids.length == 1 && ids[0] == selectedNodes[0]) {
261
- // Deselect
262
- ids = [];
331
+
332
+ if (isMultiSelect) {
333
+ dispatch({ type: "toggle-node-selected", payload: { ids } });
334
+ } else {
335
+ if (ids.length === 1 && ids[0] === selectedNodes[0]) {
336
+ ids = [];
337
+ }
338
+
339
+ dispatch({ type: "select-node", payload: { ids } });
263
340
  }
264
- dispatch({ type: "select-node", payload: { ids } });
265
341
  },
266
- children: _Node,
267
- idAccessor(d: TreeData) {
268
- return d.id.toString();
342
+ [selectedNodes, dispatch],
343
+ );
344
+
345
+ return h(
346
+ "div.selection-tree-wrapper",
347
+ { onPointerDown: handleClick },
348
+ h(Tree, {
349
+ className: "selection-tree",
350
+ height,
351
+ width,
352
+ ref,
353
+ data: tree,
354
+ onMove({ dragIds, parentId, index }) {
355
+ dispatch({
356
+ type: "move-node",
357
+ payload: {
358
+ dragIds: dragIds.map((d) => parseInt(d)),
359
+ parentId: parentId ? parseInt(parentId) : null,
360
+ index,
361
+ },
362
+ });
363
+ },
364
+ onDelete({ ids }) {
365
+ dispatch({
366
+ type: "delete-node",
367
+ payload: { ids: ids.map((d) => parseInt(d)) },
368
+ });
369
+ },
370
+ onSelect: handleSelect,
371
+ children: _Node,
372
+ idAccessor(d) {
373
+ return d.id.toString();
374
+ },
375
+ }),
376
+ );
377
+ }
378
+
379
+ function TypeList({ types, selected, dispatch, selectedNodes, tree }) {
380
+ const [selectedType, setSelectedType] = useState(null);
381
+ const isSelectedNodes = selectedNodes.length > 0;
382
+ const darkMode = useInDarkMode();
383
+ const luminance = darkMode ? 0.9 : 0.4;
384
+
385
+ return h("div.type-list-container", [
386
+ h(
387
+ "div.type-list-header",
388
+ isSelectedNodes && !selectedType
389
+ ? "Change selected nodes to:"
390
+ : "Entity Types",
391
+ ),
392
+ h(
393
+ "div.type-list",
394
+ Array.from(types.values()).map((type) =>
395
+ h(TypeTag, {
396
+ type,
397
+ luminance,
398
+ selectedType,
399
+ setSelectedType,
400
+ dispatch,
401
+ tree,
402
+ selectedNodes,
403
+ selected,
404
+ isSelectedNodes,
405
+ }),
406
+ ),
407
+ ),
408
+ h(AddType, { dispatch }),
409
+ ]);
410
+ }
411
+
412
+ function collectMatchingIds(tree, id) {
413
+ const ids = [];
414
+
415
+ function traverse(node) {
416
+ if (node.type.id === id) {
417
+ ids.push(node.id);
418
+ }
419
+ if (Array.isArray(node.children)) {
420
+ node.children.forEach(traverse);
421
+ }
422
+ }
423
+
424
+ tree.forEach(traverse);
425
+ return ids;
426
+ }
427
+
428
+ function AddType({ dispatch }) {
429
+ const [overlayOpen, setOverlayOpen] = useState(false);
430
+
431
+ const saveHandler = (payload) => {
432
+ dispatch({
433
+ type: "add-entity-type",
434
+ payload,
435
+ });
436
+ setOverlayOpen(false);
437
+ };
438
+
439
+ return h("div.add-type-container", [
440
+ h("div.add-type", { onClick: () => setOverlayOpen(true) }, [
441
+ h("p.add-type-text", "Add new type"),
442
+ h(Icon, { icon: "plus" }),
443
+ ]),
444
+ h(TypeOverlay, {
445
+ setOverlayOpen,
446
+ overlayOpen,
447
+ title: "Add New Type",
448
+ saveHandler,
449
+ }),
450
+ ]);
451
+ }
452
+
453
+ function EditType({ dispatch, type }) {
454
+ const [editorOpen, setEditorOpen] = useState(false);
455
+
456
+ const saveHandler = (payload) => {
457
+ dispatch({
458
+ type: "update-entity-type",
459
+ payload,
460
+ });
461
+ setEditorOpen(false);
462
+ };
463
+
464
+ return h("div.edit-type", [
465
+ h(Icon, {
466
+ icon: "edit",
467
+ className: "edit-icon",
468
+ onClick: (e) => {
469
+ e.stopPropagation();
470
+ setEditorOpen(true);
471
+ },
472
+ }),
473
+ h(TypeOverlay, {
474
+ setOverlayOpen: setEditorOpen,
475
+ overlayOpen: editorOpen,
476
+ originalType: type,
477
+ title: "Edit Type",
478
+ saveHandler,
479
+ }),
480
+ ]);
481
+ }
482
+
483
+ function TypeOverlay({
484
+ setOverlayOpen,
485
+ overlayOpen,
486
+ originalType,
487
+ title,
488
+ saveHandler,
489
+ }) {
490
+ const { name, description, color, id } = originalType || {};
491
+
492
+ const [nameInput, setNameInput] = useState(name || "");
493
+ const [descriptionInput, setDescriptionInput] = useState(description || "");
494
+ const [colorInput, setColorInput] = useState(color || "#fff");
495
+
496
+ return h(
497
+ Overlay2,
498
+ {
499
+ isOpen: overlayOpen,
269
500
  },
501
+ h(
502
+ "div.overlay-container",
503
+ h("div.add-type-overlay", [
504
+ h("h2.title", [
505
+ title,
506
+ h(Icon, {
507
+ icon: "cross",
508
+ className: "close-icon",
509
+ onClick: () => {
510
+ setOverlayOpen(false);
511
+ },
512
+ style: { cursor: "pointer", color: "red" },
513
+ }),
514
+ ]),
515
+ h("div.form-group", [
516
+ h("div.text-inputs", [
517
+ h("div.form-field.name", [
518
+ h("p.label", "Name"),
519
+ h("input", {
520
+ type: "text",
521
+ placeholder: "Enter type name",
522
+ onChange: (e) => setNameInput(e.target.value),
523
+ value: nameInput,
524
+ }),
525
+ ]),
526
+ h("div.form-field.form-description", [
527
+ h("p.label", "Description"),
528
+ h("input", {
529
+ type: "text",
530
+ placeholder: "Enter type description",
531
+ onChange: (e) => setDescriptionInput(e.target.value),
532
+ value: descriptionInput,
533
+ }),
534
+ ]),
535
+ ]),
536
+ h("div.form-field.color", [
537
+ h("p.label", "Color"),
538
+ h(ColorPicker, {
539
+ value: colorInput,
540
+ onChange: (color) => setColorInput(color),
541
+ style: { width: "100%" },
542
+ }),
543
+ ]),
544
+ ]),
545
+ h(
546
+ SaveButton,
547
+ {
548
+ className: "save-btn",
549
+ small: true,
550
+ onClick: () =>
551
+ saveHandler({
552
+ name: nameInput,
553
+ description: descriptionInput,
554
+ color: colorInput,
555
+ id,
556
+ }),
557
+ },
558
+ "Save changes",
559
+ ),
560
+ ]),
561
+ ),
562
+ );
563
+ }
564
+
565
+ function TypeTag({
566
+ type,
567
+ luminance,
568
+ selectedType,
569
+ setSelectedType,
570
+ dispatch,
571
+ tree,
572
+ selectedNodes,
573
+ selected,
574
+ isSelectedNodes,
575
+ }) {
576
+ const { color, name, id, description } = type;
577
+ const darkMode = useInDarkMode();
578
+ const isSelected = id === selected?.id && selectedNodes.length > 0;
579
+
580
+ const style = getTagStyle(color, {
581
+ active: isSelected,
582
+ highlighted: selectedNodes.length === 0,
270
583
  });
584
+
585
+ const payload = {
586
+ id,
587
+ name,
588
+ color,
589
+ description,
590
+ };
591
+
592
+ const ids = collectMatchingIds(tree, id);
593
+
594
+ const handleTagClick = () => {
595
+ if (!isSelectedNodes && selectedType === null) {
596
+ if (ids.length > 0) {
597
+ setSelectedType(type);
598
+ dispatch({ type: "toggle-node-selected", payload: { ids } });
599
+ }
600
+ } else if (isSelectedNodes && selectedType === null) {
601
+ if (id === selected?.id && selectedNodes.length > 0) {
602
+ dispatch({
603
+ type: "toggle-node-selected",
604
+ payload: { ids: selectedNodes },
605
+ });
606
+ } else {
607
+ dispatch({ type: "select-entity-type", payload });
608
+ }
609
+ } else if (isSelectedNodes && selectedType.id === id) {
610
+ setSelectedType(null);
611
+ dispatch({ type: "toggle-node-selected", payload: { ids } });
612
+ } else if (isSelectedNodes && selectedType.id !== id) {
613
+ if (ids.length > 0) {
614
+ setSelectedType(type);
615
+ const oldIds = collectMatchingIds(tree, selectedType.id);
616
+
617
+ dispatch({ type: "toggle-node-selected", payload: { ids: oldIds } });
618
+ dispatch({ type: "toggle-node-selected", payload: { ids } });
619
+ }
620
+ } else {
621
+ console.warn("Unexpected state in TypeTag click handler", {
622
+ isSelectedNodes,
623
+ selectedType,
624
+ selectedNodes,
625
+ ids,
626
+ id,
627
+ selected,
628
+ });
629
+ }
630
+ };
631
+
632
+ return h(
633
+ Popover,
634
+ {
635
+ autoFocus: false,
636
+ content: h("div.description", description || "No description available"),
637
+ interactionKind: "hover",
638
+ },
639
+ h(
640
+ "div.type-tag",
641
+ {
642
+ onClick: handleTagClick,
643
+ style: {
644
+ cursor:
645
+ ids.length > 0 || (isSelectedNodes && !selectedType)
646
+ ? "pointer"
647
+ : "",
648
+ color: "black",
649
+ backgroundColor: style.backgroundColor,
650
+ border: isSelected
651
+ ? `1px solid var(--text-emphasized-color)`
652
+ : `1px solid var(--background-color)`,
653
+ },
654
+ },
655
+ h("div.type-container", [
656
+ h("div.type-name", name),
657
+ h("div.icons", [
658
+ h(EditType, {
659
+ dispatch,
660
+ type,
661
+ }),
662
+ h(Icon, {
663
+ icon: "cross",
664
+ className: "delete-type-icon",
665
+ style: { color: "red", cursor: "pointer" },
666
+ onClick: (e) => {
667
+ e.stopPropagation();
668
+ dispatch({
669
+ type: "delete-entity-type",
670
+ payload: { id },
671
+ });
672
+ },
673
+ }),
674
+ ]),
675
+ ]),
676
+ ),
677
+ );
271
678
  }