virtual-tree-canvas 0.6.0 → 0.7.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.
@@ -3,7 +3,8 @@ export const darkTheme = {
3
3
  indentWidth: 18,
4
4
  font: '12px Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif',
5
5
  unitFont: '11px ui-monospace, SFMono-Regular, Consolas, monospace',
6
- monoFont: '12px "JetBrains Mono", "Cascadia Mono", "Fira Code", ui-monospace, SFMono-Regular, Consolas, monospace',
6
+ monoFont:
7
+ '12px "JetBrains Mono", "Cascadia Mono", "Fira Code", ui-monospace, SFMono-Regular, Consolas, monospace',
7
8
  colors: {
8
9
  background: '#0a0f1c',
9
10
  row: '#0a0f1c',
@@ -30,44 +31,146 @@ export const darkTheme = {
30
31
  badgeText: '#ffffff',
31
32
  border: '#1b2637',
32
33
  borderStrong: '#2b3b52',
33
- shadow: 'rgba(0,0,0,.24)',
34
+ shadow: 'rgba(0,0,0,.24)'
35
+ },
36
+ valueColors: {
37
+ number: '#8bd5ee',
38
+ bigint: '#8bd5ee',
39
+ string: '#dde5ef',
40
+ boolean: '#86d6b5',
41
+ enum: '#c4b5fd',
42
+ null: '#9aa9bc',
43
+ undefined: '#9aa9bc'
34
44
  },
35
45
  types: {
36
- root: { icon: 'folder', color: '#38bdf8' },
37
- system: { icon: 'folder', color: '#818cf8' },
38
- platform: { icon: 'aircraft', color: '#60a5fa' },
39
- air: { icon: 'aircraft', color: '#60a5fa' },
40
- ground: { icon: 'ground', color: '#a3e635' },
41
- surface: { icon: 'surface', color: '#22d3ee' },
42
- subsurface: { icon: 'subsurface', color: '#38bdf8' },
43
- space: { icon: 'space', color: '#c084fc' },
44
- munition: { icon: 'munition', color: '#fb7185' },
45
- sensor: { icon: 'radar', color: '#34d399' },
46
- track: { icon: 'track', color: '#a78bfa' },
47
- geometry: { icon: 'point', color: '#38bdf8' },
48
- control: { icon: 'control', color: '#f59e0b' },
49
- situation: { icon: 'situation', color: '#2dd4bf' },
50
- damage: { icon: 'damage', color: '#fb7185' },
51
- warning: { icon: 'warning', color: '#facc15' },
52
- error: { icon: 'error', color: '#ef4444' },
53
- task: { icon: 'task', color: '#f97316' },
54
- bus: { icon: 'data-bus', color: '#22d3ee' },
55
- object: { icon: 'inspector-object', color: '#7dd3fc' },
56
- array: { icon: 'inspector-array', color: '#a78bfa' },
57
- string: { icon: 'inspector-value', color: '#94a3b8' },
58
- number: { icon: 'inspector-value', color: '#34d399' },
59
- boolean: { icon: 'inspector-value', color: '#22c55e' },
60
- null: { icon: 'inspector-value', color: '#64748b' },
46
+ root: {
47
+ icon: 'folder',
48
+ color: '#38bdf8'
49
+ },
50
+ system: {
51
+ icon: 'folder',
52
+ color: '#818cf8'
53
+ },
54
+ platform: {
55
+ icon: 'aircraft',
56
+ color: '#60a5fa'
57
+ },
58
+ air: {
59
+ icon: 'aircraft',
60
+ color: '#60a5fa'
61
+ },
62
+ ground: {
63
+ icon: 'ground',
64
+ color: '#a3e635'
65
+ },
66
+ surface: {
67
+ icon: 'surface',
68
+ color: '#22d3ee'
69
+ },
70
+ subsurface: {
71
+ icon: 'subsurface',
72
+ color: '#38bdf8'
73
+ },
74
+ space: {
75
+ icon: 'space',
76
+ color: '#c084fc'
77
+ },
78
+ munition: {
79
+ icon: 'munition',
80
+ color: '#fb7185'
81
+ },
82
+ sensor: {
83
+ icon: 'radar',
84
+ color: '#34d399'
85
+ },
86
+ track: {
87
+ icon: 'track',
88
+ color: '#a78bfa'
89
+ },
90
+ geometry: {
91
+ icon: 'point',
92
+ color: '#38bdf8'
93
+ },
94
+ control: {
95
+ icon: 'control',
96
+ color: '#f59e0b'
97
+ },
98
+ situation: {
99
+ icon: 'situation',
100
+ color: '#2dd4bf'
101
+ },
102
+ damage: {
103
+ icon: 'damage',
104
+ color: '#fb7185'
105
+ },
106
+ warning: {
107
+ icon: 'warning',
108
+ color: '#facc15'
109
+ },
110
+ error: {
111
+ icon: 'error',
112
+ color: '#ef4444'
113
+ },
114
+ task: {
115
+ icon: 'task',
116
+ color: '#f97316'
117
+ },
118
+ bus: {
119
+ icon: 'data-bus',
120
+ color: '#22d3ee'
121
+ },
122
+ object: {
123
+ icon: 'inspector-object',
124
+ color: '#7dd3fc'
125
+ },
126
+ array: {
127
+ icon: 'inspector-array',
128
+ color: '#a78bfa'
129
+ },
130
+ string: {
131
+ icon: 'inspector-value',
132
+ color: '#94a3b8'
133
+ },
134
+ number: {
135
+ icon: 'inspector-value',
136
+ color: '#34d399'
137
+ },
138
+ boolean: {
139
+ icon: 'inspector-value',
140
+ color: '#22c55e'
141
+ },
142
+ null: {
143
+ icon: 'inspector-value',
144
+ color: '#64748b'
145
+ }
61
146
  },
62
147
  statuses: {
63
- 0: { label: 'OK', color: '#22c55e' },
64
- 1: { label: 'WARN', color: '#facc15' },
65
- 2: { label: 'ERR', color: '#ef4444' },
66
- },
148
+ 0: {
149
+ label: 'OK',
150
+ color: '#22c55e'
151
+ },
152
+ 1: {
153
+ label: 'WARN',
154
+ color: '#facc15'
155
+ },
156
+ 2: {
157
+ label: 'ERR',
158
+ color: '#ef4444'
159
+ }
160
+ }
67
161
  };
