@behindthemusictree/genre-tree-view 5.0.0 → 6.4.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.
package/dist/index.cjs CHANGED
@@ -51,7 +51,6 @@ module.exports = __toCommonJS(src_exports);
51
51
  // src/GenreTree.tsx
52
52
  var import_react8 = require("react");
53
53
  var d3 = __toESM(require("d3"), 1);
54
- var import_md6 = require("react-icons/md");
55
54
 
56
55
  // src/NodeHelper.tsx
57
56
  var import_server = __toESM(require("react-dom/server"), 1);
@@ -75,6 +74,9 @@ var MENU_WIDTH = 130;
75
74
  var PER_TREE_ACCENT_DOT = true;
76
75
  var ROOT_TINT_RATIO = 0.08;
77
76
  var POP_SECTOR_TINT_RATIO = 0.8;
77
+ var UNDERGROUND_DARKEN_RATIO = 0.62;
78
+ var UNDERGROUND_SECTOR_FILL_OPACITY = 0.15;
79
+ var SCENE_SECTOR_OUTER_RADIUS = 1e5;
78
80
  var CENTER_NODE_NAME = "Mainstream Pop";
79
81
  var TREE_COLORS = [
80
82
  "#4F46E5",
@@ -156,6 +158,11 @@ function isMainstreamPopLineage(index, nodeId) {
156
158
  const topRoot = index.ancestorsOf(nodeId)[0] ?? index.byId.get(nodeId);
157
159
  return topRoot?.name === CENTER_NODE_NAME;
158
160
  }
161
+ function shadeSurface(hex, ratio) {
162
+ const channel = (offset) => parseInt(hex.slice(offset, offset + 2), 16);
163
+ const toHex = (value) => Math.round(value * ratio).toString(16).padStart(2, "0");
164
+ return `#${toHex(channel(1))}${toHex(channel(3))}${toHex(channel(5))}`;
165
+ }
159
166
  function getGenreNodeStyle(index, nodeId) {
160
167
  if (isMainstreamPopLineage(index, nodeId)) {
161
168
  return { fill: tintSurface("#ffffff", POP_SECTOR_TINT_RATIO), textColor: TEXT_COLOR };
@@ -174,6 +181,9 @@ var MIN_NODE_WIDTH = 180;
174
181
  var MAX_NODE_WIDTH = 700;
175
182
  var MIN_NODE_HEIGHT = 35;
176
183
  var MAX_NODE_HEIGHT = 120;
184
+ var NODE_SIZE_REFERENCE_COUNT = 100;
185
+ var HIGH_DENSITY_NODE_MAX_FLOOR_SCALE = 1.5;
186
+ var LOW_DENSITY_DOT_MAX_RADIUS = 70;
177
187
  var TOOLBAR_MENU_X_GAP = 4;
178
188
  var ACTIONS_OVERLAY_WIDTH = MENU_WIDTH + TOOLBAR_MENU_X_GAP;
179
189
  var ACTIONS_OVERLAY_HEIGHT = RECT_BASE_DIMENSIONS.HEIGHT * 7;
@@ -188,23 +198,47 @@ var HORIZONTAL_SEPARATION_BETWEEN_NODES = (NODE_DIMENSIONS.WIDTH + HORIZONTAL_SE
188
198
  var VERTICAL_SEPARATION_BETWEEN_NODES = NODE_DIMENSIONS.HEIGHT + VERTICAL_SEPARATION_BETWEEN_RECTANGLES;
189
199
  var VERTICAL_ORIENTATION_DEPTH_SEPARATION = VERTICAL_SEPARATION_BETWEEN_NODES * 2 * DEPTH_AXIS_SPACING_FACTOR;
190
200
  var SIBLING_SEPARATION_BETWEEN_NODES = MAX_NODE_WIDTH + HORIZONTAL_SEPARATION_BETWEEN_RECTANGLES;
191
- function getItemCountRange(nodes) {
192
- if (nodes.length === 0) return { min: 0, max: 0 };
201
+ function getDensityAdjustedMax(baseMax, min, nodeCount) {
202
+ const safeNodeCount = Number.isFinite(nodeCount) ? Math.max(1, nodeCount) : 1;
203
+ const availableRangeScale = Math.min(1, NODE_SIZE_REFERENCE_COUNT / safeNodeCount);
204
+ const denseTreeFloor = min * HIGH_DENSITY_NODE_MAX_FLOOR_SCALE;
205
+ return denseTreeFloor + (baseMax - denseTreeFloor) * availableRangeScale;
206
+ }
207
+ function getDensityAdjustedDotMax(nodeCount) {
208
+ const safeNodeCount = Number.isFinite(nodeCount) ? Math.max(1, nodeCount) : 1;
209
+ const inverseDensityRadius = NODE_DOT_MAX_RADIUS * NODE_SIZE_REFERENCE_COUNT / safeNodeCount;
210
+ const denseTreeFloor = NODE_DOT_MIN_RADIUS * HIGH_DENSITY_NODE_MAX_FLOOR_SCALE;
211
+ return Math.max(denseTreeFloor, Math.min(LOW_DENSITY_DOT_MAX_RADIUS, inverseDensityRadius));
212
+ }
213
+ function getItemCountRange(nodes, options = {}) {
193
214
  const counts = nodes.map((node) => node.itemCount);
194
- return { min: Math.min(...counts), max: Math.max(...counts) };
215
+ const validScaleMax = options.itemCountScaleMax !== void 0 && Number.isFinite(options.itemCountScaleMax) && options.itemCountScaleMax >= 0;
216
+ const nodeCount = options.nodeCountForSizing ?? nodes.length;
217
+ const observedMin = counts.length === 0 ? 0 : Math.min(...counts);
218
+ const observedMax = counts.length === 0 ? 0 : Math.max(...counts);
219
+ const includeDensityCaps = options.nodeCountForSizing !== void 0 || validScaleMax;
220
+ return {
221
+ min: validScaleMax ? 0 : observedMin,
222
+ max: validScaleMax ? options.itemCountScaleMax : observedMax,
223
+ ...includeDensityCaps && {
224
+ maxNodeWidth: getDensityAdjustedMax(MAX_NODE_WIDTH, MIN_NODE_WIDTH, nodeCount),
225
+ maxNodeHeight: getDensityAdjustedMax(MAX_NODE_HEIGHT, MIN_NODE_HEIGHT, nodeCount),
226
+ maxDotRadius: getDensityAdjustedDotMax(nodeCount)
227
+ }
228
+ };
195
229
  }
196
230
  function logarithmicPosition(itemCount, range) {
197
231
  const { min, max } = range;
198
232
  const logMin = Math.log1p(Math.max(min, 0));
199
233
  const logMax = Math.log1p(Math.max(max, 0));
200
234
  const logValue = Math.log1p(Math.max(itemCount, 0));
201
- return logMax > logMin ? (logValue - logMin) / (logMax - logMin) : 0;
235
+ return logMax > logMin ? Math.max(0, Math.min(1, (logValue - logMin) / (logMax - logMin))) : 0;
202
236
  }
203
237
  function calculateNodeDimensions(itemCount, range) {
204
238
  const t = logarithmicPosition(itemCount, range);
205
239
  return {
206
- WIDTH: Math.round(MIN_NODE_WIDTH + t * (MAX_NODE_WIDTH - MIN_NODE_WIDTH)),
207
- HEIGHT: Math.round(MIN_NODE_HEIGHT + t * (MAX_NODE_HEIGHT - MIN_NODE_HEIGHT))
240
+ WIDTH: Math.round(MIN_NODE_WIDTH + t * ((range.maxNodeWidth ?? MAX_NODE_WIDTH) - MIN_NODE_WIDTH)),
241
+ HEIGHT: Math.round(MIN_NODE_HEIGHT + t * ((range.maxNodeHeight ?? MAX_NODE_HEIGHT) - MIN_NODE_HEIGHT))
208
242
  };
209
243
  }
210
244
  var NODE_MIN_BORDER_WIDTH = 1;
@@ -244,10 +278,13 @@ function calculateRootLabelFontSize(layoutFontSize, zoomScale) {
244
278
  return Math.max(layoutFontSize, ROOT_LABEL_MIN_SCREEN_PX / zoomScale);
245
279
  }
246
280
  var NODE_DOT_MIN_RADIUS = 3;
247
- var NODE_DOT_MAX_RADIUS = 50;
281
+ var NODE_DOT_MAX_RADIUS = 24;
248
282
  var NODE_DOT_LABEL_GAP = 6;
283
+ var NODE_LAYOUT_SLOT_RADIUS = 8;
249
284
  function calculateNodeDotRadius(itemCount, range) {
250
- return NODE_DOT_MIN_RADIUS + logarithmicPosition(itemCount, range) * (NODE_DOT_MAX_RADIUS - NODE_DOT_MIN_RADIUS);
285
+ const minArea = NODE_DOT_MIN_RADIUS ** 2;
286
+ const maxArea = (range.maxDotRadius ?? NODE_DOT_MAX_RADIUS) ** 2;
287
+ return Math.sqrt(minArea + logarithmicPosition(itemCount, range) * (maxArea - minArea));
251
288
  }
252
289
  var WHEEL_RADIUS = 260;
253
290
  var WHEEL_VIEWPORT_HEIGHT = WHEEL_RADIUS * 2 + MAX_NODE_HEIGHT / 2;
@@ -281,19 +318,22 @@ var PAN_MIN_VISIBLE_PX = 80;
281
318
  var PAN_ZOOM_SETTLE_MS = 100;
282
319
  var DEFAULT_VIEWPORT_CENTER_GUARD_MS = 1e3;
283
320
  var INFO_PANEL_WIDTH = 280;
321
+ var INFO_PANEL_SHEET_MAX_WIDTH = 767;
322
+ var INFO_PANEL_SHEET_PEEK_FRACTION = 0.4;
323
+ var INFO_PANEL_SHEET_DRAG_THRESHOLD_PX = 32;
284
324
 
285
325
  // src/NodeHelper.tsx
286
326
  var import_jsx_runtime = require("react/jsx-runtime");
287
327
  function buildTreeHierarchyStructure(d3Lib, nodes) {
288
328
  return d3Lib.stratify().id((d) => d.id).parentId((d) => d.parentId)(nodes);
289
329
  }
290
- function calculateLocalRootDimensions(d3Lib, nodes) {
330
+ function calculateLocalRootDimensions(d3Lib, nodes, options = {}) {
291
331
  const root = buildTreeHierarchyStructure(d3Lib, nodes);
292
- const range = getItemCountRange(root.descendants().map((d) => d.data));
332
+ const range = getItemCountRange(root.descendants().map((d) => d.data), options);
293
333
  return calculateNodeDimensions(root.data.itemCount, range);
294
334
  }
295
- function calculateRootAnchorClearance(d3Lib, nodes, rootItemCount, rootItemCountRange, axis) {
296
- const localExtent = calculateLocalRootDimensions(d3Lib, nodes)[axis];
335
+ function calculateRootAnchorClearance(d3Lib, nodes, rootItemCount, rootItemCountRange, axis, options = {}) {
336
+ const localExtent = calculateLocalRootDimensions(d3Lib, nodes, options)[axis];
297
337
  const displayExtent = calculateNodeDimensions(rootItemCount, rootItemCountRange)[axis];
298
338
  return (localExtent + displayExtent) / 2;
299
339
  }
@@ -377,13 +417,31 @@ function appendPaths(d3Lib, svg, treeData, itemCountRange, orientation = "horizo
377
417
  }
378
418
 
379
419
  // src/tree-renderer.ts
380
- function calculateSvgDimensions(d3Lib, treeData, orientation = "horizontal", hideRoot = false, depthSpacingScale = 1) {
381
- const nodes = treeData.descendants();
382
- const itemCountRange = getItemCountRange(nodes.map((d) => d.data));
383
- const maxNodeDimensions = { WIDTH: MAX_NODE_WIDTH, HEIGHT: MAX_NODE_HEIGHT };
420
+ function getLayoutSpacings(itemCountRange, depthSpacingScale = 1) {
421
+ const maxWidth = itemCountRange.maxNodeWidth ?? MAX_NODE_WIDTH;
422
+ const maxHeight = itemCountRange.maxNodeHeight ?? MAX_NODE_HEIGHT;
423
+ const verticalSeparation = maxHeight + VERTICAL_SEPARATION_BETWEEN_RECTANGLES;
424
+ return {
425
+ horizontalDepth: (maxWidth + ACTIONS_OVERLAY_WIDTH + HORIZONTAL_SEPARATION_BETWEEN_RECTANGLES) * DEPTH_AXIS_SPACING_FACTOR * depthSpacingScale,
426
+ verticalBreadth: verticalSeparation,
427
+ verticalDepth: verticalSeparation * 2 * DEPTH_AXIS_SPACING_FACTOR * depthSpacingScale,
428
+ verticalSibling: maxWidth + HORIZONTAL_SEPARATION_BETWEEN_RECTANGLES
429
+ };
430
+ }
431
+ function calculateSvgDimensions(d3Lib, treeData, orientation = "horizontal", hideRoot = false, depthSpacingScale = 1, suppliedItemCountRange) {
432
+ const treeNodes = treeData.descendants();
433
+ const nodes = treeNodes;
434
+ const itemCountRange = suppliedItemCountRange ?? getItemCountRange(
435
+ treeNodes.map((d) => d.data)
436
+ );
437
+ const maxNodeDimensions = {
438
+ WIDTH: itemCountRange.maxNodeWidth ?? MAX_NODE_WIDTH,
439
+ HEIGHT: itemCountRange.maxNodeHeight ?? MAX_NODE_HEIGHT
440
+ };
384
441
  const maximumLevel = d3Lib.max(nodes, (d) => d.depth);
385
- const depthSeparation = VERTICAL_ORIENTATION_DEPTH_SEPARATION * depthSpacingScale;
386
- const depthSeparationHorizontal = HORIZONTAL_SEPARATION_BETWEEN_NODES * depthSpacingScale;
442
+ const layoutSpacings = getLayoutSpacings(itemCountRange, depthSpacingScale);
443
+ const depthSeparation = layoutSpacings.verticalDepth;
444
+ const depthSeparationHorizontal = layoutSpacings.horizontalDepth;
387
445
  if (isVerticalOrientation(orientation)) {
388
446
  const svgHeight2 = hideRoot ? maximumLevel * depthSeparation : maximumLevel * depthSeparation + maxNodeDimensions.HEIGHT + ACTIONS_OVERLAY_HEIGHT / 2;
389
447
  const rootWidth = calculateNodeDimensions(
@@ -445,10 +503,15 @@ function calculateSvgDimensions(d3Lib, treeData, orientation = "horizontal", hid
445
503
  const svgWidth = maximumLevel * depthSeparationHorizontal + maxNodeDimensions.WIDTH + ACTIONS_OVERLAY_WIDTH;
446
504
  return { svgWidth, svgHeight, highestVerticalCoordinate, rootDepthOffset: 0 };
447
505
  }
448
- function setupTreeLayout(d3Lib, treeData, highestVerticalCoordinate, orientation = "horizontal", rootDepthOffset = 0, svgWidth = 0, depthSpacingScale = 1) {
506
+ function setupTreeLayout(d3Lib, treeData, highestVerticalCoordinate, orientation = "horizontal", rootDepthOffset = 0, svgWidth = 0, depthSpacingScale = 1, suppliedItemCountRange) {
507
+ const treeNodes = treeData.descendants();
508
+ const itemCountRange = suppliedItemCountRange ?? getItemCountRange(
509
+ treeNodes.map((d) => d.data)
510
+ );
511
+ const layoutSpacings = getLayoutSpacings(itemCountRange, depthSpacingScale);
449
512
  if (isVerticalOrientation(orientation)) {
450
513
  const maximumLevel = d3Lib.max(treeData.descendants(), (d) => d.depth);
451
- const depthSeparation = VERTICAL_ORIENTATION_DEPTH_SEPARATION * depthSpacingScale;
514
+ const depthSeparation = layoutSpacings.verticalDepth;
452
515
  treeData.each(function(d) {
453
516
  d.x = d.x + highestVerticalCoordinate;
454
517
  d.y = depthAxisSign(orientation) === -1 ? (maximumLevel - d.depth) * depthSeparation : d.depth * depthSeparation + rootDepthOffset;
@@ -470,24 +533,30 @@ function setupTreeLayout(d3Lib, treeData, highestVerticalCoordinate, orientation
470
533
  });
471
534
  return treeData;
472
535
  }
473
- function createTreeLayout(d3Lib, root, orientation = "horizontal", depthSpacingScale = 1) {
536
+ function createTreeLayout(d3Lib, root, orientation = "horizontal", depthSpacingScale = 1, suppliedItemCountRange) {
537
+ const nodes = root.descendants();
538
+ const itemCountRange = suppliedItemCountRange ?? getItemCountRange(
539
+ nodes.map((d) => d.data)
540
+ );
541
+ const layoutSpacings = getLayoutSpacings(itemCountRange, depthSpacingScale);
474
542
  const nodeSize = isVerticalOrientation(orientation) ? [
475
- SIBLING_SEPARATION_BETWEEN_NODES,
476
- VERTICAL_ORIENTATION_DEPTH_SEPARATION * depthSpacingScale
543
+ layoutSpacings.verticalSibling,
544
+ layoutSpacings.verticalDepth
477
545
  ] : [
478
- VERTICAL_SEPARATION_BETWEEN_NODES,
479
- HORIZONTAL_SEPARATION_BETWEEN_NODES * depthSpacingScale
546
+ layoutSpacings.verticalBreadth,
547
+ layoutSpacings.horizontalDepth
480
548
  ];
481
549
  const treeLayout = d3Lib.tree().nodeSize(nodeSize).separation(() => 1);
482
550
  return treeLayout(root);
483
551
  }
484
- function renderTree(d3Lib, svgRef, treeData, svgWidth, svgHeight, reparentingNodeId, reparentForbiddenIds, rootColor, callbacks, orientation = "horizontal", hideRoot = false, selectedNodeId = null) {
552
+ function renderTree(d3Lib, svgRef, treeData, svgWidth, svgHeight, reparentingNodeId, reparentForbiddenIds, rootColor, callbacks, orientation = "horizontal", hideRoot = false, selectedNodeId = null, suppliedItemCountRange) {
485
553
  const { onReparentTargetSelect, onNodeClick } = callbacks;
486
554
  if (!svgRef.current) {
487
555
  throw new Error("SVG reference is null");
488
556
  }
489
- const itemCountRange = getItemCountRange(
490
- treeData.descendants().map((d) => d.data)
557
+ const treeNodes = treeData.descendants();
558
+ const itemCountRange = suppliedItemCountRange ?? getItemCountRange(
559
+ treeNodes.map((d) => d.data)
491
560
  );
492
561
  const svg = d3Lib.select(svgRef.current).append("svg").attr("width", svgWidth).attr("height", svgHeight).style("overflow", "visible").append("g");
493
562
  const safeRootId = treeData.data.id.replace(/[^a-zA-Z0-9_-]/g, "_");
@@ -634,6 +703,48 @@ function renderTree(d3Lib, svgRef, treeData, svgWidth, svgHeight, reparentingNod
634
703
  return svg;
635
704
  }
636
705
 
706
+ // src/FloatingControls.tsx
707
+ var import_ui = require("@behindthemusictree/ui");
708
+
709
+ // src/BusyIndicator.tsx
710
+ var import_jsx_runtime2 = require("react/jsx-runtime");
711
+ function BusyIndicator() {
712
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "gtv-busy", role: "status", "aria-live": "polite", children: [
713
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "gtv-busy-ring", "aria-hidden": "true" }),
714
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "gtv-skeleton-sr-only", children: "Updating genre tree\u2026" })
715
+ ] });
716
+ }
717
+
718
+ // src/FloatingControls.tsx
719
+ var import_jsx_runtime3 = require("react/jsx-runtime");
720
+ function FloatingControls({ panZoom, onFit, orientation, zoomControls, leading, busy }) {
721
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
722
+ "div",
723
+ {
724
+ className: [
725
+ "gtv-wheel-floating-controls",
726
+ orientation === "horizontal" && "gtv-wheel-floating-controls--horizontal"
727
+ ].filter(Boolean).join(" "),
728
+ children: [
729
+ busy && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(BusyIndicator, {}),
730
+ leading && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "gtv-floating-controls-leading", children: leading }),
731
+ zoomControls !== "never" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
732
+ import_ui.ZoomControls,
733
+ {
734
+ onZoomIn: panZoom.zoomIn,
735
+ onZoomOut: panZoom.zoomOut,
736
+ onFit,
737
+ canZoomIn: panZoom.canZoomIn,
738
+ canZoomOut: panZoom.canZoomOut,
739
+ orientation,
740
+ className: zoomControls === "auto" ? "gtv-zoom-controls gtv-zoom-controls--auto" : "gtv-zoom-controls"
741
+ }
742
+ )
743
+ ]
744
+ }
745
+ );
746
+ }
747
+
637
748
  // src/use-pan-zoom.ts
638
749
  var import_react = require("react");
639
750
 
@@ -1032,9 +1143,10 @@ function usePanZoom(viewportRef, wheelZoom = "always", stageRef, options = {}) {
1032
1143
  const newScale = clampZoomScale(Math.max(targetScale, currentScale), minScaleRef.current);
1033
1144
  const centerX = (rect.left + rect.width / 2 - viewportRect.left - view.x) / currentScale;
1034
1145
  const centerY = (rect.top + rect.height / 2 - viewportRect.top - view.y) / currentScale;
1035
- const availableCenterX = !obscured ? viewportRect.width / 2 : obscured.side === "left" ? obscured.width + (viewportRect.width - obscured.width) / 2 : (viewportRect.width - obscured.width) / 2;
1146
+ const availableCenterX = obscured?.side === "left" ? obscured.width + (viewportRect.width - obscured.width) / 2 : obscured?.side === "right" ? (viewportRect.width - obscured.width) / 2 : viewportRect.width / 2;
1147
+ const availableCenterY = obscured?.side === "bottom" ? (viewportRect.height - obscured.height) / 2 : viewportRect.height / 2;
1036
1148
  const targetPanX = availableCenterX - centerX * newScale;
1037
- const targetPanY = viewportRect.height / 2 - centerY * newScale;
1149
+ const targetPanY = availableCenterY - centerY * newScale;
1038
1150
  zoomAnimationRef.current = null;
1039
1151
  if (animationFrameIdRef.current !== null) {
1040
1152
  cancelAnimationFrame(animationFrameIdRef.current);
@@ -1188,6 +1300,9 @@ function resolveInfoPanelSide() {
1188
1300
  }
1189
1301
  function resolveInfoPanelObscuredArea(element, viewport, panelWidth) {
1190
1302
  if (!element || !viewport) return null;
1303
+ if (viewport.clientWidth > 0 && viewport.clientWidth <= INFO_PANEL_SHEET_MAX_WIDTH) {
1304
+ return { side: "bottom", height: viewport.clientHeight * INFO_PANEL_SHEET_PEEK_FRACTION };
1305
+ }
1191
1306
  return { width: panelWidth, side: resolveInfoPanelSide() };
1192
1307
  }
1193
1308
 
@@ -1274,9 +1389,9 @@ var import_md4 = require("react-icons/md");
1274
1389
 
1275
1390
  // src/InfoPanelExpandButton.tsx
1276
1391
  var import_md = require("react-icons/md");
1277
- var import_jsx_runtime2 = require("react/jsx-runtime");
1392
+ var import_jsx_runtime4 = require("react/jsx-runtime");
1278
1393
  function InfoPanelExpandButton({ expanded, onExpandedChange, style }) {
1279
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1394
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1280
1395
  "button",
1281
1396
  {
1282
1397
  type: "button",
@@ -1284,7 +1399,7 @@ function InfoPanelExpandButton({ expanded, onExpandedChange, style }) {
1284
1399
  style,
1285
1400
  onClick: () => onExpandedChange(!expanded),
1286
1401
  "aria-label": expanded ? "Collapse panel" : "Expand panel",
1287
- children: expanded ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_md.MdCloseFullscreen, { className: "gtv-icon", size: 16 }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_md.MdOpenInFull, { className: "gtv-icon", size: 16 })
1402
+ children: expanded ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_md.MdCloseFullscreen, { className: "gtv-icon", size: 16 }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_md.MdOpenInFull, { className: "gtv-icon", size: 16 })
1288
1403
  }
1289
1404
  );
1290
1405
  }
@@ -1292,11 +1407,11 @@ function InfoPanelExpandButton({ expanded, onExpandedChange, style }) {
1292
1407
  // src/InfoPanelSection.tsx
1293
1408
  var import_react4 = require("react");
1294
1409
  var import_md2 = require("react-icons/md");
1295
- var import_jsx_runtime3 = require("react/jsx-runtime");
1410
+ var import_jsx_runtime5 = require("react/jsx-runtime");
1296
1411
  function InfoPanelSection({ title, children }) {
1297
1412
  const [open, setOpen] = (0, import_react4.useState)(true);
1298
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "gtv-info-panel-children", children: [
1299
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1413
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "gtv-info-panel-children", children: [
1414
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
1300
1415
  "button",
1301
1416
  {
1302
1417
  type: "button",
@@ -1304,8 +1419,8 @@ function InfoPanelSection({ title, children }) {
1304
1419
  "aria-expanded": open,
1305
1420
  onClick: () => setOpen((isOpen) => !isOpen),
1306
1421
  children: [
1307
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "gtv-info-panel-children-title", children: title }),
1308
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_md2.MdExpandMore, { className: "gtv-icon gtv-info-panel-section-chevron", size: 16 })
1422
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "gtv-info-panel-children-title", children: title }),
1423
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_md2.MdExpandMore, { className: "gtv-icon gtv-info-panel-section-chevron", size: 16 })
1309
1424
  ]
1310
1425
  }
1311
1426
  ),
@@ -1318,7 +1433,7 @@ var import_react5 = require("react");
1318
1433
  var import_md3 = require("react-icons/md");
1319
1434
  var import_fa = require("react-icons/fa");
1320
1435
  var import_pi2 = require("react-icons/pi");
1321
- var import_jsx_runtime4 = require("react/jsx-runtime");
1436
+ var import_jsx_runtime6 = require("react/jsx-runtime");
1322
1437
  function NodeToolbar({
1323
1438
  node,
1324
1439
  playingNodeId,
@@ -1339,9 +1454,9 @@ function NodeToolbar({
1339
1454
  const isPlayable = node.itemCount > 0;
1340
1455
  const isThisPlaying = playingNodeId === node.id;
1341
1456
  const playLabel = isThisPlaying && playState === "playing" ? "Pause" : isThisPlaying && playState === "loading" ? "Loading..." : "Play";
1342
- const playIcon = isThisPlaying && playState === "playing" ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_fa.FaPause, { className: "gtv-icon", size: "0.6em" }) : isThisPlaying && playState === "loading" ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_fa.FaSpinner, { className: "gtv-icon gtv-icon--spin", size: "0.6em" }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_fa.FaPlay, { className: "gtv-icon", size: "0.6em" });
1343
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: ["gtv-toolbar", className].filter(Boolean).join(" "), children: [
1344
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1457
+ const playIcon = isThisPlaying && playState === "playing" ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_fa.FaPause, { className: "gtv-icon", size: "0.6em" }) : isThisPlaying && playState === "loading" ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_fa.FaSpinner, { className: "gtv-icon gtv-icon--spin", size: "0.6em" }) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_fa.FaPlay, { className: "gtv-icon", size: "0.6em" });
1458
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: ["gtv-toolbar", className].filter(Boolean).join(" "), children: [
1459
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1345
1460
  "button",
1346
1461
  {
1347
1462
  type: "button",
@@ -1353,8 +1468,8 @@ function NodeToolbar({
1353
1468
  children: playIcon
1354
1469
  }
1355
1470
  ),
1356
- isActionable && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
1357
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1471
+ isActionable && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
1472
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1358
1473
  "button",
1359
1474
  {
1360
1475
  type: "button",
@@ -1362,13 +1477,13 @@ function NodeToolbar({
1362
1477
  title: "Add sub-genre",
1363
1478
  "aria-label": "Add sub-genre",
1364
1479
  onClick: () => onAddChild?.(node.id),
1365
- children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_fa.FaPlus, { className: "gtv-icon", size: "0.6em" })
1480
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_fa.FaPlus, { className: "gtv-icon", size: "0.6em" })
1366
1481
  }
1367
1482
  ),
1368
1483
  primaryExtraActions.map((action) => {
1369
1484
  const enabled = action.enabled ? action.enabled(node) : true;
1370
1485
  const label = action.label(node);
1371
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1486
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1372
1487
  "button",
1373
1488
  {
1374
1489
  type: "button",
@@ -1382,8 +1497,8 @@ function NodeToolbar({
1382
1497
  action.key
1383
1498
  );
1384
1499
  }),
1385
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "gtv-toolbar-overflow", children: [
1386
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1500
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "gtv-toolbar-overflow", children: [
1501
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
1387
1502
  "button",
1388
1503
  {
1389
1504
  type: "button",
@@ -1393,11 +1508,11 @@ function NodeToolbar({
1393
1508
  "aria-haspopup": "menu",
1394
1509
  "aria-expanded": menuOpen,
1395
1510
  onClick: () => setMenuOpen((open) => !open),
1396
- children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_md3.MdMoreVert, { size: "0.6em" })
1511
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_md3.MdMoreVert, { size: "0.6em" })
1397
1512
  }
1398
1513
  ),
1399
- menuOpen && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "gtv-menu-card gtv-toolbar-overflow-menu", children: [
1400
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
1514
+ menuOpen && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "gtv-menu-card gtv-toolbar-overflow-menu", children: [
1515
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
1401
1516
  "button",
1402
1517
  {
1403
1518
  type: "button",
@@ -1407,12 +1522,12 @@ function NodeToolbar({
1407
1522
  onRenameRequest?.(node);
1408
1523
  },
1409
1524
  children: [
1410
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "gtv-menu-row-icon", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_md3.MdModeEdit, { className: "gtv-icon", size: 13 }) }),
1411
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "gtv-menu-row-label", children: "Rename" })
1525
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "gtv-menu-row-icon", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_md3.MdModeEdit, { className: "gtv-icon", size: 13 }) }),
1526
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "gtv-menu-row-label", children: "Rename" })
1412
1527
  ]
1413
1528
  }
1414
1529
  ),
1415
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
1530
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
1416
1531
  "button",
1417
1532
  {
1418
1533
  type: "button",
@@ -1422,14 +1537,14 @@ function NodeToolbar({
1422
1537
  onReparentRequest?.(node);
1423
1538
  },
1424
1539
  children: [
1425
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "gtv-menu-row-icon", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_pi2.PiGraphFill, { className: "gtv-icon", size: 13 }) }),
1426
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "gtv-menu-row-label", children: "Change parent" })
1540
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "gtv-menu-row-icon", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_pi2.PiGraphFill, { className: "gtv-icon", size: 13 }) }),
1541
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "gtv-menu-row-label", children: "Change parent" })
1427
1542
  ]
