@devfellowship/components 3.4.0 → 3.5.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
@@ -9104,9 +9104,49 @@ function firstFreeRow(taken, columns, from) {
9104
9104
  }
9105
9105
  return row;
9106
9106
  }
9107
+ var pointText = (p) => `${p.x} ${p.y}`;
9108
+ var center = (r) => ({ x: (r.left + r.right) / 2, y: (r.top + r.bottom) / 2 });
9109
+ function segmentHits(a, b, r) {
9110
+ return a.x === b.x ? a.x > r.left && a.x < r.right && Math.max(a.y, b.y) > r.top && Math.min(a.y, b.y) < r.bottom : a.y > r.top && a.y < r.bottom && Math.max(a.x, b.x) > r.left && Math.min(a.x, b.x) < r.right;
9111
+ }
9112
+ function routeEdge(source, target, edge, obstacles = []) {
9113
+ const a = center(source), b = center(target);
9114
+ const sameColumn = Math.abs(a.x - b.x) < 1;
9115
+ const vertical = sameColumn || source.left < target.right && target.left < source.right;
9116
+ const down = b.y >= a.y, right = b.x >= a.x;
9117
+ const start = vertical ? { x: a.x, y: down ? source.bottom : source.top } : { x: right ? source.right : source.left, y: a.y };
9118
+ const end = vertical ? { x: b.x, y: down ? target.top : target.bottom } : { x: right ? target.left : target.right, y: b.y };
9119
+ const route = edge.route ?? "auto";
9120
+ if (route === "straight" || route === "auto" && sameColumn) return { start, end, d: `M ${pointText(start)} L ${pointText(end)}` };
9121
+ if (route !== "elbow") {
9122
+ const mid = vertical ? (start.y + end.y) / 2 : (start.x + end.x) / 2;
9123
+ const c1 = vertical ? { x: start.x, y: mid } : { x: mid, y: start.y };
9124
+ const c2 = vertical ? { x: end.x, y: mid } : { x: mid, y: end.y };
9125
+ return { start, end, d: `M ${pointText(start)} C ${pointText(c1)} ${pointText(c2)} ${pointText(end)}` };
9126
+ }
9127
+ const boxes = [.../* @__PURE__ */ new Set([source, target, ...obstacles])];
9128
+ const clearance = 4;
9129
+ const ys = [...new Set(boxes.flatMap((r) => [r.top - clearance, r.bottom + clearance]))];
9130
+ let best, bestLength = Infinity;
9131
+ for (const sx of [source.left, source.right]) for (const tx of [target.left, target.right]) {
9132
+ const s = { x: sx, y: a.y }, t = { x: tx, y: b.y };
9133
+ const gx = sx + (sx === source.left ? -clearance : clearance);
9134
+ const hx = tx + (tx === target.left ? -clearance : clearance);
9135
+ for (const y of [a.y, b.y, ...ys]) {
9136
+ const points2 = [s, { x: gx, y: a.y }, { x: gx, y }, { x: hx, y }, { x: hx, y: b.y }, t];
9137
+ const length = points2.slice(1).reduce((sum, p, i) => sum + Math.abs(p.x - points2[i].x) + Math.abs(p.y - points2[i].y), 0);
9138
+ if (length >= bestLength || points2.slice(1).some((p, i) => boxes.some((r) => segmentHits(points2[i], p, r)))) continue;
9139
+ best = points2;
9140
+ bestLength = length;
9141
+ }
9142
+ }
9143
+ if (!best) return { start, end, d: "" };
9144
+ const points = best.filter((p, i) => i === 0 || p.x !== best[i - 1].x || p.y !== best[i - 1].y);
9145
+ return { start: points[0], end: points[points.length - 1], points, d: points.map((p, i) => `${i ? "L" : "M"} ${pointText(p)}`).join(" ") };
9146
+ }
9107
9147
 
9108
9148
  // src/components/organisms/roadmap/Roadmap.tsx
9109
- var import_react7 = require("react");
9149
+ var import_react8 = require("react");
9110
9150
 
9111
9151
  // src/components/organisms/roadmap/RoadmapNode.tsx
9112
9152
  var import_class_variance_authority14 = require("class-variance-authority");
