@devfellowship/components 3.4.0 → 3.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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");
@@ -9139,9 +9179,9 @@ function RoadmapLegendView({ legend, testIdPrefix = "roadmap" }) {
9139
9179
 
9140
9180
  // src/components/organisms/roadmap/RoadmapNode.tsx
9141
9181
  var import_jsx_runtime66 = require("react/jsx-runtime");
9142
- var nodeVariants = (0, import_class_variance_authority14.cva)("relative flex min-w-0 flex-col justify-center rounded-[var(--radius)] border-2 px-2 py-3 text-center text-xs leading-normal md:px-4 md:text-base text-[var(--c-roadmap-node-fg)]", {
9182
+ var nodeVariants = (0, import_class_variance_authority14.cva)("relative flex min-w-0 flex-col justify-center rounded-[var(--radius)] border-2 px-1 py-3 text-center text-[length:var(--c-roadmap-font,12px)] leading-normal text-[var(--c-roadmap-node-fg)]", {
9143
9183
  variants: {
9144
- kind: { topic: "min-h-[49px] border-[var(--c-roadmap-border)] bg-[var(--c-roadmap-node-bg)]", subtopic: "min-h-[49px] border-[var(--c-roadmap-border)] bg-[var(--c-roadmap-node-bg)]", button: "min-h-[49px] border-[var(--c-roadmap-border)] bg-[var(--c-roadmap-node-bg)]", label: "border-transparent bg-transparent", title: "border-transparent bg-transparent font-semibold md:text-[28px]", paragraph: "text-left border-[var(--c-roadmap-border)] bg-[var(--c-roadmap-node-bg)]", legend: "border-[var(--c-roadmap-border)] bg-[var(--c-roadmap-node-bg)]" },
9184
+ kind: { topic: "min-h-[49px] border-[var(--c-roadmap-border)] bg-[var(--c-roadmap-node-bg)]", subtopic: "min-h-[49px] border-[var(--c-roadmap-border)] bg-[var(--c-roadmap-node-bg)]", button: "min-h-[49px] border-[var(--c-roadmap-border)] bg-[var(--c-roadmap-node-bg)]", label: "border-transparent bg-transparent", title: "border-transparent bg-transparent font-semibold text-[length:var(--c-roadmap-title-font,18px)]", paragraph: "text-left border-[var(--c-roadmap-border)] bg-[var(--c-roadmap-node-bg)]", legend: "border-[var(--c-roadmap-border)] bg-[var(--c-roadmap-node-bg)]" },
9145
9185
  state: { todo: "", done: "[--c-roadmap-node-bg:var(--c-roadmap-done-bg)] [--c-roadmap-node-fg:var(--c-roadmap-done-fg)]", learning: "[--c-roadmap-node-bg:var(--c-roadmap-learning-bg)] [--c-roadmap-node-fg:var(--c-roadmap-learning-fg)]", skipped: "[--c-roadmap-node-bg:var(--c-roadmap-skipped-bg)] [--c-roadmap-node-fg:var(--c-roadmap-skipped-fg)]", locked: "opacity-60" }
9146
9186
  }
9147
9187
  });
@@ -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 ${icon ? icon.side === "left" ? "pl-3" : "pr-3" : ""} ${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,170 @@ 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:var(--c-roadmap-edge-width,2px)]",
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 print:hidden 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, collapseEmptyColumns = true }) {
9345
+ const gridRef = (0, import_react8.useRef)(null);
9346
+ const { placements } = (0, import_react8.useMemo)(() => placeNodes(document2), [document2]);
9347
+ const used = (0, import_react8.useMemo)(() => columnsUsed({ nodes: document2.nodes.filter((node) => !((node.span ?? 1) === 3 && (node.kind === "title" || node.kind === "label"))) }), [document2]);
9348
+ const weights = ["left", "center", "right"].map((column, index) => !collapseEmptyColumns || used.length === 0 || used.includes(column) ? index === 1 ? 1.25 : 1 : 0);
9349
+ const gridStyle = {
9350
+ gridTemplateColumns: weights.map((weight) => `minmax(0, ${weight}fr)`).join(" "),
9351
+ ...weights.filter(Boolean).length === 1 ? { columnGap: 0 } : {}
9352
+ };
9353
+ const badges = (0, import_react8.useMemo)(() => new Map(document2.legend?.entries.map((entry) => [entry.id, entry])), [document2.legend]);
9193
9354
  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)),