1428
1543
  }
1429
1544
  ),
1430
1545
  overflowExtraActions.map((action) => {
1431
1546
  const enabled = action.enabled ? action.enabled(node) : true;
1432
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
1547
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
1433
1548
  "button",
1434
1549
  {
1435
1550
  type: "button",
@@ -1440,15 +1555,15 @@ function NodeToolbar({
1440
1555
  action.onClick(event, node);
1441
1556
  },
1442
1557
  children: [
1443
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "gtv-menu-row-icon", children: action.icon(node) }),
1444
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "gtv-menu-row-label", children: action.label(node) })
1558
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "gtv-menu-row-icon", children: action.icon(node) }),
1559
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "gtv-menu-row-label", children: action.label(node) })
1445
1560
  ]
1446
1561
  },
1447
1562
  action.key
1448
1563
  );
1449
1564
  }),
1450
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "gtv-menu-divider" }),
1451
- /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
1565
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "gtv-menu-divider" }),
1566
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
1452
1567
  "button",
1453
1568
  {
1454
1569
  type: "button",
@@ -1458,8 +1573,8 @@ function NodeToolbar({
1458
1573
  onDeleteRequest?.(node);
1459
1574
  },
1460
1575
  children: [
1461
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "gtv-menu-row-icon", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_fa.FaTrashAlt, { className: "gtv-icon", size: 13 }) }),
1462
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "gtv-menu-row-label", children: "Delete" })
1576
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "gtv-menu-row-icon", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_fa.FaTrashAlt, { className: "gtv-icon", size: 13 }) }),
1577
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "gtv-menu-row-label", children: "Delete" })
1463
1578
  ]
1464
1579
  }
1465
1580
  )
@@ -1470,7 +1585,7 @@ function NodeToolbar({
1470
1585
  }
1471
1586
 
1472
1587
  // src/InfoPanel.tsx
1473
- var import_jsx_runtime5 = require("react/jsx-runtime");
1588
+ var import_jsx_runtime7 = require("react/jsx-runtime");
1474
1589
  var MINI_NODE_MIN_DIAMETER = 14;
1475
1590
  var MINI_NODE_MAX_DIAMETER = 32;
1476
1591
  var MINI_NODE_MIN_FONT_SIZE = 10;
@@ -1502,10 +1617,6 @@ function InfoPanel({
1502
1617
  onZoomIn,
1503
1618
  actions,
1504
1619
  readOnly = false,
1505
- editMode = false,
1506
- onEditModeChange,
1507
- renderTitleEditor,
1508
- renderParentsEditor,
1509
1620
  renderDangerZone,
1510
1621
  expanded = false,
1511
1622
  onExpandedChange,
@@ -1513,13 +1624,32 @@ function InfoPanel({
1513
1624
  hideExpandButton = false
1514
1625
  }) {
1515
1626
  const panelRef = (0, import_react6.useRef)(null);
1627
+ const [sheetFull, setSheetFull] = (0, import_react6.useState)(false);
1628
+ const grabberDragRef = (0, import_react6.useRef)(null);
1629
+ const onGrabberPointerDown = (event) => {
1630
+ event.currentTarget.setPointerCapture?.(event.pointerId);
1631
+ grabberDragRef.current = { startY: event.clientY, dragged: false };
1632
+ };
1633
+ const onGrabberPointerUp = (event) => {
1634
+ const drag = grabberDragRef.current;
1635
+ if (!drag) return;
1636
+ const dy = event.clientY - drag.startY;
1637
+ if (Math.abs(dy) < INFO_PANEL_SHEET_DRAG_THRESHOLD_PX) return;
1638
+ drag.dragged = true;
1639
+ if (dy < 0) setSheetFull(true);
1640
+ else if (sheetFull) setSheetFull(false);
1641
+ else if (!hideClose) onClose();
1642
+ };
1643
+ const onGrabberClick = () => {
1644
+ const dragged = grabberDragRef.current?.dragged ?? false;
1645
+ grabberDragRef.current = null;
1646
+ if (!dragged) setSheetFull((full) => !full);
1647
+ };
1516
1648
  (0, import_react6.useEffect)(() => {
1517
1649
  panelRef.current?.toggleAttribute("inert", collapsed);
1518
1650
  }, [collapsed]);
1519
- const editing = editMode && !readOnly;
1520
- const shownParent = editing && renderParentsEditor ? null : parentNode;
1521
1651
  const miniTreeRange = getItemCountRange([
1522
- ...shownParent ? [shownParent.node] : [],
1652
+ ...parentNode ? [parentNode.node] : [],
1523
1653
  node,
1524
1654
  ...childNodes.map((child) => child.node)
1525
1655
  ]);
@@ -1533,9 +1663,9 @@ function InfoPanel({
1533
1663
  };
1534
1664
  const miniNodeContent = (target, background, range) => {
1535
1665
  const { diameter, fontSize } = miniNodeSize(target, range);
1536
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
1537
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "gtv-info-panel-mini-dot", style: { background, width: diameter, height: diameter } }),
1538
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "gtv-info-panel-mini-label", style: { fontSize }, children: target.name })
1666
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
1667
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "gtv-info-panel-mini-dot", style: { background, width: diameter, height: diameter } }),
1668
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "gtv-info-panel-mini-label", style: { fontSize }, children: target.name })
1539
1669
  ] });
1540
1670
  };
1541
1671
  const sortedChildNodes = [...childNodes].sort((a, b) => b.node.itemCount - a.node.itemCount);
@@ -1548,9 +1678,9 @@ function InfoPanel({
1548
1678
  document.addEventListener("keydown", onKeyDown);
1549
1679
  return () => document.removeEventListener("keydown", onKeyDown);
1550
1680
  }, [expanded, onExpandedChange]);