68
162
 
69
163
  export const lightTheme = {
70
164
  ...darkTheme,
165
+ valueColors: {
166
+ number: '#075985',
167
+ bigint: '#075985',
168
+ string: '#182438',
169
+ boolean: '#176249',
170
+ enum: '#6b3fa0',
171
+ null: '#576579',
172
+ undefined: '#576579'
173
+ },
71
174
  colors: {
72
175
  background: '#f8fafc',
73
176
  row: '#ffffff',
@@ -94,8 +197,8 @@ export const lightTheme = {
94
197
  badgeFill: '#e2e8f0',
95
198
  border: '#e2e8f0',
96
199
  borderStrong: '#cbd5e1',
97
- shadow: 'rgba(15,23,42,.1)',
98
- },
200
+ shadow: 'rgba(15,23,42,.1)'
201
+ }
99
202
  };
100
203
 
101
204
  export const tacticalTheme = {
@@ -123,34 +226,85 @@ export const tacticalTheme = {
123
226
  badgeFill: '#102217',
124
227
  border: '#183321',
125
228
  borderStrong: '#295337',
126
- shadow: 'rgba(0,0,0,.3)',
229
+ shadow: 'rgba(0,0,0,.3)'
127
230
  },
128
231
  types: {
129
232
  ...darkTheme.types,
130
- root: { icon: 'folder', color: '#7ddc92' },
131
- platform: { icon: 'aircraft', color: '#93c572' },
132
- air: { icon: 'aircraft', color: '#93c572' },
133
- ground: { icon: 'ground', color: '#c7f36f' },
134
- surface: { icon: 'surface', color: '#67e8f9' },
135
- subsurface: { icon: 'subsurface', color: '#7dd3fc' },
136
- space: { icon: 'space', color: '#d8b4fe' },
137
- munition: { icon: 'munition', color: '#ff8fab' },
138
- sensor: { icon: 'radar', color: '#45d483' },
139
- track: { icon: 'track', color: '#d6f264' },
140
- geometry: { icon: 'point', color: '#7ddc92' },
141
- control: { icon: 'control', color: '#ffd166' },
142
- situation: { icon: 'situation', color: '#8be9d9' },
143
- damage: { icon: 'damage', color: '#ff8fab' },
144
- warning: { icon: 'warning', color: '#ffd166' },
145
- error: { icon: 'error', color: '#ff5c5c' },
146
- bus: { icon: 'data-bus', color: '#67e8f9' },
147
- },
233
+ root: {
234
+ icon: 'folder',
235
+ color: '#7ddc92'
236
+ },
237
+ platform: {
238
+ icon: 'aircraft',
239
+ color: '#93c572'
240
+ },
241
+ air: {
242
+ icon: 'aircraft',
243
+ color: '#93c572'
244
+ },
245
+ ground: {
246
+ icon: 'ground',
247
+ color: '#c7f36f'
248
+ },
249
+ surface: {
250
+ icon: 'surface',
251
+ color: '#67e8f9'
252
+ },
253
+ subsurface: {
254
+ icon: 'subsurface',
255
+ color: '#7dd3fc'
256
+ },
257
+ space: {
258
+ icon: 'space',
259
+ color: '#d8b4fe'
260
+ },
261
+ munition: {
262
+ icon: 'munition',
263
+ color: '#ff8fab'
264
+ },
265
+ sensor: {
266
+ icon: 'radar',
267
+ color: '#45d483'
268
+ },
269
+ track: {
270
+ icon: 'track',
271
+ color: '#d6f264'
272
+ },
273
+ geometry: {
274
+ icon: 'point',
275
+ color: '#7ddc92'
276
+ },
277
+ control: {
278
+ icon: 'control',
279
+ color: '#ffd166'
280
+ },
281
+ situation: {
282
+ icon: 'situation',
283
+ color: '#8be9d9'
284
+ },
285
+ damage: {
286
+ icon: 'damage',
287
+ color: '#ff8fab'
288
+ },
289
+ warning: {
290
+ icon: 'warning',
291
+ color: '#ffd166'
292
+ },
293
+ error: {
294
+ icon: 'error',
295
+ color: '#ff5c5c'
296
+ },
297
+ bus: {
298
+ icon: 'data-bus',
299
+ color: '#67e8f9'
300
+ }
301
+ }
148
302
  };