9355
+ const legend = document2.legend && /* @__PURE__ */ (0, import_jsx_runtime69.jsx)(RoadmapLegendView, { legend: document2.legend, testIdPrefix });
9356
+ return /* @__PURE__ */ (0, import_jsx_runtime69.jsx)("section", { "data-testid": testIdPrefix, "aria-label": ariaLabel ?? document2.title ?? "Roadmap", style: { containerType: "inline-size", containerName: "roadmap" }, className: `mx-auto w-full max-w-[1120px] min-w-0 bg-[var(--c-roadmap-surface)] text-[var(--c-roadmap-neutral-fg)] ${className}`, children: /* @__PURE__ */ (0, import_jsx_runtime69.jsxs)("div", { className: "dfl-roadmap-body min-w-0 p-[var(--c-roadmap-padding)]", children: [
9357
+ legendPlacement === "top" && /* @__PURE__ */ (0, import_jsx_runtime69.jsx)("div", { className: "mb-6", children: legend }),
9358
+ /* @__PURE__ */ (0, import_jsx_runtime69.jsxs)("div", { ref: gridRef, "data-roadmap-grid": "", "data-testid": `${testIdPrefix}-grid`, role: "list", "aria-label": "Roadmap nodes", style: gridStyle, className: "relative isolate grid min-w-0 grid-cols-3 items-center gap-x-[var(--c-roadmap-column-gap)] gap-y-2", children: [
9359
+ /* @__PURE__ */ (0, import_jsx_runtime69.jsx)(RoadmapEdgeLayer, { containerRef: gridRef, document: document2, testIdPrefix, debugPerf }),
9360
+ document2.groups.map((group) => /* @__PURE__ */ (0, import_jsx_runtime69.jsx)(RoadmapGroupView, { group, testIdPrefix }, group.id)),
9199
9361
  placements.map(({ node, row, column }) => {
9200
9362
  const resolvedState = resolveNodeState(node, state);
9201
- return /* @__PURE__ */ (0, import_jsx_runtime68.jsx)(
9363
+ return /* @__PURE__ */ (0, import_jsx_runtime69.jsx)(
9202
9364
  "div",
9203
9365
  {
9204
9366
  role: "listitem",
9205
9367
  "data-testid": `${testIdPrefix}-node`,
9206
9368
  "data-node-id": node.id,
9369
+ "data-roadmap-node": node.id,
9207
9370
  "data-state": resolvedState,
9208
9371
  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 })
9372
+ className: `relative z-10 min-w-0 py-2 ${node.icon || node.badge ? node.icon?.side === "left" ? "pl-3 pr-1" : "pl-1 pr-3" : "px-1"}`,
9373
+ 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
9374
  },
9212
9375
  node.id
9213
9376
  );
9214
9377
  })
9215
9378
  ] }),
9216
- legendPlacement === "bottom" && /* @__PURE__ */ (0, import_jsx_runtime68.jsx)("div", { className: "mt-6", children: legend })
9217
- ] });
9379
+ legendPlacement === "bottom" && /* @__PURE__ */ (0, import_jsx_runtime69.jsx)("div", { className: "mt-6", children: legend })
9380
+ ] }) });
9218
9381
  }
9219
9382
 
9220
9383
  // src/hooks/use-iframe-auth.ts
9221
- var import_react8 = require("react");
9384
+ var import_react9 = require("react");
9222
9385
  function useIframeAuth() {
9223
- const ctx = (0, import_react8.useContext)(IframeContext);
9386
+ const ctx = (0, import_react9.useContext)(IframeContext);
9224
9387
  return { token: ctx.token, userId: ctx.userId, ready: ctx.ready };
9225
9388
  }
9226
9389
 
9227
9390
  // src/hooks/use-iframe-navigate.ts
