matterviz 0.4.2 → 0.4.3

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 (244) hide show
  1. package/dist/FilePicker.svelte +40 -50
  2. package/dist/api/optimade.js +10 -7
  3. package/dist/app.css +15 -0
  4. package/dist/brillouin/BrillouinZone.svelte +18 -9
  5. package/dist/brillouin/BrillouinZoneScene.svelte +20 -29
  6. package/dist/brillouin/compute.js +10 -16
  7. package/dist/brillouin/geometry.js +2 -2
  8. package/dist/chempot-diagram/ChemPotDiagram.svelte +18 -15
  9. package/dist/chempot-diagram/ChemPotDiagram2D.svelte +17 -113
  10. package/dist/chempot-diagram/ChemPotDiagram3D.svelte +104 -264
  11. package/dist/chempot-diagram/async-compute.svelte.js +12 -13
  12. package/dist/chempot-diagram/color.d.ts +21 -1
  13. package/dist/chempot-diagram/color.js +70 -1
  14. package/dist/chempot-diagram/compute.d.ts +7 -0
  15. package/dist/chempot-diagram/compute.js +85 -26
  16. package/dist/chempot-diagram/export.js +1 -6
  17. package/dist/chempot-diagram/temperature.js +9 -10
  18. package/dist/composition/BarChart.svelte +7 -9
  19. package/dist/composition/Composition.svelte +3 -8
  20. package/dist/composition/Formula.svelte +2 -2
  21. package/dist/composition/FormulaFilter.svelte +27 -93
  22. package/dist/composition/format.js +6 -8
  23. package/dist/composition/parse.js +50 -105
  24. package/dist/constants.d.ts +0 -1
  25. package/dist/constants.js +6 -17
  26. package/dist/convex-hull/ConvexHull2D.svelte +0 -2
  27. package/dist/convex-hull/ConvexHull3D.svelte +19 -47
  28. package/dist/convex-hull/ConvexHull4D.svelte +65 -95
  29. package/dist/convex-hull/StructurePopup.svelte +1 -1
  30. package/dist/convex-hull/gas-thermodynamics.js +21 -53
  31. package/dist/convex-hull/helpers.js +1 -7
  32. package/dist/convex-hull/thermodynamics.js +19 -27
  33. package/dist/coordination/CoordinationBarPlot.svelte +12 -21
  34. package/dist/element/BohrAtom.svelte +0 -1
  35. package/dist/element/ElementTile.svelte +26 -44
  36. package/dist/element/data.d.ts +2 -0
  37. package/dist/element/data.js +5 -1
  38. package/dist/element/index.d.ts +1 -1
  39. package/dist/element/index.js +1 -1
  40. package/dist/fermi-surface/FermiSurface.svelte +22 -23
  41. package/dist/fermi-surface/FermiSurface.svelte.d.ts +2 -6
  42. package/dist/fermi-surface/FermiSurfaceScene.svelte +32 -77
  43. package/dist/fermi-surface/compute.js +22 -55
  44. package/dist/fermi-surface/parse.js +38 -70
  45. package/dist/file-viewer/JsonBrowser.svelte +1043 -0
  46. package/dist/file-viewer/JsonBrowser.svelte.d.ts +9 -0
  47. package/dist/file-viewer/PlotPanel.svelte +350 -0
  48. package/dist/file-viewer/PlotPanel.svelte.d.ts +9 -0
  49. package/dist/file-viewer/TrajectoryWithDos.svelte +30 -0
  50. package/dist/file-viewer/TrajectoryWithDos.svelte.d.ts +10 -0
  51. package/dist/file-viewer/detect.d.ts +11 -0
  52. package/dist/file-viewer/detect.js +381 -0
  53. package/dist/file-viewer/eligibility.d.ts +4 -0
  54. package/dist/file-viewer/eligibility.js +53 -0
  55. package/dist/file-viewer/host-protocol.d.ts +50 -0
  56. package/dist/file-viewer/host-transfer.d.ts +25 -0
  57. package/dist/file-viewer/host-transfer.js +42 -0
  58. package/dist/file-viewer/index.d.ts +5 -0
  59. package/dist/file-viewer/index.js +5 -0
  60. package/dist/file-viewer/main.d.ts +44 -0
  61. package/dist/file-viewer/main.js +535 -0
  62. package/dist/file-viewer/parse.d.ts +9 -0
  63. package/dist/file-viewer/parse.js +170 -0
  64. package/dist/file-viewer/plot-utils.d.ts +24 -0
  65. package/dist/file-viewer/plot-utils.js +191 -0
  66. package/dist/file-viewer/types.d.ts +4 -0
  67. package/dist/file-viewer/types.js +5 -0
  68. package/dist/heatmap-matrix/HeatmapMatrix.svelte +57 -84
  69. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte.d.ts +1 -1
  70. package/dist/index.d.ts +2 -2
  71. package/dist/index.js +1 -1
  72. package/dist/io/ExportPane.svelte +1 -1
  73. package/dist/io/decompress.d.ts +2 -0
  74. package/dist/io/decompress.js +3 -2
  75. package/dist/io/export.d.ts +7 -4
  76. package/dist/io/export.js +70 -18
  77. package/dist/io/file-drop.js +60 -35
  78. package/dist/io/url-drop.d.ts +2 -0
  79. package/dist/io/url-drop.js +28 -16
  80. package/dist/isosurface/Isosurface.svelte +687 -196
  81. package/dist/isosurface/Isosurface.svelte.d.ts +7 -1
  82. package/dist/isosurface/IsosurfaceControls.svelte +564 -85
  83. package/dist/isosurface/IsosurfaceControls.svelte.d.ts +1 -1
  84. package/dist/isosurface/VolumeSlice.svelte +224 -0
  85. package/dist/isosurface/VolumeSlice.svelte.d.ts +28 -0
  86. package/dist/isosurface/coloring.d.ts +20 -0
  87. package/dist/isosurface/coloring.js +118 -0
  88. package/dist/isosurface/geometry-worker-types.d.ts +38 -0
  89. package/dist/isosurface/geometry.worker.d.ts +1 -0
  90. package/dist/isosurface/geometry.worker.js +59 -0
  91. package/dist/isosurface/grid.d.ts +10 -0
  92. package/dist/isosurface/grid.js +31 -0
  93. package/dist/isosurface/index.d.ts +4 -0
  94. package/dist/isosurface/index.js +4 -0
  95. package/dist/isosurface/profile.d.ts +12 -0
  96. package/dist/isosurface/profile.js +19 -0
  97. package/dist/isosurface/sampling.d.ts +34 -0
  98. package/dist/isosurface/sampling.js +408 -0
  99. package/dist/isosurface/slice-rendering.d.ts +13 -0
  100. package/dist/isosurface/slice-rendering.js +79 -0
  101. package/dist/isosurface/slice.d.ts +22 -2
  102. package/dist/isosurface/slice.js +200 -116
  103. package/dist/isosurface/types.d.ts +24 -1
  104. package/dist/isosurface/types.js +120 -8
  105. package/dist/json-path.d.ts +4 -0
  106. package/dist/json-path.js +89 -0
  107. package/dist/labels.js +1 -1
  108. package/dist/layout/InfoCard.svelte +1 -3
  109. package/dist/layout/SettingsSection.svelte +52 -61
  110. package/dist/layout/SettingsSection.svelte.d.ts +6 -7
  111. package/dist/layout/ViewerChrome.svelte +7 -1
  112. package/dist/layout/json-tree/JsonNode.svelte +14 -48
  113. package/dist/layout/json-tree/JsonTree.svelte +48 -95
  114. package/dist/layout/json-tree/JsonValue.svelte +3 -18
  115. package/dist/layout/json-tree/utils.d.ts +0 -3
  116. package/dist/layout/json-tree/utils.js +50 -179
  117. package/dist/marching-cubes.d.ts +8 -1
  118. package/dist/marching-cubes.js +115 -48
  119. package/dist/math.d.ts +3 -2
  120. package/dist/math.js +29 -38
  121. package/dist/overlays/ContextMenu.svelte +15 -32
  122. package/dist/overlays/DraggablePane.svelte +39 -58
  123. package/dist/overlays/DraggablePane.svelte.d.ts +1 -1
  124. package/dist/overlays/GlassChip.svelte +3 -1
  125. package/dist/periodic-table/PeriodicTableControls.svelte +26 -52
  126. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +18 -34
  127. package/dist/phase-diagram/build-diagram.js +1 -6
  128. package/dist/phase-diagram/svg-to-diagram.js +9 -14
  129. package/dist/plot/bar/BarPlot.svelte +38 -42
  130. package/dist/plot/bar/SpacegroupBarPlot.svelte +18 -42
  131. package/dist/plot/box/BoxPlot.svelte +27 -23
  132. package/dist/plot/core/auto-place.d.ts +1 -1
  133. package/dist/plot/core/auto-place.js +1 -1
  134. package/dist/plot/core/data-transform.d.ts +1 -2
  135. package/dist/plot/core/data-transform.js +0 -10
  136. package/dist/plot/core/layout.d.ts +12 -5
  137. package/dist/plot/core/layout.js +37 -27
  138. package/dist/plot/core/pan-zoom.svelte.d.ts +2 -4
  139. package/dist/plot/core/pan-zoom.svelte.js +34 -17
  140. package/dist/plot/core/placed-tween.svelte.d.ts +3 -0
  141. package/dist/plot/core/placed-tween.svelte.js +68 -21
  142. package/dist/plot/core/svg.js +0 -1
  143. package/dist/plot/core/utils/hierarchy-chart.d.ts +1 -0
  144. package/dist/plot/core/utils/hierarchy-chart.js +12 -0
  145. package/dist/plot/core/utils/hierarchy-labels.js +3 -2
  146. package/dist/plot/histogram/Histogram.svelte +21 -26
  147. package/dist/plot/sankey/sankey.js +13 -9
  148. package/dist/plot/scatter/BinnedScatterPlot.svelte +59 -43
  149. package/dist/plot/scatter/ScatterPlot.svelte +67 -52
  150. package/dist/plot/scatter/adaptive-density.d.ts +1 -1
  151. package/dist/plot/scatter/adaptive-density.js +36 -20
  152. package/dist/plot/sunburst/Sunburst.svelte +20 -20
  153. package/dist/plot/treemap/Treemap.svelte +175 -60
  154. package/dist/plot/treemap/Treemap.svelte.d.ts +6 -0
  155. package/dist/plot/treemap/index.d.ts +1 -0
  156. package/dist/plot/treemap/labels.d.ts +38 -0
  157. package/dist/plot/treemap/labels.js +80 -0
  158. package/dist/plot/treemap/treemap.d.ts +4 -2
  159. package/dist/plot/treemap/treemap.js +5 -2
  160. package/dist/rdf/RdfPlot.svelte +17 -35
  161. package/dist/rdf/calc-rdf.js +59 -44
  162. package/dist/rdf/index.d.ts +0 -1
  163. package/dist/sanitize.js +84 -26
  164. package/dist/scene/SceneCamera.svelte +1 -1
  165. package/dist/scene/SceneCamera.svelte.d.ts +1 -1
  166. package/dist/scene/index.d.ts +1 -2
  167. package/dist/scene/index.js +1 -1
  168. package/dist/scene/{props.js → props.svelte.js} +15 -1
  169. package/dist/settings.js +159 -482
  170. package/dist/spectral/Bands.svelte +151 -158
  171. package/dist/spectral/BrillouinBandsDos.svelte +4 -19
  172. package/dist/spectral/Dos.svelte +26 -24
  173. package/dist/spectral/helpers.js +40 -105
  174. package/dist/structure/AtomLegend.svelte +8 -13
  175. package/dist/structure/AtomLegend.svelte.d.ts +2 -3
  176. package/dist/structure/CellSelect.svelte +35 -10
  177. package/dist/structure/Structure.svelte +177 -99
  178. package/dist/structure/Structure.svelte.d.ts +1 -1
  179. package/dist/structure/StructureCarousel.svelte +10 -3
  180. package/dist/structure/StructureControls.svelte +8 -26
  181. package/dist/structure/StructureControls.svelte.d.ts +1 -1
  182. package/dist/structure/StructureInfoPane.svelte +65 -76
  183. package/dist/structure/StructureScene.svelte +75 -109
  184. package/dist/structure/StructureScene.svelte.d.ts +3 -1
  185. package/dist/structure/StructureViewport.svelte +120 -55
  186. package/dist/structure/StructureViewport.svelte.d.ts +3 -1
  187. package/dist/structure/atom-properties.d.ts +1 -1
  188. package/dist/structure/atom-properties.js +4 -3
  189. package/dist/structure/bond-order-perception.js +3 -8
  190. package/dist/structure/bonding.d.ts +0 -1
  191. package/dist/structure/bonding.js +16 -23
  192. package/dist/structure/export.js +50 -74
  193. package/dist/structure/format-detect.js +5 -7
  194. package/dist/structure/index.js +6 -12
  195. package/dist/structure/parse.js +38 -100
  196. package/dist/structure/partial-occupancy.js +7 -4
  197. package/dist/structure/pbc.js +8 -10
  198. package/dist/structure/polyhedra.js +5 -4
  199. package/dist/structure/prop-groups.d.ts +85 -0
  200. package/dist/structure/prop-groups.js +18 -0
  201. package/dist/symmetry/SymmetryStats.svelte +0 -4
  202. package/dist/symmetry/WyckoffTable.svelte +5 -4
  203. package/dist/symmetry/cell-transform.js +5 -14
  204. package/dist/symmetry/index.js +6 -9
  205. package/dist/table/HeatmapTable.svelte +61 -103
  206. package/dist/table/ToggleMenu.svelte +166 -44
  207. package/dist/table/ToggleMenu.svelte.d.ts +1 -1
  208. package/dist/theme/embedded.d.ts +18 -0
  209. package/dist/theme/embedded.js +206 -0
  210. package/dist/theme/index.d.ts +4 -0
  211. package/dist/theme/index.js +1 -2
  212. package/dist/theme/themes.mjs +31 -60
  213. package/dist/tooltip/TooltipContent.svelte +1 -1
  214. package/dist/tooltip/TooltipContent.svelte.d.ts +1 -1
  215. package/dist/tooltip/index.d.ts +8 -1
  216. package/dist/trajectory/Trajectory.svelte +140 -120
  217. package/dist/trajectory/TrajectoryError.svelte +1 -66
  218. package/dist/trajectory/TrajectoryExportPane.svelte +0 -1
  219. package/dist/trajectory/TrajectoryInfoPane.svelte +55 -80
  220. package/dist/trajectory/TrajectoryInfoPane.svelte.d.ts +2 -1
  221. package/dist/trajectory/extract.js +19 -29
  222. package/dist/trajectory/format-detect.d.ts +2 -0
  223. package/dist/trajectory/format-detect.js +7 -5
  224. package/dist/trajectory/frame-reader.js +3 -4
  225. package/dist/trajectory/helpers.js +16 -49
  226. package/dist/trajectory/index.js +2 -8
  227. package/dist/trajectory/parse/ase.js +1 -1
  228. package/dist/trajectory/parse/index.d.ts +4 -3
  229. package/dist/trajectory/parse/index.js +12 -17
  230. package/dist/trajectory/parse/vaspout-h5.d.ts +0 -1
  231. package/dist/trajectory/parse/vaspout-h5.js +1 -4
  232. package/dist/trajectory/parse/xyz.js +48 -2
  233. package/dist/trajectory/plotting.js +14 -37
  234. package/dist/xrd/XrdPlot.svelte +43 -68
  235. package/dist/xrd/calc-xrd.js +22 -58
  236. package/dist/xrd/parse.js +13 -22
  237. package/package.json +40 -8
  238. package/readme.md +6 -5
  239. package/dist/scene/types.d.ts +0 -26
  240. package/dist/tooltip/types.d.ts +0 -8
  241. package/dist/trajectory/constants.d.ts +0 -6
  242. package/dist/trajectory/constants.js +0 -11
  243. /package/dist/{scene/types.js → file-viewer/host-protocol.js} +0 -0
  244. /package/dist/{tooltip/types.js → isosurface/geometry-worker-types.js} +0 -0