149
303
 
150
304
  export const themes = {
151
305
  dark: darkTheme,
152
306
  light: lightTheme,
153
- tactical: tacticalTheme,
307
+ tactical: tacticalTheme
154
308
  };
155
309
 
156
310
  export class ThemeManager extends EventTarget {
@@ -168,7 +322,10 @@ export class ThemeManager extends EventTarget {
168
322
  return this.theme;
169
323
  }
170
324
 
171
- /** @param {import('./types.js').TreeNode} node @param {import('./types.js').NodeDynamicState} state */
325
+ /**
326
+ * @param {import('./types.js').TreeNode} node
327
+ * @param {import('./types.js').NodeDynamicState} state
328
+ */
172
329
  resolveNodeStyle(node, state = {}) {
173
330
  const typeRule = this.theme.types[node?.type ?? ''] ?? {};
174
331
  const statusRule = this.resolveStatus(state.status);
@@ -178,14 +335,20 @@ export class ThemeManager extends EventTarget {
178
335
  typeColor: typeRule.color ?? this.theme.colors.progressFill,
179
336
  status: statusRule,
180
337
  font: this.theme.font,
181
- colors: this.theme.colors,
338
+ colors: this.theme.colors
182
339
  };
183
340
  }
184
341
 
185
342
  resolveStatus(status) {
186
343
  const key = status ?? 0;
187
- return this.theme.statuses[key] ?? { label: String(key), color: this.theme.colors.textMuted };
344
+ return (
345
+ this.theme.statuses[key] ?? {
346
+ label: String(key),
347
+ color: this.theme.colors.textMuted
348
+ }
349
+ );
188
350
  }
351
+
189
352
  }