9228
- var import_react9 = require("react");
9391
+ var import_react10 = require("react");
9229
9392
  function useIframeNavigate() {
9230
- return (0, import_react9.useCallback)((path) => {
9393
+ return (0, import_react10.useCallback)((path) => {
9231
9394
  if (!window.parent || window.parent === window) return;
9232
9395
  window.parent.postMessage(
9233
9396
  { type: "DFL_NAVIGATE", path },
@@ -9237,9 +9400,9 @@ function useIframeNavigate() {
9237
9400
  }
9238
9401
 
9239
9402
  // 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)({
9403
+ var import_react11 = require("react");
9404
+ var import_jsx_runtime70 = require("react/jsx-runtime");
9405
+ var FeatureFlagContext = (0, import_react11.createContext)({
9243
9406
  flags: {},
9244
9407
  isEnabled: () => false
9245
9408
  });
@@ -9247,21 +9410,21 @@ var FeatureFlagProvider = ({
9247
9410
  children,
9248
9411
  flags
9249
9412
  }) => {
9250
- const value = (0, import_react10.useMemo)(
9413
+ const value = (0, import_react11.useMemo)(
9251
9414
  () => ({
9252
9415
  flags,
9253
9416
  isEnabled: (flag) => Boolean(flags[flag])
9254
9417
  }),
9255
9418
  [flags]
9256
9419
  );
9257
- return /* @__PURE__ */ (0, import_jsx_runtime69.jsx)(FeatureFlagContext.Provider, { value, children });
9420
+ return /* @__PURE__ */ (0, import_jsx_runtime70.jsx)(FeatureFlagContext.Provider, { value, children });
9258
9421
  };
9259
9422
  var useFeatureFlag = (flag) => {
9260
- const { isEnabled } = (0, import_react10.useContext)(FeatureFlagContext);
9423
+ const { isEnabled } = (0, import_react11.useContext)(FeatureFlagContext);
9261
9424
  return isEnabled(flag);
9262
9425
  };
9263
9426
  var useFeatureFlags = () => {
9264
- const { flags } = (0, import_react10.useContext)(FeatureFlagContext);
9427
+ const { flags } = (0, import_react11.useContext)(FeatureFlagContext);
9265
9428
  return flags;
9266
9429
  };
9267
9430
  // Annotate the CommonJS export names for ESM import in node:
package/dist/index.d.cts CHANGED
@@ -3268,11 +3268,14 @@ 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;
3272
+ /** Collapse unused lanes; full-width headings do not reserve empty side tracks. */
3273
+ collapseEmptyColumns?: boolean;
3271
3274
  testIdPrefix?: string;
3272
3275
  className?: string;
3273
3276
  ariaLabel?: string;
3274
3277
  }
3275
3278
  /** 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;
3279
+ declare function Roadmap({ document, state, renderNode, onNodeClick, onAction, testIdPrefix, className, ariaLabel, debugPerf, collapseEmptyColumns }: RoadmapProps): React__default.JSX.Element;
3277
3280
 
3278
3281
  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,14 @@ 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;
3272
+ /** Collapse unused lanes; full-width headings do not reserve empty side tracks. */
3273
+ collapseEmptyColumns?: boolean;
3271
3274
  testIdPrefix?: string;
3272
3275
  className?: string;
3273
3276
  ariaLabel?: string;
3274
3277
  }
3275
3278
  /** 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;
3279
+ declare function Roadmap({ document, state, renderNode, onNodeClick, onAction, testIdPrefix, className, ariaLabel, debugPerf, collapseEmptyColumns }: RoadmapProps): React__default.JSX.Element;
3277
3280
 
3278
3281
  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";
@@ -8750,9 +8790,9 @@ function RoadmapLegendView({ legend, testIdPrefix = "roadmap" }) {
8750
8790
 
8751
8791
  // src/components/organisms/roadmap/RoadmapNode.tsx
8752
8792
  import { jsx as jsx66, jsxs as jsxs41 } from "react/jsx-runtime";
8753
- var nodeVariants = cva14("relative flex min-w-0 flex-col justify-center rounded-[var(--radius)] border-2 px-2 py-3 text-center text-xs leading-normal md:px-4 md:text-base text-[var(--c-roadmap-node-fg)]", {
8793
+ var nodeVariants = cva14("relative flex min-w-0 flex-col justify-center rounded-[var(--radius)] border-2 px-1 py-3 text-center text-[length:var(--c-roadmap-font,12px)] leading-normal text-[var(--c-roadmap-node-fg)]", {
8754
8794
  variants: {
8755
- kind: { topic: "min-h-[49px] border-[var(--c-roadmap-border)] bg-[var(--c-roadmap-node-bg)]", subtopic: "min-h-[49px] border-[var(--c-roadmap-border)] bg-[var(--c-roadmap-node-bg)]", button: "min-h-[49px] border-[var(--c-roadmap-border)] bg-[var(--c-roadmap-node-bg)]", label: "border-transparent bg-transparent", title: "border-transparent bg-transparent font-semibold md:text-[28px]", paragraph: "text-left border-[var(--c-roadmap-border)] bg-[var(--c-roadmap-node-bg)]", legend: "border-[var(--c-roadmap-border)] bg-[var(--c-roadmap-node-bg)]" },
8795
+ kind: { topic: "min-h-[49px] border-[var(--c-roadmap-border)] bg-[var(--c-roadmap-node-bg)]", subtopic: "min-h-[49px] border-[var(--c-roadmap-border)] bg-[var(--c-roadmap-node-bg)]", button: "min-h-[49px] border-[var(--c-roadmap-border)] bg-[var(--c-roadmap-node-bg)]", label: "border-transparent bg-transparent", title: "border-transparent bg-transparent font-semibold text-[length:var(--c-roadmap-title-font,18px)]", paragraph: "text-left border-[var(--c-roadmap-border)] bg-[var(--c-roadmap-node-bg)]", legend: "border-[var(--c-roadmap-border)] bg-[var(--c-roadmap-node-bg)]" },
8756
8796
  state: { todo: "", done: "[--c-roadmap-node-bg:var(--c-roadmap-done-bg)] [--c-roadmap-node-fg:var(--c-roadmap-done-fg)]", learning: "[--c-roadmap-node-bg:var(--c-roadmap-learning-bg)] [--c-roadmap-node-fg:var(--c-roadmap-learning-fg)]", skipped: "[--c-roadmap-node-bg:var(--c-roadmap-skipped-bg)] [--c-roadmap-node-fg:var(--c-roadmap-skipped-fg)]", locked: "opacity-60" }
8757
8797
  }
8758
8798
  });
@@ -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 ${icon ? icon.side === "left" ? "pl-3" : "pr-3" : ""} ${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,36 +8838,157 @@ 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:var(--c-roadmap-edge-width,2px)]",
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 print:hidden 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, collapseEmptyColumns = true }) {
8956
+ const gridRef = useRef6(null);
8802
8957
  const { placements } = useMemo5(() => placeNodes(document2), [document2]);
8958
+ const used = useMemo5(() => columnsUsed({ nodes: document2.nodes.filter((node) => !((node.span ?? 1) === 3 && (node.kind === "title" || node.kind === "label"))) }), [document2]);
8959
+ const weights = ["left", "center", "right"].map((column, index) => !collapseEmptyColumns || used.length === 0 || used.includes(column) ? index === 1 ? 1.25 : 1 : 0);
8960
+ const gridStyle = {
8961
+ gridTemplateColumns: weights.map((weight) => `minmax(0, ${weight}fr)`).join(" "),
8962
+ ...weights.filter(Boolean).length === 1 ? { columnGap: 0 } : {}
8963
+ };
8803
8964
  const badges = useMemo5(() => new Map(document2.legend?.entries.map((entry) => [entry.id, entry])), [document2.legend]);
8804
8965
  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)),
8966
+ const legend = document2.legend && /* @__PURE__ */ jsx69(RoadmapLegendView, { legend: document2.legend, testIdPrefix });
8967
+ return /* @__PURE__ */ jsx69("section", { "data-testid": testIdPrefix, "aria-label": ariaLabel ?? document2.title ?? "Roadmap", style: { containerType: "inline-size", containerName: "roadmap" }, className: `mx-auto w-full max-w-[1120px] min-w-0 bg-[var(--c-roadmap-surface)] text-[var(--c-roadmap-neutral-fg)] ${className}`, children: /* @__PURE__ */ jsxs44("div", { className: "dfl-roadmap-body min-w-0 p-[var(--c-roadmap-padding)]", children: [
8968
+ legendPlacement === "top" && /* @__PURE__ */ jsx69("div", { className: "mb-6", children: legend }),
8969
+ /* @__PURE__ */ jsxs44("div", { ref: gridRef, "data-roadmap-grid": "", "data-testid": `${testIdPrefix}-grid`, role: "list", "aria-label": "Roadmap nodes", style: gridStyle, className: "relative isolate grid min-w-0 grid-cols-3 items-center gap-x-[var(--c-roadmap-column-gap)] gap-y-2", children: [
8970
+ /* @__PURE__ */ jsx69(RoadmapEdgeLayer, { containerRef: gridRef, document: document2, testIdPrefix, debugPerf }),
8971
+ document2.groups.map((group) => /* @__PURE__ */ jsx69(RoadmapGroupView, { group, testIdPrefix }, group.id)),
8810
8972
  placements.map(({ node, row, column }) => {
8811
8973
  const resolvedState = resolveNodeState(node, state);
8812
- return /* @__PURE__ */ jsx68(
8974
+ return /* @__PURE__ */ jsx69(
8813
8975
  "div",
8814
8976
  {
8815
8977
  role: "listitem",
8816
8978
  "data-testid": `${testIdPrefix}-node`,
8817
8979
  "data-node-id": node.id,
8980
+ "data-roadmap-node": node.id,
8818
8981
  "data-state": resolvedState,
8819
8982
  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 })
8983
+ className: `relative z-10 min-w-0 py-2 ${node.icon || node.badge ? node.icon?.side === "left" ? "pl-3 pr-1" : "pl-1 pr-3" : "px-1"}`,
8984
+ 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
8985
  },
8823
8986
  node.id
8824
8987
  );
8825
8988
  })
8826
8989
  ] }),
8827
- legendPlacement === "bottom" && /* @__PURE__ */ jsx68("div", { className: "mt-6", children: legend })
8828
- ] });
8990
+ legendPlacement === "bottom" && /* @__PURE__ */ jsx69("div", { className: "mt-6", children: legend })
8991
+ ] }) });
8829
8992
  }