package/dist/labels.js CHANGED
@@ -141,7 +141,7 @@ export const format_num = (num, fmt) => {
141
141
  return format(fmt)(num);
142
142
  };
143
143
  // Format a 3D vector as "(x, y, z)" with configurable precision
144
- export const format_vec3 = (vec, fmt_spec = `.4~`) => `(${format_num(vec[0], fmt_spec)}, ${format_num(vec[1], fmt_spec)}, ${format_num(vec[2], fmt_spec)})`;
144
+ export const format_vec3 = (vec, fmt_spec = `.3~`) => `(${format_num(vec[0], fmt_spec)}, ${format_num(vec[1], fmt_spec)}, ${format_num(vec[2], fmt_spec)})`;
145
145
  const BYTE_UNITS = [`B`, `KiB`, `MiB`, `GiB`, `TiB`, `PiB`];
146
146
  // Format file sizes using IEC binary units (1024 factor).
147
147
  export const format_bytes = (bytes) => {
@@ -26,14 +26,12 @@
26
26
  data = [],
27
27
  title = ``,
28
28
  fallback = ``,
29
- fmt = `.2f`,
29
+ fmt: default_fmt = `.2f`,
30
30
  as = `section`,
31
31
  title_snippet,
32
32
  fallback_snippet,
33
33
  ...rest
34
34
  }: Props = $props()