190
353
 
191
354
  export function resolveTheme(theme = {}) {
@@ -193,7 +356,8 @@ export function resolveTheme(theme = {}) {
193
356
  if (!Object.hasOwn(themes, theme)) throw new TypeError(`Unknown virtual tree theme: ${theme}`);
194
357
  return themes[theme];
195
358
  }
196
- if (!theme || typeof theme !== 'object' || Array.isArray(theme)) throw new TypeError('theme must be a theme name or object');
359
+ if (!theme || typeof theme !== 'object' || Array.isArray(theme))
360
+ throw new TypeError('theme must be a theme name or object');
197
361
  return theme;
198
362
  }
199
363
 
@@ -219,7 +383,7 @@ function normalizeTheme(theme = {}) {
219
383
  indentGuide: base.colors.guide,
220
384
  chevron: base.colors.chevron,
221
385
  chevronMuted: base.colors.textMuted,
222
- progress: base.colors.progressFill,
386
+ progress: base.colors.progressFill
223
387
  };
224
388
  }
225
389
 
@@ -227,8 +391,21 @@ function mergeTheme(base, override) {
227
391
  return {
228
392
  ...base,
229
393
  ...override,
230
- colors: { ...base.colors, ...(override.colors ?? {}) },
231
- types: { ...base.types, ...(override.types ?? {}) },
232
- statuses: { ...base.statuses, ...(override.statuses ?? {}) },
394
+ colors: {
395
+ ...base.colors,
396
+ ...(override.colors ?? {})
397
+ },
398
+ valueColors: {
399
+ ...base.valueColors,
400
+ ...(override.valueColors ?? {})
401
+ },
402
+ types: {
403
+ ...base.types,
404
+ ...(override.types ?? {})
405
+ },
406
+ statuses: {
407
+ ...base.statuses,
408
+ ...(override.statuses ?? {})
409
+ }
233
410
  };
234
411
  }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Share tree-cell positions between painting, hit testing and tooltips.
3
+ */
4
+ export function treeCellLayout(depth, indentWidth, hasHierarchy) {
5
+ const indent = depth * indentWidth;
6
+ return {
7
+ chevronX: hasHierarchy ? indent + 10 : null,
8
+ iconX: indent + (hasHierarchy ? 27 : 8),
9
+ labelX: indent + (hasHierarchy ? 50 : 31)
10
+ };
11
+ }
@@ -6,7 +6,7 @@ export const builtInColumns = {
6
6
  minWidth: 160,
7
7
  align: 'left',
8
8
  kind: 'tree',
9
- value: (node) => node.label ?? node.id,
9
+ value: (node) => node.label ?? node.id
10
10
  },