1551
- const dangerZone = editing && renderDangerZone && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "gtv-info-panel-danger-zone", children: renderDangerZone(node) });
1552
- const toolbar = actions ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(NodeToolbar, { node, className: "gtv-info-panel-toolbar", ...actions }, node.id) : null;
1553
- const ancestors = ancestorNodes.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(InfoPanelSection, { title: "Ancestors", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("ul", { className: "gtv-info-panel-children-list gtv-info-panel-ancestors-list", children: ancestorNodes.map(({ node: ancestor, fill: fill2 }) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1681
+ const dangerZone = !readOnly && renderDangerZone && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "gtv-info-panel-danger-zone", children: renderDangerZone(node) });
1682
+ const toolbar = actions ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(NodeToolbar, { node, className: "gtv-info-panel-toolbar", ...actions }, node.id) : null;
1683
+ const ancestors = ancestorNodes.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(InfoPanelSection, { title: "Ancestors", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("ul", { className: "gtv-info-panel-children-list gtv-info-panel-ancestors-list", children: ancestorNodes.map(({ node: ancestor, fill: fill2 }) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1554
1684
  "button",
1555
1685
  {
1556
1686
  type: "button",
@@ -1560,76 +1690,85 @@ function InfoPanel({
1560
1690
  children: miniNodeContent(ancestor, fill2, ancestorRange)
1561
1691
  }
1562
1692
  ) }, ancestor.id)) }) }) : null;
1563
- const family = /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
1564
- editing && renderParentsEditor && renderParentsEditor(node),
1565
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(InfoPanelSection, { title: "Family", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "gtv-info-panel-mini-tree", children: [
1566
- shownParent && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "gtv-info-panel-mini-tree-parent", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1693
+ const family = /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_jsx_runtime7.Fragment, { children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(InfoPanelSection, { title: "Family", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "gtv-info-panel-mini-tree", children: [
1694
+ parentNode && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "gtv-info-panel-mini-tree-parent", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1695
+ "button",
1696
+ {
1697
+ type: "button",
1698
+ className: "gtv-info-panel-child",
1699
+ title: parentNode.node.name,
1700
+ onClick: () => onSelectNode(parentNode.node.id),
1701
+ children: miniNodeContent(parentNode.node, parentNode.fill, miniTreeRange)
1702
+ }
1703
+ ) }),
1704
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1705
+ "div",
1706
+ {
1707
+ className: `gtv-info-panel-mini-tree-node${childNodes.length === 0 ? " gtv-info-panel-mini-tree-node--leaf" : ""}`,
1708
+ children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1709
+ "span",
1710
+ {
1711
+ className: "gtv-info-panel-child gtv-info-panel-mini-tree-current",
1712
+ title: node.name,
1713
+ children: miniNodeContent(node, fill, miniTreeRange)
1714
+ }
1715
+ )
1716
+ }
1717
+ ),
1718
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "gtv-info-panel-mini-tree-children", children: [
1719
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("span", { className: "gtv-info-panel-children-title", children: [
1720
+ childNodes.length,
1721
+ " ",
1722
+ childNodes.length <= 1 ? "child" : "children"
1723
+ ] }),
1724
+ childNodes.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("ul", { className: "gtv-info-panel-children-list", children: sortedChildNodes.map(({ node: child, fill: fill2 }) => /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1567
1725
  "button",
1568
1726
  {
1569
1727
  type: "button",
1570
1728
  className: "gtv-info-panel-child",
1571
- title: shownParent.node.name,
1572
- onClick: () => onSelectNode(shownParent.node.id),
1573
- children: miniNodeContent(shownParent.node, shownParent.fill, miniTreeRange)
1574
- }
1575
- ) }),
1576
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1577
- "div",
1578
- {
1579
- className: `gtv-info-panel-mini-tree-node${childNodes.length === 0 ? " gtv-info-panel-mini-tree-node--leaf" : ""}`,
1580
- children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1581
- "span",
1582
- {
1583
- className: "gtv-info-panel-child gtv-info-panel-mini-tree-current",
1584
- title: node.name,
1585
- children: miniNodeContent(node, fill, miniTreeRange)
1586
- }
1587
- )
1729
+ title: child.name,
1730
+ onClick: () => onSelectNode(child.id),
1731
+ children: miniNodeContent(child, fill2, miniTreeRange)
1588
1732
  }
1589
- ),
1590
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "gtv-info-panel-mini-tree-children", children: [
1591
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("span", { className: "gtv-info-panel-children-title", children: [
1592
- childNodes.length,
1593
- " ",
1594
- childNodes.length <= 1 ? "child" : "children"
1595
- ] }),
1596
- childNodes.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("ul", { className: "gtv-info-panel-children-list", children: sortedChildNodes.map(({ node: child, fill: fill2 }) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1597
- "button",
1598
- {
1599
- type: "button",
1600
- className: "gtv-info-panel-child",
1601
- title: child.name,
1602
- onClick: () => onSelectNode(child.id),
1603
- children: miniNodeContent(child, fill2, miniTreeRange)
1604
- }
1605
- ) }, child.id)) })
1606
- ] })
1607
- ] }) })
1608
- ] });
1609
- const itemCount = /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("dl", { className: "gtv-info-panel-fields", children: [
1610
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("dt", { children: itemCountLabel }),
1611
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("dd", { children: node.itemCount })
1733
+ ) }, child.id)) })
1734
+ ] })
1735
+ ] }) }) });
1736
+ const itemCount = /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("dl", { className: "gtv-info-panel-fields", children: [
1737
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("dt", { children: itemCountLabel }),
1738
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("dd", { children: node.itemCount })
1612
1739
  ] });
1613
- const { primary, aside } = renderInfoPanelBody ? renderInfoPanelBody(node, { toolbar, ancestors, family, itemCount }) : { primary: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
1740
+ const { primary, aside } = renderInfoPanelBody ? renderInfoPanelBody(node, { toolbar, ancestors, family, itemCount }) : { primary: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
1614
1741
  toolbar,
1615
1742
  ancestors,
1616
1743
  family,
1617
1744
  itemCount
1618
1745
  ] }) };
1619
1746
  const hasAside = aside !== void 0 && aside !== null && aside !== false;
1620
- return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
1747
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
1621
1748
  "div",
1622
1749
  {
1623
- className: `gtv-info-panel gtv-info-panel--${side}${expanded ? " gtv-info-panel--expanded" : ""}${expanded && !hasAside ? " gtv-info-panel--expanded-solo" : ""}${collapsed ? " gtv-info-panel--collapsed" : ""}`,
1750
+ className: `gtv-info-panel gtv-info-panel--sheet${sheetFull ? " gtv-info-panel--sheet-full" : ""} gtv-info-panel--${side}${expanded ? " gtv-info-panel--expanded" : ""}${expanded && !hasAside ? " gtv-info-panel--expanded-solo" : ""}${collapsed ? " gtv-info-panel--collapsed" : ""}`,
1624
1751
  ref: panelRef,
1625
1752
  children: [
1626
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
1753
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1754
+ "button",
1755
+ {
1756
+ type: "button",
1757
+ className: "gtv-info-panel-grabber",
1758
+ "aria-label": sheetFull ? "Lower panel" : "Raise panel",
1759
+ "aria-expanded": sheetFull,
1760
+ onPointerDown: onGrabberPointerDown,
1761
+ onPointerUp: onGrabberPointerUp,
1762
+ onClick: onGrabberClick
1763
+ }
1764
+ ),
1765
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
1627
1766
  "div",
1628
1767
  {
1629
1768
  className: "gtv-info-panel-header",
1630
1769
  style: { background: fill, color: textColor },
1631
1770
  children: [
1632
- onPrevious && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1771
+ onPrevious && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1633
1772
  "button",
1634
1773
  {
1635
1774
  type: "button",
@@ -1638,21 +1777,21 @@ function InfoPanel({
1638
1777
  onClick: onPrevious,
1639
1778
  "aria-label": previousLabel ? `Previous: ${previousLabel}` : "Previous",
1640
1779
  title: previousLabel,
1641
- children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_md4.MdArrowBack, { className: "gtv-icon", size: 16 })
1780
+ children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_md4.MdArrowBack, { className: "gtv-icon", size: 16 })
1642
1781
  }
1643
1782
  ),
1644
- editing && renderTitleEditor ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "gtv-info-panel-title gtv-info-panel-title--editor", children: renderTitleEditor(node) }) : /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
1783
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
1645
1784
  "span",
1646
1785
  {
1647
1786
  className: `gtv-info-panel-title${hideTitle ? " gtv-info-panel-title--hidden" : ""}`,
1648
1787
  style: { color: textColor },
1649
1788
  children: [
1650
1789
  node.name,
1651
- node.umbrella && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "gtv-info-panel-umbrella-badge", children: "umbrella" })
1790
+ node.umbrella && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { className: "gtv-info-panel-umbrella-badge", children: "umbrella" })
1652
1791
  ]
1653
1792
  }
1654
1793
  ),
1655
- onZoomIn && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1794
+ onZoomIn && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1656
1795
  "button",
1657
1796
  {
1658
1797
  type: "button",
@@ -1661,22 +1800,11 @@ function InfoPanel({
1661
1800
  onClick: onZoomIn,
1662
1801
  "aria-label": "Zoom in",
1663
1802
  title: "Zoom in",
1664
- children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_md4.MdZoomIn, { className: "gtv-icon", size: 16 })
1665
- }
1666
- ),
1667
- onEditModeChange && !readOnly && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1668
- "button",
1669
- {
1670
- type: "button",
1671
- className: "gtv-info-panel-edit",
1672
- style: { color: textColor },
1673
- onClick: () => onEditModeChange(!editMode),
1674
- "aria-pressed": editMode,
1675
- children: editMode ? "Done" : "Edit"
1803
+ children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_md4.MdZoomIn, { className: "gtv-icon", size: 16 })
1676
1804
  }
1677
1805
  ),
1678
- onExpandedChange && !hideExpandButton && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(InfoPanelExpandButton, { expanded, onExpandedChange, style: { color: textColor } }),
1679
- !hideClose && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1806
+ onExpandedChange && !hideExpandButton && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(InfoPanelExpandButton, { expanded, onExpandedChange, style: { color: textColor } }),
1807
+ !hideClose && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
1680
1808
  "button",
1681
1809
  {
1682
1810
  type: "button",
@@ -1684,18 +1812,18 @@ function InfoPanel({
1684
1812
  style: { color: textColor },
1685
1813
  onClick: onClose,
1686
1814
  "aria-label": "Close",
1687
- children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_md4.MdClose, { className: "gtv-icon", size: 16 })
1815
+ children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_md4.MdClose, { className: "gtv-icon", size: 16 })
1688
1816
  }
1689
1817
  )
1690
1818
  ]
1691
1819
  }
1692
1820
  ),
1693
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "gtv-info-panel-body", children: [
1694
- /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "gtv-info-panel-primary", children: [
1821
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "gtv-info-panel-body", children: [
1822
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "gtv-info-panel-primary", children: [
1695
1823
  primary,
1696
1824
  expanded && !hasAside && dangerZone
1697
1825
  ] }),
1698
- hasAside && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "gtv-info-panel-aside", children: [
1826
+ hasAside && /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "gtv-info-panel-aside", children: [
1699
1827
  aside,
1700
1828
  expanded && dangerZone
1701
1829
  ] })
@@ -1709,7 +1837,7 @@ function InfoPanel({
1709
1837
  // src/OverlayPanel.tsx
1710
1838
  var import_react7 = require("react");
1711
1839
  var import_md5 = require("react-icons/md");
1712
- var import_jsx_runtime6 = require("react/jsx-runtime");
1840
+ var import_jsx_runtime8 = require("react/jsx-runtime");
1713
1841
  function OverlayPanel({
1714
1842
  title,
1715
1843
  content,
@@ -1744,21 +1872,21 @@ function OverlayPanel({
1744
1872
  expanded && !extraContent && "gtv-info-panel--expanded-solo",
1745
1873
  collapsed && "gtv-info-panel--collapsed"
1746
1874
  ].filter(Boolean).join(" ");
1747
- return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className, ref: panelRef, children: [
1748
- /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "gtv-info-panel-header", children: [
1749
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: `gtv-info-panel-title${hideTitle ? " gtv-info-panel-title--hidden" : ""}`, children: title }),
1750
- onExpandedChange && !hideExpandButton && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(InfoPanelExpandButton, { expanded, onExpandedChange }),
1751
- !hideClose && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: "gtv-info-panel-close", onClick: onClose, "aria-label": "Close", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_md5.MdClose, { className: "gtv-icon", size: 16 }) })
1875
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className, ref: panelRef, children: [
1876
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "gtv-info-panel-header", children: [
1877
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: `gtv-info-panel-title${hideTitle ? " gtv-info-panel-title--hidden" : ""}`, children: title }),
1878
+ onExpandedChange && !hideExpandButton && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(InfoPanelExpandButton, { expanded, onExpandedChange }),
1879
+ !hideClose && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("button", { type: "button", className: "gtv-info-panel-close", onClick: onClose, "aria-label": "Close", children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_md5.MdClose, { className: "gtv-icon", size: 16 }) })
1752
1880
  ] }),
1753
- /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "gtv-info-panel-body", children: [
1754
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "gtv-info-panel-aside gtv-overlay-content", children: content }),
1755
- extraContent && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "gtv-info-panel-primary gtv-overlay-content", children: extraContent })
1881
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "gtv-info-panel-body", children: [
1882
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "gtv-info-panel-aside gtv-overlay-content", children: content }),
1883
+ extraContent && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "gtv-info-panel-primary gtv-overlay-content", children: extraContent })
1756
1884
  ] })
1757
1885
  ] });
1758
1886
  }
1759
1887
 
1760
1888
  // src/GenreTree.tsx