35
-
36
- let default_fmt = $derived(fmt) // rename fmt to default_fmt for internal use
37
35
  </script>
38
36
 
39
37
  <svelte:element this={as} {...rest} class={[`info-card`, rest.class]}>
@@ -4,6 +4,12 @@
4
4
  import { untrack } from 'svelte'
5
5
  import type { HTMLAttributes } from 'svelte/elements'
6
6
 
7
+ type SettingsSectionContext = {
8
+ current_values: Record<string, unknown>
9
+ has_changes: boolean
10
+ reference_values: Record<string, unknown>
11
+ }
12
+
7
13
  let {
8
14
  title,
9
15
  current_values,
@@ -13,15 +19,7 @@
13
19
  }: HTMLAttributes<HTMLElementTagNameMap[`section`]> & {
14
20
  title: string
15
21
  current_values: Record<string, unknown>
16
- children: Snippet<
17
- [
18
- {
19
- current_values: Record<string, unknown>
20
- has_changes: boolean
21
- reference_values: Record<string, unknown>
22
- },
23
- ]
24
- >
22
+ children: Snippet<[SettingsSectionContext]>
25
23
  on_reset?: () => void
26
24
  } = $props()
27
25
 
@@ -30,17 +28,10 @@
30
28
  if (obj === null || typeof obj !== `object`) return obj