@@ -9154,7 +9194,7 @@ function RoadmapNodeView({ node, state, badge, testIdPrefix, onNodeClick, onActi
9154
9194
  const Box = wholeBox && node.href ? "a" : wholeBox && onNodeClick ? "button" : "div";
9155
9195
  const primary = Box !== "div" ? label : node.href && !locked ? /* @__PURE__ */ (0, import_jsx_runtime66.jsx)("a", { href: node.href, className: interactiveClass, onClick: () => onNodeClick?.(node), children: label }) : onNodeClick && !locked ? /* @__PURE__ */ (0, import_jsx_runtime66.jsx)("button", { type: "button", className: interactiveClass, onClick: () => onNodeClick(node), children: label }) : label;
9156
9196
  const icon = node.icon ?? (badge ? { name: badge.icon ?? "check", side: "right", tone: badge.tone } : void 0);
9157
- return /* @__PURE__ */ (0, import_jsx_runtime66.jsx)("div", { style: toneStyle(nodeTone), className: "min-w-0", children: /* @__PURE__ */ (0, import_jsx_runtime66.jsxs)(Box, { "data-roadmap-node-box": node.id, href: Box === "a" ? node.href : void 0, type: Box === "button" ? "button" : void 0, onClick: Box !== "div" ? () => onNodeClick?.(node) : void 0, className: `${nodeVariants({ kind: node.kind, state })} w-full ${Box !== "div" ? focusClass : ""}`, "aria-disabled": locked || void 0, title: node.kind === "topic" || node.kind === "subtopic" ? node.description : void 0, children: [
9197
+ return /* @__PURE__ */ (0, import_jsx_runtime66.jsx)("div", { style: toneStyle(nodeTone), className: "min-w-0", children: /* @__PURE__ */ (0, import_jsx_runtime66.jsxs)(Box, { style: { borderColor: node.kind === "title" || node.kind === "label" ? "transparent" : "var(--c-roadmap-border)" }, "data-roadmap-node-box": node.id, href: Box === "a" ? node.href : void 0, type: Box === "button" ? "button" : void 0, onClick: Box !== "div" ? () => onNodeClick?.(node) : void 0, className: `${nodeVariants({ kind: node.kind, state })} w-full ${Box !== "div" ? focusClass : ""}`, "aria-disabled": locked || void 0, title: node.kind === "topic" || node.kind === "subtopic" ? node.description : void 0, children: [
9158
9198
  icon && /* @__PURE__ */ (0, import_jsx_runtime66.jsx)(RoadmapBadge, { name: icon.name, tone: icon.tone ?? nodeTone, label: badge?.label ?? icon.name, className: `absolute top-1/2 -translate-y-1/2 ${icon.side === "left" ? "left-0 -translate-x-1/2" : "right-0 translate-x-1/2"}` }),
9159
9199
  primary,
9160
9200
  node.description && node.kind === "paragraph" && /* @__PURE__ */ (0, import_jsx_runtime66.jsx)("p", { className: "mt-2 [overflow-wrap:anywhere]", children: node.description }),
@@ -9169,15 +9209,17 @@ function RoadmapNodeView({ node, state, badge, testIdPrefix, onNodeClick, onActi
9169
9209
  // src/components/organisms/roadmap/RoadmapGroup.tsx
9170
9210
  var import_jsx_runtime67 = require("react/jsx-runtime");
9171
9211
  function RoadmapGroupView({ group, testIdPrefix }) {
9212
+ const tone = resolveGroupTone(group);
9213
+ const appearance = tone === "accent" ? { "--c-roadmap-node-bg": "var(--c-roadmap-group-accent-bg)", "--c-roadmap-node-fg": "var(--c-roadmap-group-accent-fg)" } : toneStyle(tone);
9172
9214
  return /* @__PURE__ */ (0, import_jsx_runtime67.jsx)(import_jsx_runtime67.Fragment, { children: groupColumnRuns(group).map((run, index) => /* @__PURE__ */ (0, import_jsx_runtime67.jsxs)(
9173
9215
  "div",
9174
9216
  {
9175
9217
  "data-testid": `${testIdPrefix}-group`,
9176
9218
  "data-group-id": group.id,
9177
- style: { ...toneStyle(resolveGroupTone(group)), gridColumn: `${run.start} / span ${run.span}`, gridRow: `${group.from + 1} / span ${group.to - group.from + 1}` },
9219
+ style: { borderColor: "var(--c-roadmap-border)", ...appearance, gridColumn: `${run.start} / span ${run.span}`, gridRow: `${group.from + 1} / span ${group.to - group.from + 1}` },
9178
9220
  className: "pointer-events-none relative z-0 min-w-0 self-stretch rounded-[var(--radius)] border-2 border-[var(--c-roadmap-border)] bg-[var(--c-roadmap-node-bg)] text-[var(--c-roadmap-node-fg)]",
9179
9221
  children: [
9180
- index === 0 && group.title && /* @__PURE__ */ (0, import_jsx_runtime67.jsx)("span", { className: "absolute left-2 right-2 top-0 -translate-y-1/2 text-xs leading-normal", children: /* @__PURE__ */ (0, import_jsx_runtime67.jsx)("span", { className: "bg-[var(--c-roadmap-surface)] px-1 [overflow-wrap:anywhere]", children: group.title }) }),
9222
+ index === 0 && group.title && /* @__PURE__ */ (0, import_jsx_runtime67.jsx)("span", { title: group.title, className: "absolute left-2 right-2 top-0 -translate-y-1/2 text-xs leading-normal", children: /* @__PURE__ */ (0, import_jsx_runtime67.jsx)("span", { className: "inline-block max-w-full overflow-hidden text-ellipsis whitespace-nowrap bg-[var(--c-roadmap-surface)] px-1 align-top text-[var(--c-roadmap-neutral-fg)]", children: group.title }) }),
9181
9223
  index === 0 && group.description && /* @__PURE__ */ (0, import_jsx_runtime67.jsx)("span", { className: "sr-only", children: group.description })
9182
9224
  ]
9183
9225
  },
@@ -9185,49 +9227,164 @@ function RoadmapGroupView({ group, testIdPrefix }) {
9185
9227
  )) });
9186
9228
  }
9187
9229
 
9188
- // src/components/organisms/roadmap/Roadmap.tsx
9230
+ // src/components/organisms/roadmap/RoadmapEdgeLayer.tsx
9231
+ var import_react7 = require("react");
9189
9232
  var import_jsx_runtime68 = require("react/jsx-runtime");
9190
- function Roadmap({ document: document2, state, renderNode, onNodeClick, onAction, testIdPrefix = "roadmap", className = "", ariaLabel }) {
9191
- const { placements } = (0, import_react7.useMemo)(() => placeNodes(document2), [document2]);
9192
- const badges = (0, import_react7.useMemo)(() => new Map(document2.legend?.entries.map((entry) => [entry.id, entry])), [document2.legend]);
9233
+ function Edge({ edge, path, instance, prefix }) {
9234
+ const pathRef = (0, import_react7.useRef)(null);
9235
+ const [midpoint, setMidpoint] = (0, import_react7.useState)();
9236
+ (0, import_react7.useLayoutEffect)(() => {
9237
+ const element = pathRef.current;
9238
+ if (edge.label && element?.getTotalLength && path.d) {
9239
+ const p = element.getPointAtLength(element.getTotalLength() / 2);
9240
+ setMidpoint({ x: p.x, y: p.y });
9241
+ } else setMidpoint(void 0);
9242
+ }, [path.d, edge.label]);
9243
+ const arrow = resolveEdgeArrow(edge), style = resolveEdgeStyle(edge);
9244
+ const marker = `${instance}-${edge.id}`;
9245
+ const color = edge.tone ? `var(--c-roadmap-${edge.tone}-edge)` : "var(--c-roadmap-edge)";
9246
+ return /* @__PURE__ */ (0, import_jsx_runtime68.jsxs)("g", { style: { color }, "data-roadmap-edge": edge.id, children: [
9247
+ arrow !== "none" && /* @__PURE__ */ (0, import_jsx_runtime68.jsx)("defs", { children: /* @__PURE__ */ (0, import_jsx_runtime68.jsx)("marker", { id: marker, viewBox: "0 0 8 8", refX: "8", refY: "4", markerWidth: "8", markerHeight: "8", markerUnits: "userSpaceOnUse", orient: "auto-start-reverse", children: /* @__PURE__ */ (0, import_jsx_runtime68.jsx)("path", { d: "M 0 0 L 8 4 L 0 8 Z", fill: "currentColor" }) }) }),
9248
+ /* @__PURE__ */ (0, import_jsx_runtime68.jsx)(
9249
+ "path",
9250
+ {
9251
+ ref: pathRef,
9252
+ "data-testid": `${prefix}-edge`,
9253
+ "data-edge-id": edge.id,
9254
+ "data-source": edge.source,
9255
+ "data-target": edge.target,
9256
+ d: path.d,
9257
+ fill: "none",
9258
+ stroke: "currentColor",
9259
+ strokeLinecap: "round",
9260
+ strokeLinejoin: "round",
9261
+ strokeDasharray: style === "solid" ? void 0 : style === "dotted" ? "0.8 4" : "0.8 8",
9262
+ className: "[stroke-width:2px] md:[stroke-width:3px]",
9263
+ markerStart: arrow === "both" ? `url(#${marker})` : void 0,
9264
+ markerEnd: arrow !== "none" ? `url(#${marker})` : void 0
9265
+ }
9266
+ ),
9267
+ edge.label && midpoint && /* @__PURE__ */ (0, import_jsx_runtime68.jsx)("foreignObject", { "data-testid": `${prefix}-edge-label`, "data-edge-id": edge.id, x: midpoint.x, y: midpoint.y, width: "1", height: "1", overflow: "visible", children: /* @__PURE__ */ (0, import_jsx_runtime68.jsx)(
9268
+ "div",
9269
+ {
9270
+ className: "w-max max-w-[160px] -translate-x-1/2 -translate-y-1/2 rounded-full bg-[var(--c-roadmap-edge-label-bg)] px-2 py-0.5 text-center text-xs leading-normal text-[var(--c-roadmap-edge-label-fg)] [overflow-wrap:anywhere]",
9271
+ style: { boxShadow: "0 0 0 2px var(--c-roadmap-surface)" },
9272
+ children: edge.label
9273
+ }
9274
+ ) })
9275
+ ] });
9276
+ }
9277
+ function RoadmapEdgeLayer({ containerRef, document: documentModel, testIdPrefix, debugPerf }) {
9278
+ const instance = `roadmap-${(0, import_react7.useId)().replace(/:/g, "")}`;
9279
+ const [measured, setMeasured] = (0, import_react7.useState)([]);
9280
+ (0, import_react7.useEffect)(() => {
9281
+ const container = containerRef.current;
9282
+ if (!container || !documentModel.edges.length) {
9283
+ setMeasured([]);
9284
+ return;
9285
+ }
9286
+ let disposed = false, frame = 0;
9287
+ const observed = /* @__PURE__ */ new Set();
9288
+ const measure = () => {
9289
+ frame = 0;
9290
+ if (disposed) return;
9291
+ const started = performance.now();
9292
+ const root = container.getBoundingClientRect();
9293
+ const boxes = /* @__PURE__ */ new Map();
9294
+ for (const wrapper of container.querySelectorAll("[data-roadmap-node]")) {
9295
+ if (wrapper.closest("[data-roadmap-grid]") !== container) continue;
9296
+ const id = wrapper.dataset.roadmapNode;
9297
+ const border = Array.from(wrapper.querySelectorAll("[data-roadmap-node-box]")).find((el) => el.dataset.roadmapNodeBox === id);
9298
+ const element = border ?? wrapper.firstElementChild ?? wrapper;
9299
+ for (const target of [wrapper, element]) if (!observed.has(target)) {
9300
+ observer?.observe(target);
9301
+ observed.add(target);
9302
+ }
9303
+ const rect = element.getBoundingClientRect();
9304
+ boxes.set(id, { left: rect.left - root.left, right: rect.right - root.left, top: rect.top - root.top, bottom: rect.bottom - root.top });
9305
+ }
9306
+ for (const element of observed) if (!container.contains(element)) {
9307
+ observer?.unobserve(element);
9308
+ observed.delete(element);
9309
+ }
9310
+ const obstacles = [...boxes.values()];
9311
+ const next = documentModel.edges.flatMap((edge) => {
9312
+ const source = boxes.get(edge.source), target = boxes.get(edge.target);
9313
+ return source && target ? [{ edge, path: routeEdge(source, target, edge, obstacles) }] : [];
9314
+ });
9315
+ setMeasured(next);
9316
+ if (debugPerf) performance.measure("roadmap:edges", { start: started, end: performance.now() });
9317
+ };
9318
+ const schedule = () => {
9319
+ if (!disposed && !frame) frame = requestAnimationFrame(measure);
9320
+ };
9321
+ const observer = typeof ResizeObserver === "undefined" ? void 0 : new ResizeObserver(schedule);
9322
+ observer?.observe(container);
9323
+ const mutations = new MutationObserver((records) => {
9324
+ if (records.some((record) => (record.target instanceof Element ? record.target : record.target.parentElement)?.closest("[data-roadmap-node]"))) schedule();
9325
+ });
9326
+ mutations.observe(container, { childList: true, subtree: true, characterData: true, attributes: true });
9327
+ const fonts = container.ownerDocument.fonts;
9328
+ fonts?.ready.then(schedule);
9329
+ fonts?.addEventListener("loadingdone", schedule);
9330
+ schedule();
9331
+ return () => {
9332
+ disposed = true;
9333
+ cancelAnimationFrame(frame);
9334
+ observer?.disconnect();
9335
+ mutations.disconnect();
9336
+ fonts?.removeEventListener("loadingdone", schedule);
9337
+ };
9338
+ }, [containerRef, documentModel, debugPerf]);
9339
+ return /* @__PURE__ */ (0, import_jsx_runtime68.jsx)("svg", { "data-testid": `${testIdPrefix}-edges`, "aria-hidden": "true", className: "pointer-events-none absolute inset-0 z-[1] h-full w-full overflow-visible", children: measured.map((item) => /* @__PURE__ */ (0, import_jsx_runtime68.jsx)(Edge, { ...item, instance, prefix: testIdPrefix }, item.edge.id)) });
9340
+ }
9341
+
9342
+ // src/components/organisms/roadmap/Roadmap.tsx
9343
+ var import_jsx_runtime69 = require("react/jsx-runtime");
9344
+ function Roadmap({ document: document2, state, renderNode, onNodeClick, onAction, testIdPrefix = "roadmap", className = "", ariaLabel, debugPerf }) {
9345
+ const gridRef = (0, import_react8.useRef)(null);
9346
+ const { placements } = (0, import_react8.useMemo)(() => placeNodes(document2), [document2]);
9347
+ const badges = (0, import_react8.useMemo)(() => new Map(document2.legend?.entries.map((entry) => [entry.id, entry])), [document2.legend]);
9193
9348
  const legendPlacement = document2.legend ? resolveLegendPlacement(document2.legend) : void 0;
9194
- const legend = document2.legend && /* @__PURE__ */ (0, import_jsx_runtime68.jsx)(RoadmapLegendView, { legend: document2.legend, testIdPrefix });
9195
- return /* @__PURE__ */ (0, import_jsx_runtime68.jsxs)("section", { "data-testid": testIdPrefix, "aria-label": ariaLabel ?? document2.title ?? "Roadmap", className: `min-w-0 bg-[var(--c-roadmap-surface)] p-3 text-[var(--c-roadmap-neutral-fg)] ${className}`, children: [
9196
- legendPlacement === "top" && /* @__PURE__ */ (0, import_jsx_runtime68.jsx)("div", { className: "mb-6", children: legend }),
9197
- /* @__PURE__ */ (0, import_jsx_runtime68.jsxs)("div", { "data-testid": `${testIdPrefix}-grid`, role: "list", "aria-label": "Roadmap nodes", className: "relative isolate grid min-w-0 grid-cols-3 items-center gap-x-3 gap-y-8 md:gap-x-8", children: [
9198
- document2.groups.map((group) => /* @__PURE__ */ (0, import_jsx_runtime68.jsx)(RoadmapGroupView, { group, testIdPrefix }, group.id)),
9349
+ const legend = document2.legend && /* @__PURE__ */ (0, import_jsx_runtime69.jsx)(RoadmapLegendView, { legend: document2.legend, testIdPrefix });
9350
+ return /* @__PURE__ */ (0, import_jsx_runtime69.jsxs)("section", { "data-testid": testIdPrefix, "aria-label": ariaLabel ?? document2.title ?? "Roadmap", className: `min-w-0 bg-[var(--c-roadmap-surface)] p-3 text-[var(--c-roadmap-neutral-fg)] ${className}`, children: [
9351
+ legendPlacement === "top" && /* @__PURE__ */ (0, import_jsx_runtime69.jsx)("div", { className: "mb-6", children: legend }),
9352
+ /* @__PURE__ */ (0, import_jsx_runtime69.jsxs)("div", { ref: gridRef, "data-roadmap-grid": "", "data-testid": `${testIdPrefix}-grid`, role: "list", "aria-label": "Roadmap nodes", className: "relative isolate grid min-w-0 grid-cols-3 items-center gap-x-3 gap-y-2 md:gap-x-8", children: [
9353
+ /* @__PURE__ */ (0, import_jsx_runtime69.jsx)(RoadmapEdgeLayer, { containerRef: gridRef, document: document2, testIdPrefix, debugPerf }),
9354
+ document2.groups.map((group) => /* @__PURE__ */ (0, import_jsx_runtime69.jsx)(RoadmapGroupView, { group, testIdPrefix }, group.id)),
9199
9355
  placements.map(({ node, row, column }) => {
9200
9356
  const resolvedState = resolveNodeState(node, state);
9201
- return /* @__PURE__ */ (0, import_jsx_runtime68.jsx)(
9357
+ return /* @__PURE__ */ (0, import_jsx_runtime69.jsx)(
9202
9358
  "div",
9203
9359
  {
9204
9360
  role: "listitem",
9205
9361
  "data-testid": `${testIdPrefix}-node`,
9206
9362
  "data-node-id": node.id,
9363
+ "data-roadmap-node": node.id,
9207
9364
  "data-state": resolvedState,
9208
9365
  style: { gridColumn: `${column.start} / span ${column.span}`, gridRow: `${row.start} / span 1` },
9209
- className: "relative z-10 min-w-0 px-2 py-4",
9210
- children: node.kind === "legend" ? legendPlacement === "inline" ? legend : null : renderNode ? renderNode(node, resolvedState) : /* @__PURE__ */ (0, import_jsx_runtime68.jsx)(RoadmapNodeView, { node, state: resolvedState, badge: node.badge ? badges.get(node.badge) : void 0, onNodeClick, onAction, testIdPrefix })
9366
+ className: "relative z-10 min-w-0 px-2 py-2",
9367
+ children: node.kind === "legend" ? legendPlacement === "inline" ? legend : null : renderNode ? renderNode(node, resolvedState) : /* @__PURE__ */ (0, import_jsx_runtime69.jsx)(RoadmapNodeView, { node, state: resolvedState, badge: node.badge ? badges.get(node.badge) : void 0, onNodeClick, onAction, testIdPrefix })
9211
9368
  },
9212
9369
  node.id
9213
9370
  );
9214
9371
  })
9215
9372
  ] }),
9216
- legendPlacement === "bottom" && /* @__PURE__ */ (0, import_jsx_runtime68.jsx)("div", { className: "mt-6", children: legend })
9373
+ legendPlacement === "bottom" && /* @__PURE__ */ (0, import_jsx_runtime69.jsx)("div", { className: "mt-6", children: legend })
9217
9374
  ] });
9218
9375
  }
9219
9376
 
9220
9377
  // src/hooks/use-iframe-auth.ts
9221
- var import_react8 = require("react");
9378
+ var import_react9 = require("react");
9222
9379
  function useIframeAuth() {
9223
- const ctx = (0, import_react8.useContext)(IframeContext);
9380
+ const ctx = (0, import_react9.useContext)(IframeContext);
9224
9381
  return { token: ctx.token, userId: ctx.userId, ready: ctx.ready };
9225
9382
  }
9226
9383
 
9227
9384
  // src/hooks/use-iframe-navigate.ts
9228
- var import_react9 = require("react");
9385
+ var import_react10 = require("react");
9229
9386
  function useIframeNavigate() {
9230
- return (0, import_react9.useCallback)((path) => {
9387
+ return (0, import_react10.useCallback)((path) => {
9231
9388
  if (!window.parent || window.parent === window) return;
9232
9389
  window.parent.postMessage(
9233
9390
  { type: "DFL_NAVIGATE", path },
@@ -9237,9 +9394,9 @@ function useIframeNavigate() {
9237
9394
  }
9238
9395
 
9239
9396
  // src/providers/feature-flag-provider.tsx
9240
- var import_react10 = require("react");
9241
- var import_jsx_runtime69 = require("react/jsx-runtime");
9242
- var FeatureFlagContext = (0, import_react10.createContext)({
9397
+ var import_react11 = require("react");
9398
+ var import_jsx_runtime70 = require("react/jsx-runtime");
9399
+ var FeatureFlagContext = (0, import_react11.createContext)({
9243
9400
  flags: {},
9244
9401
  isEnabled: () => false
9245
9402
  });
@@ -9247,21 +9404,21 @@ var FeatureFlagProvider = ({
9247
9404
  children,
9248
9405
  flags
9249
9406
  }) => {
9250
- const value = (0, import_react10.useMemo)(
9407
+ const value = (0, import_react11.useMemo)(
9251
9408
  () => ({
9252
9409
  flags,
9253
9410
  isEnabled: (flag) => Boolean(flags[flag])
9254
9411
  }),
9255
9412
  [flags]
9256
9413
  );
9257
- return /* @__PURE__ */ (0, import_jsx_runtime69.jsx)(FeatureFlagContext.Provider, { value, children });
9414
+ return /* @__PURE__ */ (0, import_jsx_runtime70.jsx)(FeatureFlagContext.Provider, { value, children });
9258
9415
  };
9259
9416
  var useFeatureFlag = (flag) => {
9260
- const { isEnabled } = (0, import_react10.useContext)(FeatureFlagContext);
9417
+ const { isEnabled } = (0, import_react11.useContext)(FeatureFlagContext);
9261
9418
  return isEnabled(flag);
9262
9419
  };
9263
9420
  var useFeatureFlags = () => {
9264
- const { flags } = (0, import_react10.useContext)(FeatureFlagContext);
9421
+ const { flags } = (0, import_react11.useContext)(FeatureFlagContext);
9265
9422
  return flags;
9266
9423
  };
9267
9424
  // Annotate the CommonJS export names for ESM import in node:
package/dist/index.d.cts CHANGED
@@ -3268,11 +3268,12 @@ interface RoadmapProps {
3268
3268
  renderNode?: (node: RoadmapNode, state: RoadmapNodeState) => ReactNode;
3269
3269
  onNodeClick?: (node: RoadmapNode) => void;
3270
3270
  onAction?: (actionId: string, node: RoadmapNode) => void;
3271
+ debugPerf?: boolean;
3271
3272
  testIdPrefix?: string;
3272
3273
  className?: string;
3273
3274
  ariaLabel?: string;
3274
3275
  }
3275
3276
  /** Dark-only, ordinary document scroll. The JSON owns placement; CSS owns the pixels. */
3276
- declare function Roadmap({ document, state, renderNode, onNodeClick, onAction, testIdPrefix, className, ariaLabel }: RoadmapProps): React__default.JSX.Element;
3277
+ declare function Roadmap({ document, state, renderNode, onNodeClick, onAction, testIdPrefix, className, ariaLabel, debugPerf }: RoadmapProps): React__default.JSX.Element;
3277
3278
 
3278
3279
  export { ALLOWED_ORIGINS, AVATAR_MEMBER_PALETTE_SIZE, Accordion, AccordionContent, AccordionItem, AccordionTrigger, Alert, AlertDescription, AlertDialog, AlertDialogAction, type AlertDialogActionProps, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, AlertDialogPortal, AlertDialogTitle, AlertDialogTrigger, AlertTitle, AppNavbar, type AppNavbarProps, AppSidebar, type AppSidebarProps, AspectRatio, Avatar, AvatarFallback, AvatarGroup, AvatarImage, type AvatarSize, AvatarStatus, type AvatarStatusValue, type AvatarTone, Badge, type BadgeProps, Breadcrumb, BreadcrumbEllipsis, type BreadcrumbEntry, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator, Button, type ButtonProps, Calendar, type CalendarProps, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Carousel, type CarouselApi, CarouselContent, CarouselDots, CarouselItem, CarouselNext, CarouselPrevious, type ChartConfig, ChartContainer, ChartLegend, ChartLegendContent, ChartStyle, ChartTooltip, ChartTooltipContent, Checkbox, Collapsible, CollapsibleContent, CollapsibleHeader, CollapsibleItem, CollapsibleTrigger, Command, CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, CommandShortcut, ConfirmDialog, type ConfirmDialogProps, ContextMenu, ContextMenuCheckboxItem, ContextMenuContent, ContextMenuGroup, ContextMenuItem, ContextMenuLabel, ContextMenuPortal, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSeparator, ContextMenuShortcut, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger, DEFAULT_EDGE_ARROW, DEFAULT_EDGE_ROUTE, DEFAULT_EDGE_STYLE, DEFAULT_GROUP_TONE, DEFAULT_LEGEND_PLACEMENT, DEFAULT_NAME_COL_WIDTH, DEFAULT_NODE_SPAN, DEFAULT_NODE_STATE, type DflIframeMessage, type DflNavigateMessage, type DflReadyMessage, DflRemote, type DflRemoteProps, type DflResizeMessage, type DflSetTokenMessage, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerOverlay, DrawerPortal, DrawerTitle, DrawerTrigger, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, Gantt, type GanttDependency, type GanttMilestone, type GanttProps, type GanttStage, HoverCard, HoverCardContent, HoverCardTrigger, IconButton, type IconButtonProps, IframeAware, type IframeAwareProps, IframeContext, type IframeContextValue, type IframeMessageType, Input, InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot, Kbd, type KbdProps, Label, LoginPage, type LoginPageProps, LoginScreen, type LoginScreenProps, MEMBER_PALETTE_SIZE, Menubar, MenubarCheckboxItem, MenubarContent, MenubarGroup, MenubarItem, MenubarLabel, MenubarMenu, MenubarPortal, MenubarRadioGroup, MenubarRadioItem, MenubarSeparator, MenubarShortcut, MenubarSub, MenubarSubContent, MenubarSubTrigger, MenubarTrigger, NODE_KIND_DEFAULT_TONE, type NavGroup, type NavItem, type NavbarUserInfo, NavigationMenu, NavigationMenuContent, NavigationMenuIndicator, NavigationMenuItem, NavigationMenuLink, NavigationMenuList, NavigationMenuTrigger, NavigationMenuViewport, type OTPStatus, Pagination, PaginationContent, PaginationEllipsis, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious, PasswordInput, type PasswordInputProps, Popover, PopoverAnchor, PopoverContent, PopoverTrigger, Progress, type ProgressProps, ProtectedRoute, type ProtectedRouteProps, PublishDrawer, type PublishDrawerProps, type PublishDrawerSupabase, type PublishResult, type PublishStatus, type PublisherAccount, ROADMAP_ARROWS, ROADMAP_COLUMNS, ROADMAP_EDGE_LABEL_MAX, ROADMAP_EDGE_ROUTES, ROADMAP_EDGE_STYLES, ROADMAP_LABEL_MAX, ROADMAP_LEGEND_PLACEMENTS, ROADMAP_NODE_ID_PATTERN, ROADMAP_NODE_KINDS, ROADMAP_NODE_STATES, ROADMAP_TONES, RadioGroup, RadioGroupItem, RadioGroupRow, ResizableHandle, ResizablePanel, ResizablePanelGroup, Roadmap, type RoadmapAction, type RoadmapArrow, type RoadmapColumn, type RoadmapDocument, type RoadmapEdge, type RoadmapEdgeRoute, type RoadmapEdgeStyle, type RoadmapGroup, type RoadmapGroupRange, type RoadmapIcon, type RoadmapLegend, type RoadmapLegendEntry, type RoadmapLegendPlacement, type RoadmapLink, type RoadmapNode, type RoadmapNodeKind, type RoadmapNodeState, type RoadmapOverlap, type RoadmapPlacement, type RoadmapProps, type RoadmapStateOverlay, type RoadmapTone, ScrollArea, type ScrollAreaScrollbars, ScrollBar, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, Separator, SeparatorWithLabel, Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetOverlay, SheetPortal, SheetTitle, SheetTrigger, Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupAction, SidebarGroupContent, SidebarGroupLabel, SidebarHeader, SidebarInput, SidebarInset, SidebarMenu, SidebarMenuAction, SidebarMenuBadge, SidebarMenuButton, SidebarMenuItem, SidebarMenuSkeleton, SidebarMenuSub, SidebarMenuSubButton, SidebarMenuSubItem, SidebarProvider, SidebarRail, SidebarSeparator, SidebarTrigger, Skeleton, Slider, SonnerToaster, Switch, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow, Tabs, TabsContent, TabsList, TabsTrigger, Textarea, type TextareaProps, Toast, ToastAction, type ToastActionElement, ToastClose, type ToastComponentProps, ToastDescription, ToastProvider, ToastTitle, ToastViewport, Toaster, Toggle, ToggleGroup, ToggleGroupItem, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, UserAvatar, type UserAvatarProps, type UserInfo, UserMenu, type UserMenuItem, type UserMenuProps, avatarMemberColor, badgeVariants, barGridColumn, buttonVariants, clampPct, columnIndex, columnsCovered, columnsUsed, filterPublishableAccounts, firstFreeRow, getInitials, gridColumnFor, groupColumnRuns, groupRanges, iconButtonVariants, isAllowedOrigin, kbdVariants, labelVariants, maxRowOf, memberHueIndex, memberHueVar, navigationMenuTriggerStyle, nodesByRow, parseRoadmapDocument, parseRoadmapStateOverlay, parseTags, placeNodes, resolveEdgeArrow, resolveEdgeRoute, resolveEdgeStyle, resolveGroupColumns, resolveGroupTone, resolveLegendPlacement, resolveNameColWidth, resolveNodeSpan, resolveNodeState, resolveNodeTone, resolveWeekCount, resolveWeekLabels, roadmapActionSchema, roadmapDocumentSchema, roadmapEdgeSchema, roadmapGroupSchema, roadmapIconSchema, roadmapLegendEntrySchema, roadmapLegendSchema, roadmapLinkSchema, roadmapNodeSchema, roadmapNodeStateSchema, roadmapStateOverlaySchema, roadmapStateSchema, rowsOf, safeParseRoadmapDocument, safeParseRoadmapStateOverlay, stageProgress, toggleVariants, useFormField, useSidebar, validateNoOverlap, validatePublishForm };
package/dist/index.d.ts CHANGED
@@ -3268,11 +3268,12 @@ interface RoadmapProps {
3268
3268
  renderNode?: (node: RoadmapNode, state: RoadmapNodeState) => ReactNode;
3269
3269
  onNodeClick?: (node: RoadmapNode) => void;
3270
3270
  onAction?: (actionId: string, node: RoadmapNode) => void;
3271
+ debugPerf?: boolean;
3271
3272
  testIdPrefix?: string;
3272
3273
  className?: string;
3273
3274
  ariaLabel?: string;
3274
3275
  }
3275
3276
  /** Dark-only, ordinary document scroll. The JSON owns placement; CSS owns the pixels. */
3276
- declare function Roadmap({ document, state, renderNode, onNodeClick, onAction, testIdPrefix, className, ariaLabel }: RoadmapProps): React__default.JSX.Element;
3277
+ declare function Roadmap({ document, state, renderNode, onNodeClick, onAction, testIdPrefix, className, ariaLabel, debugPerf }: RoadmapProps): React__default.JSX.Element;
3277
3278
 
3278
3279
  export { ALLOWED_ORIGINS, AVATAR_MEMBER_PALETTE_SIZE, Accordion, AccordionContent, AccordionItem, AccordionTrigger, Alert, AlertDescription, AlertDialog, AlertDialogAction, type AlertDialogActionProps, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, AlertDialogPortal, AlertDialogTitle, AlertDialogTrigger, AlertTitle, AppNavbar, type AppNavbarProps, AppSidebar, type AppSidebarProps, AspectRatio, Avatar, AvatarFallback, AvatarGroup, AvatarImage, type AvatarSize, AvatarStatus, type AvatarStatusValue, type AvatarTone, Badge, type BadgeProps, Breadcrumb, BreadcrumbEllipsis, type BreadcrumbEntry, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator, Button, type ButtonProps, Calendar, type CalendarProps, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Carousel, type CarouselApi, CarouselContent, CarouselDots, CarouselItem, CarouselNext, CarouselPrevious, type ChartConfig, ChartContainer, ChartLegend, ChartLegendContent, ChartStyle, ChartTooltip, ChartTooltipContent, Checkbox, Collapsible, CollapsibleContent, CollapsibleHeader, CollapsibleItem, CollapsibleTrigger, Command, CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator, CommandShortcut, ConfirmDialog, type ConfirmDialogProps, ContextMenu, ContextMenuCheckboxItem, ContextMenuContent, ContextMenuGroup, ContextMenuItem, ContextMenuLabel, ContextMenuPortal, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSeparator, ContextMenuShortcut, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger, DEFAULT_EDGE_ARROW, DEFAULT_EDGE_ROUTE, DEFAULT_EDGE_STYLE, DEFAULT_GROUP_TONE, DEFAULT_LEGEND_PLACEMENT, DEFAULT_NAME_COL_WIDTH, DEFAULT_NODE_SPAN, DEFAULT_NODE_STATE, type DflIframeMessage, type DflNavigateMessage, type DflReadyMessage, DflRemote, type DflRemoteProps, type DflResizeMessage, type DflSetTokenMessage, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerOverlay, DrawerPortal, DrawerTitle, DrawerTrigger, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, Gantt, type GanttDependency, type GanttMilestone, type GanttProps, type GanttStage, HoverCard, HoverCardContent, HoverCardTrigger, IconButton, type IconButtonProps, IframeAware, type IframeAwareProps, IframeContext, type IframeContextValue, type IframeMessageType, Input, InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot, Kbd, type KbdProps, Label, LoginPage, type LoginPageProps, LoginScreen, type LoginScreenProps, MEMBER_PALETTE_SIZE, Menubar, MenubarCheckboxItem, MenubarContent, MenubarGroup, MenubarItem, MenubarLabel, MenubarMenu, MenubarPortal, MenubarRadioGroup, MenubarRadioItem, MenubarSeparator, MenubarShortcut, MenubarSub, MenubarSubContent, MenubarSubTrigger, MenubarTrigger, NODE_KIND_DEFAULT_TONE, type NavGroup, type NavItem, type NavbarUserInfo, NavigationMenu, NavigationMenuContent, NavigationMenuIndicator, NavigationMenuItem, NavigationMenuLink, NavigationMenuList, NavigationMenuTrigger, NavigationMenuViewport, type OTPStatus, Pagination, PaginationContent, PaginationEllipsis, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious, PasswordInput, type PasswordInputProps, Popover, PopoverAnchor, PopoverContent, PopoverTrigger, Progress, type ProgressProps, ProtectedRoute, type ProtectedRouteProps, PublishDrawer, type PublishDrawerProps, type PublishDrawerSupabase, type PublishResult, type PublishStatus, type PublisherAccount, ROADMAP_ARROWS, ROADMAP_COLUMNS, ROADMAP_EDGE_LABEL_MAX, ROADMAP_EDGE_ROUTES, ROADMAP_EDGE_STYLES, ROADMAP_LABEL_MAX, ROADMAP_LEGEND_PLACEMENTS, ROADMAP_NODE_ID_PATTERN, ROADMAP_NODE_KINDS, ROADMAP_NODE_STATES, ROADMAP_TONES, RadioGroup, RadioGroupItem, RadioGroupRow, ResizableHandle, ResizablePanel, ResizablePanelGroup, Roadmap, type RoadmapAction, type RoadmapArrow, type RoadmapColumn, type RoadmapDocument, type RoadmapEdge, type RoadmapEdgeRoute, type RoadmapEdgeStyle, type RoadmapGroup, type RoadmapGroupRange, type RoadmapIcon, type RoadmapLegend, type RoadmapLegendEntry, type RoadmapLegendPlacement, type RoadmapLink, type RoadmapNode, type RoadmapNodeKind, type RoadmapNodeState, type RoadmapOverlap, type RoadmapPlacement, type RoadmapProps, type RoadmapStateOverlay, type RoadmapTone, ScrollArea, type ScrollAreaScrollbars, ScrollBar, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, Separator, SeparatorWithLabel, Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetOverlay, SheetPortal, SheetTitle, SheetTrigger, Sidebar, SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupAction, SidebarGroupContent, SidebarGroupLabel, SidebarHeader, SidebarInput, SidebarInset, SidebarMenu, SidebarMenuAction, SidebarMenuBadge, SidebarMenuButton, SidebarMenuItem, SidebarMenuSkeleton, SidebarMenuSub, SidebarMenuSubButton, SidebarMenuSubItem, SidebarProvider, SidebarRail, SidebarSeparator, SidebarTrigger, Skeleton, Slider, SonnerToaster, Switch, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow, Tabs, TabsContent, TabsList, TabsTrigger, Textarea, type TextareaProps, Toast, ToastAction, type ToastActionElement, ToastClose, type ToastComponentProps, ToastDescription, ToastProvider, ToastTitle, ToastViewport, Toaster, Toggle, ToggleGroup, ToggleGroupItem, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, UserAvatar, type UserAvatarProps, type UserInfo, UserMenu, type UserMenuItem, type UserMenuProps, avatarMemberColor, badgeVariants, barGridColumn, buttonVariants, clampPct, columnIndex, columnsCovered, columnsUsed, filterPublishableAccounts, firstFreeRow, getInitials, gridColumnFor, groupColumnRuns, groupRanges, iconButtonVariants, isAllowedOrigin, kbdVariants, labelVariants, maxRowOf, memberHueIndex, memberHueVar, navigationMenuTriggerStyle, nodesByRow, parseRoadmapDocument, parseRoadmapStateOverlay, parseTags, placeNodes, resolveEdgeArrow, resolveEdgeRoute, resolveEdgeStyle, resolveGroupColumns, resolveGroupTone, resolveLegendPlacement, resolveNameColWidth, resolveNodeSpan, resolveNodeState, resolveNodeTone, resolveWeekCount, resolveWeekLabels, roadmapActionSchema, roadmapDocumentSchema, roadmapEdgeSchema, roadmapGroupSchema, roadmapIconSchema, roadmapLegendEntrySchema, roadmapLegendSchema, roadmapLinkSchema, roadmapNodeSchema, roadmapNodeStateSchema, roadmapStateOverlaySchema, roadmapStateSchema, rowsOf, safeParseRoadmapDocument, safeParseRoadmapStateOverlay, stageProgress, toggleVariants, useFormField, useSidebar, validateNoOverlap, validatePublishForm };
package/dist/index.js CHANGED
@@ -8715,9 +8715,49 @@ function firstFreeRow(taken, columns, from) {
8715
8715
  }
8716
8716
  return row;
8717
8717
  }
8718
+ var pointText = (p) => `${p.x} ${p.y}`;
8719
+ var center = (r) => ({ x: (r.left + r.right) / 2, y: (r.top + r.bottom) / 2 });
8720
+ function segmentHits(a, b, r) {
8721
+ return a.x === b.x ? a.x > r.left && a.x < r.right && Math.max(a.y, b.y) > r.top && Math.min(a.y, b.y) < r.bottom : a.y > r.top && a.y < r.bottom && Math.max(a.x, b.x) > r.left && Math.min(a.x, b.x) < r.right;
8722
+ }
8723
+ function routeEdge(source, target, edge, obstacles = []) {
8724
+ const a = center(source), b = center(target);
8725
+ const sameColumn = Math.abs(a.x - b.x) < 1;
8726
+ const vertical = sameColumn || source.left < target.right && target.left < source.right;
8727
+ const down = b.y >= a.y, right = b.x >= a.x;
8728
+ const start = vertical ? { x: a.x, y: down ? source.bottom : source.top } : { x: right ? source.right : source.left, y: a.y };
8729
+ const end = vertical ? { x: b.x, y: down ? target.top : target.bottom } : { x: right ? target.left : target.right, y: b.y };
8730
+ const route = edge.route ?? "auto";
8731
+ if (route === "straight" || route === "auto" && sameColumn) return { start, end, d: `M ${pointText(start)} L ${pointText(end)}` };
8732
+ if (route !== "elbow") {
8733
+ const mid = vertical ? (start.y + end.y) / 2 : (start.x + end.x) / 2;
8734
+ const c1 = vertical ? { x: start.x, y: mid } : { x: mid, y: start.y };
8735
+ const c2 = vertical ? { x: end.x, y: mid } : { x: mid, y: end.y };
8736
+ return { start, end, d: `M ${pointText(start)} C ${pointText(c1)} ${pointText(c2)} ${pointText(end)}` };
8737
+ }
8738
+ const boxes = [.../* @__PURE__ */ new Set([source, target, ...obstacles])];
8739
+ const clearance = 4;
8740
+ const ys = [...new Set(boxes.flatMap((r) => [r.top - clearance, r.bottom + clearance]))];
8741
+ let best, bestLength = Infinity;
8742
+ for (const sx of [source.left, source.right]) for (const tx of [target.left, target.right]) {
8743
+ const s = { x: sx, y: a.y }, t = { x: tx, y: b.y };
8744
+ const gx = sx + (sx === source.left ? -clearance : clearance);
8745
+ const hx = tx + (tx === target.left ? -clearance : clearance);
8746
+ for (const y of [a.y, b.y, ...ys]) {
8747
+ const points2 = [s, { x: gx, y: a.y }, { x: gx, y }, { x: hx, y }, { x: hx, y: b.y }, t];
8748
+ const length = points2.slice(1).reduce((sum, p, i) => sum + Math.abs(p.x - points2[i].x) + Math.abs(p.y - points2[i].y), 0);
8749
+ if (length >= bestLength || points2.slice(1).some((p, i) => boxes.some((r) => segmentHits(points2[i], p, r)))) continue;
8750
+ best = points2;
8751
+ bestLength = length;
8752
+ }
8753
+ }
8754
+ if (!best) return { start, end, d: "" };
8755
+ const points = best.filter((p, i) => i === 0 || p.x !== best[i - 1].x || p.y !== best[i - 1].y);
8756
+ return { start: points[0], end: points[points.length - 1], points, d: points.map((p, i) => `${i ? "L" : "M"} ${pointText(p)}`).join(" ") };
8757
+ }
8718
8758
 
8719
8759
  // src/components/organisms/roadmap/Roadmap.tsx
8720
- import { useMemo as useMemo5 } from "react";
8760
+ import { useMemo as useMemo5, useRef as useRef6 } from "react";
8721
8761
 
8722
8762
  // src/components/organisms/roadmap/RoadmapNode.tsx
8723
8763
  import { cva as cva14 } from "class-variance-authority";
@@ -8765,7 +8805,7 @@ function RoadmapNodeView({ node, state, badge, testIdPrefix, onNodeClick, onActi
8765
8805
  const Box = wholeBox && node.href ? "a" : wholeBox && onNodeClick ? "button" : "div";
8766
8806
  const primary = Box !== "div" ? label : node.href && !locked ? /* @__PURE__ */ jsx66("a", { href: node.href, className: interactiveClass, onClick: () => onNodeClick?.(node), children: label }) : onNodeClick && !locked ? /* @__PURE__ */ jsx66("button", { type: "button", className: interactiveClass, onClick: () => onNodeClick(node), children: label }) : label;
8767
8807
  const icon = node.icon ?? (badge ? { name: badge.icon ?? "check", side: "right", tone: badge.tone } : void 0);
8768
- return /* @__PURE__ */ jsx66("div", { style: toneStyle(nodeTone), className: "min-w-0", children: /* @__PURE__ */ jsxs41(Box, { "data-roadmap-node-box": node.id, href: Box === "a" ? node.href : void 0, type: Box === "button" ? "button" : void 0, onClick: Box !== "div" ? () => onNodeClick?.(node) : void 0, className: `${nodeVariants({ kind: node.kind, state })} w-full ${Box !== "div" ? focusClass : ""}`, "aria-disabled": locked || void 0, title: node.kind === "topic" || node.kind === "subtopic" ? node.description : void 0, children: [
8808
+ return /* @__PURE__ */ jsx66("div", { style: toneStyle(nodeTone), className: "min-w-0", children: /* @__PURE__ */ jsxs41(Box, { style: { borderColor: node.kind === "title" || node.kind === "label" ? "transparent" : "var(--c-roadmap-border)" }, "data-roadmap-node-box": node.id, href: Box === "a" ? node.href : void 0, type: Box === "button" ? "button" : void 0, onClick: Box !== "div" ? () => onNodeClick?.(node) : void 0, className: `${nodeVariants({ kind: node.kind, state })} w-full ${Box !== "div" ? focusClass : ""}`, "aria-disabled": locked || void 0, title: node.kind === "topic" || node.kind === "subtopic" ? node.description : void 0, children: [
8769
8809
  icon && /* @__PURE__ */ jsx66(RoadmapBadge, { name: icon.name, tone: icon.tone ?? nodeTone, label: badge?.label ?? icon.name, className: `absolute top-1/2 -translate-y-1/2 ${icon.side === "left" ? "left-0 -translate-x-1/2" : "right-0 translate-x-1/2"}` }),
8770
8810
  primary,
8771
8811
  node.description && node.kind === "paragraph" && /* @__PURE__ */ jsx66("p", { className: "mt-2 [overflow-wrap:anywhere]", children: node.description }),
@@ -8780,15 +8820,17 @@ function RoadmapNodeView({ node, state, badge, testIdPrefix, onNodeClick, onActi
8780
8820
  // src/components/organisms/roadmap/RoadmapGroup.tsx
8781
8821
  import { Fragment as Fragment10, jsx as jsx67, jsxs as jsxs42 } from "react/jsx-runtime";
8782
8822
  function RoadmapGroupView({ group, testIdPrefix }) {
8823
+ const tone = resolveGroupTone(group);
8824
+ const appearance = tone === "accent" ? { "--c-roadmap-node-bg": "var(--c-roadmap-group-accent-bg)", "--c-roadmap-node-fg": "var(--c-roadmap-group-accent-fg)" } : toneStyle(tone);
8783
8825
  return /* @__PURE__ */ jsx67(Fragment10, { children: groupColumnRuns(group).map((run, index) => /* @__PURE__ */ jsxs42(
8784
8826
  "div",
8785
8827
  {
8786
8828
  "data-testid": `${testIdPrefix}-group`,
8787
8829
  "data-group-id": group.id,
8788
- style: { ...toneStyle(resolveGroupTone(group)), gridColumn: `${run.start} / span ${run.span}`, gridRow: `${group.from + 1} / span ${group.to - group.from + 1}` },
8830
+ style: { borderColor: "var(--c-roadmap-border)", ...appearance, gridColumn: `${run.start} / span ${run.span}`, gridRow: `${group.from + 1} / span ${group.to - group.from + 1}` },
8789
8831
  className: "pointer-events-none relative z-0 min-w-0 self-stretch rounded-[var(--radius)] border-2 border-[var(--c-roadmap-border)] bg-[var(--c-roadmap-node-bg)] text-[var(--c-roadmap-node-fg)]",
8790
8832
  children: [
8791
- index === 0 && group.title && /* @__PURE__ */ jsx67("span", { className: "absolute left-2 right-2 top-0 -translate-y-1/2 text-xs leading-normal", children: /* @__PURE__ */ jsx67("span", { className: "bg-[var(--c-roadmap-surface)] px-1 [overflow-wrap:anywhere]", children: group.title }) }),
8833
+ index === 0 && group.title && /* @__PURE__ */ jsx67("span", { title: group.title, className: "absolute left-2 right-2 top-0 -translate-y-1/2 text-xs leading-normal", children: /* @__PURE__ */ jsx67("span", { className: "inline-block max-w-full overflow-hidden text-ellipsis whitespace-nowrap bg-[var(--c-roadmap-surface)] px-1 align-top text-[var(--c-roadmap-neutral-fg)]", children: group.title }) }),
8792
8834
  index === 0 && group.description && /* @__PURE__ */ jsx67("span", { className: "sr-only", children: group.description })
8793
8835
  ]
8794
8836
  },
@@ -8796,35 +8838,150 @@ function RoadmapGroupView({ group, testIdPrefix }) {
8796
8838
  )) });
8797
8839
  }
8798
8840
 
8799
- // src/components/organisms/roadmap/Roadmap.tsx
8841
+ // src/components/organisms/roadmap/RoadmapEdgeLayer.tsx
8842
+ import { useId as useId4, useEffect as useEffect10, useLayoutEffect as useLayoutEffect2, useRef as useRef5, useState as useState13 } from "react";
8800
8843
  import { jsx as jsx68, jsxs as jsxs43 } from "react/jsx-runtime";
8801
- function Roadmap({ document: document2, state, renderNode, onNodeClick, onAction, testIdPrefix = "roadmap", className = "", ariaLabel }) {
8844
+ function Edge({ edge, path, instance, prefix }) {
8845
+ const pathRef = useRef5(null);
8846
+ const [midpoint, setMidpoint] = useState13();
8847
+ useLayoutEffect2(() => {
8848
+ const element = pathRef.current;
8849
+ if (edge.label && element?.getTotalLength && path.d) {
8850
+ const p = element.getPointAtLength(element.getTotalLength() / 2);
8851
+ setMidpoint({ x: p.x, y: p.y });
8852
+ } else setMidpoint(void 0);
8853
+ }, [path.d, edge.label]);
8854
+ const arrow = resolveEdgeArrow(edge), style = resolveEdgeStyle(edge);
8855
+ const marker = `${instance}-${edge.id}`;
8856
+ const color = edge.tone ? `var(--c-roadmap-${edge.tone}-edge)` : "var(--c-roadmap-edge)";
8857
+ return /* @__PURE__ */ jsxs43("g", { style: { color }, "data-roadmap-edge": edge.id, children: [
8858
+ arrow !== "none" && /* @__PURE__ */ jsx68("defs", { children: /* @__PURE__ */ jsx68("marker", { id: marker, viewBox: "0 0 8 8", refX: "8", refY: "4", markerWidth: "8", markerHeight: "8", markerUnits: "userSpaceOnUse", orient: "auto-start-reverse", children: /* @__PURE__ */ jsx68("path", { d: "M 0 0 L 8 4 L 0 8 Z", fill: "currentColor" }) }) }),
8859
+ /* @__PURE__ */ jsx68(
8860
+ "path",
8861
+ {
8862
+ ref: pathRef,
8863
+ "data-testid": `${prefix}-edge`,
8864
+ "data-edge-id": edge.id,
8865
+ "data-source": edge.source,
8866
+ "data-target": edge.target,
8867
+ d: path.d,
8868
+ fill: "none",
8869
+ stroke: "currentColor",
8870
+ strokeLinecap: "round",
8871
+ strokeLinejoin: "round",
8872
+ strokeDasharray: style === "solid" ? void 0 : style === "dotted" ? "0.8 4" : "0.8 8",
8873
+ className: "[stroke-width:2px] md:[stroke-width:3px]",
8874
+ markerStart: arrow === "both" ? `url(#${marker})` : void 0,
8875
+ markerEnd: arrow !== "none" ? `url(#${marker})` : void 0
8876
+ }
8877
+ ),
8878
+ edge.label && midpoint && /* @__PURE__ */ jsx68("foreignObject", { "data-testid": `${prefix}-edge-label`, "data-edge-id": edge.id, x: midpoint.x, y: midpoint.y, width: "1", height: "1", overflow: "visible", children: /* @__PURE__ */ jsx68(
8879
+ "div",
8880
+ {
8881
+ className: "w-max max-w-[160px] -translate-x-1/2 -translate-y-1/2 rounded-full bg-[var(--c-roadmap-edge-label-bg)] px-2 py-0.5 text-center text-xs leading-normal text-[var(--c-roadmap-edge-label-fg)] [overflow-wrap:anywhere]",
8882
+ style: { boxShadow: "0 0 0 2px var(--c-roadmap-surface)" },
8883
+ children: edge.label
8884
+ }
8885
+ ) })
8886
+ ] });
8887
+ }
8888
+ function RoadmapEdgeLayer({ containerRef, document: documentModel, testIdPrefix, debugPerf }) {
8889
+ const instance = `roadmap-${useId4().replace(/:/g, "")}`;
8890
+ const [measured, setMeasured] = useState13([]);
8891
+ useEffect10(() => {
8892
+ const container = containerRef.current;
8893
+ if (!container || !documentModel.edges.length) {
8894
+ setMeasured([]);
8895
+ return;
8896
+ }
8897
+ let disposed = false, frame = 0;
8898
+ const observed = /* @__PURE__ */ new Set();
8899
+ const measure = () => {
8900
+ frame = 0;
8901
+ if (disposed) return;
8902
+ const started = performance.now();
8903
+ const root = container.getBoundingClientRect();
8904
+ const boxes = /* @__PURE__ */ new Map();
8905
+ for (const wrapper of container.querySelectorAll("[data-roadmap-node]")) {
8906
+ if (wrapper.closest("[data-roadmap-grid]") !== container) continue;
8907
+ const id = wrapper.dataset.roadmapNode;
8908
+ const border = Array.from(wrapper.querySelectorAll("[data-roadmap-node-box]")).find((el) => el.dataset.roadmapNodeBox === id);
8909
+ const element = border ?? wrapper.firstElementChild ?? wrapper;
8910
+ for (const target of [wrapper, element]) if (!observed.has(target)) {
8911
+ observer?.observe(target);
8912
+ observed.add(target);
8913
+ }
8914
+ const rect = element.getBoundingClientRect();
8915
+ boxes.set(id, { left: rect.left - root.left, right: rect.right - root.left, top: rect.top - root.top, bottom: rect.bottom - root.top });
8916
+ }
8917
+ for (const element of observed) if (!container.contains(element)) {
8918
+ observer?.unobserve(element);
8919
+ observed.delete(element);
8920
+ }
8921
+ const obstacles = [...boxes.values()];
8922
+ const next = documentModel.edges.flatMap((edge) => {
8923
+ const source = boxes.get(edge.source), target = boxes.get(edge.target);
8924
+ return source && target ? [{ edge, path: routeEdge(source, target, edge, obstacles) }] : [];
8925
+ });
8926
+ setMeasured(next);
8927
+ if (debugPerf) performance.measure("roadmap:edges", { start: started, end: performance.now() });
8928
+ };
8929
+ const schedule = () => {
8930
+ if (!disposed && !frame) frame = requestAnimationFrame(measure);
8931
+ };
8932
+ const observer = typeof ResizeObserver === "undefined" ? void 0 : new ResizeObserver(schedule);
8933
+ observer?.observe(container);
8934
+ const mutations = new MutationObserver((records) => {
8935
+ if (records.some((record) => (record.target instanceof Element ? record.target : record.target.parentElement)?.closest("[data-roadmap-node]"))) schedule();
8936
+ });
8937
+ mutations.observe(container, { childList: true, subtree: true, characterData: true, attributes: true });
8938
+ const fonts = container.ownerDocument.fonts;
8939
+ fonts?.ready.then(schedule);
8940
+ fonts?.addEventListener("loadingdone", schedule);
8941
+ schedule();
8942
+ return () => {
8943
+ disposed = true;
8944
+ cancelAnimationFrame(frame);
8945
+ observer?.disconnect();
8946
+ mutations.disconnect();
8947
+ fonts?.removeEventListener("loadingdone", schedule);
8948
+ };
8949
+ }, [containerRef, documentModel, debugPerf]);
8950
+ return /* @__PURE__ */ jsx68("svg", { "data-testid": `${testIdPrefix}-edges`, "aria-hidden": "true", className: "pointer-events-none absolute inset-0 z-[1] h-full w-full overflow-visible", children: measured.map((item) => /* @__PURE__ */ jsx68(Edge, { ...item, instance, prefix: testIdPrefix }, item.edge.id)) });
8951
+ }
8952
+
8953
+ // src/components/organisms/roadmap/Roadmap.tsx
8954
+ import { jsx as jsx69, jsxs as jsxs44 } from "react/jsx-runtime";
8955
+ function Roadmap({ document: document2, state, renderNode, onNodeClick, onAction, testIdPrefix = "roadmap", className = "", ariaLabel, debugPerf }) {
8956
+ const gridRef = useRef6(null);
8802
8957
  const { placements } = useMemo5(() => placeNodes(document2), [document2]);
8803
8958
  const badges = useMemo5(() => new Map(document2.legend?.entries.map((entry) => [entry.id, entry])), [document2.legend]);
8804
8959
  const legendPlacement = document2.legend ? resolveLegendPlacement(document2.legend) : void 0;
8805
- const legend = document2.legend && /* @__PURE__ */ jsx68(RoadmapLegendView, { legend: document2.legend, testIdPrefix });
8806
- return /* @__PURE__ */ jsxs43("section", { "data-testid": testIdPrefix, "aria-label": ariaLabel ?? document2.title ?? "Roadmap", className: `min-w-0 bg-[var(--c-roadmap-surface)] p-3 text-[var(--c-roadmap-neutral-fg)] ${className}`, children: [
8807
- legendPlacement === "top" && /* @__PURE__ */ jsx68("div", { className: "mb-6", children: legend }),
8808
- /* @__PURE__ */ jsxs43("div", { "data-testid": `${testIdPrefix}-grid`, role: "list", "aria-label": "Roadmap nodes", className: "relative isolate grid min-w-0 grid-cols-3 items-center gap-x-3 gap-y-8 md:gap-x-8", children: [
8809
- document2.groups.map((group) => /* @__PURE__ */ jsx68(RoadmapGroupView, { group, testIdPrefix }, group.id)),
8960
+ const legend = document2.legend && /* @__PURE__ */ jsx69(RoadmapLegendView, { legend: document2.legend, testIdPrefix });
8961
+ return /* @__PURE__ */ jsxs44("section", { "data-testid": testIdPrefix, "aria-label": ariaLabel ?? document2.title ?? "Roadmap", className: `min-w-0 bg-[var(--c-roadmap-surface)] p-3 text-[var(--c-roadmap-neutral-fg)] ${className}`, children: [
8962
+ legendPlacement === "top" && /* @__PURE__ */ jsx69("div", { className: "mb-6", children: legend }),
8963
+ /* @__PURE__ */ jsxs44("div", { ref: gridRef, "data-roadmap-grid": "", "data-testid": `${testIdPrefix}-grid`, role: "list", "aria-label": "Roadmap nodes", className: "relative isolate grid min-w-0 grid-cols-3 items-center gap-x-3 gap-y-2 md:gap-x-8", children: [
8964
+ /* @__PURE__ */ jsx69(RoadmapEdgeLayer, { containerRef: gridRef, document: document2, testIdPrefix, debugPerf }),
8965
+ document2.groups.map((group) => /* @__PURE__ */ jsx69(RoadmapGroupView, { group, testIdPrefix }, group.id)),
8810
8966
  placements.map(({ node, row, column }) => {
8811
8967
  const resolvedState = resolveNodeState(node, state);
8812
- return /* @__PURE__ */ jsx68(
8968
+ return /* @__PURE__ */ jsx69(
8813
8969
  "div",
8814
8970
  {
8815
8971
  role: "listitem",
8816
8972
  "data-testid": `${testIdPrefix}-node`,
8817
8973
  "data-node-id": node.id,
8974
+ "data-roadmap-node": node.id,
8818
8975
  "data-state": resolvedState,
8819
8976
  style: { gridColumn: `${column.start} / span ${column.span}`, gridRow: `${row.start} / span 1` },
8820
- className: "relative z-10 min-w-0 px-2 py-4",
8821
- children: node.kind === "legend" ? legendPlacement === "inline" ? legend : null : renderNode ? renderNode(node, resolvedState) : /* @__PURE__ */ jsx68(RoadmapNodeView, { node, state: resolvedState, badge: node.badge ? badges.get(node.badge) : void 0, onNodeClick, onAction, testIdPrefix })
8977
+ className: "relative z-10 min-w-0 px-2 py-2",
8978
+ children: node.kind === "legend" ? legendPlacement === "inline" ? legend : null : renderNode ? renderNode(node, resolvedState) : /* @__PURE__ */ jsx69(RoadmapNodeView, { node, state: resolvedState, badge: node.badge ? badges.get(node.badge) : void 0, onNodeClick, onAction, testIdPrefix })
8822
8979
  },
8823
8980
  node.id
8824
8981
  );
8825
8982
  })
8826
8983
  ] }),
8827
- legendPlacement === "bottom" && /* @__PURE__ */ jsx68("div", { className: "mt-6", children: legend })
8984
+ legendPlacement === "bottom" && /* @__PURE__ */ jsx69("div", { className: "mt-6", children: legend })
8828
8985
  ] });
8829
8986
  }
8830
8987
 
@@ -8853,7 +9010,7 @@ import {
8853
9010
  useContext as useContext10,
8854
9011
  useMemo as useMemo6
8855
9012
  } from "react";
8856
- import { jsx as jsx69 } from "react/jsx-runtime";
9013
+ import { jsx as jsx70 } from "react/jsx-runtime";
8857
9014
  var FeatureFlagContext = createContext10({
8858
9015
  flags: {},
8859
9016
  isEnabled: () => false
@@ -8869,7 +9026,7 @@ var FeatureFlagProvider = ({
8869
9026
  }),
8870
9027
  [flags]
8871
9028
  );
8872
- return /* @__PURE__ */ jsx69(FeatureFlagContext.Provider, { value, children });
9029
+ return /* @__PURE__ */ jsx70(FeatureFlagContext.Provider, { value, children });
8873
9030
  };
8874
9031
  var useFeatureFlag = (flag) => {
8875
9032
  const { isEnabled } = useContext10(FeatureFlagContext);
@@ -1709,6 +1709,19 @@
1709
1709
 
1710
1710
  /* Origin: agent — Roadmap. Q1: dark default and dark only. */
1711
1711
  :root {
1712
+ --c-roadmap-group-accent-bg: var(--s-surface-elevated);
1713
+ --c-roadmap-group-accent-fg: var(--s-ink-primary);
1714
+ --c-roadmap-edge: var(--s-ink-muted);
1715
+ --c-roadmap-edge-label-bg: var(--s-surface-elevated);
1716
+ --c-roadmap-edge-label-fg: var(--s-ink-secondary);
1717
+ --c-roadmap-primary-edge: var(--s-brand-solid);
1718
+ --c-roadmap-secondary-edge: var(--s-ink-secondary);
1719
+ --c-roadmap-accent-edge: var(--s-ink-primary);
1720
+ --c-roadmap-muted-edge: var(--s-ink-muted);
1721
+ --c-roadmap-neutral-edge: var(--s-ink-secondary);
1722
+ --c-roadmap-success-edge: var(--s-success-fg);
1723
+ --c-roadmap-info-edge: var(--s-info-fg);
1724
+ --c-roadmap-danger-edge: var(--s-danger-fg);
1712
1725
  --c-roadmap-surface: var(--s-surface-page);
1713
1726
  --c-roadmap-border: var(--s-border-strong);
1714
1727
  --c-roadmap-focus: var(--s-border-focus);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devfellowship/components",
3
- "version": "3.4.0",
3
+ "version": "3.5.0",
4
4
  "description": "DFL Design System — UI components, hooks, utils and providers",
5
5
  "type": "module",
6
6
  "sideEffects": [