11
11
  status: {
12
12
  id: 'status',
@@ -15,7 +15,7 @@ export const builtInColumns = {
15
15
  minWidth: 64,
16
16
  align: 'center',
17
17
  kind: 'status',
18
- value: (_node, state) => state.status ?? 0,
18
+ value: (_node, state) => state.status ?? 0
19
19
  },
20
20
  value: {
21
21
  id: 'value',
@@ -24,7 +24,7 @@ export const builtInColumns = {
24
24
  minWidth: 56,
25
25
  align: 'right',
26
26
  kind: 'value',
27
- value: (_node, state) => state.value ?? '',
27
+ value: (_node, state) => state.value ?? ''
28
28
  },
29
29
  progress: {
30
30
  id: 'progress',
@@ -33,7 +33,7 @@ export const builtInColumns = {
33
33
  minWidth: 80,
34
34
  align: 'left',
35
35
  kind: 'progress',
36
- value: (_node, state) => state.progress ?? '',
36
+ value: (_node, state) => state.progress ?? ''
37
37
  },
38
38
  type: {
39
39
  id: 'type',
@@ -42,7 +42,7 @@ export const builtInColumns = {
42
42
  minWidth: 70,
43
43
  align: 'left',
44
44
  kind: 'type',
45
- value: (node) => node.type ?? '',
45
+ value: (node) => node.type ?? ''
46
46
  },
47
47
  updated: {
48
48
  id: 'updated',
@@ -51,15 +51,18 @@ export const builtInColumns = {
51
51
  minWidth: 84,
52
52
  align: 'right',
53
53
  kind: 'updated',
54
- value: (_node, state) => state.updatedAt ?? '',
55
- },
54
+ value: (_node, state) => state.updatedAt ?? ''
55
+ }
56
56
  };
57
57
 
58
58
  export class TreeColumnModel {
59
59
  constructor(columns = [builtInColumns.tree]) {
60
60
  this.columns = [];
61
61
  this.contentWidth = 0;
62
- this.sort = { columnId: null, direction: null };
62
+ this.sort = {
63
+ columnId: null,
64
+ direction: null
65
+ };
63
66
  this.setColumns(columns);
64
67
  }
65
68
 
@@ -69,21 +72,57 @@ export class TreeColumnModel {
69
72
  if (!this.columns.some((column) => column.kind === 'tree' || column.kind === 'inspectorPane')) {
70
73
  this.columns.unshift(normalizeColumn(builtInColumns.tree, 0));
71
74
  }
75
+ this.setRowActions(this.rowActionCount ?? 0);
72
76
  this.setRowReorder(this.rowReorder);
73
77
  this.#layout();
74
78
  }
75
79
 
76
80
  setRowReorder(enabled) {
77
81
  this.rowReorder = Boolean(enabled);
78
- this.columns = this.columns.filter(column => column.id !== '__vtc_row_order');
79
- if (this.rowReorder) this.columns.unshift(normalizeColumn({
80
- id: '__vtc_row_order', label: '', kind: 'rowOrder', width: 72, minWidth: 72,
81
- sortable: false, resizable: false
82
- }, 0));
82
+ this.columns = this.columns.filter((column) => column.id !== '__vtc_row_order');
83
+ if (this.rowReorder)
84
+ this.columns.unshift(
85
+ normalizeColumn(
86
+ {
87
+ id: '__vtc_row_order',
88
+ label: '',
89
+ kind: 'rowOrder',
90
+ width: 72,
91
+ minWidth: 72,
92
+ sortable: false,
93
+ resizable: false
94
+ },
95
+ 0
96
+ )
97
+ );
83
98
  this.#layout();
84
99
  }
85
100
 
86
- /** @param {number} x */
101
+ setRowActions(count) {
102
+ this.rowActionCount = count;
103
+ this.columns = this.columns.filter((column) => column.id !== '__vtc_actions');
104
+ if (count)
105
+ this.columns.push(
106
+ normalizeColumn(
107
+ {
108
+ id: '__vtc_actions',
109
+ label: '',
110
+ kind: 'text',
111
+ width: count * 28,
112
+ minWidth: count * 28,
113
+ sortable: false,
114
+ resizable: false,
115
+ value: () => ''
116
+ },
117
+ 0
118
+ )
119
+ );
120
+ this.#layout();
121
+ }
122
+
123
+ /**
124
+ * @param {number} x
125
+ */
87
126
  getColumnAt(x) {
88
127
  return this.columns.find((column) => x >= column.x && x < column.x + column.width) ?? null;
89
128
  }
@@ -101,25 +140,37 @@ export class TreeColumnModel {
101
140
  }
102
141
 
103
142
  moveColumn(id, targetIndex) {
104
- if (id === '__vtc_row_order') return false;
143
+ if (id === '__vtc_row_order' || id === '__vtc_actions') return false;
105
144
  const currentIndex = this.columns.findIndex((column) => column.id === id);
106
145
  if (currentIndex === -1) return false;
107
146
  const [column] = this.columns.splice(currentIndex, 1);
108
- const nextIndex = Math.max(this.rowReorder ? 1 : 0, Math.min(this.columns.length, targetIndex));
147
+ const nextIndex = Math.max(
148
+ this.rowReorder ? 1 : 0,
149
+ Math.min(this.columns.length - (this.rowActionCount ? 1 : 0), targetIndex)
150
+ );
109
151
  this.columns.splice(nextIndex, 0, column);
110
- if (!this.columns.some((item) => item.kind === 'tree')) this.columns.unshift(normalizeColumn(builtInColumns.tree, 0));
152
+ if (!this.columns.some((item) => item.kind === 'tree'))
153
+ this.columns.unshift(normalizeColumn(builtInColumns.tree, 0));
111
154
  this.#layout();
112
155
  return true;
113
156
  }
114
157
 
115
158
  setSort(columnId, direction) {
116
159
  if (columnId !== null && !this.getColumn(columnId)) return false;
117
- this.sort = { columnId, direction };
160
+ this.sort = {
161
+ columnId,
162
+ direction
163
+ };
118
164
  return true;
119
165
  }
120
166
 
121
167
  getResizeHandleAt(x, tolerance = 5) {
122
- return this.columns.find((column) => column.resizable !== false && Math.abs(x - (column.x + column.width)) <= tolerance) ?? null;
168
+ return (
169
+ this.columns.find(
170
+ (column) =>
171
+ column.resizable !== false && Math.abs(x - (column.x + column.width)) <= tolerance
172
+ ) ?? null
173
+ );
123
174
  }
124
175
 
125
176
  #layout() {
@@ -130,16 +181,22 @@ export class TreeColumnModel {
130
181
  }
131
182
  this.contentWidth = x;
132
183
  }
184
+
133
185
  }
134
186
 
135
187
  export function defaultTreeTableColumns() {
136
188
  return [
137
- { ...builtInColumns.tree, id: 'name', label: 'Name', width: 340 },
189
+ {
190
+ ...builtInColumns.tree,
191
+ id: 'name',
192
+ label: 'Name',
193
+ width: 340
194
+ },
138
195
  builtInColumns.type,
139
196
  builtInColumns.status,
140
197
  builtInColumns.value,
141
198
  builtInColumns.progress,
142
- builtInColumns.updated,
199
+ builtInColumns.updated
143
200
  ];
144
201
  }
145
202
 
@@ -158,7 +215,9 @@ function normalizeColumn(column, index) {
158
215
  sortable: source.sortable ?? true,
159
216
  resizable: source.resizable ?? true,
160
217
  value: source.value ?? ((node) => node[source.id] ?? ''),
218
+ format: source.format,
219
+ valueType: source.valueType,
161
220
  render: source.render,
162
- x: 0,
221
+ x: 0
163
222
  };
164
223
  }
@@ -1,5 +1,5 @@
1
1
  import { TreeIndex } from './tree-index.js';
2
- import { mergeDynamicState } from './dynamic-state.js';
2
+ import { mergeDynamicStateChanged } from './dynamic-state.js';
3
3
 
4
4
  /**
5
5
  * Owns structural nodes, expanded state, and per-node dynamic state.
@@ -86,11 +86,13 @@ export class TreeModel extends EventTarget {
86
86
  * @param {Array<import('./types.js').DynamicPatch>} patches
87
87
  */
88
88
  applyDynamicPatches(patches) {
89
+ const changedIds = new Set();
89
90
  for (const patch of patches) {
90
91
  const current = this.dynamicState.get(patch.id);
91
92
  if (!current) continue;
92
- mergeDynamicState(current, patch.state);
93
+ if (mergeDynamicStateChanged(current, patch.state)) changedIds.add(patch.id);
93
94
  }
95
+ return changedIds;
94
96
  }
95
97
 
96
98
  /** @param {string} id */
package/src/core/types.js CHANGED
@@ -7,6 +7,7 @@
7
7
  * @property {string} [icon]
8
8
  * @property {string} [image]
9
9
  * @property {string[]} [tags]
10
+ * @property {boolean} [reorderable] Allow manual reordering of this node (defaults to true).
10
11
  * @property {any} [data]
11
12
  */
12
13