31
29
  if (obj instanceof Date) return new Date(obj)
32
30
  if (obj instanceof RegExp) return new RegExp(obj)
33
- if (Array.isArray(obj)) {
34
- return obj.map((item) =>
35
- typeof item === `object` && item !== null ? deep_copy(item) : item,
36
- )
37
- }
38
-
39
- const copy: Record<string, unknown> = {}
40
- for (const [key, value] of Object.entries(obj as Record<string, unknown>)) {
41
- copy[key] = typeof value === `object` && value !== null ? deep_copy(value) : value
42
- }
43
- return copy
31
+ if (Array.isArray(obj)) return obj.map(deep_copy)
32
+ return Object.fromEntries(
33
+ Object.entries(obj).map(([key, value]) => [key, deep_copy(value)]),
34
+ )
44
35
  }
45
36
 
46
37
  // Capture initial values once at mount - must NOT be $derived or it tracks changes
@@ -49,45 +40,50 @@
49
40
  // unique per-instance id so aria-labelledby stays valid with multiple sections on a page
50
41
  const title_id = `settings-section-title-${crypto.randomUUID()}`
51
42
 
52
- // Check if any values have changed from reference values
53
- let has_changes = $derived.by(() => {
54
- for (const [key, reference_value] of Object.entries(reference_values)) {
55
- const current_value = current_values[key]
56
-
57
- // Deep comparison for arrays
58
- if (Array.isArray(reference_value) && Array.isArray(current_value)) {
59
- if (reference_value.length !== current_value.length) return true
60
- if (
61
- reference_value.some((val, idx) => {
62
- const curr_val = current_value[idx]
63
- // Handle nested objects/arrays in arrays
64
- if (
65
- typeof val === `object` &&
66
- val !== null &&
67
- typeof curr_val === `object` &&
68
- curr_val !== null
69
- )
70
- return JSON.stringify(val) !== JSON.stringify(curr_val) // Quick deep comparison fallback
71
- return val !== curr_val
72
- })
73
- ) {
74
- return true
75
- }
76
- continue
77
- }
78
-
79
- // Handle undefined/null comparisons properly
80
- if (reference_value === undefined && current_value === undefined) continue
81
- if (reference_value === null && current_value === null) continue
82
-
83
- // Basic comparison for primitives
84
- if (current_value !== reference_value) {
85
- return true
43
+ // Order-independent deep equality for setting values
44
+ const setting_equal = (left: unknown, right: unknown): boolean => {
45
+ if (Object.is(left, right)) return true
46
+ if (left == null || right == null) return false
47
+ if (typeof left !== `object` || typeof right !== `object`) return false
48
+ if (left instanceof Date || right instanceof Date) {
49
+ return (
50
+ left instanceof Date && right instanceof Date && left.getTime() === right.getTime()
51
+ )
52
+ }
53
+ if (left instanceof RegExp || right instanceof RegExp) {
54
+ return (
55
+ left instanceof RegExp &&
56
+ right instanceof RegExp &&
57
+ left.toString() === right.toString()
58
+ )
59
+ }
60
+ if (Array.isArray(left) || Array.isArray(right)) {
61
+ if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) {
62
+ return false
86
63
  }
64
+ return left.every((item, idx) => setting_equal(item, right[idx]))
87
65
  }
88
- return false
89
- })
66
+ const left_obj = left as Record<string, unknown>
67
+ const right_obj = right as Record<string, unknown>
68
+ const left_keys = Object.keys(left_obj)
69
+ if (left_keys.length !== Object.keys(right_obj).length) return false
70
+ return left_keys.every(
71
+ (key) => Object.hasOwn(right_obj, key) && setting_equal(left_obj[key], right_obj[key]),
72
+ )
73
+ }
90
74
 
75
+ // Key presence is independent of value: additions/removals count even when the
76
+ // value is undefined. Only compare values when both sides own the key.
77
+ let has_changes = $derived(
78
+ [...new Set([...Object.keys(reference_values), ...Object.keys(current_values)])].some(
79
+ (key) => {
80
+ const in_reference = Object.hasOwn(reference_values, key)
81
+ const in_current = Object.hasOwn(current_values, key)
82
+ if (in_reference !== in_current) return true
83
+ return !setting_equal(reference_values[key], current_values[key])
84
+ },
85
+ ),
86
+ )
91
87
  function handle_reset(event: MouseEvent) {
92
88
  event.stopPropagation()
93
89
  event.preventDefault()
@@ -144,9 +140,4 @@
144
140
  opacity: 1;
145
141
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
146
142
  }
147
- .reset-button.standalone {
148
- position: absolute;
149
- top: -8pt;
150
- right: -8pt;
151
- }
152
143
  </style>
@@ -1,15 +1,14 @@
1
1
  import type { Snippet } from 'svelte';
2
2
  import type { HTMLAttributes } from 'svelte/elements';
3
+ type SettingsSectionContext = {
4
+ current_values: Record<string, unknown>;
5
+ has_changes: boolean;
6
+ reference_values: Record<string, unknown>;
7
+ };
3
8
  type $$ComponentProps = HTMLAttributes<HTMLElementTagNameMap[`section`]> & {
4
9
  title: string;
5
10
  current_values: Record<string, unknown>;
6
- children: Snippet<[
7
- {
8
- current_values: Record<string, unknown>;
9
- has_changes: boolean;
10
- reference_values: Record<string, unknown>;
11
- }
12
- ]>;
11
+ children: Snippet<[SettingsSectionContext]>;
13
12
  on_reset?: () => void;
14
13
  };
15
14
  declare const SettingsSection: import("svelte").Component<$$ComponentProps, {}, "">;
@@ -70,6 +70,8 @@
70
70
  right: var(--viewer-buttons-right, var(--ctrl-btn-right, 1ex));
71
71
  gap: var(--viewer-buttons-gap, clamp(6pt, 1cqmin, 9pt));
72
72
  z-index: var(--viewer-buttons-z-index, var(--z-index-overlay-controls, 100000000));
73
+ /* own compositing layer, or WKWebView paints the canvas over this (see app.css) */
74
+ will-change: transform;
73
75
  opacity: 0;
74
76
  pointer-events: none;
75
77
  transition: opacity 0.2s ease;
@@ -99,7 +101,11 @@
99
101
  font-size: var(--ctrl-btn-icon-size, clamp(0.7rem, 2cqmin, 0.85rem));
100
102
  }