1761
- var import_jsx_runtime7 = require("react/jsx-runtime");
1889
+ var import_jsx_runtime9 = require("react/jsx-runtime");
1762
1890
  function GenreTree({
1763
1891
  nodes,
1764
1892
  className,
@@ -1785,11 +1913,9 @@ function GenreTree({
1785
1913
  hideInfoPanelClose = false,
1786
1914
  hideInfoPanelTitle = false,
1787
1915
  itemCountLabel,
1916
+ itemCountScaleMax,
1917
+ nodeCountForSizing,
1788
1918
  readOnly,
1789
- editMode,
1790
- onEditModeChange,
1791
- renderTitleEditor,
1792
- renderParentsEditor,
1793
1919
  renderDangerZone,
1794
1920
  expanded,
1795
1921
  onExpandedChange,
@@ -1799,7 +1925,10 @@ function GenreTree({
1799
1925
  wheelZoom,
1800
1926
  defaultViewport,
1801
1927
  onViewportChange,
1802
- controlsOrientation = "vertical"
1928
+ controlsOrientation = "vertical",
1929
+ showZoomControls = "auto",
1930
+ floatingControlsLeading,
1931
+ busy
1803
1932
  }) {
1804
1933
  const index = useTreeIndex(nodes);
1805
1934
  const svgRef = (0, import_react8.useRef)(null);
@@ -1807,6 +1936,10 @@ function GenreTree({
1807
1936
  const stageRef = (0, import_react8.useRef)(null);
1808
1937
  const panZoom = usePanZoom(viewportRef, wheelZoom, stageRef, { defaultViewport, onViewportChange });
1809
1938
  const { panel, showNodeInfo, closeNodeInfo } = useNodeInfoPanel({ selectedNodeId: selectedNodeIdProp, onSelectedNodeChange });
1939
+ const itemCountRange = (0, import_react8.useMemo)(
1940
+ () => getItemCountRange(nodes, { itemCountScaleMax, nodeCountForSizing: nodeCountForSizing ?? nodes.length }),
1941
+ [nodes, itemCountScaleMax, nodeCountForSizing]
1942
+ );
1810
1943
  const showNodeInfoRef = (0, import_react8.useRef)(showNodeInfo);
1811
1944
  (0, import_react8.useEffect)(() => {
1812
1945
  showNodeInfoRef.current = showNodeInfo;
@@ -1821,7 +1954,8 @@ function GenreTree({
1821
1954
  d3,
1822
1955
  root,
1823
1956
  orientation,
1824
- depthSpacingScale
1957
+ depthSpacingScale,
1958
+ itemCountRange
1825
1959
  );
1826
1960
  const {
1827
1961
  svgWidth: width,
@@ -1833,7 +1967,8 @@ function GenreTree({
1833
1967
  originalTreeData,
1834
1968
  orientation,
1835
1969
  hideRoot,
1836
- depthSpacingScale
1970
+ depthSpacingScale,
1971
+ itemCountRange
1837
1972
  );
1838
1973
  const reshapedTreeData = setupTreeLayout(
1839
1974
  d3,
@@ -1842,7 +1977,8 @@ function GenreTree({
1842
1977
  orientation,
1843
1978
  rootDepthOffset,
1844
1979
  width,
1845
- depthSpacingScale
1980
+ depthSpacingScale,
1981
+ itemCountRange
1846
1982
  );
1847
1983
  return {
1848
1984
  treeData: reshapedTreeData,
@@ -1850,7 +1986,7 @@ function GenreTree({
1850
1986
  svgWidth: width,
1851
1987
  svgHeight: height
1852
1988
  };
1853
- }, [nodes, rootColor, orientation, hideRoot, depthSpacingScale]);
1989
+ }, [nodes, rootColor, orientation, hideRoot, depthSpacingScale, itemCountRange]);
1854
1990
  const reparentForbiddenIds = (0, import_react8.useMemo)(() => {
1855
1991
  if (!reparentingNodeId) return [];
1856
1992
  const reparentingHierarchyNode = treeData.descendants().find((d) => d.data.id === reparentingNodeId);
@@ -1901,7 +2037,8 @@ function GenreTree({
1901
2037
  },
1902
2038
  orientation,
1903
2039
  hideRoot,
1904
- effectiveSelectedNodeId
2040
+ effectiveSelectedNodeId,
2041
+ itemCountRange
1905
2042
  );
1906
2043
  }, [
1907
2044
  treeData,
@@ -1922,7 +2059,8 @@ function GenreTree({
1922
2059
  additionalActions,
1923
2060
  orientation,
1924
2061
  hideRoot,
1925
- effectiveSelectedNodeId
2062
+ effectiveSelectedNodeId,
2063
+ itemCountRange
1926
2064
  ]);
1927
2065
  (0, import_react8.useEffect)(() => {
1928
2066
  if (!interactive || !selectedNodeIdProp) return;
@@ -1955,7 +2093,7 @@ function GenreTree({
1955
2093
  hasInitialFitRef.current = true;
1956
2094
  panZoom.fitToFrame(elements, { silent: true });
1957
2095
  });
1958
- const svg = /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2096
+ const svg = /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1959
2097
  "svg",
1960
2098
  {
1961
2099
  ref: svgRef,
@@ -1965,13 +2103,14 @@ function GenreTree({
1965
2103
  }
1966
2104
  );
1967
2105
  if (!interactive) {
1968
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className, children: svg });
2106
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className, children: svg });
1969
2107
  }
1970
- return /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(
2108
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
1971
2109
  "div",
1972
2110
  {
1973
2111
  ref: viewportRef,
1974
2112
  className,
2113
+ "aria-busy": busy,
1975
2114
  style: {
1976
2115
  position: "relative",
1977
2116
  overflow: "hidden",
@@ -1994,7 +2133,7 @@ function GenreTree({
1994
2133
  },
1995
2134
  onPointerDown: panZoom.handlePointerDown,
1996
2135
  children: [
1997
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2136
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1998
2137
  "div",
1999
2138
  {
2000
2139
  ref: stageRef,
@@ -2008,7 +2147,7 @@ function GenreTree({
2008
2147
  children: svg
2009
2148
  }
2010
2149
  ),
2011
- panel && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2150
+ panel && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
2012
2151
  InfoPanel,
2013
2152
  {
2014
2153
  node: panel.node,
@@ -2058,10 +2197,6 @@ function GenreTree({
2058
2197
  hideTitle: hideInfoPanelTitle,
2059
2198
  itemCountLabel,
2060
2199
  readOnly,
2061
- editMode,
2062
- onEditModeChange,
2063
- renderTitleEditor,
2064
- renderParentsEditor,
2065
2200
  renderDangerZone,
2066
2201
  expanded,
2067
2202
  onExpandedChange,
@@ -2080,54 +2215,16 @@ function GenreTree({
2080
2215
  } : void 0
2081
2216
  }
2082
2217
  ),
2083
- overlayPanel && /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(OverlayPanel, { ...overlayPanel }),
2084
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2085
- "div",
2218
+ overlayPanel && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(OverlayPanel, { ...overlayPanel }),
2219
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
2220
+ FloatingControls,
2086
2221
  {
2087
- className: [
2088
- "gtv-wheel-floating-controls",
2089
- controlsOrientation === "horizontal" && "gtv-wheel-floating-controls--horizontal"
2090
- ].filter(Boolean).join(" "),
2091
- children: /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)("div", { className: "gtv-zoom-controls", children: [
2092
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2093
- "button",
2094
- {
2095
- type: "button",
2096
- className: [
2097
- "gtv-zoom-btn",
2098
- !panZoom.canZoomIn && "gtv-zoom-btn--disabled"
2099
- ].filter(Boolean).join(" "),
2100
- disabled: !panZoom.canZoomIn,
2101
- onClick: panZoom.zoomIn,
2102
- "aria-label": "Zoom in",
2103
- children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_md6.MdZoomIn, { className: "gtv-icon", size: 18 })
2104
- }
2105
- ),
2106
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2107
- "button",
2108
- {
2109
- type: "button",
2110
- className: [
2111
- "gtv-zoom-btn",
2112
- !panZoom.canZoomOut && "gtv-zoom-btn--disabled"
2113
- ].filter(Boolean).join(" "),
2114
- disabled: !panZoom.canZoomOut,
2115
- onClick: panZoom.zoomOut,
2116
- "aria-label": "Zoom out",
2117
- children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_md6.MdZoomOut, { className: "gtv-icon", size: 18 })
2118
- }
2119
- ),
2120
- /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
2121
- "button",
2122
- {
2123
- type: "button",
2124
- className: "gtv-zoom-btn",
2125
- onClick: () => panZoom.fitToFrame(queryTreeContentElements(svgRef.current)),
2126
- "aria-label": "Fit to frame",
2127
- children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_md6.MdFitScreen, { className: "gtv-icon", size: 18 })
2128
- }
2129
- )
2130
- ] })
2222
+ panZoom,
2223
+ orientation: controlsOrientation,
2224
+ zoomControls: showZoomControls,
2225
+ leading: floatingControlsLeading,
2226
+ busy,
2227
+ onFit: () => panZoom.fitToFrame(queryTreeContentElements(svgRef.current))
2131
2228
  }
2132
2229
  )
2133
2230
  ]
@@ -2137,7 +2234,6 @@ function GenreTree({
2137
2234
 
2138
2235
  // src/GenreTreeWheelBase.tsx
2139
2236
  var import_react9 = require("react");
2140
- var import_md7 = require("react-icons/md");
2141
2237
  var d32 = __toESM(require("d3"), 1);
2142
2238
 
2143
2239
  // src/root-grouping.ts
@@ -2213,7 +2309,7 @@ function computeRotationForSelection(currentRotationDeg, targetAngle, landingAng
2213
2309
  }
2214
2310
 
2215
2311
  // src/GenreTreeWheelBase.tsx
2216
- var import_jsx_runtime8 = require("react/jsx-runtime");
2312
+ var import_jsx_runtime10 = require("react/jsx-runtime");
2217
2313
  function WheelCore({
2218
2314
  nodes,
2219
2315
  className,
@@ -2238,11 +2334,9 @@ function WheelCore({
2238
2334
  hideInfoPanelClose = false,
2239
2335
  hideInfoPanelTitle = false,
2240
2336
  itemCountLabel,
2337
+ itemCountScaleMax,
2338
+ nodeCountForSizing,
2241
2339
  readOnly,
2242
- editMode,
2243
- onEditModeChange,
2244
- renderTitleEditor,
2245
- renderParentsEditor,
2246
2340
  renderDangerZone,
2247
2341
  expanded,
2248
2342
  onExpandedChange,
@@ -2253,8 +2347,16 @@ function WheelCore({
2253
2347
  defaultViewport,
2254
2348
  onViewportChange,
2255
2349
  controlsOrientation = "vertical",
2350
+ showZoomControls = "auto",
2351
+ floatingControlsLeading,
2352
+ busy,
2256
2353
  selectedNodeId
2257
2354
  }) {
2355
+ const fullNodeCountForSizing = nodeCountForSizing ?? nodes.length;
2356
+ const wheelSizeRange = (0, import_react9.useMemo)(
2357
+ () => getItemCountRange(nodes, { itemCountScaleMax, nodeCountForSizing: fullNodeCountForSizing }),
2358
+ [nodes, itemCountScaleMax, fullNodeCountForSizing]
2359
+ );
2258
2360
  const treeOrientation = direction === "left" ? "horizontal-anchored" : "vertical";
2259
2361
  const landingAngle = direction === "left" ? 90 : 0;
2260
2362
  const chipHalfExtentVar = direction === "left" ? "--gtv-wheel-chip-half-width" : "--gtv-wheel-chip-half-height";
@@ -2264,10 +2366,10 @@ function WheelCore({
2264
2366
  return getGenreTreeColor(nodes.find((n) => n.id === rootId)?.wikidataId);
2265
2367
  };
2266
2368
  const wheelRadius = (0, import_react9.useMemo)(
2267
- () => calculateWheelRadius(groups.length, MAX_NODE_WIDTH, WHEEL_RADIUS),
2268
- [groups.length]
2369
+ () => calculateWheelRadius(groups.length, wheelSizeRange.maxNodeWidth ?? MAX_NODE_WIDTH, WHEEL_RADIUS),
2370
+ [groups.length, wheelSizeRange.maxNodeWidth]
2269
2371
  );
2270
- const wheelViewportHeight = wheelRadius * 2 + MAX_NODE_HEIGHT / 2;
2372
+ const wheelViewportHeight = wheelRadius * 2 + (wheelSizeRange.maxNodeHeight ?? MAX_NODE_HEIGHT) / 2;
2271
2373
  const rootItemCountById = (0, import_react9.useMemo)(
2272
2374
  () => new Map(groups.map((group) => [group.root.id, group.root.itemCount])),
2273
2375
  [groups]
@@ -2314,16 +2416,18 @@ function WheelCore({
2314
2416
  () => getItemCountRange(
2315
2417
  groups.map((group) => ({
2316
2418
  itemCount: rootItemCountById.get(group.root.id)
2317
- }))
2419
+ })),
2420
+ { itemCountScaleMax, nodeCountForSizing: fullNodeCountForSizing }
2318
2421
  ),
2319
- [groups, rootItemCountById]
2422
+ [groups, rootItemCountById, itemCountScaleMax, fullNodeCountForSizing]
2320
2423
  );
2321
2424
  const rootChipHalfExtent = selectedGroup ? calculateRootAnchorClearance(
2322
2425
  d32,
2323
2426
  selectedGroup.nodes,
2324
2427
  rootItemCountById.get(selectedGroup.root.id),
2325
2428
  rootItemCountRange,
2326
- direction === "left" ? "WIDTH" : "HEIGHT"
2429
+ direction === "left" ? "WIDTH" : "HEIGHT",
2430
+ { itemCountScaleMax, nodeCountForSizing: fullNodeCountForSizing }
2327
2431
  ) : 0;
2328
2432
  const sectorFillGradient = (0, import_react9.useMemo)(
2329
2433
  () => buildWheelSectorGradient(
@@ -2368,10 +2472,11 @@ function WheelCore({
2368
2472
  );
2369
2473
  showNodeInfo(targetNode, element, viewportRef.current, { notify: false });
2370
2474
  }, [selectedNodeId, effectiveRootId, nodes, panel, showNodeInfo]);
2371
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
2475
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
2372
2476
  "div",
2373
2477
  {
2374
2478
  ref: viewportRef,
2479
+ "aria-busy": busy,
2375
2480
  className: [
2376
2481
  "gtv-wheel-container",
2377
2482
  direction === "left" && "gtv-wheel-container--left",
@@ -2386,7 +2491,7 @@ function WheelCore({
2386
2491
  },
2387
2492
  onPointerDown: panZoom.handlePointerDown,
2388
2493
  children: [
2389
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
2494
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
2390
2495
  "div",
2391
2496
  {
2392
2497
  ref: stageRef,
@@ -2399,7 +2504,7 @@ function WheelCore({
2399
2504
  transform: panZoom.transform,
2400
2505
  transformOrigin: "0 0"
2401
2506
  },
2402
- children: /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
2507
+ children: /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
2403
2508
  "div",
2404
2509
  {
2405
2510
  className: "gtv-wheel-stage",
@@ -2407,11 +2512,13 @@ function WheelCore({
2407
2512
  [chipHalfExtentVar]: `${rootChipHalfExtent}px`
2408
2513
  },
2409
2514
  children: [
2410
- selectedGroup && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "gtv-wheel-tree-anchor", ref: treeAnchorRef, children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
2515
+ selectedGroup && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: "gtv-wheel-tree-anchor", ref: treeAnchorRef, children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
2411
2516
  GenreTree,
2412
2517
  {
2413
2518
  nodes: selectedGroup.nodes,
2414
2519
  itemCountLabel,
2520
+ itemCountScaleMax,
2521
+ nodeCountForSizing: fullNodeCountForSizing,
2415
2522
  orientation: treeOrientation,
2416
2523
  hideRoot: true,
2417
2524
  interactive: false,
@@ -2445,9 +2552,9 @@ function WheelCore({
2445
2552
  },
2446
2553
  selectedGroup.root.id
2447
2554
  ) }),
2448
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "gtv-wheel-circle", ref: wheelCircleRef }),
2449
- centerLabel && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "gtv-wheel-center-label", children: centerLabel }),
2450
- /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
2555
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: "gtv-wheel-circle", ref: wheelCircleRef }),
2556
+ centerLabel && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: "gtv-wheel-center-label", children: centerLabel }),
2557
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
2451
2558
  "div",
2452
2559
  {
2453
2560
  className: "gtv-wheel",
@@ -2455,7 +2562,7 @@ function WheelCore({
2455
2562
  "--gtv-wheel-rotation": `${rotationDeg}deg`
2456
2563
  },
2457
2564
  children: [
2458
- sectorFillGradient && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
2565
+ sectorFillGradient && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
2459
2566
  "div",
2460
2567
  {
2461
2568
  className: "gtv-wheel-sector-fill",
@@ -2464,7 +2571,7 @@ function WheelCore({
2464
2571
  }
2465
2572
  }
2466
2573
  ),
2467
- groups.length > 1 && groups.map((group, index) => /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
2574
+ groups.length > 1 && groups.map((group, index) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
2468
2575
  "div",
2469
2576
  {
2470
2577
  className: "gtv-wheel-divider",
@@ -2487,12 +2594,12 @@ function WheelCore({
2487
2594
  rootItemCount,
2488
2595
  rootItemCountRange
2489
2596
  );
2490
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
2597
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
2491
2598
  "div",
2492
2599
  {
2493
2600
  className: "gtv-wheel-slot",
2494
2601
  style: { "--gtv-chip-angle": `${angle}deg` },
2495
- children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "gtv-wheel-chip-anchor", children: /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
2602
+ children: /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: "gtv-wheel-chip-anchor", children: /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
2496
2603
  "button",
2497
2604
  {
2498
2605
  type: "button",
@@ -2528,8 +2635,8 @@ function WheelCore({
2528
2635
  }
2529
2636
  },
2530
2637
  children: [
2531
- PER_TREE_ACCENT_DOT && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "gtv-wheel-chip-dot" }),
2532
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
2638
+ PER_TREE_ACCENT_DOT && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("span", { className: "gtv-wheel-chip-dot" }),
2639
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
2533
2640
  "span",
2534
2641
  {
2535
2642
  className: "gtv-node-label gtv-node-label--root",
@@ -2552,7 +2659,7 @@ function WheelCore({
2552
2659
  )
2553
2660
  }
2554
2661
  ),
2555
- panel && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
2662
+ panel && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
2556
2663
  InfoPanel,
2557
2664
  {
2558
2665
  node: panel.node,
@@ -2599,10 +2706,6 @@ function WheelCore({
2599
2706
  hideTitle: hideInfoPanelTitle,
2600
2707
  itemCountLabel,
2601
2708
  readOnly,
2602
- editMode,
2603
- onEditModeChange,
2604
- renderTitleEditor,
2605
- renderParentsEditor,
2606
2709
  renderDangerZone,
2607
2710
  expanded,
2608
2711
  onExpandedChange,
@@ -2621,57 +2724,19 @@ function WheelCore({
2621
2724
  } : void 0
2622
2725
  }
2623
2726
  ),
2624
- overlayPanel && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(OverlayPanel, { ...overlayPanel }),
2625
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
2626
- "div",
2727
+ overlayPanel && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(OverlayPanel, { ...overlayPanel }),
2728
+ /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
2729
+ FloatingControls,
2627
2730
  {
2628
- className: [
2629
- "gtv-wheel-floating-controls",
2630
- controlsOrientation === "horizontal" && "gtv-wheel-floating-controls--horizontal"
2631
- ].filter(Boolean).join(" "),
2632
- children: /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "gtv-zoom-controls", children: [
2633
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
2634
- "button",
2635
- {
2636
- type: "button",
2637
- className: [
2638
- "gtv-zoom-btn",
2639
- !panZoom.canZoomIn && "gtv-zoom-btn--disabled"
2640
- ].filter(Boolean).join(" "),
2641
- disabled: !panZoom.canZoomIn,
2642
- onClick: panZoom.zoomIn,
2643
- "aria-label": "Zoom in",
2644
- children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_md7.MdZoomIn, { className: "gtv-icon", size: 18 })
2645
- }
2646
- ),
2647
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
2648
- "button",
2649
- {
2650
- type: "button",
2651
- className: [
2652
- "gtv-zoom-btn",
2653
- !panZoom.canZoomOut && "gtv-zoom-btn--disabled"
2654
- ].filter(Boolean).join(" "),
2655
- disabled: !panZoom.canZoomOut,
2656
- onClick: panZoom.zoomOut,
2657
- "aria-label": "Zoom out",
2658
- children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_md7.MdZoomOut, { className: "gtv-icon", size: 18 })
2659
- }
2660
- ),
2661
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
2662
- "button",
2663
- {
2664
- type: "button",
2665
- className: "gtv-zoom-btn",
2666
- onClick: () => panZoom.fitToFrame([
2667
- wheelCircleRef.current,
2668
- ...queryTreeContentElements(treeAnchorRef.current)
2669
- ]),
2670
- "aria-label": "Fit to frame",
2671
- children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_md7.MdFitScreen, { className: "gtv-icon", size: 18 })
2672
- }
2673
- )
2674
- ] })
2731
+ panZoom,
2732
+ orientation: controlsOrientation,
2733
+ zoomControls: showZoomControls,
2734
+ leading: floatingControlsLeading,
2735
+ busy,
2736
+ onFit: () => panZoom.fitToFrame([
2737
+ wheelCircleRef.current,
2738
+ ...queryTreeContentElements(treeAnchorRef.current)
2739
+ ])
2675
2740
  }
2676
2741
  )
2677
2742
  ]
@@ -2680,37 +2745,50 @@ function WheelCore({
2680
2745
  }
2681
2746
 
2682
2747
  // src/GenreTreeWheel.tsx
2683
- var import_jsx_runtime9 = require("react/jsx-runtime");
2748
+ var import_jsx_runtime11 = require("react/jsx-runtime");
2684
2749
  function GenreTreeWheel(props) {
2685
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(WheelCore, { ...props, direction: "bottom" });
2750
+ return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(WheelCore, { ...props, direction: "bottom" });
2686
2751
  }
2687
2752
 
2688
2753
  // src/GenreTreeWheelRight.tsx
2689
- var import_jsx_runtime10 = require("react/jsx-runtime");
2754
+ var import_jsx_runtime12 = require("react/jsx-runtime");
2690
2755
  function GenreTreeWheelRight(props) {
2691
- return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(WheelCore, { ...props, direction: "left" });
2756
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(WheelCore, { ...props, direction: "left" });
2692
2757
  }
2693
2758
 
2694
2759
  // src/GenreTreeWheelRadialBase.tsx
2695
2760
  var import_react10 = require("react");
2696
- var import_md8 = require("react-icons/md");
2697
2761
  var d33 = __toESM(require("d3"), 1);
2698
2762
 
2699
2763
  // src/defer-click.ts
2700
2764
  function createDeferredClick() {
2701
2765
  let timeoutId = null;
2766
+ let pendingKey = void 0;
2767
+ let doubledAt = -Infinity;
2702
2768
  const cancel = () => {
2703
2769
  if (timeoutId !== null) clearTimeout(timeoutId);
2704
2770
  timeoutId = null;
2705
2771
  };
2706
- const schedule = (action) => {
2772
+ const schedule = (action, onDouble, key) => {
2773
+ if (onDouble && timeoutId !== null && key === pendingKey) {
2774
+ cancel();
2775
+ doubledAt = Date.now();
2776
+ onDouble();
2777
+ return;
2778
+ }
2707
2779
  cancel();
2780
+ pendingKey = key;
2708
2781
  timeoutId = setTimeout(() => {
2709
2782
  timeoutId = null;
2710
2783
  action();
2711
2784
  }, DOUBLE_CLICK_DELAY_MS);
2712
2785
  };
2713
- return { schedule, cancel };
2786
+ const doubleClick = (onDouble) => {
2787
+ cancel();
2788
+ if (Date.now() - doubledAt < DOUBLE_CLICK_DELAY_MS) return;
2789
+ onDouble();
2790
+ };
2791
+ return { schedule, cancel, doubleClick };
2714
2792
  }
2715
2793
 
2716
2794
  // src/radial-wheel-geometry.ts
@@ -2877,6 +2955,7 @@ function pickEmptyLabelSpot(area, width, height, obstacles, prefer = () => true)
2877
2955
  var POP_WEDGE_SPAN_DEGREES = 80;
2878
2956
  var POP_SUBTREE_OUTER_MARGIN = 24;
2879
2957
  var POP_HIERARCHY_ROOT_ABSOLUTE_DEPTH = 1;
2958
+ var DOT_RADIUS_TRANSITION_MS = 250;
2880
2959
  function getRadialDepthRadius(depth, coreRootCircleRadius, depthSpacing) {
2881
2960
  return coreRootCircleRadius + depth * depthSpacing;
2882
2961
  }
@@ -2899,14 +2978,15 @@ function calculatePopSubtreeRadialExtent(hierarchy, coreRootCircleRadius = 0) {
2899
2978
  POP_TREE_DEPTH_RADIAL_SPACING
2900
2979
  ) + MAX_NODE_WIDTH / 2 + POP_SUBTREE_OUTER_MARGIN;
2901
2980
  }
2902
- function computePopRadialLayout(d3Lib, hierarchy, wedgeCenterAngleDegrees, coreRootCircleRadius, wedgeSpanDegrees = POP_WEDGE_SPAN_DEGREES, depthSpacing = POP_TREE_DEPTH_RADIAL_SPACING, itemCountRange) {
2981
+ function computePopRadialLayout(d3Lib, hierarchy, wedgeCenterAngleDegrees, coreRootCircleRadius, wedgeSpanDegrees = POP_WEDGE_SPAN_DEGREES, depthSpacing = POP_TREE_DEPTH_RADIAL_SPACING, dotRadius = NODE_LAYOUT_SLOT_RADIUS) {
2903
2982
  return layoutWedge(
2904
2983
  d3Lib,
2905
2984
  hierarchy,
2906
2985
  wedgeCenterAngleDegrees,
2907
2986
  wedgeSpanDegrees,
2908
2987
  popRadiusOfDepth(coreRootCircleRadius, depthSpacing),
2909
- itemCountRange
2988
+ "center",
2989
+ dotRadius
2910
2990
  );
2911
2991
  }
2912
2992
  var MIN_DOT_GAP = 5.6;
@@ -2916,15 +2996,15 @@ function popRadiusOfDepth(coreRootCircleRadius, depthSpacing) {
2916
2996
  function coreRadiusOfDepth(coreRootCircleRadius, depthSpacing) {
2917
2997
  return (depth) => getRadialDepthRadius(depth + 1, coreRootCircleRadius, depthSpacing);
2918
2998
  }
2919
- function measureWedge(_d3Lib, hierarchy, radiusOfDepth, itemCountRange) {
2920
- const dotRadius = (d) => itemCountRange ? calculateNodeDotRadius(d.data.itemCount, itemCountRange) : NODE_DOT_MAX_RADIUS;
2999
+ function measureWedge(_d3Lib, hierarchy, radiusOfDepth, dotRadius = NODE_LAYOUT_SLOT_RADIUS) {
3000
+ const slotRadius = () => dotRadius;
2921
3001
  const offsetFromParent = /* @__PURE__ */ new Map();
2922
3002
  const shiftLevel = (level, by) => ({
2923
3003
  left: { ...level.left, x: level.left.x + by },
2924
3004
  right: { ...level.right, x: level.right.x + by }
2925
3005
  });
2926
3006
  const pack = (node) => {
2927
- const own = { x: 0, dotRadius: dotRadius(node) };
3007
+ const own = { x: 0, dotRadius: slotRadius() };
2928
3008
  if (!node.children) return [{ left: own, right: own }];
2929
3009
  const below = [];
2930
3010
  const childOffsets = [];
@@ -2956,7 +3036,7 @@ function measureWedge(_d3Lib, hierarchy, radiusOfDepth, itemCountRange) {
2956
3036
  let min = Infinity;
2957
3037
  let max = -Infinity;
2958
3038
  hierarchy.each((d) => {
2959
- const inset = (dotRadius(d) + MIN_DOT_GAP / 2) / radiusOfDepth(d.depth);
3039
+ const inset = (slotRadius() + MIN_DOT_GAP / 2) / radiusOfDepth(d.depth);
2960
3040
  min = Math.min(min, d.x - inset);
2961
3041
  max = Math.max(max, d.x + inset);
2962
3042
  });
@@ -2966,13 +3046,44 @@ function measureWedge(_d3Lib, hierarchy, radiusOfDepth, itemCountRange) {
2966
3046
  return max - min;
2967
3047
  }
2968
3048
  var MAX_WEDGE_STRETCH = 1.42;
2969
- function layoutWedge(d3Lib, hierarchy, wedgeCenterAngleDegrees, wedgeSpanDegrees, radiusOfDepth, itemCountRange) {
2970
- const neededRad = measureWedge(d3Lib, hierarchy, radiusOfDepth, itemCountRange);
3049
+ function splitAtBranching(hierarchy) {
3050
+ let branching = hierarchy;
3051
+ const chain = [];
3052
+ while (branching.children?.length === 1) {
3053
+ chain.push(branching);
3054
+ branching = branching.children[0];
3055
+ }
3056
+ const children = branching.children;
3057
+ return children && children.length > 1 ? { chain, branching, children } : null;
3058
+ }
3059
+ function layoutWedge(d3Lib, hierarchy, wedgeCenterAngleDegrees, wedgeSpanDegrees, radiusOfDepth, slack = "center", dotRadius = NODE_LAYOUT_SLOT_RADIUS) {
3060
+ const neededRad = measureWedge(d3Lib, hierarchy, radiusOfDepth, dotRadius);
2971
3061
  const wedgeSpanRad = wedgeSpanDegrees * Math.PI / 180;
2972
3062
  const scale = Math.min(MAX_WEDGE_STRETCH, wedgeSpanRad / neededRad);
2973
- const wedgeStartRad = wedgeCenterAngleDegrees * Math.PI / 180 - neededRad * scale / 2;
3063
+ const wedgeCenterRad = wedgeCenterAngleDegrees * Math.PI / 180;
3064
+ const centeredStartRad = wedgeCenterRad - neededRad * scale / 2;
3065
+ const branchingSplit = slack === "between-children" ? splitAtBranching(hierarchy) : null;
3066
+ if (!branchingSplit) {
3067
+ hierarchy.each((d) => {
3068
+ const angleRad = centeredStartRad + d.x * scale;
3069
+ const radius = radiusOfDepth(d.depth);
3070
+ d.x = radius * Math.sin(angleRad);
3071
+ d.y = -radius * Math.cos(angleRad);
3072
+ });
3073
+ return hierarchy;
3074
+ }
3075
+ const { chain, branching, children } = branchingSplit;
3076
+ const gap = Math.max(0, wedgeSpanRad - neededRad * scale) / (children.length - 1);
3077
+ const wedgeStartRad = wedgeCenterRad - wedgeSpanRad / 2;
3078
+ const angleOffsetRad = /* @__PURE__ */ new Map();
3079
+ hierarchy.each((d) => angleOffsetRad.set(d, d.x * scale));
3080
+ children.forEach((child, i) => {
3081
+ child.each((d) => angleOffsetRad.set(d, angleOffsetRad.get(d) + i * gap));
3082
+ });
3083
+ const recenter = gap * (children.length - 1) / 2;
3084
+ [...chain, branching].forEach((d) => angleOffsetRad.set(d, angleOffsetRad.get(d) + recenter));
2974
3085
  hierarchy.each((d) => {
2975
- const angleRad = wedgeStartRad + d.x * scale;
3086
+ const angleRad = wedgeStartRad + angleOffsetRad.get(d);
2976
3087
  const radius = radiusOfDepth(d.depth);
2977
3088
  d.x = radius * Math.sin(angleRad);
2978
3089
  d.y = -radius * Math.cos(angleRad);
@@ -3059,7 +3170,8 @@ function renderPopSubtree(d3Lib, svg, hierarchy, rootColor, reparentingNodeId, r
3059
3170
  rootLinkOrigin,
3060
3171
  isMainstreamSector = false,
3061
3172
  selectedNodeId = null,
3062
- nodeFillOf
3173
+ nodeFillOf,
3174
+ dotRadiusMemory
3063
3175
  } = options;
3064
3176
  const { onReparentTargetSelect, onNodeClick, onNodeDoubleClick, onNodeHover } = callbacks;
3065
3177
  const isForbidden = (d) => reparentForbiddenIds.includes(d.data.id);
@@ -3122,13 +3234,18 @@ function renderPopSubtree(d3Lib, svg, hierarchy, rootColor, reparentingNodeId, r
3122
3234
  );
3123
3235
  d3Lib.select(this).attr("x", hitArea.x0).attr("y", hitArea.y0).attr("width", hitArea.x1 - hitArea.x0).attr("height", hitArea.y1 - hitArea.y0);
3124
3236
  }).attr("fill", "transparent").attr("pointer-events", "all");
3125
- nodes.append("circle").attr("class", "gtv-node-dot").attr("r", (d) => calculateNodeDotRadius(d.data.itemCount, itemCountRange)).attr("fill", nodeFill).attr(
3237
+ const targetDotRadius = (d) => calculateNodeDotRadius(d.data.itemCount, itemCountRange);
3238
+ const animateDots = dotRadiusMemory !== void 0 && !(typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)").matches);
3239
+ const startDotRadius = (d) => (animateDots ? dotRadiusMemory?.get(d.data.id) : void 0) ?? targetDotRadius(d);
3240
+ nodes.append("circle").attr("class", "gtv-node-dot").attr("r", startDotRadius).attr("fill", nodeFill).attr(
3126
3241
  "stroke",
3127
3242
  (d) => isSelected(d) ? ACCENT_COLOR : isMainstreamSector ? TEXT_COLOR : SURFACE_BORDER_COLOR
3128
3243
  ).attr(
3129
3244
  "stroke-width",
3130
3245
  (d) => isSelected(d) ? calculateSelectedNodeBorderWidth(calculateNodeDotRadius(d.data.itemCount, itemCountRange)) : calculateNodeBorderWidth(d.data.itemCount, itemCountRange)
3131
- ).style("pointer-events", "none");
3246
+ ).style("pointer-events", "none").filter((d) => startDotRadius(d) !== targetDotRadius(d)).transition().duration(DOT_RADIUS_TRANSITION_MS).ease(d3Lib.easeCubicOut).attr("r", targetDotRadius);
3247
+ nodes.each((d) => dotRadiusMemory?.set(d.data.id, targetDotRadius(d)));
3248
+ nodes.sort((a, b) => targetDotRadius(b) - targetDotRadius(a));
3132
3249
  nodes.append("text").attr(
3133
3250
  "class",
3134
3251
  "gtv-node-label"
@@ -3169,12 +3286,12 @@ function renderPopSubtree(d3Lib, svg, hierarchy, rootColor, reparentingNodeId, r
3169
3286
  group.on("click", (event) => {
3170
3287
  if (reparentingNodeId) return;
3171
3288
  const element = event.currentTarget;
3172
- deferredClick.schedule(() => onNodeClick?.(d.data, event, element));
3173
- });
3174
- group.on("dblclick", () => {
3175
- deferredClick.cancel();
3176
- onNodeDoubleClick?.(d.data);
3289
+ deferredClick.schedule(
3290
+ () => onNodeClick?.(d.data, event, element),
3291
+ () => onNodeDoubleClick?.(d.data)
3292
+ );
3177
3293
  });
3294
+ group.on("dblclick", () => deferredClick.doubleClick(() => onNodeDoubleClick?.(d.data)));
3178
3295
  });
3179
3296
  }
3180
3297
  function hideCollidingLabels(d3Lib, container, itemCountRange, zoomScale) {
@@ -3251,14 +3368,15 @@ function buildCoreHierarchy(d3Lib, coreNodes) {
3251
3368
  });
3252
3369
  return hierarchy;
3253
3370
  }
3254
- function computeCoreRadialLayout(d3Lib, hierarchy, wedgeCenterAngleDegrees, wedgeSpanDegrees, coreRootCircleRadius, depthSpacing, itemCountRange) {
3371
+ function computeCoreRadialLayout(d3Lib, hierarchy, wedgeCenterAngleDegrees, wedgeSpanDegrees, coreRootCircleRadius, depthSpacing, dotRadius = NODE_LAYOUT_SLOT_RADIUS) {
3255
3372
  return layoutWedge(
3256
3373
  d3Lib,
3257
3374
  hierarchy,
3258
3375
  wedgeCenterAngleDegrees,
3259
3376
  wedgeSpanDegrees,
3260
3377
  coreRadiusOfDepth(coreRootCircleRadius, depthSpacing),
3261
- itemCountRange
3378
+ "between-children",
3379
+ dotRadius
3262
3380
  );
3263
3381
  }
3264
3382
  function calculateCoreSubtreeRadialExtent(hierarchy, depthSpacing, coreRootCircleRadius = 0) {
@@ -3298,7 +3416,7 @@ function splitRootGroupBySide(group) {
3298
3416
  }
3299
3417
 
3300
3418
  // src/GenreTreeWheelRadialBase.tsx
3301
- var import_jsx_runtime11 = require("react/jsx-runtime");
3419
+ var import_jsx_runtime13 = require("react/jsx-runtime");
3302
3420
  var LANDING_ANGLE = 90;
3303
3421
  function computeContinuousAngles(groups, layout, previous) {
3304
3422
  const angles = /* @__PURE__ */ new Map();
@@ -3332,11 +3450,9 @@ function WheelRadialCore({
3332
3450
  hideInfoPanelClose = false,
3333
3451
  hideInfoPanelTitle = false,
3334
3452
  itemCountLabel,
3453
+ itemCountScaleMax,
3454
+ nodeCountForSizing,
3335
3455
  readOnly,
3336
- editMode,
3337
- onEditModeChange,
3338
- renderTitleEditor,
3339
- renderParentsEditor,
3340
3456
  renderDangerZone,
3341
3457
  expanded,
3342
3458
  onExpandedChange,
@@ -3347,8 +3463,12 @@ function WheelRadialCore({
3347
3463
  defaultViewport,
3348
3464
  onViewportChange,
3349
3465
  controlsOrientation = "vertical",
3466
+ showZoomControls = "auto",
3467
+ floatingControlsLeading,
3468
+ busy,
3350
3469
  selectedNodeId
3351
3470
  }) {
3471
+ const fullNodeCountForSizing = nodeCountForSizing ?? nodes.length;
3352
3472
  const index = useTreeIndex(nodes);
3353
3473
  const groups = (0, import_react10.useMemo)(() => groupNodesByRoot(nodes), [nodes]);
3354
3474
  const rootColorOf = (node) => getGenreTreeColor(index.byId.get(index.rootIdById.get(node.id) ?? node.id)?.wikidataId);
@@ -3401,11 +3521,15 @@ function WheelRadialCore({
3401
3521
  () => getItemCountRange(
3402
3522
  groups.map((group) => ({
3403
3523
  itemCount: rootItemCountById.get(group.root.id)
3404
- }))
3524
+ })),
3525
+ { itemCountScaleMax, nodeCountForSizing: fullNodeCountForSizing }
3405
3526
  ),
3406
- [groups, rootItemCountById]
3527
+ [groups, rootItemCountById, itemCountScaleMax, fullNodeCountForSizing]
3528
+ );
3529
+ const wheelItemCountRange = (0, import_react10.useMemo)(
3530
+ () => getItemCountRange(nodes, { itemCountScaleMax, nodeCountForSizing: fullNodeCountForSizing }),
3531
+ [nodes, itemCountScaleMax, fullNodeCountForSizing]
3407
3532
  );
3408
- const wheelItemCountRange = (0, import_react10.useMemo)(() => getItemCountRange(nodes), [nodes]);
3409
3533
  const sectorSpanByRootId = (0, import_react10.useMemo)(() => {
3410
3534
  const map = /* @__PURE__ */ new Map();
3411
3535
  if (groups.length <= 1) return map;
@@ -3455,10 +3579,10 @@ function WheelRadialCore({
3455
3579
  const chipClearanceFloor = (0, import_react10.useMemo)(
3456
3580
  () => calculateWheelRadiusForAngles(
3457
3581
  layout.map((slot) => slot.angle),
3458
- MAX_NODE_WIDTH,
3582
+ wheelItemCountRange.maxNodeWidth ?? MAX_NODE_WIDTH,
3459
3583
  WHEEL_RADIUS
3460
3584
  ),
3461
- [layout]
3585
+ [layout, wheelItemCountRange.maxNodeWidth]
3462
3586
  );
3463
3587
  const maxCoreExtentDelta = (0, import_react10.useMemo)(() => {
3464
3588
  let extent = 0;
@@ -3505,8 +3629,7 @@ function WheelRadialCore({
3505
3629
  angle,
3506
3630
  span,
3507
3631
  wheelRadius,
3508
- POP_TREE_DEPTH_RADIAL_SPACING,
3509
- wheelItemCountRange
3632
+ POP_TREE_DEPTH_RADIAL_SPACING
3510
3633
  );
3511
3634
  const reparentForbiddenIds = reparentingNodeId ? laidOut.descendants().find((d) => d.data.id === reparentingNodeId)?.descendants().map((d) => d.data.id) ?? [] : [];
3512
3635
  const branchGroup = sectorGroup.append("g").attr("class", "gtv-wheel-core-branch").attr("data-gtv-branch-index", branchIndex);
@@ -3622,10 +3745,11 @@ function WheelRadialCore({
3622
3745
  };
3623
3746
  });
3624
3747
  }, [groups, continuousAngleByRootId, sectorSpanByRootId]);
3625
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
3748
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
3626
3749
  "div",
3627
3750
  {
3628
3751
  ref: viewportRef,
3752
+ "aria-busy": busy,
3629
3753
  className: [
3630
3754
  "gtv-wheel-container",
3631
3755
  "gtv-wheel-container--radial",
@@ -3639,7 +3763,7 @@ function WheelRadialCore({
3639
3763
  },
3640
3764
  onPointerDown: panZoom.handlePointerDown,
3641
3765
  children: [
3642
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3766
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
3643
3767
  "div",
3644
3768
  {
3645
3769
  ref: stageRef,
@@ -3652,9 +3776,9 @@ function WheelRadialCore({
3652
3776
  transform: panZoom.transform,
3653
3777
  transformOrigin: "0 0"
3654
3778
  },
3655
- children: /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "gtv-wheel-stage", children: [
3656
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "gtv-wheel-circle", ref: wheelCircleRef }),
3657
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "gtv-wheel", children: sectorFills.map((sector) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3779
+ children: /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "gtv-wheel-stage", children: [
3780
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "gtv-wheel-circle", ref: wheelCircleRef }),
3781
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "gtv-wheel", children: sectorFills.map((sector) => /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
3658
3782
  "div",
3659
3783
  {
3660
3784
  className: "gtv-wheel-sector",
@@ -3666,7 +3790,7 @@ function WheelRadialCore({
3666
3790
  },
3667
3791
  `sector-${sector.rootId}`
3668
3792
  )) }),
3669
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3793
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
3670
3794
  "svg",
3671
3795
  {
3672
3796
  ref: coreSvgRef,
@@ -3675,9 +3799,9 @@ function WheelRadialCore({
3675
3799
  height: wheelRadius * 2
3676
3800
  }
3677
3801
  ),
3678
- centerLabel && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "gtv-wheel-center-label", children: centerLabel }),
3679
- /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "gtv-wheel", children: [
3680
- dividerAngles.map((angle, index2) => /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3802
+ centerLabel && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "gtv-wheel-center-label", children: centerLabel }),
3803
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "gtv-wheel", children: [
3804
+ dividerAngles.map((angle, index2) => /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
3681
3805
  "div",
3682
3806
  {
3683
3807
  className: "gtv-wheel-divider",
@@ -3700,12 +3824,12 @@ function WheelRadialCore({
3700
3824
  rootItemCount,
3701
3825
  rootItemCountRange
3702
3826
  );
3703
- return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3827
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
3704
3828
  "div",
3705
3829
  {
3706
3830
  className: "gtv-wheel-slot",
3707
3831
  style: { "--gtv-chip-angle": `${angle}deg` },
3708
- children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("div", { className: "gtv-wheel-chip-anchor", children: /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(
3832
+ children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("div", { className: "gtv-wheel-chip-anchor", children: /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
3709
3833
  "button",
3710
3834
  {
3711
3835
  type: "button",
@@ -3738,8 +3862,8 @@ function WheelRadialCore({
3738
3862
  }
3739
3863
  },
3740
3864
  children: [
3741
- PER_TREE_ACCENT_DOT && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)("span", { className: "gtv-wheel-chip-dot" }),
3742
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3865
+ PER_TREE_ACCENT_DOT && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { className: "gtv-wheel-chip-dot" }),
3866
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
3743
3867
  "span",
3744
3868
  {
3745
3869
  className: "gtv-node-label gtv-node-label--root",
@@ -3758,7 +3882,7 @@ function WheelRadialCore({
3758
3882
  ] })
3759
3883
  }
3760
3884
  ),
3761
- panel && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3885
+ panel && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
3762
3886
  InfoPanel,
3763
3887
  {
3764
3888
  node: panel.node,
@@ -3804,10 +3928,6 @@ function WheelRadialCore({
3804
3928
  hideTitle: hideInfoPanelTitle,
3805
3929
  itemCountLabel,
3806
3930
  readOnly,
3807
- editMode,
3808
- onEditModeChange,
3809
- renderTitleEditor,
3810
- renderParentsEditor,
3811
3931
  renderDangerZone,
3812
3932
  expanded,
3813
3933
  onExpandedChange,
@@ -3826,54 +3946,16 @@ function WheelRadialCore({
3826
3946
  } : void 0
3827
3947
  }
3828
3948
  ),
3829
- overlayPanel && /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(OverlayPanel, { ...overlayPanel }),
3830
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3831
- "div",
3949
+ overlayPanel && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(OverlayPanel, { ...overlayPanel }),
3950
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
3951
+ FloatingControls,
3832
3952
  {
3833
- className: [
3834
- "gtv-wheel-floating-controls",
3835
- controlsOrientation === "horizontal" && "gtv-wheel-floating-controls--horizontal"
3836
- ].filter(Boolean).join(" "),
3837
- children: /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)("div", { className: "gtv-zoom-controls", children: [
3838
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3839
- "button",
3840
- {
3841
- type: "button",
3842
- className: [
3843
- "gtv-zoom-btn",
3844
- !panZoom.canZoomIn && "gtv-zoom-btn--disabled"
3845
- ].filter(Boolean).join(" "),
3846
- disabled: !panZoom.canZoomIn,
3847
- onClick: panZoom.zoomIn,
3848
- "aria-label": "Zoom in",
3849
- children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_md8.MdZoomIn, { className: "gtv-icon", size: 18 })
3850
- }
3851
- ),
3852
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3853
- "button",
3854
- {
3855
- type: "button",
3856
- className: [
3857
- "gtv-zoom-btn",
3858
- !panZoom.canZoomOut && "gtv-zoom-btn--disabled"
3859
- ].filter(Boolean).join(" "),
3860
- disabled: !panZoom.canZoomOut,
3861
- onClick: panZoom.zoomOut,
3862
- "aria-label": "Zoom out",
3863
- children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_md8.MdZoomOut, { className: "gtv-icon", size: 18 })
3864
- }
3865
- ),
3866
- /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(
3867
- "button",
3868
- {
3869
- type: "button",
3870
- className: "gtv-zoom-btn",
3871
- onClick: () => panZoom.fitToFrame([wheelCircleRef.current, coreSvgRef.current]),
3872
- "aria-label": "Fit to frame",
3873
- children: /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(import_md8.MdFitScreen, { className: "gtv-icon", size: 18 })
3874
- }
3875
- )
3876
- ] })
3953
+ panZoom,
3954
+ orientation: controlsOrientation,
3955
+ zoomControls: showZoomControls,
3956
+ leading: floatingControlsLeading,
3957
+ busy,
3958
+ onFit: () => panZoom.fitToFrame([wheelCircleRef.current, coreSvgRef.current])
3877
3959
  }
3878
3960
  )
3879
3961
  ]
@@ -3882,14 +3964,13 @@ function WheelRadialCore({
3882
3964
  }
3883
3965
 
3884
3966
  // src/GenreTreeWheelRadial.tsx
3885
- var import_jsx_runtime12 = require("react/jsx-runtime");
3967
+ var import_jsx_runtime14 = require("react/jsx-runtime");
3886
3968
  function GenreTreeWheelRadial(props) {
3887
- return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(WheelRadialCore, { ...props });
3969
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(WheelRadialCore, { ...props });
3888
3970
  }
3889
3971
 
3890
3972
  // src/GenreTreeWheelRadialPopCoreBase.tsx
3891
3973
  var import_react13 = require("react");
3892
- var import_md10 = require("react-icons/md");
3893
3974
  var d34 = __toESM(require("d3"), 1);
3894
3975
 
3895
3976
  // src/use-focus-history.ts
@@ -3918,17 +3999,17 @@ function useFocusHistory(focusNodeId, isFocusable) {
3918
3999
 
3919
4000
  // src/WheelBreadcrumb.tsx
3920
4001
  var import_react12 = require("react");
3921
- var import_md9 = require("react-icons/md");
3922
- var import_jsx_runtime13 = require("react/jsx-runtime");
4002
+ var import_md6 = require("react-icons/md");
4003
+ var import_jsx_runtime15 = require("react/jsx-runtime");
3923
4004
  var MAX_VISIBLE_ANCESTORS = 3;
3924
4005
  var ANCESTORS_KEPT_WHEN_COLLAPSED = 2;
3925
4006
  function WheelBreadcrumb({ ancestors, currentName, onHome, onAncestor }) {
3926
4007
  const [expanded, setExpanded] = (0, import_react12.useState)(false);
3927
4008
  const collapsed = !expanded && ancestors.length > MAX_VISIBLE_ANCESTORS;
3928
4009
  const visibleAncestors = collapsed ? ancestors.slice(-ANCESTORS_KEPT_WHEN_COLLAPSED) : ancestors;
3929
- const separator = /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { "aria-hidden": "true", children: "\u203A" });
3930
- return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("nav", { className: "gtv-wheel-breadcrumb", "aria-label": "Breadcrumb", children: [
3931
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
4010
+ const separator = /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { "aria-hidden": "true", children: "\u203A" });
4011
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("nav", { className: "gtv-wheel-breadcrumb", "aria-label": "Breadcrumb", children: [
4012
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
3932
4013
  "button",
3933
4014
  {
3934
4015
  type: "button",
@@ -3936,12 +4017,12 @@ function WheelBreadcrumb({ ancestors, currentName, onHome, onAncestor }) {
3936
4017
  "aria-label": "All genres",
3937
4018
  title: "All genres",
3938
4019
  onClick: onHome,
3939
- children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_md9.MdHome, { className: "gtv-icon", size: 14 })
4020
+ children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(import_md6.MdHome, { className: "gtv-icon", size: 14 })
3940
4021
  }
3941
4022
  ),
3942
4023
  separator,
3943
- collapsed && /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(import_jsx_runtime13.Fragment, { children: [
3944
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
4024
+ collapsed && /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(import_jsx_runtime15.Fragment, { children: [
4025
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
3945
4026
  "button",
3946
4027
  {
3947
4028
  type: "button",
@@ -3954,11 +4035,11 @@ function WheelBreadcrumb({ ancestors, currentName, onHome, onAncestor }) {
3954
4035
  ),
3955
4036
  separator
3956
4037
  ] }),
3957
- visibleAncestors.map((ancestor) => /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(import_react12.Fragment, { children: [
3958
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("button", { type: "button", className: "gtv-wheel-breadcrumb-crumb", onClick: () => onAncestor(ancestor.id), children: ancestor.name }),
4038
+ visibleAncestors.map((ancestor) => /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(import_react12.Fragment, { children: [
4039
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("button", { type: "button", className: "gtv-wheel-breadcrumb-crumb", onClick: () => onAncestor(ancestor.id), children: ancestor.name }),
3959
4040
  separator
3960
4041
  ] }, ancestor.id)),
3961
- /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { className: "gtv-wheel-breadcrumb-current", "aria-current": "page", children: currentName })
4042
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "gtv-wheel-breadcrumb-current", "aria-current": "page", children: currentName })
3962
4043
  ] });
3963
4044
  }
3964
4045
 
@@ -4019,7 +4100,7 @@ function resolvePopCoreFrame(nodes, focusNodeId) {
4019
4100
  }
4020
4101
 
4021
4102
  // src/GenreTreeWheelRadialPopCoreBase.tsx
4022
- var import_jsx_runtime14 = require("react/jsx-runtime");
4103
+ var import_jsx_runtime16 = require("react/jsx-runtime");
4023
4104
  var LANDING_ANGLE2 = 90;
4024
4105
  var CHIP_GAP = 16;
4025
4106
  var sum = (values) => values.reduce((total, value) => total + value, 0);
@@ -4055,11 +4136,9 @@ function WheelRadialPopCoreCore({
4055
4136
  hideInfoPanelClose = false,
4056
4137
  hideInfoPanelTitle = false,
4057
4138
  itemCountLabel,
4139
+ itemCountScaleMax,
4140
+ nodeCountForSizing,
4058
4141
  readOnly,
4059
- editMode,
4060
- onEditModeChange,
4061
- renderTitleEditor,
4062
- renderParentsEditor,
4063
4142
  renderDangerZone,
4064
4143
  expanded,
4065
4144
  onExpandedChange,
@@ -4070,11 +4149,15 @@ function WheelRadialPopCoreCore({
4070
4149
  defaultViewport,
4071
4150
  onViewportChange,
4072
4151
  controlsOrientation = "vertical",
4152
+ showZoomControls = "auto",
4153
+ floatingControlsLeading,
4154
+ busy,
4073
4155
  allowWheelRotation = true,
4074
4156
  selectedNodeId,
4075
4157
  focusNodeId: requestedFocusNodeId = null,
4076
4158
  onFocusChange
4077
4159
  }) {
4160
+ const fullNodeCountForSizing = nodeCountForSizing ?? nodes.length;
4078
4161
  const index = useTreeIndex(nodes);
4079
4162
  const focusNodeId = (0, import_react13.useMemo)(
4080
4163
  () => requestedFocusNodeId !== null && isZoomableNode(nodes, requestedFocusNodeId) ? requestedFocusNodeId : null,
@@ -4142,6 +4225,7 @@ function WheelRadialPopCoreCore({
4142
4225
  }
4143
4226
  const viewportRef = (0, import_react13.useRef)(null);
4144
4227
  const stageRef = (0, import_react13.useRef)(null);
4228
+ const dotRadiusMemoryRef = (0, import_react13.useRef)(/* @__PURE__ */ new Map());
4145
4229
  const panZoom = usePanZoom(viewportRef, wheelZoom, stageRef, { defaultViewport, onViewportChange });
4146
4230
  const { panel: openPanel, showNodeInfo, closeNodeInfo } = useNodeInfoPanel({ selectedNodeId, onSelectedNodeChange });
4147
4231
  const isInFrame = (id) => id === focusNodeId || centerSubtreeNodeIds.has(id) || ringRootById.has(id);
@@ -4213,8 +4297,8 @@ function WheelRadialPopCoreCore({
4213
4297
  () => getItemCountRange([
4214
4298
  ...nodes,
4215
4299
  ...[...rootItemCountById.values()].map((itemCount) => ({ itemCount }))
4216
- ]),
4217
- [nodes, rootItemCountById]
4300
+ ], { itemCountScaleMax, nodeCountForSizing: fullNodeCountForSizing }),
4301
+ [nodes, rootItemCountById, itemCountScaleMax, fullNodeCountForSizing]
4218
4302
  );
4219
4303
  const isCenterMainstream = isMainstreamPopLineage(index, centerNode.id);
4220
4304
  const centerNodeColor = isCenterMainstream ? "#ffffff" : getGenreNodeStyle(index, centerNode.id).fill;
@@ -4248,8 +4332,8 @@ function WheelRadialPopCoreCore({
4248
4332
  centerSubtreeHierarchy,
4249
4333
  0,
4250
4334
  POP_TREE_DEPTH_RADIAL_SPACING
4251
- ) : centerChipDiameter / 2,
4252
- [centerSubtreeHierarchy, centerChipDiameter]
4335
+ ) : wheelItemCountRange.maxDotRadius ?? NODE_LAYOUT_SLOT_RADIUS,
4336
+ [centerSubtreeHierarchy, wheelItemCountRange.maxDotRadius]
4253
4337
  );
4254
4338
  const maxCoreExtentDelta = (0, import_react13.useMemo)(() => {
4255
4339
  let extent = 0;
@@ -4257,11 +4341,7 @@ function WheelRadialPopCoreCore({
4257
4341
  core.forEach((hierarchy) => {
4258
4342
  extent = Math.max(
4259
4343
  extent,
4260
- calculateCoreSubtreeRadialExtent(
4261
- hierarchy,
4262
- POP_TREE_DEPTH_RADIAL_SPACING,
4263
- 0
4264
- )
4344
+ calculateCoreSubtreeRadialExtent(hierarchy, POP_TREE_DEPTH_RADIAL_SPACING, 0)
4265
4345
  );
4266
4346
  });
4267
4347
  });
@@ -4275,7 +4355,8 @@ function WheelRadialPopCoreCore({
4275
4355
  (radius) => new Map(
4276
4356
  groups.map((group) => {
4277
4357
  const { pop, core } = branchHierarchiesByRootId.get(group.root.id);
4278
- const chipWidth = 2 * calculateNodeDotRadius(rootItemCountById.get(group.root.id), wheelItemCountRange);
4358
+ const dotRadius = wheelItemCountRange.maxDotRadius ?? NODE_LAYOUT_SLOT_RADIUS;
4359
+ const chipWidth = 2 * dotRadius;
4279
4360
  return [
4280
4361
  group.root.id,
4281
4362
  {
@@ -4284,7 +4365,7 @@ function WheelRadialPopCoreCore({
4284
4365
  d34,
4285
4366
  hierarchy,
4286
4367
  popRadiusOfDepth(radius, POP_TREE_DEPTH_RADIAL_SPACING),
4287
- wheelItemCountRange
4368
+ dotRadius
4288
4369
  )
4289
4370
  ),
4290
4371
  core: core.map(
@@ -4292,7 +4373,7 @@ function WheelRadialPopCoreCore({
4292
4373
  d34,
4293
4374
  hierarchy,
4294
4375
  coreRadiusOfDepth(radius, POP_TREE_DEPTH_RADIAL_SPACING),
4295
- wheelItemCountRange
4376
+ dotRadius
4296
4377
  )
4297
4378
  ),
4298
4379
  chip: 2 * Math.asin(Math.min(1, (chipWidth / 2 + CHIP_GAP) / radius))
@@ -4300,7 +4381,7 @@ function WheelRadialPopCoreCore({
4300
4381
  ];
4301
4382
  })
4302
4383
  ),
4303
- [groups, branchHierarchiesByRootId, rootItemCountById, wheelItemCountRange]
4384
+ [groups, branchHierarchiesByRootId, wheelItemCountRange.maxDotRadius]
4304
4385
  );
4305
4386
  const coreRootCircleRadius = (0, import_react13.useMemo)(
4306
4387
  () => solveRingRadius(
@@ -4310,11 +4391,11 @@ function WheelRadialPopCoreCore({
4310
4391
  ),
4311
4392
  Math.max(
4312
4393
  WHEEL_POP_CORE_RADIUS,
4313
- mainstreamCircleRadius + NODE_DOT_MAX_RADIUS + POP_SUBTREE_OUTER_MARGIN,
4394
+ mainstreamCircleRadius + (wheelItemCountRange.maxDotRadius ?? NODE_DOT_MAX_RADIUS) + POP_SUBTREE_OUTER_MARGIN,
4314
4395
  popReachRequiredRadius
4315
4396
  )
4316
4397
  ),
4317
- [measureBranchNeedsAt, mainstreamCircleRadius, popReachRequiredRadius]
4398
+ [measureBranchNeedsAt, mainstreamCircleRadius, popReachRequiredRadius, wheelItemCountRange.maxDotRadius]
4318
4399
  );
4319
4400
  const branchNeedsByRootId = (0, import_react13.useMemo)(
4320
4401
  () => measureBranchNeedsAt(coreRootCircleRadius),
@@ -4419,7 +4500,7 @@ function WheelRadialPopCoreCore({
4419
4500
  coreRootCircleRadius,
4420
4501
  span,
4421
4502
  POP_TREE_DEPTH_RADIAL_SPACING,
4422
- wheelItemCountRange
4503
+ wheelItemCountRange.maxDotRadius ?? NODE_LAYOUT_SLOT_RADIUS
4423
4504
  );
4424
4505
  const reparentForbiddenIds = reparentingNodeId ? laidOut.descendants().find((d) => d.data.id === reparentingNodeId)?.descendants().map((d) => d.data.id) ?? [] : [];
4425
4506
  const branchGroup = sectorGroup.append("g").attr("class", "gtv-wheel-pop-branch").attr("data-gtv-branch-index", branchIndex);
@@ -4463,6 +4544,7 @@ function WheelRadialPopCoreCore({
4463
4544
  {
4464
4545
  rootLinkOrigin,
4465
4546
  selectedNodeId: panel?.node.id ?? null,
4547
+ dotRadiusMemory: dotRadiusMemoryRef.current,
4466
4548
  nodeFillOf: (node) => getGenreNodeStyle(index, node.id).fill
4467
4549
  }
4468
4550
  );
@@ -4475,6 +4557,9 @@ function WheelRadialPopCoreCore({
4475
4557
  );
4476
4558
  const sectorGroup = originGroup.append("g").attr("class", "gtv-wheel-core-sector").attr("data-gtv-root-id", rootId);
4477
4559
  branches.forEach(({ hierarchy, angle, span }, branchIndex) => {
4560
+ const undergroundIds = new Set(
4561
+ hierarchy.descendants().filter((d) => d.ancestors().some((a) => a.data.scene === "underground")).map((d) => d.data.id)
4562
+ );
4478
4563
  const laidOut = computeCoreRadialLayout(
4479
4564
  d34,
4480
4565
  hierarchy,
@@ -4482,9 +4567,33 @@ function WheelRadialPopCoreCore({
4482
4567
  span,
4483
4568
  coreRootCircleRadius,
4484
4569
  POP_TREE_DEPTH_RADIAL_SPACING,
4485
- wheelItemCountRange
4570
+ wheelItemCountRange.maxDotRadius ?? NODE_LAYOUT_SLOT_RADIUS
4486
4571
  );
4487
4572
  const reparentForbiddenIds = reparentingNodeId ? laidOut.descendants().find((d) => d.data.id === reparentingNodeId)?.descendants().map((d) => d.data.id) ?? [] : [];
4573
+ const angleOf = (d) => {
4574
+ const deg = Math.atan2(d.x, -d.y) * 180 / Math.PI;
4575
+ return angle + ((deg - angle + 540) % 360 + 360) % 360 - 180;
4576
+ };
4577
+ const subtreeRange = (d) => {
4578
+ const angles = d.descendants().map(angleOf);
4579
+ return { min: Math.min(...angles), max: Math.max(...angles) };
4580
+ };
4581
+ laidOut.descendants().filter((d) => d.data.scene === "underground" && !d.ancestors().slice(1).some((a) => a.data.scene === "underground")).forEach((ug) => {
4582
+ const mine = subtreeRange(ug);
4583
+ const siblings = ug.parent?.children ?? [];
4584
+ const at = siblings.indexOf(ug);
4585
+ const prev = at > 0 ? siblings[at - 1] : void 0;
4586
+ const next = at >= 0 ? siblings[at + 1] : void 0;
4587
+ const startDeg = prev ? (subtreeRange(prev).max + mine.min) / 2 : angle - span / 2;
4588
+ const endDeg = next ? (mine.max + subtreeRange(next).min) / 2 : angle + span / 2;
4589
+ const arcPath = d34.arc()({
4590
+ innerRadius: coreRootCircleRadius,
4591
+ outerRadius: SCENE_SECTOR_OUTER_RADIUS,
4592
+ startAngle: startDeg * Math.PI / 180,
4593
+ endAngle: endDeg * Math.PI / 180
4594
+ });
4595
+ sectorGroup.append("path").attr("class", "gtv-wheel-scene-sector").attr("d", arcPath).attr("fill", shadeSurface(getGenreNodeColor(index, rootId), UNDERGROUND_DARKEN_RATIO)).attr("fill-opacity", UNDERGROUND_SECTOR_FILL_OPACITY);
4596
+ });
4488
4597
  const branchGroup = sectorGroup.append("g").attr("class", "gtv-wheel-core-branch").attr("data-gtv-branch-index", branchIndex);
4489
4598
  renderPopSubtree(
4490
4599
  d34,
@@ -4527,7 +4636,11 @@ function WheelRadialPopCoreCore({
4527
4636
  isCoreSector: true,
4528
4637
  rootLinkOrigin,
4529
4638
  selectedNodeId: panel?.node.id ?? null,
4530
- nodeFillOf: (node) => getGenreNodeStyle(index, node.id).fill
4639
+ dotRadiusMemory: dotRadiusMemoryRef.current,
4640
+ nodeFillOf: (node) => {
4641
+ const fill = getGenreNodeStyle(index, node.id).fill;
4642
+ return undergroundIds.has(node.id) ? shadeSurface(fill, UNDERGROUND_DARKEN_RATIO) : fill;
4643
+ }
4531
4644
  }
4532
4645
  );
4533
4646
  });
@@ -4582,6 +4695,7 @@ function WheelRadialPopCoreCore({
4582
4695
  skipRootNode: true,
4583
4696
  isMainstreamSector: isCenterMainstream,
4584
4697
  selectedNodeId: panel?.node.id ?? null,
4698
+ dotRadiusMemory: dotRadiusMemoryRef.current,
4585
4699
  nodeFillOf: (node) => getGenreNodeStyle(index, node.id).fill
4586
4700
  }
4587
4701
  );
@@ -4617,7 +4731,7 @@ function WheelRadialPopCoreCore({
4617
4731
  originGroup.selectAll("g.gtv-root-label").select("text").style("pointer-events", "all").style("cursor", "pointer").on("click", (_event, d) => {
4618
4732
  const chip = d.id === centerNode.id ? centerChipRef.current : viewportRef.current?.querySelector(`#group-${CSS.escape(d.id)}`);
4619
4733
  chip?.click();
4620
- }).on("dblclick", (_event, d) => focusOnNodeRef.current(d.id));
4734
+ }).on("dblclick", (_event, d) => chipClickRef.current.doubleClick(() => focusOnNodeRef.current(d.id)));
4621
4735
  }, [
4622
4736
  centerNode,
4623
4737
  centerHasPopDisc,
@@ -4776,11 +4890,12 @@ function WheelRadialPopCoreCore({
4776
4890
  )
4777
4891
  };
4778
4892
  });
4779
- }, [groups, continuousAngleByRootId, sectorSpanByRootId]);
4780
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
4893
+ }, [groups, continuousAngleByRootId, sectorSpanByRootId, index]);
4894
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
4781
4895
  "div",
4782
4896
  {
4783
4897
  ref: viewportRef,
4898
+ "aria-busy": busy,
4784
4899
  className: [
4785
4900
  "gtv-wheel-container",
4786
4901
  "gtv-wheel-container--radial",
@@ -4795,7 +4910,7 @@ function WheelRadialPopCoreCore({
4795
4910
  },
4796
4911
  onPointerDown: panZoom.handlePointerDown,
4797
4912
  children: [
4798
- /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
4913
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
4799
4914
  "div",
4800
4915
  {
4801
4916
  ref: stageRef,
@@ -4809,9 +4924,9 @@ function WheelRadialPopCoreCore({
4809
4924
  transformOrigin: "0 0"
4810
4925
  },
4811
4926
  children: [
4812
- /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "gtv-wheel-stage", children: [
4813
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "gtv-wheel-circle", ref: wheelCircleRef }),
4814
- isPopExpanded && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
4927
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "gtv-wheel-stage", children: [
4928
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "gtv-wheel-circle", ref: wheelCircleRef }),
4929
+ isPopExpanded && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4815
4930
  "div",
4816
4931
  {
4817
4932
  className: "gtv-wheel-middle-circle",
@@ -4820,8 +4935,8 @@ function WheelRadialPopCoreCore({
4820
4935
  }
4821
4936
  }
4822
4937
  ),
4823
- /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "gtv-wheel", children: [
4824
- sectorFills.map((sector) => /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
4938
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "gtv-wheel", children: [
4939
+ sectorFills.map((sector) => /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4825
4940
  "div",
4826
4941
  {
4827
4942
  className: "gtv-wheel-sector",
@@ -4833,8 +4948,8 @@ function WheelRadialPopCoreCore({
4833
4948
  },
4834
4949
  `sector-${sector.rootId}`
4835
4950
  )),
4836
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "gtv-wheel-inner-tint" }),
4837
- isPopExpanded && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
4951
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "gtv-wheel-inner-tint" }),
4952
+ isPopExpanded && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4838
4953
  "div",
4839
4954
  {
4840
4955
  className: "gtv-wheel-middle-tint",
@@ -4844,7 +4959,7 @@ function WheelRadialPopCoreCore({
4844
4959
  }
4845
4960
  )
4846
4961
  ] }),
4847
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
4962
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4848
4963
  "svg",
4849
4964
  {
4850
4965
  ref: popSvgRef,
@@ -4853,7 +4968,7 @@ function WheelRadialPopCoreCore({
4853
4968
  height: svgCanvasRadius * 2
4854
4969
  }
4855
4970
  ),
4856
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "gtv-wheel-center-node", children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "gtv-wheel-chip-anchor", children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
4971
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "gtv-wheel-center-node", children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "gtv-wheel-chip-anchor", children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4857
4972
  "button",
4858
4973
  {
4859
4974
  ref: centerChipRef,
@@ -4885,14 +5000,14 @@ function WheelRadialPopCoreCore({
4885
5000
  showNodeInfo(centerNode, chip, viewportRef.current);
4886
5001
  onNodeClick?.(centerNode, nativeEvent);
4887
5002
  }
4888
- });
5003
+ }, () => focusOnNode(centerNode.id), centerNode.id);
4889
5004
  },
4890
- onDoubleClick: () => focusOnNode(centerNode.id),
5005
+ onDoubleClick: () => chipClickRef.current.doubleClick(() => focusOnNode(centerNode.id)),
4891
5006
  "aria-label": centerNode.name
4892
5007
  }
4893
5008
  ) }) }),
4894
- /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "gtv-wheel", children: [
4895
- dividerAngles.map((angle, index2) => /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
5009
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "gtv-wheel", children: [
5010
+ dividerAngles.map((angle, index2) => /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4896
5011
  "div",
4897
5012
  {
4898
5013
  className: "gtv-wheel-divider",
@@ -4911,12 +5026,12 @@ function WheelRadialPopCoreCore({
4911
5026
  group.root.id
4912
5027
  );
4913
5028
  const diameter = 2 * calculateNodeDotRadius(rootItemCount, wheelItemCountRange);
4914
- return /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
5029
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4915
5030
  "div",
4916
5031
  {
4917
5032
  className: "gtv-wheel-slot",
4918
5033
  style: { "--gtv-chip-angle": `${angle}deg` },
4919
- children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "gtv-wheel-chip-anchor", children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
5034
+ children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "gtv-wheel-chip-anchor", children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4920
5035
  "button",
4921
5036
  {
4922
5037
  type: "button",
@@ -4947,9 +5062,9 @@ function WheelRadialPopCoreCore({
4947
5062
  showNodeInfo(root, chip, viewportRef.current);
4948
5063
  onNodeClick?.(root, nativeEvent);
4949
5064
  }
4950
- });
5065
+ }, () => focusOnNode(group.root.id), group.root.id);
4951
5066
  },
4952
- onDoubleClick: () => focusOnNode(group.root.id)
5067
+ onDoubleClick: () => chipClickRef.current.doubleClick(() => focusOnNode(group.root.id))
4953
5068
  }
4954
5069
  ) })
4955
5070
  },
@@ -4958,7 +5073,7 @@ function WheelRadialPopCoreCore({
4958
5073
  })
4959
5074
  ] })
4960
5075
  ] }),
4961
- regionLabel && index.byId.has(regionLabel.rootId) && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
5076
+ regionLabel && index.byId.has(regionLabel.rootId) && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4962
5077
  "div",
4963
5078
  {
4964
5079
  ref: regionLabelRef,
@@ -4976,7 +5091,7 @@ function WheelRadialPopCoreCore({
4976
5091
  ]
4977
5092
  }
4978
5093
  ),
4979
- panel && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
5094
+ panel && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
4980
5095
  InfoPanel,
4981
5096
  {
4982
5097
  node: panel.node,
@@ -5003,10 +5118,6 @@ function WheelRadialPopCoreCore({
5003
5118
  hideTitle: hideInfoPanelTitle,
5004
5119
  itemCountLabel,
5005
5120
  readOnly,
5006
- editMode,
5007
- onEditModeChange,
5008
- renderTitleEditor,
5009
- renderParentsEditor,
5010
5121
  renderDangerZone,
5011
5122
  expanded,
5012
5123
  onExpandedChange,
@@ -5026,8 +5137,8 @@ function WheelRadialPopCoreCore({
5026
5137
  onZoomIn: onFocusChange && panel.node.id !== focusNodeId && isZoomableNode(nodes, panel.node.id) ? () => onFocusChange(panel.node.id, "zoom-in") : void 0
5027
5138
  }
5028
5139
  ),
5029
- overlayPanel && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(OverlayPanel, { ...overlayPanel }),
5030
- onFocusChange && focusNodeId !== null && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
5140
+ overlayPanel && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(OverlayPanel, { ...overlayPanel }),
5141
+ onFocusChange && focusNodeId !== null && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
5031
5142
  WheelBreadcrumb,
5032
5143
  {
5033
5144
  ancestors: index.ancestorsOf(focusNodeId).filter((ancestor) => isZoomableNode(nodes, ancestor.id)),
@@ -5043,56 +5154,18 @@ function WheelRadialPopCoreCore({
5043
5154
  },
5044
5155
  focusNodeId
5045
5156
  ),
5046
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
5047
- "div",
5157
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
5158
+ FloatingControls,
5048
5159
  {
5049
- className: [
5050
- "gtv-wheel-floating-controls",
5051
- controlsOrientation === "horizontal" && "gtv-wheel-floating-controls--horizontal"
5052
- ].filter(Boolean).join(" "),
5053
- children: /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "gtv-zoom-controls", children: [
5054
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
5055
- "button",
5056
- {
5057
- type: "button",
5058
- className: [
5059
- "gtv-zoom-btn",
5060
- !panZoom.canZoomIn && "gtv-zoom-btn--disabled"
5061
- ].filter(Boolean).join(" "),
5062
- disabled: !panZoom.canZoomIn,
5063
- onClick: panZoom.zoomIn,
5064
- "aria-label": "Zoom in",
5065
- children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_md10.MdZoomIn, { className: "gtv-icon", size: 18 })
5066
- }
5067
- ),
5068
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
5069
- "button",
5070
- {
5071
- type: "button",
5072
- className: [
5073
- "gtv-zoom-btn",
5074
- !panZoom.canZoomOut && "gtv-zoom-btn--disabled"
5075
- ].filter(Boolean).join(" "),
5076
- disabled: !panZoom.canZoomOut,
5077
- onClick: panZoom.zoomOut,
5078
- "aria-label": "Zoom out",
5079
- children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_md10.MdZoomOut, { className: "gtv-icon", size: 18 })
5080
- }
5081
- ),
5082
- /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
5083
- "button",
5084
- {
5085
- type: "button",
5086
- className: "gtv-zoom-btn",
5087
- onClick: () => panZoom.fitToFrame([
5088
- wheelCircleRef.current,
5089
- ...queryTreeContentElements(popSvgRef.current)
5090
- ]),
5091
- "aria-label": "Fit to frame",
5092
- children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_md10.MdFitScreen, { className: "gtv-icon", size: 18 })
5093
- }
5094
- )
5095
- ] })
5160
+ panZoom,
5161
+ orientation: controlsOrientation,
5162
+ zoomControls: showZoomControls,
5163
+ leading: floatingControlsLeading,
5164
+ busy,
5165
+ onFit: () => panZoom.fitToFrame([
5166
+ wheelCircleRef.current,
5167
+ ...queryTreeContentElements(popSvgRef.current)
5168
+ ])
5096
5169
  }
5097
5170
  )
5098
5171
  ]
@@ -5101,14 +5174,14 @@ function WheelRadialPopCoreCore({
5101
5174
  }
5102
5175
 
5103
5176
  // src/GenreTreeWheelRadialPopCore.tsx
5104
- var import_jsx_runtime15 = require("react/jsx-runtime");
5177
+ var import_jsx_runtime17 = require("react/jsx-runtime");
5105
5178
  function GenreTreeWheelRadialPopCore(props) {
5106
- return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(WheelRadialPopCoreCore, { ...props });
5179
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(WheelRadialPopCoreCore, { ...props });
5107
5180
  }
5108
5181
 
5109
5182
  // src/GenreTreeOutline.tsx
5110
5183
  var import_react14 = require("react");
5111
- var import_jsx_runtime16 = require("react/jsx-runtime");
5184
+ var import_jsx_runtime18 = require("react/jsx-runtime");
5112
5185
  function createOutlineStore(initial) {
5113
5186
  let state = initial;
5114
5187
  const listeners = /* @__PURE__ */ new Set();
@@ -5182,9 +5255,9 @@ function OutlineNodeView({
5182
5255
  const { itemCount } = node;
5183
5256
  const children = model.index.childrenByParentId.get(nodeId) ?? [];
5184
5257
  const split = isRoot ? model.ringSplitById.get(nodeId) : void 0;
5185
- const row = /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("span", { className: ["gtv-outline-row", isSelected && "gtv-outline-row--selected"].filter(Boolean).join(" "), children: [
5186
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "gtv-outline-dot", style: { background: model.styleOf(node).fill } }),
5187
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
5258
+ const row = /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("span", { className: ["gtv-outline-row", isSelected && "gtv-outline-row--selected"].filter(Boolean).join(" "), children: [
5259
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: "gtv-outline-dot", style: { background: model.styleOf(node).fill } }),
5260
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
5188
5261
  "button",
5189
5262
  {
5190
5263
  type: "button",
@@ -5201,16 +5274,16 @@ function OutlineNodeView({
5201
5274
  children: node.name
5202
5275
  }
5203
5276
  ),
5204
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "gtv-outline-count", children: itemCount })
5277
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: "gtv-outline-count", children: itemCount })
5205
5278
  ] });
5206
5279
  const itemProps = { model, store, callbacks };
5207
- return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("li", { className: "gtv-outline-item", "data-gtv-node-id": nodeId, children: children.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("details", { open: isOpen, children: [
5208
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("summary", { onClick: (event) => handleSummaryClick(event, () => callbacks.toggleOpen(nodeId)), children: row }),
5209
- isOpen && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("ul", { className: "gtv-outline-list", children: split ? /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_jsx_runtime16.Fragment, { children: [
5210
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(OutlineSection, { rootId: nodeId, kind: "core", branches: split.coreBranches, ...itemProps }),
5211
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(OutlineSection, { rootId: nodeId, kind: "pop", branches: split.popBranches, ...itemProps })
5212
- ] }) : children.map((child) => /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(OutlineNode, { nodeId: child.id, isRoot: false, ...itemProps }, child.id)) })
5213
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "gtv-outline-leaf", children: row }) });
5280
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("li", { className: "gtv-outline-item", "data-gtv-node-id": nodeId, children: children.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("details", { open: isOpen, children: [
5281
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("summary", { onClick: (event) => handleSummaryClick(event, () => callbacks.toggleOpen(nodeId)), children: row }),
5282
+ isOpen && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("ul", { className: "gtv-outline-list", children: split ? /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(import_jsx_runtime18.Fragment, { children: [
5283
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(OutlineSection, { rootId: nodeId, kind: "core", branches: split.coreBranches, ...itemProps }),
5284
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(OutlineSection, { rootId: nodeId, kind: "pop", branches: split.popBranches, ...itemProps })
5285
+ ] }) : children.map((child) => /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(OutlineNode, { nodeId: child.id, isRoot: false, ...itemProps }, child.id)) })
5286
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "gtv-outline-leaf", children: row }) });
5214
5287
  }
5215
5288
  var OutlineNode = (0, import_react14.memo)(OutlineNodeView);
5216
5289
  var OutlineSection = (0, import_react14.memo)(function OutlineSection2({
@@ -5224,9 +5297,9 @@ var OutlineSection = (0, import_react14.memo)(function OutlineSection2({
5224
5297
  const key = sectionKey(kind, rootId);
5225
5298
  const isOpen = useOutlineState(store, (s) => s.openIds.has(key));
5226
5299
  if (branches.length === 0) return null;
5227
- return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("li", { className: "gtv-outline-item", children: /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("details", { open: isOpen, children: [
5228
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("summary", { onClick: (event) => handleSummaryClick(event, () => callbacks.toggleOpen(key)), children: /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "gtv-outline-section-label", children: kind === "core" ? "Core" : "Pop" }) }),
5229
- isOpen && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("ul", { className: "gtv-outline-list", children: branches.map(({ child }) => /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(OutlineNode, { nodeId: child.id, isRoot: false, model, store, callbacks }, child.id)) })
5300
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("li", { className: "gtv-outline-item", children: /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("details", { open: isOpen, children: [
5301
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("summary", { onClick: (event) => handleSummaryClick(event, () => callbacks.toggleOpen(key)), children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: "gtv-outline-section-label", children: kind === "core" ? "Core" : "Pop" }) }),
5302
+ isOpen && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("ul", { className: "gtv-outline-list", children: branches.map(({ child }) => /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(OutlineNode, { nodeId: child.id, isRoot: false, model, store, callbacks }, child.id)) })
5230
5303
  ] }) });
5231
5304
  });
5232
5305
  function GenreTreeOutline(props) {
@@ -5243,10 +5316,6 @@ function GenreTreeOutline(props) {
5243
5316
  hideInfoPanelTitle = false,
5244
5317
  itemCountLabel,
5245
5318
  readOnly,
5246
- editMode,
5247
- onEditModeChange,
5248
- renderTitleEditor,
5249
- renderParentsEditor,
5250
5319
  renderDangerZone,
5251
5320
  expanded,
5252
5321
  onExpandedChange,
@@ -5347,12 +5416,12 @@ function GenreTreeOutline(props) {
5347
5416
  }, [selectedNodeId, store]);
5348
5417
  const selectedNode = selectedId === null ? void 0 : index.byId.get(selectedId);
5349
5418
  const itemProps = { model, store, callbacks };
5350
- return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: ["gtv-outline", selectedNode && "gtv-outline--panel-open", className].filter(Boolean).join(" "), children: [
5351
- /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "gtv-outline-scroll", ref: scrollRef, children: [
5352
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("ul", { className: "gtv-outline-list gtv-outline-list--roots", children: [model.centerRoot, ...model.ringRoots].map((root) => /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(OutlineNode, { nodeId: root.id, isRoot: true, ...itemProps }, root.id)) }),
5353
- model.detachedRoots.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("ul", { className: "gtv-outline-list gtv-outline-list--roots gtv-outline-list--detached", children: model.detachedRoots.map((root) => /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(OutlineNode, { nodeId: root.id, isRoot: true, ...itemProps }, root.id)) })
5419
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: ["gtv-outline", selectedNode && "gtv-outline--panel-open", className].filter(Boolean).join(" "), children: [
5420
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "gtv-outline-scroll", ref: scrollRef, children: [
5421
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("ul", { className: "gtv-outline-list gtv-outline-list--roots", children: [model.centerRoot, ...model.ringRoots].map((root) => /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(OutlineNode, { nodeId: root.id, isRoot: true, ...itemProps }, root.id)) }),
5422
+ model.detachedRoots.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("ul", { className: "gtv-outline-list gtv-outline-list--roots gtv-outline-list--detached", children: model.detachedRoots.map((root) => /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(OutlineNode, { nodeId: root.id, isRoot: true, ...itemProps }, root.id)) })
5354
5423
  ] }),
5355
- selectedNode && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
5424
+ selectedNode && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
5356
5425
  InfoPanel,
5357
5426
  {
5358
5427
  node: selectedNode,
@@ -5373,10 +5442,6 @@ function GenreTreeOutline(props) {
5373
5442
  hideTitle: hideInfoPanelTitle,
5374
5443
  itemCountLabel,
5375
5444
  readOnly,
5376
- editMode,
5377
- onEditModeChange,
5378
- renderTitleEditor,
5379
- renderParentsEditor,
5380
5445
  renderDangerZone,
5381
5446
  expanded,
5382
5447
  onExpandedChange,
@@ -5395,13 +5460,13 @@ function GenreTreeOutline(props) {
5395
5460
  } : void 0
5396
5461
  }
5397
5462
  ),
5398
- overlayPanel && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(OverlayPanel, { ...overlayPanel })
5463
+ overlayPanel && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(OverlayPanel, { ...overlayPanel })
5399
5464
  ] });
5400
5465
  }
5401
5466
 
5402
5467
  // src/GenreTreeSkeleton.tsx
5403
5468
  var import_react15 = require("react");
5404
- var import_jsx_runtime17 = require("react/jsx-runtime");
5469
+ var import_jsx_runtime19 = require("react/jsx-runtime");
5405
5470
  var CARD_FILL = "#F4F4F5";
5406
5471
  var CARD_BORDER_COLOR = "#E4E4E7";
5407
5472
  var CONNECTOR_COLOR2 = "#D4D4D8";
@@ -5506,8 +5571,8 @@ function SkeletonCard({
5506
5571
  card,
5507
5572
  rootAccent
5508
5573
  }) {
5509
- return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_jsx_runtime17.Fragment, { children: [
5510
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
5574
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(import_jsx_runtime19.Fragment, { children: [
5575
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
5511
5576
  "rect",
5512
5577
  {
5513
5578
  x: card.x,
@@ -5521,7 +5586,7 @@ function SkeletonCard({
5521
5586
  strokeWidth: rootAccent ? 1.5 : 1
5522
5587
  }
5523
5588
  ),
5524
- rootAccent && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
5589
+ rootAccent && /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
5525
5590
  "circle",
5526
5591
  {
5527
5592
  cx: card.x + 18,
@@ -5539,9 +5604,9 @@ function GenreTreeSkeleton() {
5539
5604
  const animationName = `genre-tree-skeleton-sweep-${uid}`;
5540
5605
  const shimmerBandClass = `genre-tree-skeleton-shimmer-band-${uid}`;
5541
5606
  const sweepDistance = VIEWBOX_WIDTH + SHIMMER_BAND_WIDTH;
5542
- return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "gtv-skeleton", children: [
5543
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "gtv-skeleton-sr-only", children: "Loading genre tree\u2026" }),
5544
- /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
5607
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "gtv-skeleton", children: [
5608
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { className: "gtv-skeleton-sr-only", children: "Loading genre tree\u2026" }),
5609
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
5545
5610
  "svg",
5546
5611
  {
5547
5612
  viewBox: `0 0 ${VIEWBOX_WIDTH} ${VIEWBOX_HEIGHT}`,
@@ -5550,7 +5615,7 @@ function GenreTreeSkeleton() {
5550
5615
  className: "gtv-skeleton-svg",
5551
5616
  "aria-hidden": "true",
5552
5617
  children: [
5553
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("style", { children: `
5618
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("style", { children: `
5554
5619
  @keyframes ${animationName} {
5555
5620
  from { transform: translateX(0); }
5556
5621
  to { transform: translateX(${sweepDistance}px); }
@@ -5564,9 +5629,9 @@ function GenreTreeSkeleton() {
5564
5629
  }
5565
5630
  }
5566
5631
  ` }),
5567
- /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("defs", { children: [
5568
- /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("linearGradient", { id: gradientId, x1: "0", y1: "0", x2: "1", y2: "0", children: [
5569
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
5632
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("defs", { children: [
5633
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("linearGradient", { id: gradientId, x1: "0", y1: "0", x2: "1", y2: "0", children: [
5634
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
5570
5635
  "stop",
5571
5636
  {
5572
5637
  offset: "0%",
@@ -5574,7 +5639,7 @@ function GenreTreeSkeleton() {
5574
5639
  stopOpacity: 0
5575
5640
  }
5576
5641
  ),
5577
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
5642
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
5578
5643
  "stop",
5579
5644
  {
5580
5645
  offset: "50%",
@@ -5582,7 +5647,7 @@ function GenreTreeSkeleton() {
5582
5647
  stopOpacity: 0.9
5583
5648
  }
5584
5649
  ),
5585
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
5650
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
5586
5651
  "stop",
5587
5652
  {
5588
5653
  offset: "100%",
@@ -5591,8 +5656,8 @@ function GenreTreeSkeleton() {
5591
5656
  }
5592
5657
  )
5593
5658
  ] }),
5594
- /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("mask", { id: maskId, children: [
5595
- ALL_LINKS.map((link, i) => /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
5659
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("mask", { id: maskId, children: [
5660
+ ALL_LINKS.map((link, i) => /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
5596
5661
  "path",
5597
5662
  {
5598
5663
  d: connectorPath(link.from, link.to),
@@ -5602,7 +5667,7 @@ function GenreTreeSkeleton() {
5602
5667
  },
5603
5668
  `link-mask-${i}`
5604
5669
  )),
5605
- ALL_CARDS.map(({ card }, i) => /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
5670
+ ALL_CARDS.map(({ card }, i) => /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
5606
5671
  "rect",
5607
5672
  {
5608
5673
  x: card.x,
@@ -5617,7 +5682,7 @@ function GenreTreeSkeleton() {
5617
5682
  ))
5618
5683
  ] })
5619
5684
  ] }),
5620
- ALL_LINKS.map((link, i) => /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
5685
+ ALL_LINKS.map((link, i) => /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
5621
5686
  "path",
5622
5687
  {
5623
5688
  d: connectorPath(link.from, link.to),
@@ -5627,8 +5692,8 @@ function GenreTreeSkeleton() {
5627
5692
  },
5628
5693
  `link-${i}`
5629
5694
  )),
5630
- ALL_CARDS.map(({ card, rootAccent }, i) => /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(SkeletonCard, { card, rootAccent }, `card-${i}`)),
5631
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("g", { mask: `url(#${maskId})`, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
5695
+ ALL_CARDS.map(({ card, rootAccent }, i) => /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(SkeletonCard, { card, rootAccent }, `card-${i}`)),
5696
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("g", { mask: `url(#${maskId})`, children: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
5632
5697
  "rect",
5633
5698
  {
5634
5699
  className: shimmerBandClass,
@@ -5647,7 +5712,7 @@ function GenreTreeSkeleton() {
5647
5712
 
5648
5713
  // src/GenreTreeWheelSkeleton.tsx
5649
5714
  var import_react16 = require("react");
5650
- var import_jsx_runtime18 = require("react/jsx-runtime");
5715
+ var import_jsx_runtime20 = require("react/jsx-runtime");
5651
5716
  var SHAPE_COLOR = "#D4D4D8";
5652
5717
  var SECTOR_FILL_OPACITY = 0.22;
5653
5718
  var INNER_TINT_OPACITY = 0.55;
@@ -5792,8 +5857,8 @@ function GenreTreeWheelSkeleton() {
5792
5857
  sharedStartTime ??= animation.startTime ?? document.timeline.currentTime;
5793
5858
  animation.startTime = sharedStartTime;
5794
5859
  }, []);
5795
- return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "gtv-wheel-skeleton", children: [
5796
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
5860
+ return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { className: "gtv-wheel-skeleton", children: [
5861
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
5797
5862
  "div",
5798
5863
  {
5799
5864
  className: "gtv-wheel-skeleton-sector-layer",
@@ -5801,8 +5866,8 @@ function GenreTreeWheelSkeleton() {
5801
5866
  "aria-hidden": "true"
5802
5867
  }
5803
5868
  ),
5804
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: "gtv-skeleton-sr-only", children: "Loading genre tree\u2026" }),
5805
- /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
5869
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("span", { className: "gtv-skeleton-sr-only", children: "Loading genre tree\u2026" }),
5870
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(
5806
5871
  "svg",
5807
5872
  {
5808
5873
  viewBox: `${CANVAS_ORIGIN} ${CANVAS_ORIGIN} ${CANVAS_SIZE} ${CANVAS_SIZE}`,
@@ -5810,14 +5875,14 @@ function GenreTreeWheelSkeleton() {
5810
5875
  className: "gtv-wheel-skeleton-svg",
5811
5876
  "aria-hidden": "true",
5812
5877
  children: [
5813
- /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("defs", { children: [
5814
- /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("radialGradient", { id: mainstreamId, children: [
5815
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("stop", { offset: "0%", stopColor: "#FFFFFF", stopOpacity: 0.45 }),
5816
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("stop", { offset: "65%", stopColor: "#FFFFFF", stopOpacity: 0.15 }),
5817
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("stop", { offset: "100%", stopColor: "#FFFFFF", stopOpacity: 0 })
5878
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("defs", { children: [
5879
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("radialGradient", { id: mainstreamId, children: [
5880
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("stop", { offset: "0%", stopColor: "#FFFFFF", stopOpacity: 0.45 }),
5881
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("stop", { offset: "65%", stopColor: "#FFFFFF", stopOpacity: 0.15 }),
5882
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("stop", { offset: "100%", stopColor: "#FFFFFF", stopOpacity: 0 })
5818
5883
  ] }),
5819
- /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("g", { id: shapesId, children: [
5820
- LINKS.map((link, i) => /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
5884
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("g", { id: shapesId, children: [
5885
+ LINKS.map((link, i) => /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
5821
5886
  "path",
5822
5887
  {
5823
5888
  d: link.d,
@@ -5829,21 +5894,21 @@ function GenreTreeWheelSkeleton() {
5829
5894
  },
5830
5895
  `link-${i}`
5831
5896
  )),
5832
- DOTS.map((dot, i) => /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("circle", { cx: dot.point.x, cy: dot.point.y, r: dot.r, fill: "currentColor" }, `dot-${i}`))
5897
+ DOTS.map((dot, i) => /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("circle", { cx: dot.point.x, cy: dot.point.y, r: dot.r, fill: "currentColor" }, `dot-${i}`))
5833
5898
  ] })
5834
5899
  ] }),
5835
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("circle", { cx: 0, cy: 0, r: RING_RADIUS, fill: "#FFFFFF", fillOpacity: INNER_TINT_OPACITY }),
5836
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("circle", { cx: 0, cy: 0, r: MAINSTREAM_RADIUS, fill: `url(#${mainstreamId})` }),
5837
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("use", { href: `#${shapesId}`, color: SHAPE_COLOR })
5900
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("circle", { cx: 0, cy: 0, r: RING_RADIUS, fill: "#FFFFFF", fillOpacity: INNER_TINT_OPACITY }),
5901
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("circle", { cx: 0, cy: 0, r: MAINSTREAM_RADIUS, fill: `url(#${mainstreamId})` }),
5902
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("use", { href: `#${shapesId}`, color: SHAPE_COLOR })
5838
5903
  ]
5839
5904
  }
5840
5905
  ),
5841
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "gtv-wheel-skeleton-shimmer", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { ref: wedgeRef, className: "gtv-wheel-skeleton-shimmer-wedge" }) })
5906
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("div", { className: "gtv-wheel-skeleton-shimmer", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("div", { ref: wedgeRef, className: "gtv-wheel-skeleton-shimmer-wedge" }) })
5842
5907
  ] });
5843
5908
  }
5844
5909
 
5845
5910
  // src/GenreTreeOutlineSkeleton.tsx
5846
- var import_jsx_runtime19 = require("react/jsx-runtime");
5911
+ var import_jsx_runtime21 = require("react/jsx-runtime");
5847
5912
  var ROWS = [
5848
5913
  { depth: 0, width: 38 },
5849
5914
  { depth: 0, width: 26 },
@@ -5855,9 +5920,9 @@ var ROWS = [
5855
5920
  { depth: 0, width: 22 }
5856
5921
  ];
5857
5922
  function GenreTreeOutlineSkeleton() {
5858
- return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "gtv-skeleton gtv-outline-skeleton", children: [
5859
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { className: "gtv-skeleton-sr-only", children: "Loading genre tree\u2026" }),
5860
- ROWS.map((row, i) => /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
5923
+ return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { className: "gtv-skeleton gtv-outline-skeleton", children: [
5924
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("span", { className: "gtv-skeleton-sr-only", children: "Loading genre tree\u2026" }),
5925
+ ROWS.map((row, i) => /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
5861
5926
  "div",
5862
5927
  {
5863
5928
  className: "gtv-outline-skeleton-row",
@@ -5870,13 +5935,13 @@ function GenreTreeOutlineSkeleton() {
5870
5935
  }
5871
5936
 
5872
5937
  // src/GenreTreeViewSkeleton.tsx
5873
- var import_jsx_runtime20 = require("react/jsx-runtime");
5938
+ var import_jsx_runtime22 = require("react/jsx-runtime");
5874
5939
  function GenreTreeViewSkeleton({ viewMode }) {
5875
5940
  if (viewMode === "wheel" || viewMode === "pop-core") {
5876
- return /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("div", { className: "gtv-view-skeleton-wheel-wrapper", children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(GenreTreeWheelSkeleton, {}) });
5941
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("div", { className: "gtv-view-skeleton-wheel-wrapper", children: /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(GenreTreeWheelSkeleton, {}) });
5877
5942
  }
5878
- if (viewMode === "outline") return /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(GenreTreeOutlineSkeleton, {});
5879
- return /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(GenreTreeSkeleton, {});
5943
+ if (viewMode === "outline") return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(GenreTreeOutlineSkeleton, {});
5944
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(GenreTreeSkeleton, {});
5880
5945
  }
5881
5946
  // Annotate the CommonJS export names for ESM import in node:
5882
5947
  0 && (module.exports = {