8830
8993
 
8831
8994
  // src/hooks/use-iframe-auth.ts
@@ -8853,7 +9016,7 @@ import {
8853
9016
  useContext as useContext10,
8854
9017
  useMemo as useMemo6
8855
9018
  } from "react";
8856
- import { jsx as jsx69 } from "react/jsx-runtime";
9019
+ import { jsx as jsx70 } from "react/jsx-runtime";
8857
9020
  var FeatureFlagContext = createContext10({
8858
9021
  flags: {},
8859
9022
  isEnabled: () => false
@@ -8869,7 +9032,7 @@ var FeatureFlagProvider = ({
8869
9032
  }),
8870
9033
  [flags]
8871
9034
  );
8872
- return /* @__PURE__ */ jsx69(FeatureFlagContext.Provider, { value, children });
9035
+ return /* @__PURE__ */ jsx70(FeatureFlagContext.Provider, { value, children });
8873
9036
  };
8874
9037
  var useFeatureFlag = (flag) => {
8875
9038
  const { isEnabled } = useContext10(FeatureFlagContext);
@@ -56,3 +56,32 @@ body {
56
56
  .dark {
57
57
  /* All tokens already render dark at :root. Intentionally empty. */
58
58
  }
59
+
60
+ /* Roadmap sizes follow container width, including narrow embedded panels.
61
+ * Plain CSS supports Tailwind 3 and 4. Row spacing remains compact:
62
+ * 8px gap + two 8px node insets = 24px between node borders. */
63
+ .dfl-roadmap-body {
64
+ --c-roadmap-edge-width: 2px;
65
+ --c-roadmap-font: 12px;
66
+ --c-roadmap-title-font: 18px;
67
+ --c-roadmap-column-gap: 8px;
68
+ --c-roadmap-padding: 8px;
69
+ }
70
+ @container roadmap (min-width: 360px) {
71
+ .dfl-roadmap-body { --c-roadmap-padding: 12px; }
72
+ }
73
+ @container roadmap (min-width: 390px) {
74
+ .dfl-roadmap-body { --c-roadmap-font: 13px; --c-roadmap-title-font: 20px; --c-roadmap-column-gap: 12px; }
75
+ }
76
+ @container roadmap (min-width: 768px) {
77
+ .dfl-roadmap-body { --c-roadmap-edge-width: 3px; --c-roadmap-font: 15px; --c-roadmap-title-font: 22px; --c-roadmap-column-gap: 24px; --c-roadmap-padding: 16px; }
78
+ }
79
+ @container roadmap (min-width: 1024px) {
80
+ .dfl-roadmap-body { --c-roadmap-font: 17px; --c-roadmap-title-font: 24px; --c-roadmap-column-gap: 32px; --c-roadmap-padding: 24px; }
81
+ }
82
+ /* The map stops at 1120px; the outer viewport selects the wide rhythm. */
83
+ @media (min-width: 1280px) {
84
+ @container roadmap (min-width: 1120px) {
85
+ .dfl-roadmap-body { --c-roadmap-title-font: 28px; --c-roadmap-column-gap: 48px; }
86
+ }
87
+ }
@@ -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.1",
4
4
  "description": "DFL Design System — UI components, hooks, utils and providers",
5
5
  "type": "module",
6
6
  "sideEffects": [