101
103
  section.control-buttons :global(button:hover) {
102
- background-color: color-mix(in srgb, currentColor 8%, transparent);
104
+ background-color: var(
105
+ --viewer-buttons-hover-bg,
106
+ color-mix(in srgb, currentColor 8%, transparent)
107
+ );
108
+ color: var(--viewer-buttons-hover-color, currentColor);
103
109
  }
104
110
  .filename {
105
111
  font-family: monospace;
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import Icon from '../../Icon.svelte'
3
+ import { build_path } from '../../json-path'
3
4
  import { getContext, onMount } from 'svelte'
4
5
  // oxlint-disable-next-line import/no-self-import -- recursive Svelte component
5
6
  import JsonNode from './JsonNode.svelte'
@@ -7,7 +8,6 @@
7
8
  import type { JsonTreeContext } from './types'
8
9
  import { JSON_TREE_CONTEXT_KEY } from './types'
9
10
  import {
10
- build_path,
11
11
  estimate_byte_size,
12
12
  format_byte_size,
13
13
  format_preview,
@@ -40,13 +40,8 @@
40
40
  }
41
41
  })
42
42
 
43
- // Determine value type
44
43
  let value_type = $derived(get_value_type(value))
45
-
46
- // Check if this node is expandable
47
44
  let expandable = $derived(is_expandable(value))
48
-
49
- // Get child count for preview
50
45
  let child_count = $derived(expandable ? get_child_count(value) : 0)
51
46
 
52
47
  // Determine if this node should be collapsed
@@ -77,14 +72,9 @@
77
72
  })
78
73
 
79
74
  // Note: Search highlighting is handled by CSS Highlight API in JsonTree.svelte
