@diwauhris/ui 1.7.41 → 1.7.43
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/lib/assets/ui.css +12 -0
- package/lib/index.cjs +320 -120
- package/lib/index.mjs +321 -121
- package/lib/types/lib/utils.d.ts +9 -0
- package/lib/types/ui-library/gallery/org-chart/OrgChart.d.ts +27 -11
- package/package.json +1 -1
package/lib/assets/ui.css
CHANGED
|
@@ -514,6 +514,11 @@
|
|
|
514
514
|
margin-right: auto;
|
|
515
515
|
}
|
|
516
516
|
|
|
517
|
+
.my-0\.5 {
|
|
518
|
+
margin-top: 0.125rem;
|
|
519
|
+
margin-bottom: 0.125rem;
|
|
520
|
+
}
|
|
521
|
+
|
|
517
522
|
.-mr-1 {
|
|
518
523
|
margin-right: -0.25rem;
|
|
519
524
|
}
|
|
@@ -1207,6 +1212,10 @@
|
|
|
1207
1212
|
min-width: 240px;
|
|
1208
1213
|
}
|
|
1209
1214
|
|
|
1215
|
+
.min-w-\[3rem\] {
|
|
1216
|
+
min-width: 3rem;
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1210
1219
|
.min-w-\[400px\] {
|
|
1211
1220
|
min-width: 400px;
|
|
1212
1221
|
}
|
|
@@ -5136,6 +5145,9 @@ body {
|
|
|
5136
5145
|
--tw-shadow-colored: 0 0 #0000;
|
|
5137
5146
|
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
|
|
5138
5147
|
}
|
|
5148
|
+
.disabled\:hover\:bg-transparent:hover:disabled {
|
|
5149
|
+
background-color: transparent;
|
|
5150
|
+
}
|
|
5139
5151
|
.disabled\:active\:scale-100:active:disabled {
|
|
5140
5152
|
--tw-scale-x: 1;
|
|
5141
5153
|
--tw-scale-y: 1;
|
package/lib/index.cjs
CHANGED
|
@@ -54,6 +54,7 @@ let sonner = require("sonner");
|
|
|
54
54
|
function cn(...inputs) {
|
|
55
55
|
return (0, tailwind_merge.twMerge)((0, clsx.clsx)(inputs));
|
|
56
56
|
}
|
|
57
|
+
var FLOATING_LAYER_SELECTOR = `.pis-datepicker-popover, [data-pis-floating]`;
|
|
57
58
|
//#endregion
|
|
58
59
|
//#region node_modules/@radix-ui/react-compose-refs/dist/index.mjs
|
|
59
60
|
var __defProp$1 = Object.defineProperty;
|
|
@@ -2471,7 +2472,7 @@ function Modal({ isOpen, onClose, title, children, icon, subtitle, headerMeta, f
|
|
|
2471
2472
|
onCloseAutoFocus,
|
|
2472
2473
|
onEscapeKeyDown: preventClose ? (e) => e.preventDefault() : onEscapeKeyDown,
|
|
2473
2474
|
onInteractOutside: (e) => {
|
|
2474
|
-
if (e.target?.closest(
|
|
2475
|
+
if (e.target?.closest(FLOATING_LAYER_SELECTOR)) {
|
|
2475
2476
|
e.preventDefault();
|
|
2476
2477
|
return;
|
|
2477
2478
|
}
|
|
@@ -2596,7 +2597,7 @@ function Dialog({ open, onClose, title, description, preventClose = false, size
|
|
|
2596
2597
|
className: cn("fixed left-1/2 top-1/2 z-50 -translate-x-1/2 -translate-y-1/2", "flex w-full flex-col overflow-hidden", "rounded-lg bg-white shadow-xl", "max-h-[90vh] outline-none", SIZE_CLASS[size]),
|
|
2597
2598
|
onEscapeKeyDown: preventClose ? (e) => e.preventDefault() : onEscapeKeyDown,
|
|
2598
2599
|
onInteractOutside: (e) => {
|
|
2599
|
-
if (e.target?.closest(
|
|
2600
|
+
if (e.target?.closest(FLOATING_LAYER_SELECTOR)) {
|
|
2600
2601
|
e.preventDefault();
|
|
2601
2602
|
return;
|
|
2602
2603
|
}
|
|
@@ -4265,7 +4266,11 @@ function Menu({ trigger, items, ariaLabel = "Menu", align = "right", open: contr
|
|
|
4265
4266
|
ref: panelRef,
|
|
4266
4267
|
role: "menu",
|
|
4267
4268
|
"aria-label": ariaLabel,
|
|
4268
|
-
|
|
4269
|
+
["data-pis-floating"]: "",
|
|
4270
|
+
style: {
|
|
4271
|
+
...panelStyle,
|
|
4272
|
+
pointerEvents: "auto"
|
|
4273
|
+
},
|
|
4269
4274
|
className: cn("overflow-y-auto overscroll-contain rounded-xl border border-slate-200 bg-white py-1 shadow-xl", panelClassName),
|
|
4270
4275
|
children: items.map((item, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
4271
4276
|
ref: (node) => {
|
|
@@ -6201,25 +6206,30 @@ function Heatmap({ data, scale = "blue", showLegend = true, showMonthLabels = tr
|
|
|
6201
6206
|
* - 250 × 84 px rounded rectangles (rx=14)
|
|
6202
6207
|
* - Root (depth 0): dark navy fill (#0f172a), white text
|
|
6203
6208
|
* - Children: white fill (#fff), slate border (#e2e8f0)
|
|
6209
|
+
* - Muted tone: dashed border, slate-50 fill, muted text (inactive units)
|
|
6204
6210
|
* - drop-shadow: 0 6px 12px rgba(15,23,42,0.08)
|
|
6205
|
-
* - Name: 13px, font-weight 800, centered
|
|
6211
|
+
* - Name: 13px, font-weight 800, centered, ellipsis + tooltip when too long
|
|
6206
6212
|
* - Description: 11px, font-weight 600, #64748b / #cbd5e1 (root)
|
|
6207
|
-
* - Selected: indigo border overlay ring
|
|
6213
|
+
* - Hover: indigo-300 border; Selected: indigo border + overlay ring
|
|
6208
6214
|
* Connectors:
|
|
6209
6215
|
* - Stepped orthogonal path: M→V→H→V
|
|
6210
6216
|
* - Stroke: #cbd5e1, stroke-width 1.5
|
|
6211
6217
|
* Canvas:
|
|
6212
|
-
* -
|
|
6213
|
-
*
|
|
6214
|
-
*
|
|
6218
|
+
* - Fits the tree to the viewport until the user zooms or pans; after that
|
|
6219
|
+
* the user's view is kept across re-renders, data refreshes and resizes
|
|
6220
|
+
* (e.g. a modal hiding the page scrollbar). "Fit to screen" or a new
|
|
6221
|
+
* `fitKey` hands control back to auto-fit.
|
|
6222
|
+
* - Zoom: scaleExtent [0.05, 1.8]; wheel / pinch / buttons / keyboard
|
|
6223
|
+
* - Pan: mouse/touch drag (a drag never counts as a node click)
|
|
6215
6224
|
* - Layout: nodeSize [280, 185] — horizontal × vertical spacing
|
|
6216
6225
|
* Controls:
|
|
6217
|
-
* -
|
|
6226
|
+
* - Zoom in, zoom %, Zoom out, Fit to screen (top-right overlay panel)
|
|
6218
6227
|
*
|
|
6219
6228
|
* Accessibility:
|
|
6220
|
-
* - SVG has role="img"
|
|
6221
|
-
* -
|
|
6222
|
-
* - Keyboard:
|
|
6229
|
+
* - SVG has an aria-label; role="img" when read-only, role="group" when nodes are selectable
|
|
6230
|
+
* - Selectable nodes are focusable buttons (Tab, Enter / Space to select)
|
|
6231
|
+
* - Keyboard on the chart: + / = zoom in, - zoom out, 0 fit to screen
|
|
6232
|
+
* - Respects prefers-reduced-motion for button zoom transitions
|
|
6223
6233
|
* - Note: full ARIA tree semantics are not possible in an SVG diagram;
|
|
6224
6234
|
* use OrgUnitTree for a fully keyboard-accessible list-based alternative
|
|
6225
6235
|
*
|
|
@@ -6231,6 +6241,13 @@ function Heatmap({ data, scale = "blue", showLegend = true, showMonthLabels = tr
|
|
|
6231
6241
|
* aria-label="Company organization chart"
|
|
6232
6242
|
* />
|
|
6233
6243
|
*/
|
|
6244
|
+
var SYNTHETIC_ROOT_ID = "__synthetic_root__";
|
|
6245
|
+
var NODE_W = 250;
|
|
6246
|
+
var NODE_H = 84;
|
|
6247
|
+
var TEXT_MAX_W = 218;
|
|
6248
|
+
var MIN_SCALE = .05;
|
|
6249
|
+
var MAX_SCALE = 1.8;
|
|
6250
|
+
var BUTTON_ZOOM_STEP = 1.2;
|
|
6234
6251
|
function buildD3Tree(nodes) {
|
|
6235
6252
|
function convert(node, depth) {
|
|
6236
6253
|
return {
|
|
@@ -6239,161 +6256,344 @@ function buildD3Tree(nodes) {
|
|
|
6239
6256
|
description: node.description ?? "",
|
|
6240
6257
|
isRoot: depth === 0,
|
|
6241
6258
|
isSyntheticRoot: false,
|
|
6259
|
+
isMuted: node.tone === "muted",
|
|
6242
6260
|
originalId: node.id,
|
|
6243
6261
|
children: node.children?.map((c) => convert(c, depth + 1))
|
|
6244
6262
|
};
|
|
6245
6263
|
}
|
|
6246
6264
|
if (nodes.length === 1) return convert(nodes[0], 0);
|
|
6247
6265
|
return {
|
|
6248
|
-
id:
|
|
6266
|
+
id: SYNTHETIC_ROOT_ID,
|
|
6249
6267
|
name: "",
|
|
6250
6268
|
description: "",
|
|
6251
6269
|
isRoot: true,
|
|
6252
6270
|
isSyntheticRoot: true,
|
|
6253
|
-
|
|
6271
|
+
isMuted: false,
|
|
6272
|
+
originalId: SYNTHETIC_ROOT_ID,
|
|
6254
6273
|
children: nodes.map((n) => convert(n, 0))
|
|
6255
6274
|
};
|
|
6256
6275
|
}
|
|
6257
|
-
|
|
6276
|
+
/** Writes `text` into an SVG text element, truncating with an ellipsis to fit `maxWidth`. */
|
|
6277
|
+
function fitText(el, text, maxWidth, fontSize) {
|
|
6278
|
+
el.textContent = text;
|
|
6279
|
+
if (!text) return;
|
|
6280
|
+
const measured = typeof el.getComputedTextLength === "function" ? el.getComputedTextLength() : 0;
|
|
6281
|
+
if (!measured) {
|
|
6282
|
+
const maxChars = Math.max(1, Math.floor(maxWidth / (fontSize * .6)));
|
|
6283
|
+
if (text.length > maxChars) el.textContent = `${text.slice(0, maxChars - 1).trimEnd()}…`;
|
|
6284
|
+
return;
|
|
6285
|
+
}
|
|
6286
|
+
if (measured <= maxWidth) return;
|
|
6287
|
+
let lo = 0;
|
|
6288
|
+
let hi = text.length - 1;
|
|
6289
|
+
while (lo < hi) {
|
|
6290
|
+
const mid = Math.ceil((lo + hi) / 2);
|
|
6291
|
+
el.textContent = `${text.slice(0, mid).trimEnd()}…`;
|
|
6292
|
+
if (el.getComputedTextLength() <= maxWidth) lo = mid;
|
|
6293
|
+
else hi = mid - 1;
|
|
6294
|
+
}
|
|
6295
|
+
el.textContent = `${text.slice(0, lo).trimEnd()}…`;
|
|
6296
|
+
}
|
|
6297
|
+
var prefersReducedMotion = () => typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
6298
|
+
function OrgChart({ nodes, selectedId, onSelect, fitKey, height = "75vh", "aria-label": ariaLabel = "Organization chart", className = "" }) {
|
|
6258
6299
|
const svgRef = (0, react.useRef)(null);
|
|
6259
6300
|
const zoomRef = (0, react.useRef)(null);
|
|
6260
|
-
const initialTransformRef = (0, react.useRef)(null);
|
|
6261
6301
|
const treeBoundsRef = (0, react.useRef)(null);
|
|
6262
|
-
const
|
|
6302
|
+
const autoFitRef = (0, react.useRef)(true);
|
|
6303
|
+
const programmaticRef = (0, react.useRef)(false);
|
|
6304
|
+
const pendingFitRef = (0, react.useRef)(false);
|
|
6305
|
+
const lastSizeRef = (0, react.useRef)(null);
|
|
6306
|
+
const onSelectRef = (0, react.useRef)(onSelect);
|
|
6307
|
+
const selectedIdRef = (0, react.useRef)(selectedId);
|
|
6308
|
+
const scaleFrameRef = (0, react.useRef)(0);
|
|
6309
|
+
const [scale, setScale] = (0, react.useState)(1);
|
|
6310
|
+
const uid = `oc-${(0, react.useId)().replace(/[^a-zA-Z0-9_-]/g, "")}`;
|
|
6311
|
+
const interactive = Boolean(onSelect);
|
|
6312
|
+
const isEmpty = nodes.length === 0;
|
|
6313
|
+
const getSize = () => {
|
|
6314
|
+
const rect = svgRef.current?.getBoundingClientRect();
|
|
6315
|
+
return {
|
|
6316
|
+
width: rect?.width ?? 0,
|
|
6317
|
+
height: rect?.height ?? 0
|
|
6318
|
+
};
|
|
6319
|
+
};
|
|
6320
|
+
/** Applies a transform without handing control away from auto-fit. */
|
|
6321
|
+
const applyTransform = (t) => {
|
|
6322
|
+
if (!svgRef.current || !zoomRef.current) return;
|
|
6323
|
+
const svg = d3.select(svgRef.current);
|
|
6324
|
+
svg.interrupt();
|
|
6325
|
+
programmaticRef.current = true;
|
|
6326
|
+
try {
|
|
6327
|
+
svg.call(zoomRef.current.transform, t);
|
|
6328
|
+
} finally {
|
|
6329
|
+
programmaticRef.current = false;
|
|
6330
|
+
}
|
|
6331
|
+
};
|
|
6263
6332
|
const fitToView = () => {
|
|
6264
6333
|
if (!svgRef.current || !zoomRef.current || !treeBoundsRef.current) return;
|
|
6265
|
-
const { width: W, height: H } =
|
|
6266
|
-
if (W === 0 || H === 0)
|
|
6334
|
+
const { width: W, height: H } = getSize();
|
|
6335
|
+
if (W === 0 || H === 0) {
|
|
6336
|
+
pendingFitRef.current = true;
|
|
6337
|
+
return;
|
|
6338
|
+
}
|
|
6339
|
+
pendingFitRef.current = false;
|
|
6267
6340
|
const { minX, maxX, minY, maxY } = treeBoundsRef.current;
|
|
6268
|
-
const contentLeft = minX -
|
|
6269
|
-
const contentRight = maxX +
|
|
6270
|
-
const contentTop = minY -
|
|
6271
|
-
const contentBottom = maxY +
|
|
6341
|
+
const contentLeft = minX - NODE_W / 2;
|
|
6342
|
+
const contentRight = maxX + NODE_W / 2;
|
|
6343
|
+
const contentTop = minY - NODE_H / 2;
|
|
6344
|
+
const contentBottom = maxY + NODE_H / 2;
|
|
6272
6345
|
const contentWidth = contentRight - contentLeft;
|
|
6273
6346
|
const contentHeight = contentBottom - contentTop;
|
|
6274
6347
|
const scaleX = (W - 80) / Math.max(1, contentWidth);
|
|
6275
6348
|
const scaleY = (H - 80) / Math.max(1, contentHeight);
|
|
6276
|
-
const
|
|
6277
|
-
const
|
|
6278
|
-
const
|
|
6279
|
-
|
|
6280
|
-
const ty = H / 2 - contentCenterY * scale;
|
|
6281
|
-
const t = d3.zoomIdentity.translate(tx, ty).scale(scale);
|
|
6282
|
-
initialTransformRef.current = t;
|
|
6283
|
-
d3.select(svgRef.current).call(zoomRef.current.transform, t);
|
|
6349
|
+
const k = Math.max(MIN_SCALE, Math.min(MAX_SCALE, scaleX, scaleY));
|
|
6350
|
+
const tx = W / 2 - (contentLeft + contentRight) / 2 * k;
|
|
6351
|
+
const ty = H / 2 - (contentTop + contentBottom) / 2 * k;
|
|
6352
|
+
applyTransform(d3.zoomIdentity.translate(tx, ty).scale(k));
|
|
6284
6353
|
};
|
|
6285
|
-
(
|
|
6286
|
-
|
|
6354
|
+
/** Fit, and go back to keeping the chart fitted (Fit button, new fitKey). */
|
|
6355
|
+
const resetToFit = () => {
|
|
6356
|
+
autoFitRef.current = true;
|
|
6357
|
+
fitToView();
|
|
6358
|
+
};
|
|
6359
|
+
const zoomBy = (factor) => {
|
|
6360
|
+
if (!svgRef.current || !zoomRef.current) return;
|
|
6361
|
+
autoFitRef.current = false;
|
|
6287
6362
|
const svg = d3.select(svgRef.current);
|
|
6288
|
-
svg.
|
|
6289
|
-
|
|
6290
|
-
|
|
6291
|
-
|
|
6292
|
-
|
|
6363
|
+
if (prefersReducedMotion()) svg.interrupt().call(zoomRef.current.scaleBy, factor);
|
|
6364
|
+
else svg.transition().duration(250).call(zoomRef.current.scaleBy, factor);
|
|
6365
|
+
};
|
|
6366
|
+
(0, react.useEffect)(() => {
|
|
6367
|
+
onSelectRef.current = onSelect;
|
|
6368
|
+
}, [onSelect]);
|
|
6369
|
+
(0, react.useEffect)(() => {
|
|
6370
|
+
const svgEl = svgRef.current;
|
|
6371
|
+
if (!svgEl) return;
|
|
6372
|
+
const svg = d3.select(svgEl);
|
|
6373
|
+
const viewport = svg.append("g").attr("class", "oc-viewport");
|
|
6374
|
+
viewport.append("g").attr("class", "oc-links");
|
|
6375
|
+
viewport.append("g").attr("class", "oc-nodes");
|
|
6376
|
+
const zoom = d3.zoom().scaleExtent([MIN_SCALE, MAX_SCALE]).extent(() => {
|
|
6377
|
+
const { width, height } = getSize();
|
|
6378
|
+
return [[0, 0], [Math.max(1, width), Math.max(1, height)]];
|
|
6379
|
+
}).clickDistance(5).filter((event) => {
|
|
6380
|
+
if (event.type === "dblclick") return false;
|
|
6381
|
+
const e = event;
|
|
6382
|
+
return (!e.ctrlKey || event.type === "wheel") && !e.button;
|
|
6383
|
+
}).on("zoom", (event) => {
|
|
6384
|
+
viewport.attr("transform", event.transform.toString());
|
|
6385
|
+
if (!programmaticRef.current) autoFitRef.current = false;
|
|
6386
|
+
cancelAnimationFrame(scaleFrameRef.current);
|
|
6387
|
+
scaleFrameRef.current = requestAnimationFrame(() => setScale(event.transform.k));
|
|
6388
|
+
});
|
|
6389
|
+
zoomRef.current = zoom;
|
|
6390
|
+
svg.call(zoom);
|
|
6391
|
+
const ro = new ResizeObserver(() => {
|
|
6392
|
+
const size = getSize();
|
|
6393
|
+
if (size.width === 0 || size.height === 0) return;
|
|
6394
|
+
const last = lastSizeRef.current;
|
|
6395
|
+
lastSizeRef.current = size;
|
|
6396
|
+
if (autoFitRef.current || pendingFitRef.current || !last) {
|
|
6397
|
+
fitToView();
|
|
6398
|
+
return;
|
|
6399
|
+
}
|
|
6400
|
+
const dx = (size.width - last.width) / 2;
|
|
6401
|
+
const dy = (size.height - last.height) / 2;
|
|
6402
|
+
if (dx === 0 && dy === 0) return;
|
|
6403
|
+
const t = d3.zoomTransform(svgEl);
|
|
6404
|
+
applyTransform(d3.zoomIdentity.translate(t.x + dx, t.y + dy).scale(t.k));
|
|
6405
|
+
});
|
|
6406
|
+
ro.observe(svgEl);
|
|
6407
|
+
return () => {
|
|
6408
|
+
ro.disconnect();
|
|
6409
|
+
cancelAnimationFrame(scaleFrameRef.current);
|
|
6410
|
+
svg.interrupt().on(".zoom", null);
|
|
6411
|
+
svg.selectAll("*").remove();
|
|
6412
|
+
zoomRef.current = null;
|
|
6413
|
+
};
|
|
6414
|
+
}, []);
|
|
6415
|
+
(0, react.useEffect)(() => {
|
|
6416
|
+
const svgEl = svgRef.current;
|
|
6417
|
+
if (!svgEl) return;
|
|
6418
|
+
const svg = d3.select(svgEl);
|
|
6419
|
+
const linkLayer = svg.select("g.oc-links");
|
|
6420
|
+
const nodeLayer = svg.select("g.oc-nodes");
|
|
6421
|
+
if (nodes.length === 0) {
|
|
6422
|
+
linkLayer.selectAll("*").remove();
|
|
6423
|
+
nodeLayer.selectAll("*").remove();
|
|
6424
|
+
treeBoundsRef.current = null;
|
|
6425
|
+
return;
|
|
6426
|
+
}
|
|
6427
|
+
const root = d3.hierarchy(buildD3Tree(nodes));
|
|
6428
|
+
const pointRoot = d3.tree().nodeSize([280, 185])(root);
|
|
6429
|
+
const descendants = pointRoot.descendants();
|
|
6293
6430
|
treeBoundsRef.current = {
|
|
6294
6431
|
minX: d3.min(descendants, (d) => d.x) ?? 0,
|
|
6295
6432
|
maxX: d3.max(descendants, (d) => d.x) ?? 0,
|
|
6296
6433
|
minY: d3.min(descendants, (d) => d.y) ?? 0,
|
|
6297
6434
|
maxY: d3.max(descendants, (d) => d.y) ?? 0
|
|
6298
6435
|
};
|
|
6299
|
-
|
|
6300
|
-
|
|
6301
|
-
|
|
6302
|
-
svg.call(zoom);
|
|
6303
|
-
g.selectAll("path").data(root.links().filter((l) => !l.source.data.isSyntheticRoot)).join("path").attr("fill", "none").attr("stroke", "#cbd5e1").attr("stroke-width", 1.5).attr("d", (link) => {
|
|
6304
|
-
const sx = link.source.x ?? 0;
|
|
6305
|
-
const sy = link.source.y ?? 0;
|
|
6306
|
-
const tx = link.target.x ?? 0;
|
|
6307
|
-
const ty = link.target.y ?? 0;
|
|
6308
|
-
return `M${sx},${sy}V${(sy + ty) / 2}H${tx}V${ty}`;
|
|
6436
|
+
linkLayer.selectAll("path").data(pointRoot.links().filter((l) => !l.source.data.isSyntheticRoot), (l) => `${l.source.data.originalId}>${l.target.data.originalId}`).join("path").attr("fill", "none").attr("stroke", "#cbd5e1").attr("stroke-width", 1.5).attr("d", (link) => {
|
|
6437
|
+
const mid = (link.source.y + link.target.y) / 2;
|
|
6438
|
+
return `M${link.source.x},${link.source.y}V${mid}H${link.target.x}V${link.target.y}`;
|
|
6309
6439
|
});
|
|
6310
|
-
const node =
|
|
6311
|
-
|
|
6312
|
-
|
|
6440
|
+
const node = nodeLayer.selectAll("g.oc-node").data(descendants.filter((d) => !d.data.isSyntheticRoot), (d) => d.data.originalId).join((enter) => {
|
|
6441
|
+
const g = enter.append("g").attr("class", "oc-node");
|
|
6442
|
+
g.append("title");
|
|
6443
|
+
g.append("rect").attr("class", "oc-card").attr("x", -125).attr("y", -42).attr("width", NODE_W).attr("height", NODE_H).attr("rx", 14).attr("filter", "drop-shadow(0 6px 12px rgba(15,23,42,0.08))");
|
|
6444
|
+
g.append("rect").attr("class", "oc-ring").attr("x", -128).attr("y", -45).attr("width", 256).attr("height", 90).attr("rx", 16).attr("fill", "none");
|
|
6445
|
+
g.append("text").attr("class", "oc-name").attr("text-anchor", "middle").attr("y", -8).attr("font-size", 13).attr("font-weight", 800);
|
|
6446
|
+
g.append("text").attr("class", "oc-desc").attr("text-anchor", "middle").attr("y", 16).attr("font-size", 11).attr("font-weight", 600);
|
|
6447
|
+
return g;
|
|
6448
|
+
}).attr("transform", (d) => `translate(${d.x},${d.y})`).classed("is-root", (d) => d.data.isRoot).classed("is-muted", (d) => d.data.isMuted).classed("is-selected", (d) => d.data.originalId === selectedIdRef.current).attr("aria-label", (d) => d.data.description ? `${d.data.name}, ${d.data.description}` : d.data.name).on("click", (_e, d) => onSelectRef.current?.(d.data.originalId)).on("keydown", (event, d) => {
|
|
6449
|
+
if (event.key !== "Enter" && event.key !== " ") return;
|
|
6450
|
+
event.preventDefault();
|
|
6451
|
+
onSelectRef.current?.(d.data.originalId);
|
|
6452
|
+
}).on("focus", (_e, d) => {
|
|
6453
|
+
if (!zoomRef.current) return;
|
|
6454
|
+
const { width: W, height: H } = getSize();
|
|
6455
|
+
const [sx, sy] = d3.zoomTransform(svgEl).apply([d.x, d.y]);
|
|
6456
|
+
const margin = 24;
|
|
6457
|
+
if (sx >= margin && sx <= W - margin && sy >= margin && sy <= H - margin) return;
|
|
6458
|
+
autoFitRef.current = false;
|
|
6459
|
+
svg.interrupt().call(zoomRef.current.translateTo, d.x, d.y);
|
|
6313
6460
|
});
|
|
6314
|
-
node.
|
|
6315
|
-
|
|
6316
|
-
|
|
6317
|
-
}).attr("stroke-width", (d) => selectedId && d.data.originalId === selectedId ? 2.5 : 1).attr("filter", "drop-shadow(0 6px 12px rgba(15,23,42,0.08))");
|
|
6318
|
-
node.append("text").attr("text-anchor", "middle").attr("y", -8).attr("font-size", 13).attr("font-weight", 800).attr("fill", (d) => d.data.isRoot ? "#fff" : "#0f172a").text((d) => d.data.name);
|
|
6319
|
-
node.append("text").attr("text-anchor", "middle").attr("y", 16).attr("font-size", 11).attr("font-weight", 600).attr("fill", (d) => d.data.isRoot ? "#cbd5e1" : "#64748b").text((d) => d.data.description);
|
|
6320
|
-
if (selectedId) node.filter((d) => d.data.originalId === selectedId).append("rect").attr("x", -128).attr("y", -45).attr("width", 256).attr("height", 90).attr("rx", 16).attr("fill", "none").attr("stroke", "#6366f1").attr("stroke-width", 2.5).attr("opacity", .6);
|
|
6321
|
-
requestAnimationFrame(() => {
|
|
6322
|
-
fitToView();
|
|
6461
|
+
node.select("title").text((d) => d.data.description ? `${d.data.name}\n${d.data.description}` : d.data.name);
|
|
6462
|
+
node.select("text.oc-name").each(function(d) {
|
|
6463
|
+
fitText(this, d.data.name, TEXT_MAX_W, 13);
|
|
6323
6464
|
});
|
|
6465
|
+
node.select("text.oc-desc").each(function(d) {
|
|
6466
|
+
fitText(this, d.data.description, TEXT_MAX_W, 11);
|
|
6467
|
+
});
|
|
6468
|
+
if (autoFitRef.current || pendingFitRef.current) fitToView();
|
|
6469
|
+
}, [nodes]);
|
|
6470
|
+
(0, react.useEffect)(() => {
|
|
6471
|
+
if (!svgRef.current) return;
|
|
6472
|
+
d3.select(svgRef.current).selectAll("g.oc-node").attr("tabindex", interactive ? 0 : null).attr("role", interactive ? "button" : null);
|
|
6473
|
+
}, [interactive, nodes]);
|
|
6474
|
+
(0, react.useEffect)(() => {
|
|
6475
|
+
selectedIdRef.current = selectedId;
|
|
6476
|
+
if (!svgRef.current) return;
|
|
6477
|
+
d3.select(svgRef.current).selectAll("g.oc-node").classed("is-selected", (d) => d.data.originalId === selectedId).attr("aria-pressed", interactive ? (d) => String(d.data.originalId === selectedId) : null);
|
|
6324
6478
|
}, [
|
|
6325
|
-
nodes,
|
|
6326
6479
|
selectedId,
|
|
6327
|
-
|
|
6480
|
+
interactive,
|
|
6481
|
+
nodes
|
|
6328
6482
|
]);
|
|
6483
|
+
const fitKeyRef = (0, react.useRef)(fitKey);
|
|
6329
6484
|
(0, react.useEffect)(() => {
|
|
6330
|
-
|
|
6331
|
-
|
|
6332
|
-
|
|
6333
|
-
|
|
6334
|
-
|
|
6335
|
-
|
|
6336
|
-
|
|
6337
|
-
|
|
6338
|
-
|
|
6339
|
-
|
|
6340
|
-
|
|
6341
|
-
};
|
|
6342
|
-
const resetZoom = () => {
|
|
6343
|
-
fitToView();
|
|
6485
|
+
if (fitKeyRef.current === fitKey) return;
|
|
6486
|
+
fitKeyRef.current = fitKey;
|
|
6487
|
+
resetToFit();
|
|
6488
|
+
}, [fitKey]);
|
|
6489
|
+
const handleKeyDown = (event) => {
|
|
6490
|
+
if (event.ctrlKey || event.metaKey || event.altKey || isEmpty) return;
|
|
6491
|
+
if (event.key === "+" || event.key === "=") zoomBy(BUTTON_ZOOM_STEP);
|
|
6492
|
+
else if (event.key === "-" || event.key === "_") zoomBy(1 / BUTTON_ZOOM_STEP);
|
|
6493
|
+
else if (event.key === "0") resetToFit();
|
|
6494
|
+
else return;
|
|
6495
|
+
event.preventDefault();
|
|
6344
6496
|
};
|
|
6497
|
+
const atMax = scale >= 1.7990000000000002;
|
|
6498
|
+
const atMin = scale <= .051000000000000004;
|
|
6345
6499
|
const controlBtnClass = [
|
|
6346
6500
|
"appearance-none border-0 bg-none font-[inherit]",
|
|
6347
6501
|
"flex items-center justify-center rounded-lg p-2",
|
|
6348
|
-
"text-slate-
|
|
6349
|
-
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-blue/30"
|
|
6502
|
+
"text-slate-500 transition hover:bg-slate-100 hover:text-slate-700",
|
|
6503
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-blue/30",
|
|
6504
|
+
"disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:bg-transparent"
|
|
6350
6505
|
].join(" ");
|
|
6351
6506
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6352
|
-
className: `relative w-full overflow-hidden bg-slate-50/50 ${className}`,
|
|
6507
|
+
className: `relative w-full overflow-hidden bg-slate-50/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-brand-blue/30 ${className}`,
|
|
6353
6508
|
style: { height },
|
|
6354
|
-
|
|
6355
|
-
|
|
6356
|
-
|
|
6357
|
-
|
|
6358
|
-
|
|
6359
|
-
|
|
6360
|
-
|
|
6361
|
-
|
|
6362
|
-
|
|
6363
|
-
|
|
6364
|
-
|
|
6365
|
-
|
|
6366
|
-
|
|
6367
|
-
|
|
6368
|
-
|
|
6369
|
-
|
|
6370
|
-
|
|
6371
|
-
|
|
6372
|
-
|
|
6373
|
-
|
|
6374
|
-
|
|
6375
|
-
|
|
6376
|
-
|
|
6377
|
-
|
|
6378
|
-
|
|
6379
|
-
|
|
6380
|
-
|
|
6381
|
-
|
|
6382
|
-
|
|
6383
|
-
|
|
6509
|
+
tabIndex: isEmpty ? void 0 : 0,
|
|
6510
|
+
"aria-keyshortcuts": isEmpty ? void 0 : "+ - 0",
|
|
6511
|
+
onKeyDown: handleKeyDown,
|
|
6512
|
+
children: [
|
|
6513
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: `
|
|
6514
|
+
#${uid} .oc-card { fill: #fff; stroke: #e2e8f0; stroke-width: 1; transition: stroke 120ms ease; }
|
|
6515
|
+
#${uid} .oc-name { fill: #0f172a; }
|
|
6516
|
+
#${uid} .oc-desc { fill: #64748b; }
|
|
6517
|
+
#${uid} .oc-node.is-root .oc-card { fill: #0f172a; stroke: #0f172a; }
|
|
6518
|
+
#${uid} .oc-node.is-root .oc-name { fill: #fff; }
|
|
6519
|
+
#${uid} .oc-node.is-root .oc-desc { fill: #cbd5e1; }
|
|
6520
|
+
#${uid} .oc-node.is-muted .oc-card { fill: #f8fafc; stroke: #94a3b8; stroke-dasharray: 5 4; }
|
|
6521
|
+
#${uid} .oc-node.is-muted .oc-name { fill: #475569; }
|
|
6522
|
+
#${uid} .oc-node.is-muted .oc-desc { fill: #94a3b8; }
|
|
6523
|
+
#${uid} .oc-node.is-root.is-muted .oc-card { fill: #334155; stroke: #94a3b8; }
|
|
6524
|
+
#${uid} .oc-node.is-root.is-muted .oc-name { fill: #e2e8f0; }
|
|
6525
|
+
#${uid} .oc-node.is-root.is-muted .oc-desc { fill: #cbd5e1; }
|
|
6526
|
+
#${uid} .oc-ring { stroke: #6366f1; stroke-width: 2.5; opacity: 0; pointer-events: none; }
|
|
6527
|
+
#${uid}.is-interactive .oc-node { cursor: pointer; }
|
|
6528
|
+
#${uid}.is-interactive .oc-node:hover .oc-card { stroke: #a5b4fc; stroke-width: 1.5; }
|
|
6529
|
+
#${uid} .oc-node.is-selected .oc-card,
|
|
6530
|
+
#${uid}.is-interactive .oc-node.is-selected:hover .oc-card { stroke: #6366f1; stroke-width: 2.5; }
|
|
6531
|
+
#${uid} .oc-node.is-selected .oc-ring { opacity: 0.6; }
|
|
6532
|
+
#${uid} .oc-node:focus { outline: none; }
|
|
6533
|
+
#${uid} .oc-node:focus-visible .oc-ring { opacity: 1; stroke: #2563eb; }
|
|
6534
|
+
` }),
|
|
6535
|
+
!isEmpty && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6536
|
+
className: "absolute right-4 top-4 z-10 flex flex-col items-center gap-1 rounded-lg border border-slate-100 bg-white p-1.5 shadow-md",
|
|
6537
|
+
children: [
|
|
6538
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
6539
|
+
type: "button",
|
|
6540
|
+
className: controlBtnClass,
|
|
6541
|
+
onClick: () => zoomBy(BUTTON_ZOOM_STEP),
|
|
6542
|
+
disabled: atMax,
|
|
6543
|
+
"aria-label": "Zoom in",
|
|
6544
|
+
title: "Zoom in (+)",
|
|
6545
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ZoomIn, {
|
|
6546
|
+
size: 16,
|
|
6547
|
+
"aria-hidden": "true"
|
|
6548
|
+
})
|
|
6549
|
+
}),
|
|
6550
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
6551
|
+
className: "min-w-[3rem] select-none text-center text-[11px] font-semibold tabular-nums text-slate-500",
|
|
6552
|
+
children: [Math.round(scale * 100), "%"]
|
|
6553
|
+
}),
|
|
6554
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
6555
|
+
type: "button",
|
|
6556
|
+
className: controlBtnClass,
|
|
6557
|
+
onClick: () => zoomBy(1 / BUTTON_ZOOM_STEP),
|
|
6558
|
+
disabled: atMin,
|
|
6559
|
+
"aria-label": "Zoom out",
|
|
6560
|
+
title: "Zoom out (-)",
|
|
6561
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ZoomOut, {
|
|
6562
|
+
size: 16,
|
|
6563
|
+
"aria-hidden": "true"
|
|
6564
|
+
})
|
|
6565
|
+
}),
|
|
6566
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6567
|
+
className: "my-0.5 h-px w-6 bg-slate-100",
|
|
6384
6568
|
"aria-hidden": "true"
|
|
6569
|
+
}),
|
|
6570
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
6571
|
+
type: "button",
|
|
6572
|
+
className: controlBtnClass,
|
|
6573
|
+
onClick: resetToFit,
|
|
6574
|
+
"aria-label": "Fit to screen",
|
|
6575
|
+
title: "Fit to screen (0)",
|
|
6576
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Scan, {
|
|
6577
|
+
size: 16,
|
|
6578
|
+
"aria-hidden": "true"
|
|
6579
|
+
})
|
|
6385
6580
|
})
|
|
6386
|
-
|
|
6387
|
-
|
|
6388
|
-
|
|
6389
|
-
|
|
6390
|
-
|
|
6391
|
-
|
|
6392
|
-
|
|
6393
|
-
|
|
6394
|
-
|
|
6395
|
-
|
|
6396
|
-
|
|
6581
|
+
]
|
|
6582
|
+
}),
|
|
6583
|
+
isEmpty && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6584
|
+
className: "absolute inset-0 flex items-center justify-center p-6 text-center text-sm font-semibold text-slate-500",
|
|
6585
|
+
children: "No units to display"
|
|
6586
|
+
}),
|
|
6587
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
6588
|
+
ref: svgRef,
|
|
6589
|
+
id: uid,
|
|
6590
|
+
width: "100%",
|
|
6591
|
+
height: "100%",
|
|
6592
|
+
className: `block touch-none ${interactive ? "is-interactive" : ""}`,
|
|
6593
|
+
role: interactive ? "group" : "img",
|
|
6594
|
+
"aria-label": ariaLabel
|
|
6595
|
+
})
|
|
6596
|
+
]
|
|
6397
6597
|
});
|
|
6398
6598
|
}
|
|
6399
6599
|
//#endregion
|
package/lib/index.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import * as React$1 from "react";
|
|
|
4
4
|
import React, { cloneElement, createContext, createElement, isValidElement, use, useCallback, useContext, useEffect, useId, useMemo, useRef, useState } from "react";
|
|
5
5
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
6
6
|
import { createPortal } from "react-dom";
|
|
7
|
-
import { AlertCircle, AlertTriangle, Archive, ArchiveRestore, ArrowDown, Ban, CalendarDays, Check, CheckCircle2, ChevronDown, ChevronLeft, ChevronRight, Clock, Command, File, Home, Info, Minus,
|
|
7
|
+
import { AlertCircle, AlertTriangle, Archive, ArchiveRestore, ArrowDown, Ban, CalendarDays, Check, CheckCircle2, ChevronDown, ChevronLeft, ChevronRight, Clock, Command, File, Home, Info, Minus, Scan, Search as Search$1, ShieldCheck, TrendingDown, TrendingUp, UploadCloud, X, ZoomIn, ZoomOut } from "lucide-react";
|
|
8
8
|
import Calendar$1 from "react-calendar";
|
|
9
9
|
import { format, isValid, parse } from "date-fns";
|
|
10
10
|
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
|
@@ -30,6 +30,7 @@ var __exportAll = (all, no_symbols) => {
|
|
|
30
30
|
function cn(...inputs) {
|
|
31
31
|
return twMerge(clsx(inputs));
|
|
32
32
|
}
|
|
33
|
+
var FLOATING_LAYER_SELECTOR = `.pis-datepicker-popover, [data-pis-floating]`;
|
|
33
34
|
//#endregion
|
|
34
35
|
//#region node_modules/@radix-ui/react-compose-refs/dist/index.mjs
|
|
35
36
|
var __defProp$1 = Object.defineProperty;
|
|
@@ -2447,7 +2448,7 @@ function Modal({ isOpen, onClose, title, children, icon, subtitle, headerMeta, f
|
|
|
2447
2448
|
onCloseAutoFocus,
|
|
2448
2449
|
onEscapeKeyDown: preventClose ? (e) => e.preventDefault() : onEscapeKeyDown,
|
|
2449
2450
|
onInteractOutside: (e) => {
|
|
2450
|
-
if (e.target?.closest(
|
|
2451
|
+
if (e.target?.closest(FLOATING_LAYER_SELECTOR)) {
|
|
2451
2452
|
e.preventDefault();
|
|
2452
2453
|
return;
|
|
2453
2454
|
}
|
|
@@ -2572,7 +2573,7 @@ function Dialog({ open, onClose, title, description, preventClose = false, size
|
|
|
2572
2573
|
className: cn("fixed left-1/2 top-1/2 z-50 -translate-x-1/2 -translate-y-1/2", "flex w-full flex-col overflow-hidden", "rounded-lg bg-white shadow-xl", "max-h-[90vh] outline-none", SIZE_CLASS[size]),
|
|
2573
2574
|
onEscapeKeyDown: preventClose ? (e) => e.preventDefault() : onEscapeKeyDown,
|
|
2574
2575
|
onInteractOutside: (e) => {
|
|
2575
|
-
if (e.target?.closest(
|
|
2576
|
+
if (e.target?.closest(FLOATING_LAYER_SELECTOR)) {
|
|
2576
2577
|
e.preventDefault();
|
|
2577
2578
|
return;
|
|
2578
2579
|
}
|
|
@@ -4241,7 +4242,11 @@ function Menu({ trigger, items, ariaLabel = "Menu", align = "right", open: contr
|
|
|
4241
4242
|
ref: panelRef,
|
|
4242
4243
|
role: "menu",
|
|
4243
4244
|
"aria-label": ariaLabel,
|
|
4244
|
-
|
|
4245
|
+
["data-pis-floating"]: "",
|
|
4246
|
+
style: {
|
|
4247
|
+
...panelStyle,
|
|
4248
|
+
pointerEvents: "auto"
|
|
4249
|
+
},
|
|
4245
4250
|
className: cn("overflow-y-auto overscroll-contain rounded-xl border border-slate-200 bg-white py-1 shadow-xl", panelClassName),
|
|
4246
4251
|
children: items.map((item, index) => /* @__PURE__ */ jsxs("button", {
|
|
4247
4252
|
ref: (node) => {
|
|
@@ -6130,25 +6135,30 @@ function Heatmap({ data, scale = "blue", showLegend = true, showMonthLabels = tr
|
|
|
6130
6135
|
* - 250 × 84 px rounded rectangles (rx=14)
|
|
6131
6136
|
* - Root (depth 0): dark navy fill (#0f172a), white text
|
|
6132
6137
|
* - Children: white fill (#fff), slate border (#e2e8f0)
|
|
6138
|
+
* - Muted tone: dashed border, slate-50 fill, muted text (inactive units)
|
|
6133
6139
|
* - drop-shadow: 0 6px 12px rgba(15,23,42,0.08)
|
|
6134
|
-
* - Name: 13px, font-weight 800, centered
|
|
6140
|
+
* - Name: 13px, font-weight 800, centered, ellipsis + tooltip when too long
|
|
6135
6141
|
* - Description: 11px, font-weight 600, #64748b / #cbd5e1 (root)
|
|
6136
|
-
* - Selected: indigo border overlay ring
|
|
6142
|
+
* - Hover: indigo-300 border; Selected: indigo border + overlay ring
|
|
6137
6143
|
* Connectors:
|
|
6138
6144
|
* - Stepped orthogonal path: M→V→H→V
|
|
6139
6145
|
* - Stroke: #cbd5e1, stroke-width 1.5
|
|
6140
6146
|
* Canvas:
|
|
6141
|
-
* -
|
|
6142
|
-
*
|
|
6143
|
-
*
|
|
6147
|
+
* - Fits the tree to the viewport until the user zooms or pans; after that
|
|
6148
|
+
* the user's view is kept across re-renders, data refreshes and resizes
|
|
6149
|
+
* (e.g. a modal hiding the page scrollbar). "Fit to screen" or a new
|
|
6150
|
+
* `fitKey` hands control back to auto-fit.
|
|
6151
|
+
* - Zoom: scaleExtent [0.05, 1.8]; wheel / pinch / buttons / keyboard
|
|
6152
|
+
* - Pan: mouse/touch drag (a drag never counts as a node click)
|
|
6144
6153
|
* - Layout: nodeSize [280, 185] — horizontal × vertical spacing
|
|
6145
6154
|
* Controls:
|
|
6146
|
-
* -
|
|
6155
|
+
* - Zoom in, zoom %, Zoom out, Fit to screen (top-right overlay panel)
|
|
6147
6156
|
*
|
|
6148
6157
|
* Accessibility:
|
|
6149
|
-
* - SVG has role="img"
|
|
6150
|
-
* -
|
|
6151
|
-
* - Keyboard:
|
|
6158
|
+
* - SVG has an aria-label; role="img" when read-only, role="group" when nodes are selectable
|
|
6159
|
+
* - Selectable nodes are focusable buttons (Tab, Enter / Space to select)
|
|
6160
|
+
* - Keyboard on the chart: + / = zoom in, - zoom out, 0 fit to screen
|
|
6161
|
+
* - Respects prefers-reduced-motion for button zoom transitions
|
|
6152
6162
|
* - Note: full ARIA tree semantics are not possible in an SVG diagram;
|
|
6153
6163
|
* use OrgUnitTree for a fully keyboard-accessible list-based alternative
|
|
6154
6164
|
*
|
|
@@ -6160,6 +6170,13 @@ function Heatmap({ data, scale = "blue", showLegend = true, showMonthLabels = tr
|
|
|
6160
6170
|
* aria-label="Company organization chart"
|
|
6161
6171
|
* />
|
|
6162
6172
|
*/
|
|
6173
|
+
var SYNTHETIC_ROOT_ID = "__synthetic_root__";
|
|
6174
|
+
var NODE_W = 250;
|
|
6175
|
+
var NODE_H = 84;
|
|
6176
|
+
var TEXT_MAX_W = 218;
|
|
6177
|
+
var MIN_SCALE = .05;
|
|
6178
|
+
var MAX_SCALE = 1.8;
|
|
6179
|
+
var BUTTON_ZOOM_STEP = 1.2;
|
|
6163
6180
|
function buildD3Tree(nodes) {
|
|
6164
6181
|
function convert(node, depth) {
|
|
6165
6182
|
return {
|
|
@@ -6168,161 +6185,344 @@ function buildD3Tree(nodes) {
|
|
|
6168
6185
|
description: node.description ?? "",
|
|
6169
6186
|
isRoot: depth === 0,
|
|
6170
6187
|
isSyntheticRoot: false,
|
|
6188
|
+
isMuted: node.tone === "muted",
|
|
6171
6189
|
originalId: node.id,
|
|
6172
6190
|
children: node.children?.map((c) => convert(c, depth + 1))
|
|
6173
6191
|
};
|
|
6174
6192
|
}
|
|
6175
6193
|
if (nodes.length === 1) return convert(nodes[0], 0);
|
|
6176
6194
|
return {
|
|
6177
|
-
id:
|
|
6195
|
+
id: SYNTHETIC_ROOT_ID,
|
|
6178
6196
|
name: "",
|
|
6179
6197
|
description: "",
|
|
6180
6198
|
isRoot: true,
|
|
6181
6199
|
isSyntheticRoot: true,
|
|
6182
|
-
|
|
6200
|
+
isMuted: false,
|
|
6201
|
+
originalId: SYNTHETIC_ROOT_ID,
|
|
6183
6202
|
children: nodes.map((n) => convert(n, 0))
|
|
6184
6203
|
};
|
|
6185
6204
|
}
|
|
6186
|
-
|
|
6205
|
+
/** Writes `text` into an SVG text element, truncating with an ellipsis to fit `maxWidth`. */
|
|
6206
|
+
function fitText(el, text, maxWidth, fontSize) {
|
|
6207
|
+
el.textContent = text;
|
|
6208
|
+
if (!text) return;
|
|
6209
|
+
const measured = typeof el.getComputedTextLength === "function" ? el.getComputedTextLength() : 0;
|
|
6210
|
+
if (!measured) {
|
|
6211
|
+
const maxChars = Math.max(1, Math.floor(maxWidth / (fontSize * .6)));
|
|
6212
|
+
if (text.length > maxChars) el.textContent = `${text.slice(0, maxChars - 1).trimEnd()}…`;
|
|
6213
|
+
return;
|
|
6214
|
+
}
|
|
6215
|
+
if (measured <= maxWidth) return;
|
|
6216
|
+
let lo = 0;
|
|
6217
|
+
let hi = text.length - 1;
|
|
6218
|
+
while (lo < hi) {
|
|
6219
|
+
const mid = Math.ceil((lo + hi) / 2);
|
|
6220
|
+
el.textContent = `${text.slice(0, mid).trimEnd()}…`;
|
|
6221
|
+
if (el.getComputedTextLength() <= maxWidth) lo = mid;
|
|
6222
|
+
else hi = mid - 1;
|
|
6223
|
+
}
|
|
6224
|
+
el.textContent = `${text.slice(0, lo).trimEnd()}…`;
|
|
6225
|
+
}
|
|
6226
|
+
var prefersReducedMotion = () => typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
6227
|
+
function OrgChart({ nodes, selectedId, onSelect, fitKey, height = "75vh", "aria-label": ariaLabel = "Organization chart", className = "" }) {
|
|
6187
6228
|
const svgRef = useRef(null);
|
|
6188
6229
|
const zoomRef = useRef(null);
|
|
6189
|
-
const initialTransformRef = useRef(null);
|
|
6190
6230
|
const treeBoundsRef = useRef(null);
|
|
6191
|
-
const
|
|
6231
|
+
const autoFitRef = useRef(true);
|
|
6232
|
+
const programmaticRef = useRef(false);
|
|
6233
|
+
const pendingFitRef = useRef(false);
|
|
6234
|
+
const lastSizeRef = useRef(null);
|
|
6235
|
+
const onSelectRef = useRef(onSelect);
|
|
6236
|
+
const selectedIdRef = useRef(selectedId);
|
|
6237
|
+
const scaleFrameRef = useRef(0);
|
|
6238
|
+
const [scale, setScale] = useState(1);
|
|
6239
|
+
const uid = `oc-${useId().replace(/[^a-zA-Z0-9_-]/g, "")}`;
|
|
6240
|
+
const interactive = Boolean(onSelect);
|
|
6241
|
+
const isEmpty = nodes.length === 0;
|
|
6242
|
+
const getSize = () => {
|
|
6243
|
+
const rect = svgRef.current?.getBoundingClientRect();
|
|
6244
|
+
return {
|
|
6245
|
+
width: rect?.width ?? 0,
|
|
6246
|
+
height: rect?.height ?? 0
|
|
6247
|
+
};
|
|
6248
|
+
};
|
|
6249
|
+
/** Applies a transform without handing control away from auto-fit. */
|
|
6250
|
+
const applyTransform = (t) => {
|
|
6251
|
+
if (!svgRef.current || !zoomRef.current) return;
|
|
6252
|
+
const svg = d3.select(svgRef.current);
|
|
6253
|
+
svg.interrupt();
|
|
6254
|
+
programmaticRef.current = true;
|
|
6255
|
+
try {
|
|
6256
|
+
svg.call(zoomRef.current.transform, t);
|
|
6257
|
+
} finally {
|
|
6258
|
+
programmaticRef.current = false;
|
|
6259
|
+
}
|
|
6260
|
+
};
|
|
6192
6261
|
const fitToView = () => {
|
|
6193
6262
|
if (!svgRef.current || !zoomRef.current || !treeBoundsRef.current) return;
|
|
6194
|
-
const { width: W, height: H } =
|
|
6195
|
-
if (W === 0 || H === 0)
|
|
6263
|
+
const { width: W, height: H } = getSize();
|
|
6264
|
+
if (W === 0 || H === 0) {
|
|
6265
|
+
pendingFitRef.current = true;
|
|
6266
|
+
return;
|
|
6267
|
+
}
|
|
6268
|
+
pendingFitRef.current = false;
|
|
6196
6269
|
const { minX, maxX, minY, maxY } = treeBoundsRef.current;
|
|
6197
|
-
const contentLeft = minX -
|
|
6198
|
-
const contentRight = maxX +
|
|
6199
|
-
const contentTop = minY -
|
|
6200
|
-
const contentBottom = maxY +
|
|
6270
|
+
const contentLeft = minX - NODE_W / 2;
|
|
6271
|
+
const contentRight = maxX + NODE_W / 2;
|
|
6272
|
+
const contentTop = minY - NODE_H / 2;
|
|
6273
|
+
const contentBottom = maxY + NODE_H / 2;
|
|
6201
6274
|
const contentWidth = contentRight - contentLeft;
|
|
6202
6275
|
const contentHeight = contentBottom - contentTop;
|
|
6203
6276
|
const scaleX = (W - 80) / Math.max(1, contentWidth);
|
|
6204
6277
|
const scaleY = (H - 80) / Math.max(1, contentHeight);
|
|
6205
|
-
const
|
|
6206
|
-
const
|
|
6207
|
-
const
|
|
6208
|
-
|
|
6209
|
-
const ty = H / 2 - contentCenterY * scale;
|
|
6210
|
-
const t = d3.zoomIdentity.translate(tx, ty).scale(scale);
|
|
6211
|
-
initialTransformRef.current = t;
|
|
6212
|
-
d3.select(svgRef.current).call(zoomRef.current.transform, t);
|
|
6278
|
+
const k = Math.max(MIN_SCALE, Math.min(MAX_SCALE, scaleX, scaleY));
|
|
6279
|
+
const tx = W / 2 - (contentLeft + contentRight) / 2 * k;
|
|
6280
|
+
const ty = H / 2 - (contentTop + contentBottom) / 2 * k;
|
|
6281
|
+
applyTransform(d3.zoomIdentity.translate(tx, ty).scale(k));
|
|
6213
6282
|
};
|
|
6214
|
-
|
|
6215
|
-
|
|
6283
|
+
/** Fit, and go back to keeping the chart fitted (Fit button, new fitKey). */
|
|
6284
|
+
const resetToFit = () => {
|
|
6285
|
+
autoFitRef.current = true;
|
|
6286
|
+
fitToView();
|
|
6287
|
+
};
|
|
6288
|
+
const zoomBy = (factor) => {
|
|
6289
|
+
if (!svgRef.current || !zoomRef.current) return;
|
|
6290
|
+
autoFitRef.current = false;
|
|
6216
6291
|
const svg = d3.select(svgRef.current);
|
|
6217
|
-
svg.
|
|
6218
|
-
|
|
6219
|
-
|
|
6220
|
-
|
|
6221
|
-
|
|
6292
|
+
if (prefersReducedMotion()) svg.interrupt().call(zoomRef.current.scaleBy, factor);
|
|
6293
|
+
else svg.transition().duration(250).call(zoomRef.current.scaleBy, factor);
|
|
6294
|
+
};
|
|
6295
|
+
useEffect(() => {
|
|
6296
|
+
onSelectRef.current = onSelect;
|
|
6297
|
+
}, [onSelect]);
|
|
6298
|
+
useEffect(() => {
|
|
6299
|
+
const svgEl = svgRef.current;
|
|
6300
|
+
if (!svgEl) return;
|
|
6301
|
+
const svg = d3.select(svgEl);
|
|
6302
|
+
const viewport = svg.append("g").attr("class", "oc-viewport");
|
|
6303
|
+
viewport.append("g").attr("class", "oc-links");
|
|
6304
|
+
viewport.append("g").attr("class", "oc-nodes");
|
|
6305
|
+
const zoom = d3.zoom().scaleExtent([MIN_SCALE, MAX_SCALE]).extent(() => {
|
|
6306
|
+
const { width, height } = getSize();
|
|
6307
|
+
return [[0, 0], [Math.max(1, width), Math.max(1, height)]];
|
|
6308
|
+
}).clickDistance(5).filter((event) => {
|
|
6309
|
+
if (event.type === "dblclick") return false;
|
|
6310
|
+
const e = event;
|
|
6311
|
+
return (!e.ctrlKey || event.type === "wheel") && !e.button;
|
|
6312
|
+
}).on("zoom", (event) => {
|
|
6313
|
+
viewport.attr("transform", event.transform.toString());
|
|
6314
|
+
if (!programmaticRef.current) autoFitRef.current = false;
|
|
6315
|
+
cancelAnimationFrame(scaleFrameRef.current);
|
|
6316
|
+
scaleFrameRef.current = requestAnimationFrame(() => setScale(event.transform.k));
|
|
6317
|
+
});
|
|
6318
|
+
zoomRef.current = zoom;
|
|
6319
|
+
svg.call(zoom);
|
|
6320
|
+
const ro = new ResizeObserver(() => {
|
|
6321
|
+
const size = getSize();
|
|
6322
|
+
if (size.width === 0 || size.height === 0) return;
|
|
6323
|
+
const last = lastSizeRef.current;
|
|
6324
|
+
lastSizeRef.current = size;
|
|
6325
|
+
if (autoFitRef.current || pendingFitRef.current || !last) {
|
|
6326
|
+
fitToView();
|
|
6327
|
+
return;
|
|
6328
|
+
}
|
|
6329
|
+
const dx = (size.width - last.width) / 2;
|
|
6330
|
+
const dy = (size.height - last.height) / 2;
|
|
6331
|
+
if (dx === 0 && dy === 0) return;
|
|
6332
|
+
const t = d3.zoomTransform(svgEl);
|
|
6333
|
+
applyTransform(d3.zoomIdentity.translate(t.x + dx, t.y + dy).scale(t.k));
|
|
6334
|
+
});
|
|
6335
|
+
ro.observe(svgEl);
|
|
6336
|
+
return () => {
|
|
6337
|
+
ro.disconnect();
|
|
6338
|
+
cancelAnimationFrame(scaleFrameRef.current);
|
|
6339
|
+
svg.interrupt().on(".zoom", null);
|
|
6340
|
+
svg.selectAll("*").remove();
|
|
6341
|
+
zoomRef.current = null;
|
|
6342
|
+
};
|
|
6343
|
+
}, []);
|
|
6344
|
+
useEffect(() => {
|
|
6345
|
+
const svgEl = svgRef.current;
|
|
6346
|
+
if (!svgEl) return;
|
|
6347
|
+
const svg = d3.select(svgEl);
|
|
6348
|
+
const linkLayer = svg.select("g.oc-links");
|
|
6349
|
+
const nodeLayer = svg.select("g.oc-nodes");
|
|
6350
|
+
if (nodes.length === 0) {
|
|
6351
|
+
linkLayer.selectAll("*").remove();
|
|
6352
|
+
nodeLayer.selectAll("*").remove();
|
|
6353
|
+
treeBoundsRef.current = null;
|
|
6354
|
+
return;
|
|
6355
|
+
}
|
|
6356
|
+
const root = d3.hierarchy(buildD3Tree(nodes));
|
|
6357
|
+
const pointRoot = d3.tree().nodeSize([280, 185])(root);
|
|
6358
|
+
const descendants = pointRoot.descendants();
|
|
6222
6359
|
treeBoundsRef.current = {
|
|
6223
6360
|
minX: d3.min(descendants, (d) => d.x) ?? 0,
|
|
6224
6361
|
maxX: d3.max(descendants, (d) => d.x) ?? 0,
|
|
6225
6362
|
minY: d3.min(descendants, (d) => d.y) ?? 0,
|
|
6226
6363
|
maxY: d3.max(descendants, (d) => d.y) ?? 0
|
|
6227
6364
|
};
|
|
6228
|
-
|
|
6229
|
-
|
|
6230
|
-
|
|
6231
|
-
svg.call(zoom);
|
|
6232
|
-
g.selectAll("path").data(root.links().filter((l) => !l.source.data.isSyntheticRoot)).join("path").attr("fill", "none").attr("stroke", "#cbd5e1").attr("stroke-width", 1.5).attr("d", (link) => {
|
|
6233
|
-
const sx = link.source.x ?? 0;
|
|
6234
|
-
const sy = link.source.y ?? 0;
|
|
6235
|
-
const tx = link.target.x ?? 0;
|
|
6236
|
-
const ty = link.target.y ?? 0;
|
|
6237
|
-
return `M${sx},${sy}V${(sy + ty) / 2}H${tx}V${ty}`;
|
|
6365
|
+
linkLayer.selectAll("path").data(pointRoot.links().filter((l) => !l.source.data.isSyntheticRoot), (l) => `${l.source.data.originalId}>${l.target.data.originalId}`).join("path").attr("fill", "none").attr("stroke", "#cbd5e1").attr("stroke-width", 1.5).attr("d", (link) => {
|
|
6366
|
+
const mid = (link.source.y + link.target.y) / 2;
|
|
6367
|
+
return `M${link.source.x},${link.source.y}V${mid}H${link.target.x}V${link.target.y}`;
|
|
6238
6368
|
});
|
|
6239
|
-
const node =
|
|
6240
|
-
|
|
6241
|
-
|
|
6369
|
+
const node = nodeLayer.selectAll("g.oc-node").data(descendants.filter((d) => !d.data.isSyntheticRoot), (d) => d.data.originalId).join((enter) => {
|
|
6370
|
+
const g = enter.append("g").attr("class", "oc-node");
|
|
6371
|
+
g.append("title");
|
|
6372
|
+
g.append("rect").attr("class", "oc-card").attr("x", -125).attr("y", -42).attr("width", NODE_W).attr("height", NODE_H).attr("rx", 14).attr("filter", "drop-shadow(0 6px 12px rgba(15,23,42,0.08))");
|
|
6373
|
+
g.append("rect").attr("class", "oc-ring").attr("x", -128).attr("y", -45).attr("width", 256).attr("height", 90).attr("rx", 16).attr("fill", "none");
|
|
6374
|
+
g.append("text").attr("class", "oc-name").attr("text-anchor", "middle").attr("y", -8).attr("font-size", 13).attr("font-weight", 800);
|
|
6375
|
+
g.append("text").attr("class", "oc-desc").attr("text-anchor", "middle").attr("y", 16).attr("font-size", 11).attr("font-weight", 600);
|
|
6376
|
+
return g;
|
|
6377
|
+
}).attr("transform", (d) => `translate(${d.x},${d.y})`).classed("is-root", (d) => d.data.isRoot).classed("is-muted", (d) => d.data.isMuted).classed("is-selected", (d) => d.data.originalId === selectedIdRef.current).attr("aria-label", (d) => d.data.description ? `${d.data.name}, ${d.data.description}` : d.data.name).on("click", (_e, d) => onSelectRef.current?.(d.data.originalId)).on("keydown", (event, d) => {
|
|
6378
|
+
if (event.key !== "Enter" && event.key !== " ") return;
|
|
6379
|
+
event.preventDefault();
|
|
6380
|
+
onSelectRef.current?.(d.data.originalId);
|
|
6381
|
+
}).on("focus", (_e, d) => {
|
|
6382
|
+
if (!zoomRef.current) return;
|
|
6383
|
+
const { width: W, height: H } = getSize();
|
|
6384
|
+
const [sx, sy] = d3.zoomTransform(svgEl).apply([d.x, d.y]);
|
|
6385
|
+
const margin = 24;
|
|
6386
|
+
if (sx >= margin && sx <= W - margin && sy >= margin && sy <= H - margin) return;
|
|
6387
|
+
autoFitRef.current = false;
|
|
6388
|
+
svg.interrupt().call(zoomRef.current.translateTo, d.x, d.y);
|
|
6242
6389
|
});
|
|
6243
|
-
node.
|
|
6244
|
-
|
|
6245
|
-
|
|
6246
|
-
}).attr("stroke-width", (d) => selectedId && d.data.originalId === selectedId ? 2.5 : 1).attr("filter", "drop-shadow(0 6px 12px rgba(15,23,42,0.08))");
|
|
6247
|
-
node.append("text").attr("text-anchor", "middle").attr("y", -8).attr("font-size", 13).attr("font-weight", 800).attr("fill", (d) => d.data.isRoot ? "#fff" : "#0f172a").text((d) => d.data.name);
|
|
6248
|
-
node.append("text").attr("text-anchor", "middle").attr("y", 16).attr("font-size", 11).attr("font-weight", 600).attr("fill", (d) => d.data.isRoot ? "#cbd5e1" : "#64748b").text((d) => d.data.description);
|
|
6249
|
-
if (selectedId) node.filter((d) => d.data.originalId === selectedId).append("rect").attr("x", -128).attr("y", -45).attr("width", 256).attr("height", 90).attr("rx", 16).attr("fill", "none").attr("stroke", "#6366f1").attr("stroke-width", 2.5).attr("opacity", .6);
|
|
6250
|
-
requestAnimationFrame(() => {
|
|
6251
|
-
fitToView();
|
|
6390
|
+
node.select("title").text((d) => d.data.description ? `${d.data.name}\n${d.data.description}` : d.data.name);
|
|
6391
|
+
node.select("text.oc-name").each(function(d) {
|
|
6392
|
+
fitText(this, d.data.name, TEXT_MAX_W, 13);
|
|
6252
6393
|
});
|
|
6394
|
+
node.select("text.oc-desc").each(function(d) {
|
|
6395
|
+
fitText(this, d.data.description, TEXT_MAX_W, 11);
|
|
6396
|
+
});
|
|
6397
|
+
if (autoFitRef.current || pendingFitRef.current) fitToView();
|
|
6398
|
+
}, [nodes]);
|
|
6399
|
+
useEffect(() => {
|
|
6400
|
+
if (!svgRef.current) return;
|
|
6401
|
+
d3.select(svgRef.current).selectAll("g.oc-node").attr("tabindex", interactive ? 0 : null).attr("role", interactive ? "button" : null);
|
|
6402
|
+
}, [interactive, nodes]);
|
|
6403
|
+
useEffect(() => {
|
|
6404
|
+
selectedIdRef.current = selectedId;
|
|
6405
|
+
if (!svgRef.current) return;
|
|
6406
|
+
d3.select(svgRef.current).selectAll("g.oc-node").classed("is-selected", (d) => d.data.originalId === selectedId).attr("aria-pressed", interactive ? (d) => String(d.data.originalId === selectedId) : null);
|
|
6253
6407
|
}, [
|
|
6254
|
-
nodes,
|
|
6255
6408
|
selectedId,
|
|
6256
|
-
|
|
6409
|
+
interactive,
|
|
6410
|
+
nodes
|
|
6257
6411
|
]);
|
|
6412
|
+
const fitKeyRef = useRef(fitKey);
|
|
6258
6413
|
useEffect(() => {
|
|
6259
|
-
|
|
6260
|
-
|
|
6261
|
-
|
|
6262
|
-
|
|
6263
|
-
|
|
6264
|
-
|
|
6265
|
-
|
|
6266
|
-
|
|
6267
|
-
|
|
6268
|
-
|
|
6269
|
-
|
|
6270
|
-
};
|
|
6271
|
-
const resetZoom = () => {
|
|
6272
|
-
fitToView();
|
|
6414
|
+
if (fitKeyRef.current === fitKey) return;
|
|
6415
|
+
fitKeyRef.current = fitKey;
|
|
6416
|
+
resetToFit();
|
|
6417
|
+
}, [fitKey]);
|
|
6418
|
+
const handleKeyDown = (event) => {
|
|
6419
|
+
if (event.ctrlKey || event.metaKey || event.altKey || isEmpty) return;
|
|
6420
|
+
if (event.key === "+" || event.key === "=") zoomBy(BUTTON_ZOOM_STEP);
|
|
6421
|
+
else if (event.key === "-" || event.key === "_") zoomBy(1 / BUTTON_ZOOM_STEP);
|
|
6422
|
+
else if (event.key === "0") resetToFit();
|
|
6423
|
+
else return;
|
|
6424
|
+
event.preventDefault();
|
|
6273
6425
|
};
|
|
6426
|
+
const atMax = scale >= 1.7990000000000002;
|
|
6427
|
+
const atMin = scale <= .051000000000000004;
|
|
6274
6428
|
const controlBtnClass = [
|
|
6275
6429
|
"appearance-none border-0 bg-none font-[inherit]",
|
|
6276
6430
|
"flex items-center justify-center rounded-lg p-2",
|
|
6277
|
-
"text-slate-
|
|
6278
|
-
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-blue/30"
|
|
6431
|
+
"text-slate-500 transition hover:bg-slate-100 hover:text-slate-700",
|
|
6432
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-blue/30",
|
|
6433
|
+
"disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:bg-transparent"
|
|
6279
6434
|
].join(" ");
|
|
6280
6435
|
return /* @__PURE__ */ jsxs("div", {
|
|
6281
|
-
className: `relative w-full overflow-hidden bg-slate-50/50 ${className}`,
|
|
6436
|
+
className: `relative w-full overflow-hidden bg-slate-50/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-brand-blue/30 ${className}`,
|
|
6282
6437
|
style: { height },
|
|
6283
|
-
|
|
6284
|
-
|
|
6285
|
-
|
|
6286
|
-
|
|
6287
|
-
|
|
6288
|
-
|
|
6289
|
-
|
|
6290
|
-
|
|
6291
|
-
|
|
6292
|
-
|
|
6293
|
-
|
|
6294
|
-
|
|
6295
|
-
|
|
6296
|
-
|
|
6297
|
-
|
|
6298
|
-
|
|
6299
|
-
|
|
6300
|
-
|
|
6301
|
-
|
|
6302
|
-
|
|
6303
|
-
|
|
6304
|
-
|
|
6305
|
-
|
|
6306
|
-
|
|
6307
|
-
|
|
6308
|
-
|
|
6309
|
-
|
|
6310
|
-
|
|
6311
|
-
|
|
6312
|
-
|
|
6438
|
+
tabIndex: isEmpty ? void 0 : 0,
|
|
6439
|
+
"aria-keyshortcuts": isEmpty ? void 0 : "+ - 0",
|
|
6440
|
+
onKeyDown: handleKeyDown,
|
|
6441
|
+
children: [
|
|
6442
|
+
/* @__PURE__ */ jsx("style", { children: `
|
|
6443
|
+
#${uid} .oc-card { fill: #fff; stroke: #e2e8f0; stroke-width: 1; transition: stroke 120ms ease; }
|
|
6444
|
+
#${uid} .oc-name { fill: #0f172a; }
|
|
6445
|
+
#${uid} .oc-desc { fill: #64748b; }
|
|
6446
|
+
#${uid} .oc-node.is-root .oc-card { fill: #0f172a; stroke: #0f172a; }
|
|
6447
|
+
#${uid} .oc-node.is-root .oc-name { fill: #fff; }
|
|
6448
|
+
#${uid} .oc-node.is-root .oc-desc { fill: #cbd5e1; }
|
|
6449
|
+
#${uid} .oc-node.is-muted .oc-card { fill: #f8fafc; stroke: #94a3b8; stroke-dasharray: 5 4; }
|
|
6450
|
+
#${uid} .oc-node.is-muted .oc-name { fill: #475569; }
|
|
6451
|
+
#${uid} .oc-node.is-muted .oc-desc { fill: #94a3b8; }
|
|
6452
|
+
#${uid} .oc-node.is-root.is-muted .oc-card { fill: #334155; stroke: #94a3b8; }
|
|
6453
|
+
#${uid} .oc-node.is-root.is-muted .oc-name { fill: #e2e8f0; }
|
|
6454
|
+
#${uid} .oc-node.is-root.is-muted .oc-desc { fill: #cbd5e1; }
|
|
6455
|
+
#${uid} .oc-ring { stroke: #6366f1; stroke-width: 2.5; opacity: 0; pointer-events: none; }
|
|
6456
|
+
#${uid}.is-interactive .oc-node { cursor: pointer; }
|
|
6457
|
+
#${uid}.is-interactive .oc-node:hover .oc-card { stroke: #a5b4fc; stroke-width: 1.5; }
|
|
6458
|
+
#${uid} .oc-node.is-selected .oc-card,
|
|
6459
|
+
#${uid}.is-interactive .oc-node.is-selected:hover .oc-card { stroke: #6366f1; stroke-width: 2.5; }
|
|
6460
|
+
#${uid} .oc-node.is-selected .oc-ring { opacity: 0.6; }
|
|
6461
|
+
#${uid} .oc-node:focus { outline: none; }
|
|
6462
|
+
#${uid} .oc-node:focus-visible .oc-ring { opacity: 1; stroke: #2563eb; }
|
|
6463
|
+
` }),
|
|
6464
|
+
!isEmpty && /* @__PURE__ */ jsxs("div", {
|
|
6465
|
+
className: "absolute right-4 top-4 z-10 flex flex-col items-center gap-1 rounded-lg border border-slate-100 bg-white p-1.5 shadow-md",
|
|
6466
|
+
children: [
|
|
6467
|
+
/* @__PURE__ */ jsx("button", {
|
|
6468
|
+
type: "button",
|
|
6469
|
+
className: controlBtnClass,
|
|
6470
|
+
onClick: () => zoomBy(BUTTON_ZOOM_STEP),
|
|
6471
|
+
disabled: atMax,
|
|
6472
|
+
"aria-label": "Zoom in",
|
|
6473
|
+
title: "Zoom in (+)",
|
|
6474
|
+
children: /* @__PURE__ */ jsx(ZoomIn, {
|
|
6475
|
+
size: 16,
|
|
6476
|
+
"aria-hidden": "true"
|
|
6477
|
+
})
|
|
6478
|
+
}),
|
|
6479
|
+
/* @__PURE__ */ jsxs("span", {
|
|
6480
|
+
className: "min-w-[3rem] select-none text-center text-[11px] font-semibold tabular-nums text-slate-500",
|
|
6481
|
+
children: [Math.round(scale * 100), "%"]
|
|
6482
|
+
}),
|
|
6483
|
+
/* @__PURE__ */ jsx("button", {
|
|
6484
|
+
type: "button",
|
|
6485
|
+
className: controlBtnClass,
|
|
6486
|
+
onClick: () => zoomBy(1 / BUTTON_ZOOM_STEP),
|
|
6487
|
+
disabled: atMin,
|
|
6488
|
+
"aria-label": "Zoom out",
|
|
6489
|
+
title: "Zoom out (-)",
|
|
6490
|
+
children: /* @__PURE__ */ jsx(ZoomOut, {
|
|
6491
|
+
size: 16,
|
|
6492
|
+
"aria-hidden": "true"
|
|
6493
|
+
})
|
|
6494
|
+
}),
|
|
6495
|
+
/* @__PURE__ */ jsx("div", {
|
|
6496
|
+
className: "my-0.5 h-px w-6 bg-slate-100",
|
|
6313
6497
|
"aria-hidden": "true"
|
|
6498
|
+
}),
|
|
6499
|
+
/* @__PURE__ */ jsx("button", {
|
|
6500
|
+
type: "button",
|
|
6501
|
+
className: controlBtnClass,
|
|
6502
|
+
onClick: resetToFit,
|
|
6503
|
+
"aria-label": "Fit to screen",
|
|
6504
|
+
title: "Fit to screen (0)",
|
|
6505
|
+
children: /* @__PURE__ */ jsx(Scan, {
|
|
6506
|
+
size: 16,
|
|
6507
|
+
"aria-hidden": "true"
|
|
6508
|
+
})
|
|
6314
6509
|
})
|
|
6315
|
-
|
|
6316
|
-
|
|
6317
|
-
|
|
6318
|
-
|
|
6319
|
-
|
|
6320
|
-
|
|
6321
|
-
|
|
6322
|
-
|
|
6323
|
-
|
|
6324
|
-
|
|
6325
|
-
|
|
6510
|
+
]
|
|
6511
|
+
}),
|
|
6512
|
+
isEmpty && /* @__PURE__ */ jsx("div", {
|
|
6513
|
+
className: "absolute inset-0 flex items-center justify-center p-6 text-center text-sm font-semibold text-slate-500",
|
|
6514
|
+
children: "No units to display"
|
|
6515
|
+
}),
|
|
6516
|
+
/* @__PURE__ */ jsx("svg", {
|
|
6517
|
+
ref: svgRef,
|
|
6518
|
+
id: uid,
|
|
6519
|
+
width: "100%",
|
|
6520
|
+
height: "100%",
|
|
6521
|
+
className: `block touch-none ${interactive ? "is-interactive" : ""}`,
|
|
6522
|
+
role: interactive ? "group" : "img",
|
|
6523
|
+
"aria-label": ariaLabel
|
|
6524
|
+
})
|
|
6525
|
+
]
|
|
6326
6526
|
});
|
|
6327
6527
|
}
|
|
6328
6528
|
//#endregion
|
package/lib/types/lib/utils.d.ts
CHANGED
|
@@ -1,2 +1,11 @@
|
|
|
1
1
|
import { type ClassValue } from 'clsx';
|
|
2
2
|
export declare function cn(...inputs: ClassValue[]): string;
|
|
3
|
+
/**
|
|
4
|
+
* Floating layers (Menu panels, DatePicker calendars) portal to document.body.
|
|
5
|
+
* Inside an open Modal/Dialog, Radix blocks pointer events outside its content
|
|
6
|
+
* and treats any interaction there as "outside" (closing the dialog). Floating
|
|
7
|
+
* layers opt out by setting `pointer-events: auto` and carrying this marker;
|
|
8
|
+
* Modal and Dialog then ignore interactions that land inside them.
|
|
9
|
+
*/
|
|
10
|
+
export declare const FLOATING_LAYER_ATTR = "data-pis-floating";
|
|
11
|
+
export declare const FLOATING_LAYER_SELECTOR = ".pis-datepicker-popover, [data-pis-floating]";
|
|
@@ -13,25 +13,30 @@
|
|
|
13
13
|
* - 250 × 84 px rounded rectangles (rx=14)
|
|
14
14
|
* - Root (depth 0): dark navy fill (#0f172a), white text
|
|
15
15
|
* - Children: white fill (#fff), slate border (#e2e8f0)
|
|
16
|
+
* - Muted tone: dashed border, slate-50 fill, muted text (inactive units)
|
|
16
17
|
* - drop-shadow: 0 6px 12px rgba(15,23,42,0.08)
|
|
17
|
-
* - Name: 13px, font-weight 800, centered
|
|
18
|
+
* - Name: 13px, font-weight 800, centered, ellipsis + tooltip when too long
|
|
18
19
|
* - Description: 11px, font-weight 600, #64748b / #cbd5e1 (root)
|
|
19
|
-
* - Selected: indigo border overlay ring
|
|
20
|
+
* - Hover: indigo-300 border; Selected: indigo border + overlay ring
|
|
20
21
|
* Connectors:
|
|
21
22
|
* - Stepped orthogonal path: M→V→H→V
|
|
22
23
|
* - Stroke: #cbd5e1, stroke-width 1.5
|
|
23
24
|
* Canvas:
|
|
24
|
-
* -
|
|
25
|
-
*
|
|
26
|
-
*
|
|
25
|
+
* - Fits the tree to the viewport until the user zooms or pans; after that
|
|
26
|
+
* the user's view is kept across re-renders, data refreshes and resizes
|
|
27
|
+
* (e.g. a modal hiding the page scrollbar). "Fit to screen" or a new
|
|
28
|
+
* `fitKey` hands control back to auto-fit.
|
|
29
|
+
* - Zoom: scaleExtent [0.05, 1.8]; wheel / pinch / buttons / keyboard
|
|
30
|
+
* - Pan: mouse/touch drag (a drag never counts as a node click)
|
|
27
31
|
* - Layout: nodeSize [280, 185] — horizontal × vertical spacing
|
|
28
32
|
* Controls:
|
|
29
|
-
* -
|
|
33
|
+
* - Zoom in, zoom %, Zoom out, Fit to screen (top-right overlay panel)
|
|
30
34
|
*
|
|
31
35
|
* Accessibility:
|
|
32
|
-
* - SVG has role="img"
|
|
33
|
-
* -
|
|
34
|
-
* - Keyboard:
|
|
36
|
+
* - SVG has an aria-label; role="img" when read-only, role="group" when nodes are selectable
|
|
37
|
+
* - Selectable nodes are focusable buttons (Tab, Enter / Space to select)
|
|
38
|
+
* - Keyboard on the chart: + / = zoom in, - zoom out, 0 fit to screen
|
|
39
|
+
* - Respects prefers-reduced-motion for button zoom transitions
|
|
35
40
|
* - Note: full ARIA tree semantics are not possible in an SVG diagram;
|
|
36
41
|
* use OrgUnitTree for a fully keyboard-accessible list-based alternative
|
|
37
42
|
*
|
|
@@ -50,6 +55,11 @@ export interface OrgChartNode {
|
|
|
50
55
|
label: string;
|
|
51
56
|
/** Secondary text line inside the node. */
|
|
52
57
|
description?: string;
|
|
58
|
+
/**
|
|
59
|
+
* Visual tone. `'muted'` renders a dashed, greyed-out card — use it for
|
|
60
|
+
* inactive or archived units. Default: `'default'`.
|
|
61
|
+
*/
|
|
62
|
+
tone?: 'default' | 'muted';
|
|
53
63
|
/** Child nodes. */
|
|
54
64
|
children?: OrgChartNode[];
|
|
55
65
|
}
|
|
@@ -62,8 +72,14 @@ export interface OrgChartProps {
|
|
|
62
72
|
nodes: OrgChartNode[];
|
|
63
73
|
/** Currently selected node id — renders an indigo ring on the node. */
|
|
64
74
|
selectedId?: string;
|
|
65
|
-
/** Called when a node is clicked. */
|
|
75
|
+
/** Called when a node is clicked (or activated with Enter / Space). */
|
|
66
76
|
onSelect?: (id: string) => void;
|
|
77
|
+
/**
|
|
78
|
+
* Changing this value re-fits the tree to the viewport, discarding the
|
|
79
|
+
* user's zoom/pan. Use it when the chart shows a new context (e.g. a
|
|
80
|
+
* different company scope). Data refreshes alone keep the user's view.
|
|
81
|
+
*/
|
|
82
|
+
fitKey?: string | number;
|
|
67
83
|
/**
|
|
68
84
|
* Container height. Default: '75vh' — matching the production CompanyStructurePage.
|
|
69
85
|
* Pass a fixed pixel value for demos: '480px'.
|
|
@@ -74,4 +90,4 @@ export interface OrgChartProps {
|
|
|
74
90
|
/** Additional class on the root container div. */
|
|
75
91
|
className?: string;
|
|
76
92
|
}
|
|
77
|
-
export declare function OrgChart({ nodes, selectedId, onSelect, height, 'aria-label': ariaLabel, className, }: OrgChartProps): import("react").JSX.Element;
|
|
93
|
+
export declare function OrgChart({ nodes, selectedId, onSelect, fitKey, height, 'aria-label': ariaLabel, className, }: OrgChartProps): import("react").JSX.Element;
|