80
-
81
- // Check if this node is focused
82
75
  let is_focused = $derived(ctx?.focused_path === path)
83
-
84
- // Check if this is the current search match being navigated
76
+ // Current search match being navigated
85
77
  let is_current_match = $derived(ctx?.current_match_path === path)
86
-
87
- // Check if this node is selected
88
78
  let is_selected = $derived(ctx?.selected_paths.has(path) ?? false)
89
79
 
90
80
  // Diff status for this node (null if no diff or unchanged)
@@ -103,58 +93,34 @@
103
93
  }
104
94
  }
105
95
 
106
- // Toggle collapse recursively on double-click
96
+ // Toggle collapse recursively on double-click (collapsed expands all, and vice versa)
107
97
  function toggle_collapse_recursive(event: MouseEvent) {
108
98
  event.stopPropagation()
109
- if (ctx && expandable) {
110
- // If collapsed, expand all; if expanded, collapse all
111
- ctx.toggle_collapse_recursive(path, !is_collapsed)
112
- }
113
- }
114
-
115
- // Focus this node
116
- function focus_node() {
117
- ctx?.set_focused(path)
99
+ if (ctx && expandable) ctx.toggle_collapse_recursive(path, !is_collapsed)
118
100
  }
119
101
 
120
- // Get children based on value type
121
- function get_children(): { key: string | number; value: unknown }[] {
122
- if (!expandable) return []
123
-
102
+ // Children as { key, value } pairs based on value type (Map entries are wrapped so
103
+ // key and value render as an expandable pair; Set values get numeric keys)
104
+ let children = $derived.by((): { key: string | number; value: unknown }[] => {
124
105
  if (value_type === `array`) {
125
106
  return (value as unknown[]).map((val, idx) => ({ key: idx, value: val }))
126
107
  }
127
-
128
108
  if (value_type === `object`) {
129
- let keys = Object.keys(value as Record<string, unknown>)
130
- if (ctx?.settings.sort_keys) {
131
- keys = keys.sort()
132
- }
133
- return keys.map((key) => ({
134
- key,
135
- value: (value as Record<string, unknown>)[key],
136
- }))
109
+ const keys = Object.keys(value as Record<string, unknown>)
110
+ if (ctx?.settings.sort_keys) keys.sort()
111
+ return keys.map((key) => ({ key, value: (value as Record<string, unknown>)[key] }))
137
112
  }
138
-
139
113
  if (value_type === `map`) {
140
- const map = value as Map<unknown, unknown>
141
- return Array.from(map.entries()).map(([key, val], idx) => ({
114
+ return Array.from(value as Map<unknown, unknown>, ([key, val], idx) => ({
142
115
  key: idx,
143
116
  value: { key, value: val },
144
117
  }))
145
118
  }
146
-
147
119
  if (value_type === `set`) {
148
- return Array.from(value as Set<unknown>).map((val, idx) => ({
149
- key: idx,
150
- value: val,
151
- }))
120
+ return Array.from(value as Set<unknown>, (val, idx) => ({ key: idx, value: val }))
152
121
  }
153
-
154
122
  return []
155
- }
156
-
157
- let children = $derived(get_children())
123
+ })
158
124
 
159
125
  // Ghost children: removed entries from diff (pre-computed in JsonTree for O(1) lookup)
160
126
  let ghost_children = $derived.by(() => {
@@ -233,7 +199,7 @@
233
199
  event.stopPropagation()
234
200
  ctx?.toggle_select(path, event.shiftKey)
235
201
  } else {
236
- focus_node()
202
+ ctx?.set_focused(path)
237
203
  }
238
204
  }}
239
205
  onauxclick={(event) => {
@@ -1,6 +1,7 @@
1
1
  <script lang="ts">
2
2
  import Icon from '../../Icon.svelte'
3
3
  import { download } from '../../io/fetch'
4
+ import { parse_path } from '../../json-path'
4
5
  import { make_change_detector } from '../../utils'
5
6
  import { setContext, tick } from 'svelte'
6
7
  import { highlight_matches, tooltip } from 'svelte-multiselect/attachments'
@@ -16,7 +17,6 @@
16
17
  find_matching_paths,
17
18
  format_preview,
18
19
  get_ancestor_paths,
19
- parse_path,
20
20
  serialize_for_copy,
21
21
  } from './utils'
22
22
 
@@ -147,31 +147,10 @@
147
147
  current_match_index = -1
148
148
  return
149
149
  }
150
- const paths_to_expand = new Set<string>()
151
- for (const match of search_matches) {
152
- for (const ancestor of get_ancestor_paths(match)) {
153
- paths_to_expand.add(ancestor)
154
- }
155
- }
156
- let collapsed_changed = false
157
- let force_expanded_changed = false
158
- for (const path_to_expand of paths_to_expand) {
159
- if (collapsed_paths.has(path_to_expand)) {
160
- collapsed_paths.delete(path_to_expand)
161
- collapsed_changed = true
162
- }
163
- // Also add to force_expanded to override auto-fold thresholds (depth/size)
164
- if (!force_expanded.has(path_to_expand)) {
165
- force_expanded.add(path_to_expand)
166
- force_expanded_changed = true
167
- }
168
- }
169
- if (collapsed_changed) {
170
- collapsed_paths = new SvelteSet(collapsed_paths)
171
- }
172
- if (force_expanded_changed) {
173
- force_expanded = new SvelteSet(force_expanded)
174
- }
150
+ const paths_to_expand = new Set([...search_matches].flatMap(get_ancestor_paths))
151
+ // Also add to force_expanded to override auto-fold thresholds (depth/size)
152
+ // Only reassign (which rerenders every node) the sets that actually changed
153
+ set_collapsed(paths_to_expand, () => false)
175
154
  // Wait for DOM to update before scrolling to match
176
155
  await tick()
177
156
  if (sorted_matches.length > 0) {
@@ -213,32 +192,34 @@
213
192
  // Previous values map for change detection
214
193
  const prev_values = new Map<string, unknown>()
215
194
 
216
- // Toggle collapse - tracks force_expanded to override auto-fold thresholds
217
- function toggle_collapse(path: string, is_currently_collapsed: boolean): void {
218
- if (is_currently_collapsed) {
219
- collapsed_paths.delete(path)
220
- force_expanded.add(path)
221
- } else {
222
- force_expanded.delete(path)
223
- collapsed_paths.add(path)
195
+ // Move each path into collapsed or force_expanded (which overrides auto-fold
196
+ // thresholds), then reassign only the sets that changed (reassignment rerenders
197
+ // every node subscribed to the set's identity)
198
+ function set_collapsed(paths: Iterable<string>, collapse: (path: string) => boolean) {
199
+ let collapsed_changed = false
200
+ let force_expanded_changed = false
201
+ for (const path of paths) {
202
+ if (collapse(path)) {
203
+ force_expanded_changed = force_expanded.delete(path) || force_expanded_changed
204
+ collapsed_changed = !collapsed_paths.has(path) || collapsed_changed
205
+ collapsed_paths.add(path)
206
+ } else {
207
+ collapsed_changed = collapsed_paths.delete(path) || collapsed_changed
208
+ force_expanded_changed = !force_expanded.has(path) || force_expanded_changed
209
+ force_expanded.add(path)
210
+ }
224
211
  }
225
- collapsed_paths = new SvelteSet(collapsed_paths)
226
- force_expanded = new SvelteSet(force_expanded)
212
+ if (collapsed_changed) collapsed_paths = new SvelteSet(collapsed_paths)
213
+ if (force_expanded_changed) force_expanded = new SvelteSet(force_expanded)
214
+ }
215
+
216
+ function toggle_collapse(path: string, is_currently_collapsed: boolean): void {
217
+ set_collapsed([path], () => !is_currently_collapsed)
227
218
  }
228
219
 
229
220
  // Toggle collapse recursively for all descendants
230
221
  function toggle_collapse_recursive(path: string, collapse: boolean): void {
231
- for (const desc of get_descendants(path)) {
232
- if (collapse) {
233
- force_expanded.delete(desc)
234
- collapsed_paths.add(desc)
235
- } else {
236
- collapsed_paths.delete(desc)
237
- force_expanded.add(desc)
238
- }
239
- }
240
- collapsed_paths = new SvelteSet(collapsed_paths)
241
- force_expanded = new SvelteSet(force_expanded)
222
+ set_collapsed(get_descendants(path), () => collapse)
242
223
  }
243
224
 
244
225
  // Get all descendant paths of a given path (including the path itself)
@@ -358,19 +339,9 @@
358
339
  // Pre-compute ghost children map for O(1) lookup per node
359
340
  let ghost_map = $derived(diff_map ? build_ghost_map(diff_map) : new Map())
360
341
 
361
- // Collapse all descendants but keep the given node expanded (single batch)
342
+ // Collapse all descendants but keep the given node expanded
362
343
  function collapse_children_only(target_path: string): void {
363
- for (const desc of get_descendants(target_path)) {
364
- if (desc === target_path) {
365
- collapsed_paths.delete(desc)
366
- force_expanded.add(desc)
367
- } else {
368
- force_expanded.delete(desc)
369
- collapsed_paths.add(desc)
370
- }
371
- }
372
- collapsed_paths = new SvelteSet(collapsed_paths)
373
- force_expanded = new SvelteSet(force_expanded)
344
+ set_collapsed(get_descendants(target_path), (desc) => desc !== target_path)
374
345
  }
375
346
 
376
347
  // Context menu handlers
@@ -512,6 +483,13 @@
512
483
 
513
484
  // Keyboard navigation at tree level
514
485
  function handle_tree_keydown(event: KeyboardEvent) {
486
+ // F3 navigates search matches (search input handles its own F3/Enter)
487
+ if (event.key === `F3`) {
488
+ event.preventDefault()
489
+ if (event.shiftKey) go_to_prev_match()
490
+ else go_to_next_match()
491
+ return
492
+ }
515
493
  // Escape closes context menu first, then clears selection
516
494
  if (event.key === `Escape`) {
517
495
  if (context_menu_state) {
@@ -600,15 +578,7 @@
600
578
  role="tree"
601
579
  aria-label="JSON tree viewer"
602
580
  {...rest}
603
- onkeydown={(event) => {
604
- handle_tree_keydown(event)
605
- // F3 at tree level for match navigation
606
- if (event.key === `F3`) {
607
- event.preventDefault()
608
- if (event.shiftKey) go_to_prev_match()
609
- else go_to_next_match()
610
- }
611
- }}
581
+ onkeydown={handle_tree_keydown}
612
582
  >
613
583
  {#if show_header}
614
584
  <header class="json-tree-header">
@@ -686,30 +656,16 @@
686
656
  <button type="button" onclick={collapse_all} title="Collapse all" {@attach tooltip()}>
687
657
  <Icon icon="Collapse" style="width: 14px; height: 14px" />
688
658
  </button>
689
- <button
690
- type="button"
691
- onclick={() => collapse_to_level(1)}
692
- title="Collapse to level 1"
693
- {@attach tooltip()}
694
- >
695
- 1
696
- </button>
697
- <button
698
- type="button"
699
- onclick={() => collapse_to_level(2)}
700
- title="Collapse to level 2"
701
- {@attach tooltip()}
702
- >
703
- 2
704
- </button>
705
- <button
706
- type="button"
707
- onclick={() => collapse_to_level(3)}
708
- title="Collapse to level 3"
709
- {@attach tooltip()}
710
- >
711
- 3
712
- </button>
659
+ {#each [1, 2, 3] as level (level)}
660
+ <button
661
+ type="button"
662
+ onclick={() => collapse_to_level(level)}
663
+ title="Collapse to level {level}"
664
+ {@attach tooltip()}
665
+ >
666
+ {level}
667
+ </button>
668
+ {/each}
713
669
  </div>
714
670
  <div class="divider"></div>
715
671
  <div class="controls">
@@ -910,9 +866,6 @@
910
866
  border-radius: var(--jt-border-radius, 4px);
911
867
  overflow: hidden;
912
868
  }
913
- /* --jt-header-bg, --jt-header-border, --jt-btn-bg, --jt-btn-hover-bg, --jt-btn-active-bg
914
- intentionally removed in favor of transparent/opacity-based button styling.
915
- Use --jt-hover-bg to customize the button hover background. */
916
869
  .json-tree-header {
917
870
  display: flex;
918
871
  align-items: center;
@@ -28,8 +28,6 @@
28
28
 
29
29
  // Expanded state for long strings
30
30
  let is_expanded = $state(false)
31
-
32
- // Extract max_string_length threshold for reuse
33
31
  let max_len = $derived(ctx?.settings.max_string_length ?? 200)
34
32
  let is_long_string = $derived(value_type === `string` && (value as string).length > max_len)
35
33
 
@@ -84,7 +82,6 @@
84
82
 
85
83
  // Format display value - strings use custom truncation, others use format_preview
86
84
  let display_value = $derived.by(() => {
87
- if (value_type === `circular`) return `[Circular]`
88
85
  if (value_type === `string`) {
89
86
  const str = value as string
90
87
  return is_long_string && !is_expanded ? `"${str.slice(0, max_len)}..."` : `"${str}"`
@@ -92,10 +89,6 @@
92
89
  return format_preview(value)
93
90
  })
94
91
 
95
- // Check if string is truncated
96
- let is_truncated = $derived(is_long_string && !is_expanded)
97
-
98
- // Toggle string expansion
99
92
  function toggle_expand(event: MouseEvent) {
100
93
  event.stopPropagation()
101
94
  is_expanded = !is_expanded
@@ -186,18 +179,14 @@
186
179
  {:else}
187
180
  {display_value}
188
181
  {/if}
189
- {#if is_truncated}
182
+ {#if is_long_string}
190
183
  <button
191
184
  type="button"
192
185
  class="expand-btn"
193
186
  onclick={toggle_expand}
194
- title="Show full string"
187
+ title={is_expanded ? `Collapse string` : `Show full string`}
195
188
  >
196
- ...
197
- </button>
198
- {:else if is_long_string && is_expanded}
199
- <button type="button" class="expand-btn" onclick={toggle_expand} title="Collapse string">
200
-
189
+ {is_expanded ? `▲` : `...`}
201
190
  </button>
202
191
  {/if}
203
192
  {#if ctx?.settings.show_data_types && value_type !== `null` && value_type !== `undefined`}
@@ -259,10 +248,6 @@
259
248
  &.error {
260
249
  color: var(--jt-error, light-dark(#a31515, #f48771));
261
250
  }
262
- &.circular {
263
- color: var(--jt-circular, light-dark(#808080, #808080));
264
- font-style: italic;
265
- }
266
251
  &.changed {
267
252
  animation: value-change 1s ease-out;
268
253
  }
@@ -4,15 +4,12 @@ export declare const is_expandable_type: (value_type: JsonValueType) => boolean;
4
4
  export declare const is_primitive_type: (value_type: JsonValueType) => boolean;
5
5
  export declare const is_expandable: (value: unknown) => boolean;
6
6
  export declare function get_child_count(value: unknown): number;
7
- export declare function format_path(segments: (string | number)[]): string;
8
- export declare function build_path(parent_path: string, key: string | number): string;
9
7
  export declare function serialize_for_copy(value: unknown): string;
10
8
  export declare function format_preview(value: unknown, max_length?: number): string;
11
9
  export declare function matches_search(path: string, key: string | number | null, value: unknown, query: string): boolean;
12
10
  export declare function collect_all_paths(value: unknown, current_path?: string, max_depth?: number, current_depth?: number, seen?: WeakSet<object>): string[];
13
11
  export declare function find_matching_paths(value: unknown, query: string, current_path?: string, current_key?: string | number | null, seen?: WeakSet<object>): Set<string>;
14
12
  export declare function get_ancestor_paths(path: string): string[];
15
- export declare function parse_path(path: string): (string | number)[];
16
13
  export declare function values_equal(val_a: unknown, val_b: unknown): boolean;
17
14
  export declare function parse_edited_value(text: string): unknown;
18
15
  export declare function set_at_path(root: unknown, path_str: string, new_value: unknown, root_label?: string): unknown;